codex-flow 2.1.13__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.
Files changed (113) hide show
  1. codex_flow/__init__.py +28 -0
  2. codex_flow/__main__.py +9 -0
  3. codex_flow/cli.py +242 -0
  4. codex_flow/data/LICENSE +21 -0
  5. codex_flow/data/README.en.md +303 -0
  6. codex_flow/data/README.md +305 -0
  7. codex_flow/data/VERSION +1 -0
  8. codex_flow/data/apps/chatgpt-mcp/README.md +86 -0
  9. codex_flow/data/apps/chatgpt-mcp/__init__.py +1 -0
  10. codex_flow/data/apps/chatgpt-mcp/adapter.py +458 -0
  11. codex_flow/data/apps/chatgpt-mcp/server.py +358 -0
  12. codex_flow/data/apps/chatgpt-mcp/widget.html +927 -0
  13. codex_flow/data/apps/macos-overlay/README.en.md +121 -0
  14. codex_flow/data/apps/macos-overlay/README.md +123 -0
  15. codex_flow/data/apps/macos-overlay/Sources/Controllers/OverlayRuntimeState.swift +126 -0
  16. codex_flow/data/apps/macos-overlay/Sources/Controllers/OverlayScreenGeometry.swift +82 -0
  17. codex_flow/data/apps/macos-overlay/Sources/Controllers/OverlayWindowController.swift +1052 -0
  18. codex_flow/data/apps/macos-overlay/Sources/Localization.swift +197 -0
  19. codex_flow/data/apps/macos-overlay/Sources/Models/TelemetryData.swift +1557 -0
  20. codex_flow/data/apps/macos-overlay/Sources/Services/AccountSnapshotService.swift +1101 -0
  21. codex_flow/data/apps/macos-overlay/Sources/Services/FlowPilotInstanceLock.swift +153 -0
  22. codex_flow/data/apps/macos-overlay/Sources/Services/IPCServer.swift +298 -0
  23. codex_flow/data/apps/macos-overlay/Sources/Services/TelemetryQueryEngine.swift +800 -0
  24. codex_flow/data/apps/macos-overlay/Sources/Services/TelemetryWatcher.swift +135 -0
  25. codex_flow/data/apps/macos-overlay/Sources/Services/UpdateService.swift +610 -0
  26. codex_flow/data/apps/macos-overlay/Sources/Views/AccountView.swift +610 -0
  27. codex_flow/data/apps/macos-overlay/Sources/Views/AnalyticsView.swift +566 -0
  28. codex_flow/data/apps/macos-overlay/Sources/Views/AutostartView.swift +293 -0
  29. codex_flow/data/apps/macos-overlay/Sources/Views/BubbleView.swift +317 -0
  30. codex_flow/data/apps/macos-overlay/Sources/Views/HistoryView.swift +1124 -0
  31. codex_flow/data/apps/macos-overlay/Sources/Views/HoverRevealText.swift +165 -0
  32. codex_flow/data/apps/macos-overlay/Sources/Views/InspectorSkillsToolsView.swift +121 -0
  33. codex_flow/data/apps/macos-overlay/Sources/Views/LogoView.swift +182 -0
  34. codex_flow/data/apps/macos-overlay/Sources/Views/SleekSwitch.swift +117 -0
  35. codex_flow/data/apps/macos-overlay/Sources/Views/StrategyModeView.swift +561 -0
  36. codex_flow/data/apps/macos-overlay/Sources/Views/SummaryView.swift +1273 -0
  37. codex_flow/data/apps/macos-overlay/Sources/Views/UpdateView.swift +352 -0
  38. codex_flow/data/apps/macos-overlay/Sources/main.swift +340 -0
  39. codex_flow/data/apps/macos-overlay/Tests/OverlayScreenGeometryTests.swift +163 -0
  40. codex_flow/data/apps/macos-overlay/Tests/TelemetryPhase1ContractTests.swift +357 -0
  41. codex_flow/data/apps/macos-overlay/Tests/TelemetryQueryEngineConcurrencyTests.swift +221 -0
  42. codex_flow/data/apps/macos-overlay/Tests/TelemetryQuotaSelectionTests.swift +158 -0
  43. codex_flow/data/apps/macos-overlay/Tests/TelemetryWorkerTokenTests.swift +122 -0
  44. codex_flow/data/apps/macos-overlay/build.sh +75 -0
  45. codex_flow/data/benchmark/corpus.json +103 -0
  46. codex_flow/data/benchmark/manifest.example.json +41 -0
  47. codex_flow/data/benchmark/manifest.schema.json +137 -0
  48. codex_flow/data/benchmark/prices/gpt-5.6-2026-08-30.json +5 -0
  49. codex_flow/data/benchmark/profiles.json +90 -0
  50. codex_flow/data/benchmark/schema.json +77 -0
  51. codex_flow/data/benchmark/tasks.json +50 -0
  52. codex_flow/data/completions/codex-flow.bash +34 -0
  53. codex_flow/data/completions/codex-flow.zsh +52 -0
  54. codex_flow/data/glama.json +6 -0
  55. codex_flow/data/install-release.ps1 +126 -0
  56. codex_flow/data/install-release.sh +155 -0
  57. codex_flow/data/install.ps1 +349 -0
  58. codex_flow/data/install.sh +362 -0
  59. codex_flow/data/policy/benchmark.toml +49 -0
  60. codex_flow/data/policy/defaults.toml +70 -0
  61. codex_flow/data/scripts/analyze-benchmark.py +510 -0
  62. codex_flow/data/scripts/benchmark-local.py +171 -0
  63. codex_flow/data/scripts/check-recommendation.py +277 -0
  64. codex_flow/data/scripts/doctor.py +449 -0
  65. codex_flow/data/scripts/generate-release-manifest.py +74 -0
  66. codex_flow/data/scripts/localization.py +192 -0
  67. codex_flow/data/scripts/manage-hooks.py +448 -0
  68. codex_flow/data/scripts/manage-instructions.py +389 -0
  69. codex_flow/data/scripts/manage-shell.py +151 -0
  70. codex_flow/data/scripts/materialize-corpus.py +193 -0
  71. codex_flow/data/scripts/menu.py +646 -0
  72. codex_flow/data/scripts/migrations/0001_update_settings.py +80 -0
  73. codex_flow/data/scripts/package-release.py +132 -0
  74. codex_flow/data/scripts/render-benchmark-report.py +292 -0
  75. codex_flow/data/scripts/run-benchmark.py +829 -0
  76. codex_flow/data/scripts/strategies/__init__.py +28 -0
  77. codex_flow/data/scripts/strategies/balanced.py +115 -0
  78. codex_flow/data/scripts/strategies/base.py +363 -0
  79. codex_flow/data/scripts/strategies/efficient.py +158 -0
  80. codex_flow/data/scripts/strategies/lifecycle_runtime.py +590 -0
  81. codex_flow/data/scripts/strategies/quality.py +209 -0
  82. codex_flow/data/scripts/strategies/speed.py +108 -0
  83. codex_flow/data/scripts/strategies/task_budget_runtime.py +644 -0
  84. codex_flow/data/scripts/strategies/task_phase_runtime.py +341 -0
  85. codex_flow/data/scripts/strategies/work_unit_runtime.py +421 -0
  86. codex_flow/data/scripts/strategy_runtime.py +1091 -0
  87. codex_flow/data/scripts/telemetry.py +400 -0
  88. codex_flow/data/scripts/telemetry_core/__init__.py +192 -0
  89. codex_flow/data/scripts/telemetry_core/app_server.py +1192 -0
  90. codex_flow/data/scripts/telemetry_core/collector.py +1247 -0
  91. codex_flow/data/scripts/telemetry_core/common.py +421 -0
  92. codex_flow/data/scripts/telemetry_core/latency.py +593 -0
  93. codex_flow/data/scripts/telemetry_core/query.py +427 -0
  94. codex_flow/data/scripts/telemetry_core/quota_ledger.py +598 -0
  95. codex_flow/data/scripts/telemetry_core/render.py +460 -0
  96. codex_flow/data/scripts/telemetry_core/repair.py +223 -0
  97. codex_flow/data/scripts/ui.py +266 -0
  98. codex_flow/data/scripts/update-homebrew-formula.py +146 -0
  99. codex_flow/data/scripts/update_runtime_config.py +134 -0
  100. codex_flow/data/scripts/updater.py +1718 -0
  101. codex_flow/data/smithery.yaml +18 -0
  102. codex_flow/data/templates/agents/worker-explorer.toml +24 -0
  103. codex_flow/data/templates/agents/worker-implementer.toml +49 -0
  104. codex_flow/data/templates/agents/worker-reviewer.toml +25 -0
  105. codex_flow/data/templates/flow-pilot-instructions.md +35 -0
  106. codex_flow/data/templates/skills/flow-pilot/SKILL.md +577 -0
  107. codex_flow/mcp.py +35 -0
  108. codex_flow-2.1.13.dist-info/METADATA +342 -0
  109. codex_flow-2.1.13.dist-info/RECORD +113 -0
  110. codex_flow-2.1.13.dist-info/WHEEL +5 -0
  111. codex_flow-2.1.13.dist-info/entry_points.txt +3 -0
  112. codex_flow-2.1.13.dist-info/licenses/LICENSE +21 -0
  113. codex_flow-2.1.13.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1091 @@
1
+ #!/usr/bin/env python3
2
+ """Deterministic strategy resolver and ExecutionPlan compiler for codex-flow.
3
+
4
+ FlowPilot owns semantic TaskProfile construction. This module is the single source
5
+ of truth for policy precedence, release defaults, modifiers, runtime constraints,
6
+ and the final ExecutionPlan. Strategy-specific optimization decisions are loaded
7
+ from the built-in strategy registry.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import json
13
+ import os
14
+ import re
15
+ import sys
16
+ from dataclasses import asdict, dataclass, field, replace
17
+ from pathlib import Path
18
+ from typing import Any, Iterable
19
+
20
+ from strategies import all_specs as all_strategy_specs
21
+ from strategies import get as get_strategy
22
+ from strategies import names as strategy_names
23
+ from strategies.base import (
24
+ REASONING_ROLLOUT_MODES,
25
+ ReasoningRolloutDecision,
26
+ ReasoningRolloutPolicy,
27
+ StagePolicy,
28
+ TaskBudgetPolicy,
29
+ WorkerBudget,
30
+ )
31
+
32
+ try:
33
+ from telemetry_core.app_server import quota_windows as app_server_quota_windows
34
+ except ImportError: # pragma: no cover
35
+ app_server_quota_windows = None
36
+
37
+ CODEX_HOME = Path(os.environ.get("CODEX_HOME", Path.home() / ".codex"))
38
+ DEFAULT_POLICY = CODEX_HOME / "codex-flow.toml"
39
+ REPO_POLICY_NAME = ".codex-flow.toml"
40
+ TEMPORARY_BYPASS_NAME = ".strategy-bypass-once"
41
+
42
+ STRATEGIES = strategy_names()
43
+ ROUTING_MODES = ("adaptive", "direct", "delegate")
44
+ COMPLEXITIES = ("small", "routine", "complex", "critical")
45
+ LEVELS = ("low", "medium", "high", "critical")
46
+ EFFORTS = ("high", "xhigh", "max")
47
+ SCOPES = ("local", "module", "cross-module", "repo-wide")
48
+ PARALLELISM = ("none", "limited", "high")
49
+ ITERATION = ("one-shot", "iterative", "heavy-loop")
50
+ QUALITY_INTENTS = ("normal", "strong", "absolute")
51
+ REVIEW_MODES = ("auto", "standard", "strict")
52
+ FANOUT_MODES = ("auto", "conservative", "aggressive")
53
+ EFFORT_RANK = {name: idx for idx, name in enumerate(EFFORTS)}
54
+ RUNTIME_MAX_WORKER_IDLE_SECONDS = 600
55
+ RUNTIME_MAX_WORKER_WALL_SECONDS = 3600
56
+
57
+
58
+ def _release_defaults_path() -> Path:
59
+ candidates = (
60
+ Path(__file__).resolve().with_name("defaults.toml"),
61
+ Path(__file__).resolve().parent.parent / "policy" / "defaults.toml",
62
+ )
63
+ for candidate in candidates:
64
+ if candidate.is_file():
65
+ return candidate
66
+ raise FileNotFoundError("codex-flow release defaults not found; reinstall codex-flow")
67
+
68
+
69
+ @dataclass(frozen=True)
70
+ class TaskProfile:
71
+ complexity: str = "routine"
72
+ uncertainty: str = "medium"
73
+ risk: str = "medium"
74
+ scope: str = "module"
75
+ parallelism: str = "limited"
76
+ write_conflict: str = "low"
77
+ exploration_need: str = "medium"
78
+ verification_cost: str = "medium"
79
+ iteration_intensity: str = "iterative"
80
+ writable_workstreams: int = 1
81
+ quality_intent: str = "normal"
82
+
83
+ def validate(self) -> None:
84
+ if self.complexity not in COMPLEXITIES:
85
+ raise ValueError(f"invalid complexity: {self.complexity}")
86
+ if self.uncertainty not in LEVELS[:3]:
87
+ raise ValueError(f"invalid uncertainty: {self.uncertainty}")
88
+ if self.risk not in LEVELS:
89
+ raise ValueError(f"invalid risk: {self.risk}")
90
+ if self.scope not in SCOPES:
91
+ raise ValueError(f"invalid scope: {self.scope}")
92
+ if self.parallelism not in PARALLELISM:
93
+ raise ValueError(f"invalid parallelism: {self.parallelism}")
94
+ if self.write_conflict not in {"low", "high"}:
95
+ raise ValueError(f"invalid write_conflict: {self.write_conflict}")
96
+ if self.exploration_need not in LEVELS[:3]:
97
+ raise ValueError(f"invalid exploration_need: {self.exploration_need}")
98
+ if self.verification_cost not in LEVELS[:3]:
99
+ raise ValueError(f"invalid verification_cost: {self.verification_cost}")
100
+ if self.iteration_intensity not in ITERATION:
101
+ raise ValueError(f"invalid iteration_intensity: {self.iteration_intensity}")
102
+ if self.writable_workstreams < 1:
103
+ raise ValueError("writable_workstreams must be positive")
104
+ if self.quality_intent not in QUALITY_INTENTS:
105
+ raise ValueError(f"invalid quality_intent: {self.quality_intent}")
106
+
107
+
108
+ @dataclass(frozen=True)
109
+ class Modifiers:
110
+ review: str = "auto"
111
+ fanout: str = "auto"
112
+
113
+ def validate(self) -> None:
114
+ if self.review not in REVIEW_MODES:
115
+ raise ValueError(f"invalid review modifier: {self.review}")
116
+ if self.fanout not in FANOUT_MODES:
117
+ raise ValueError(f"invalid fanout modifier: {self.fanout}")
118
+
119
+
120
+ @dataclass(frozen=True)
121
+ class PolicySnapshot:
122
+ parent_capability_policy: str
123
+ parent_model_floor: str
124
+ parent_min_reasoning: str
125
+ parent_routine_reasoning: str
126
+ parent_complex_reasoning: str
127
+ parent_critical_reasoning: str
128
+ worker_capability_policy: str
129
+ worker_model: str
130
+ worker_resolved_model: str
131
+ worker_min_reasoning: str
132
+ worker_routine_reasoning: str
133
+ worker_complex_reasoning: str
134
+ worker_critical_reasoning: str
135
+ reasoning_rollout: ReasoningRolloutPolicy = ReasoningRolloutPolicy()
136
+
137
+ def validate(self) -> None:
138
+ for label, value in (
139
+ ("parent_min_reasoning", self.parent_min_reasoning),
140
+ ("parent_routine_reasoning", self.parent_routine_reasoning),
141
+ ("parent_complex_reasoning", self.parent_complex_reasoning),
142
+ ("parent_critical_reasoning", self.parent_critical_reasoning),
143
+ ("worker_min_reasoning", self.worker_min_reasoning),
144
+ ("worker_routine_reasoning", self.worker_routine_reasoning),
145
+ ("worker_complex_reasoning", self.worker_complex_reasoning),
146
+ ("worker_critical_reasoning", self.worker_critical_reasoning),
147
+ ):
148
+ if value not in EFFORTS:
149
+ raise ValueError(f"invalid {label}: {value}")
150
+ self.reasoning_rollout.validate()
151
+
152
+
153
+ @dataclass(frozen=True)
154
+ class ResolvedPolicy:
155
+ enabled: bool
156
+ strategy: str
157
+ routing: str
158
+ modifiers: Modifiers
159
+ capability: PolicySnapshot
160
+ max_concurrent_threads: int
161
+ max_repair_cycles: int
162
+ repo_policy: str | None = None
163
+
164
+ def validate(self) -> None:
165
+ if self.strategy not in STRATEGIES:
166
+ raise ValueError(f"invalid strategy: {self.strategy}")
167
+ if self.routing not in ROUTING_MODES:
168
+ raise ValueError(f"invalid routing mode: {self.routing}")
169
+ self.modifiers.validate()
170
+ self.capability.validate()
171
+ if self.max_concurrent_threads < 1:
172
+ raise ValueError("max_concurrent_threads must be positive")
173
+ if self.max_repair_cycles < 0:
174
+ raise ValueError("max_repair_cycles cannot be negative")
175
+
176
+
177
+ @dataclass(frozen=True)
178
+ class RuntimeState:
179
+ quota_pressure: str = "unknown"
180
+ max_concurrent_threads: int = 4
181
+ max_repair_cycles: int = 2
182
+
183
+ def validate(self) -> None:
184
+ if self.quota_pressure not in {"unknown", "low", "medium", "high", "critical"}:
185
+ raise ValueError(f"invalid quota_pressure: {self.quota_pressure}")
186
+ if self.max_concurrent_threads < 1:
187
+ raise ValueError("max_concurrent_threads must be positive")
188
+ if self.max_repair_cycles < 0:
189
+ raise ValueError("max_repair_cycles cannot be negative")
190
+
191
+
192
+ @dataclass(frozen=True)
193
+ class ExecutionPlan:
194
+ schema_version: int
195
+ strategy: str
196
+ routing: str
197
+ review_modifier: str
198
+ fanout_modifier: str
199
+ quality_intent: str
200
+ parent_capability_policy: str
201
+ parent_model_floor: str
202
+ parent_reasoning: str
203
+ reasoning_rollout: ReasoningRolloutDecision | None
204
+ explorer_capability_policy: str | None
205
+ explorer_model: str | None
206
+ explorer_reasoning: str | None
207
+ implementer_capability_policy: str | None
208
+ implementer_model: str | None
209
+ implementer_reasoning: str | None
210
+ reviewer_capability_policy: str | None
211
+ reviewer_model: str | None
212
+ reviewer_reasoning: str | None
213
+ worker_budget: WorkerBudget
214
+ task_budget: TaskBudgetPolicy | None
215
+ exploration_workers: int
216
+ implementation_workers: int
217
+ reviewer_workers: int
218
+ planned_worker_count: int
219
+ exploration_stage: StagePolicy | None
220
+ implementation_stage: StagePolicy | None
221
+ review_stage: StagePolicy | None
222
+ review_mode: str
223
+ max_repair_cycles: int
224
+ max_concurrent_threads: int
225
+ escalate_on_failure: bool
226
+ quota_pressure: str
227
+ repo_policy: str | None = None
228
+ context_mode: str = "compact-fresh"
229
+ notes: tuple[str, ...] = field(default_factory=tuple)
230
+
231
+ def to_dict(self) -> dict[str, Any]:
232
+ data = asdict(self)
233
+ data["notes"] = list(self.notes)
234
+ return data
235
+
236
+
237
+ def _section_body(text: str, section: str) -> re.Match[str] | None:
238
+ return re.search(rf"(?ms)^\[{re.escape(section)}\]\s*\n(.*?)(?=^\[[^\n]+\]\s*$|\Z)", text)
239
+
240
+
241
+ def policy_value(path: Path | None, section: str, key: str, default: str = "") -> str:
242
+ if path is None:
243
+ return default
244
+ try:
245
+ text = path.read_text(encoding="utf-8-sig")
246
+ except OSError:
247
+ return default
248
+ match = _section_body(text, section)
249
+ if not match:
250
+ return default
251
+ key_match = re.search(rf"(?m)^\s*{re.escape(key)}\s*=\s*(.*?)\s*$", match.group(1))
252
+ if not key_match:
253
+ return default
254
+ value = re.sub(r"\s+#.*$", "", key_match.group(1)).strip()
255
+ if len(value) >= 2 and value[0] == value[-1] == '"':
256
+ value = value[1:-1]
257
+ return value or default
258
+
259
+
260
+ def _policy_int(path: Path | None, section: str, key: str, default: int) -> int:
261
+ raw = policy_value(path, section, key, str(default))
262
+ try:
263
+ return int(raw)
264
+ except ValueError:
265
+ return default
266
+
267
+
268
+ def _policy_bool(path: Path | None, section: str, key: str, default: bool, *, strict: bool = False) -> bool:
269
+ if strict and path is not None:
270
+ try:
271
+ text = path.read_text(encoding="utf-8-sig")
272
+ except OSError:
273
+ return default
274
+ match = _section_body(text, section)
275
+ if not match:
276
+ return default
277
+ key_match = re.search(rf"(?m)^\s*{re.escape(key)}\s*=\s*(.*?)\s*$", match.group(1))
278
+ if not key_match:
279
+ return default
280
+ raw = re.sub(r"\s+#.*$", "", key_match.group(1)).strip()
281
+ if len(raw) >= 2 and raw[0] == raw[-1] == '"':
282
+ raw = raw[1:-1]
283
+ else:
284
+ raw = policy_value(path, section, key, "true" if default else "false")
285
+ raw = raw.strip().lower()
286
+ if raw in {"1", "true", "yes", "on"}:
287
+ return True
288
+ if raw in {"0", "false", "no", "off"}:
289
+ return False
290
+ if strict:
291
+ raise ValueError(f"invalid [{section}].{key}: {raw or '<empty>'}; expected true or false")
292
+ return default
293
+
294
+
295
+ def _release_value(section: str, key: str, fallback: str = "") -> str:
296
+ return policy_value(_release_defaults_path(), section, key, fallback)
297
+
298
+
299
+ def _release_int(section: str, key: str, fallback: int) -> int:
300
+ return _policy_int(_release_defaults_path(), section, key, fallback)
301
+
302
+
303
+ def _release_bool(section: str, key: str, fallback: bool) -> bool:
304
+ return _policy_bool(_release_defaults_path(), section, key, fallback)
305
+
306
+
307
+ def _release_reasoning_rollout() -> ReasoningRolloutPolicy:
308
+ rollout = ReasoningRolloutPolicy(
309
+ mode=_release_value("reasoning.rollout", "mode", "shadow"),
310
+ minimum=_release_value("reasoning.rollout", "minimum", "high"),
311
+ routine=_release_value("reasoning.rollout", "routine", "high"),
312
+ complex=_release_value("reasoning.rollout", "complex", "xhigh"),
313
+ critical=_release_value("reasoning.rollout", "critical", "max"),
314
+ )
315
+ rollout.validate()
316
+ return rollout
317
+
318
+
319
+ def _quoted(value: str) -> str:
320
+ return '"' + value.replace("\\", "\\\\").replace('"', '\\"') + '"'
321
+
322
+
323
+ def set_policy_value(path: Path, section: str, key: str, value: str, *, quote: bool = True) -> None:
324
+ if not path.exists():
325
+ raise FileNotFoundError(f"policy not found: {path}")
326
+ text = path.read_text(encoding="utf-8-sig")
327
+ match = _section_body(text, section)
328
+ line = f"{key} = {_quoted(value) if quote else value}"
329
+ if match:
330
+ body = match.group(1)
331
+ key_re = re.compile(rf"(?m)^\s*{re.escape(key)}\s*=.*$")
332
+ if key_re.search(body):
333
+ body = key_re.sub(line, body)
334
+ else:
335
+ if body and not body.endswith("\n"):
336
+ body += "\n"
337
+ body += line + "\n"
338
+ text = text[: match.start(1)] + body + text[match.end(1):]
339
+ else:
340
+ if text and not text.endswith("\n"):
341
+ text += "\n"
342
+ if text and not text.endswith("\n\n"):
343
+ text += "\n"
344
+ text += f"[{section}]\n{line}\n"
345
+ path.write_text(text, encoding="utf-8")
346
+
347
+
348
+ def _effort_max(*values: str) -> str:
349
+ valid = [value for value in values if value in EFFORT_RANK]
350
+ if not valid:
351
+ raise ValueError("no valid reasoning effort supplied")
352
+ return max(valid, key=lambda value: EFFORT_RANK[value])
353
+
354
+
355
+ def _effort_next(value: str) -> str:
356
+ if value not in EFFORT_RANK:
357
+ raise ValueError(f"invalid reasoning effort: {value}")
358
+ return EFFORTS[min(len(EFFORTS) - 1, EFFORT_RANK[value] + 1)]
359
+
360
+
361
+ def _release_capability() -> PolicySnapshot:
362
+ worker_model = _release_value("models", "worker_model")
363
+ snapshot = PolicySnapshot(
364
+ parent_capability_policy=_release_value("models", "parent_policy"),
365
+ parent_model_floor=_release_value("models", "parent_min_model"),
366
+ parent_min_reasoning=_release_value("reasoning.parent", "minimum"),
367
+ parent_routine_reasoning=_release_value("reasoning.parent", "routine"),
368
+ parent_complex_reasoning=_release_value("reasoning.parent", "complex"),
369
+ parent_critical_reasoning=_release_value("reasoning.parent", "critical"),
370
+ worker_capability_policy=_release_value("models", "worker_policy"),
371
+ worker_model="auto",
372
+ worker_resolved_model=worker_model,
373
+ worker_min_reasoning=_release_value("reasoning.worker", "minimum"),
374
+ worker_routine_reasoning=_release_value("reasoning.worker", "routine"),
375
+ worker_complex_reasoning=_release_value("reasoning.worker", "complex"),
376
+ worker_critical_reasoning=_release_value("reasoning.worker", "critical"),
377
+ reasoning_rollout=_release_reasoning_rollout(),
378
+ )
379
+ snapshot.validate()
380
+ return snapshot
381
+
382
+
383
+ def discover_repo_policy(start: Path | None = None) -> Path | None:
384
+ current = (start or Path.cwd()).resolve()
385
+ if current.is_file():
386
+ current = current.parent
387
+ while True:
388
+ candidate = current / REPO_POLICY_NAME
389
+ if candidate.is_file() and candidate.resolve() != DEFAULT_POLICY.resolve():
390
+ return candidate
391
+ if (current / ".git").exists() or current.parent == current:
392
+ return None
393
+ current = current.parent
394
+
395
+
396
+ def _user_capability(path: Path) -> PolicySnapshot:
397
+ release = _release_capability()
398
+ parent_floor = policy_value(path, "parent", "min_reasoning_effort", release.parent_min_reasoning)
399
+ worker_floor = policy_value(path, "worker", "min_reasoning_effort", release.worker_min_reasoning)
400
+ rollout = ReasoningRolloutPolicy(
401
+ mode=policy_value(path, "reasoning.rollout", "mode", release.reasoning_rollout.mode),
402
+ minimum=policy_value(path, "reasoning.rollout", "minimum", release.reasoning_rollout.minimum),
403
+ routine=policy_value(path, "reasoning.rollout", "routine", release.reasoning_rollout.routine),
404
+ complex=policy_value(path, "reasoning.rollout", "complex", release.reasoning_rollout.complex),
405
+ critical=policy_value(path, "reasoning.rollout", "critical", release.reasoning_rollout.critical),
406
+ )
407
+ rollout.validate()
408
+ snapshot = PolicySnapshot(
409
+ parent_capability_policy=policy_value(path, "parent", "model_policy", release.parent_capability_policy),
410
+ parent_model_floor=policy_value(path, "parent", "min_model", release.parent_model_floor),
411
+ parent_min_reasoning=parent_floor,
412
+ parent_routine_reasoning=policy_value(path, "parent", "routine_effort", release.parent_routine_reasoning),
413
+ parent_complex_reasoning=policy_value(path, "parent", "complex_effort", release.parent_complex_reasoning),
414
+ parent_critical_reasoning=policy_value(path, "parent", "critical_effort", release.parent_critical_reasoning),
415
+ worker_capability_policy=policy_value(path, "worker", "model_policy", release.worker_capability_policy),
416
+ worker_model=policy_value(path, "worker", "model", "auto"),
417
+ worker_resolved_model=policy_value(path, "worker", "resolved_model", release.worker_resolved_model),
418
+ worker_min_reasoning=worker_floor,
419
+ worker_routine_reasoning=policy_value(path, "worker", "routine_effort", release.worker_routine_reasoning),
420
+ worker_complex_reasoning=policy_value(path, "worker", "complex_effort", release.worker_complex_reasoning),
421
+ worker_critical_reasoning=policy_value(path, "worker", "critical_effort", release.worker_critical_reasoning),
422
+ reasoning_rollout=rollout,
423
+ )
424
+ snapshot.validate()
425
+ return snapshot
426
+
427
+
428
+ def _raise_capability_floors(base: PolicySnapshot, repo: Path | None) -> PolicySnapshot:
429
+ if repo is None:
430
+ base.validate()
431
+ return base
432
+
433
+ def repo_effort(section: str, key: str, fallback: str) -> str:
434
+ value = policy_value(repo, section, key, "")
435
+ return _effort_max(fallback, value) if value else fallback
436
+
437
+ result = PolicySnapshot(
438
+ parent_capability_policy=base.parent_capability_policy,
439
+ parent_model_floor=base.parent_model_floor,
440
+ parent_min_reasoning=repo_effort("parent", "min_reasoning_effort", base.parent_min_reasoning),
441
+ parent_routine_reasoning=repo_effort("parent", "routine_effort", base.parent_routine_reasoning),
442
+ parent_complex_reasoning=repo_effort("parent", "complex_effort", base.parent_complex_reasoning),
443
+ parent_critical_reasoning=repo_effort("parent", "critical_effort", base.parent_critical_reasoning),
444
+ worker_capability_policy=base.worker_capability_policy,
445
+ worker_model=base.worker_model,
446
+ worker_resolved_model=base.worker_resolved_model,
447
+ worker_min_reasoning=repo_effort("worker", "min_reasoning_effort", base.worker_min_reasoning),
448
+ worker_routine_reasoning=repo_effort("worker", "routine_effort", base.worker_routine_reasoning),
449
+ worker_complex_reasoning=repo_effort("worker", "complex_effort", base.worker_complex_reasoning),
450
+ worker_critical_reasoning=repo_effort("worker", "critical_effort", base.worker_critical_reasoning),
451
+ reasoning_rollout=ReasoningRolloutPolicy(
452
+ mode=base.reasoning_rollout.mode,
453
+ minimum=repo_effort("reasoning.rollout", "minimum", base.reasoning_rollout.minimum),
454
+ routine=repo_effort("reasoning.rollout", "routine", base.reasoning_rollout.routine),
455
+ complex=repo_effort("reasoning.rollout", "complex", base.reasoning_rollout.complex),
456
+ critical=repo_effort("reasoning.rollout", "critical", base.reasoning_rollout.critical),
457
+ ),
458
+ )
459
+ result.validate()
460
+ return result
461
+
462
+
463
+ def resolve_policy(user_policy: Path, repo_policy: Path | None = None) -> ResolvedPolicy:
464
+ repo = repo_policy
465
+ enabled = _policy_bool(user_policy, "strategy", "enabled", _release_bool("strategy", "enabled", True), strict=True)
466
+ strategy = policy_value(user_policy, "strategy", "profile", _release_value("strategy", "profile"))
467
+ routing = policy_value(user_policy, "routing", "mode", _release_value("routing", "mode"))
468
+ review = policy_value(user_policy, "modifiers", "review", _release_value("modifiers", "review"))
469
+ fanout = policy_value(user_policy, "modifiers", "fanout", _release_value("modifiers", "fanout"))
470
+ max_threads = _policy_int(user_policy, "runtime", "max_concurrent_threads", _release_int("runtime", "max_concurrent_threads", 4))
471
+ max_repairs = _policy_int(user_policy, "runtime", "max_repair_cycles", _release_int("runtime", "max_repair_cycles", 2))
472
+ if repo is not None:
473
+ strategy = policy_value(repo, "strategy", "profile", strategy)
474
+ routing = policy_value(repo, "routing", "mode", routing)
475
+ review = policy_value(repo, "modifiers", "review", review)
476
+ fanout = policy_value(repo, "modifiers", "fanout", fanout)
477
+ repo_threads = policy_value(repo, "runtime", "max_concurrent_threads", "")
478
+ repo_repairs = policy_value(repo, "runtime", "max_repair_cycles", "")
479
+ if repo_threads:
480
+ try:
481
+ max_threads = min(max_threads, int(repo_threads))
482
+ except ValueError:
483
+ pass
484
+ if repo_repairs:
485
+ try:
486
+ max_repairs = min(max_repairs, int(repo_repairs))
487
+ except ValueError:
488
+ pass
489
+ resolved = ResolvedPolicy(
490
+ enabled=enabled,
491
+ strategy=strategy,
492
+ routing=routing,
493
+ modifiers=Modifiers(review=review, fanout=fanout),
494
+ capability=_raise_capability_floors(_user_capability(user_policy), repo),
495
+ max_concurrent_threads=max_threads,
496
+ max_repair_cycles=max_repairs,
497
+ repo_policy=str(repo) if repo is not None else None,
498
+ )
499
+ resolved.validate()
500
+ return resolved
501
+
502
+
503
+ def _configured_effort(task: TaskProfile, policy: PolicySnapshot, role: str) -> str:
504
+ if role == "parent":
505
+ floor, routine = policy.parent_min_reasoning, policy.parent_routine_reasoning
506
+ complex_effort, critical = policy.parent_complex_reasoning, policy.parent_critical_reasoning
507
+ else:
508
+ floor, routine = policy.worker_min_reasoning, policy.worker_routine_reasoning
509
+ complex_effort, critical = policy.worker_complex_reasoning, policy.worker_critical_reasoning
510
+ if task.complexity == "critical" or task.risk == "critical":
511
+ target = critical
512
+ elif task.complexity == "complex" or task.risk == "high":
513
+ target = complex_effort
514
+ else:
515
+ target = routine
516
+ return _effort_max(floor, target)
517
+
518
+
519
+ def _rollout_policy_with_override(policy: ReasoningRolloutPolicy, mode: str | None) -> ReasoningRolloutPolicy:
520
+ if mode is None:
521
+ policy.validate()
522
+ return policy
523
+ if mode not in REASONING_ROLLOUT_MODES:
524
+ raise ValueError(f"invalid efficient reasoning rollout mode: {mode}")
525
+ overridden = replace(policy, mode=mode)
526
+ overridden.validate()
527
+ return overridden
528
+
529
+
530
+ def quota_pressure_from_snapshot(snapshot: dict[str, Any] | None) -> str:
531
+ if not isinstance(snapshot, dict):
532
+ return "unknown"
533
+ used_values: list[float] = []
534
+ windows = app_server_quota_windows(snapshot) if app_server_quota_windows else []
535
+ for window in windows:
536
+ raw = window.get("used_percent")
537
+ if isinstance(raw, (int, float)) and not isinstance(raw, bool):
538
+ used_values.append(float(raw))
539
+ elif isinstance(raw, str):
540
+ try:
541
+ used_values.append(float(raw))
542
+ except ValueError:
543
+ continue
544
+ if not used_values:
545
+ return "unknown"
546
+ used = max(used_values)
547
+ if used >= 90:
548
+ return "critical"
549
+ if used >= 75:
550
+ return "high"
551
+ if used >= 50:
552
+ return "medium"
553
+ return "low"
554
+
555
+
556
+ def detect_quota_pressure() -> str:
557
+ override = os.environ.get("CODEX_FLOW_QUOTA_PRESSURE")
558
+ if override in {"unknown", "low", "medium", "high", "critical"}:
559
+ return override
560
+ try:
561
+ from telemetry_core.app_server import AppServer
562
+ with AppServer() as app:
563
+ if not app.available:
564
+ return "unknown"
565
+ return quota_pressure_from_snapshot(app.rate_limits())
566
+ except Exception:
567
+ return "unknown"
568
+
569
+
570
+ def _desired_role_capability(task: TaskProfile, spec, policy: PolicySnapshot, role: str) -> tuple[str, str | None]:
571
+ target = spec.capability(task, role)
572
+ if target == "parent":
573
+ model = None if policy.parent_model_floor == "auto" else policy.parent_model_floor
574
+ return policy.parent_capability_policy, model
575
+ if target == "worker":
576
+ return policy.worker_capability_policy, policy.worker_resolved_model or policy.worker_model
577
+ raise ValueError(f"invalid capability target for {role}: {target}")
578
+
579
+
580
+ def _exploration_demand(task: TaskProfile, bonus: int = 0) -> int:
581
+ if task.parallelism == "none":
582
+ return 0
583
+ if bonus < 0:
584
+ raise ValueError("exploration bonus cannot be negative")
585
+ score = 0
586
+ score += {"low": 0, "medium": 1, "high": 2}[task.uncertainty]
587
+ score += {"low": 0, "medium": 1, "high": 2}[task.exploration_need]
588
+ score += {"small": 0, "routine": 0, "complex": 1, "critical": 2}[task.complexity]
589
+ score += {"local": 0, "module": 0, "cross-module": 1, "repo-wide": 2}[task.scope]
590
+ if task.verification_cost == "high":
591
+ score += 1
592
+ score += bonus
593
+ if score <= 1:
594
+ return 0
595
+ return min(4, max(1, (score + 1) // 2))
596
+
597
+
598
+ def _reviewer_demand(task: TaskProfile, review_mode: str, bonus: int = 0) -> int:
599
+ if review_mode != "independent+parent":
600
+ return 0
601
+ if bonus < 0:
602
+ raise ValueError("reviewer bonus cannot be negative")
603
+ demand = 1 + bonus
604
+ if task.complexity == "critical" or task.risk == "critical" or task.verification_cost == "high":
605
+ demand += 1
606
+ return demand
607
+
608
+
609
+ def _fit_total_worker_budget(exploration_workers: int, implementation_workers: int, reviewer_workers: int, budget: WorkerBudget) -> tuple[int, int, int]:
610
+ while exploration_workers + implementation_workers + reviewer_workers > budget.max_total_workers:
611
+ if exploration_workers > 0:
612
+ exploration_workers -= 1
613
+ elif implementation_workers > 1:
614
+ implementation_workers -= 1
615
+ elif reviewer_workers > 1:
616
+ reviewer_workers -= 1
617
+ else:
618
+ break
619
+ return exploration_workers, implementation_workers, reviewer_workers
620
+
621
+
622
+ def _bounded_stage_policy(spec, task: TaskProfile, stage: str, worker_count: int, modifiers: Modifiers) -> StagePolicy | None:
623
+ if worker_count <= 0:
624
+ return None
625
+ policy = spec.lifecycle(task, stage)
626
+ policy.validate()
627
+ hard_timeout = min(policy.hard_timeout_seconds, RUNTIME_MAX_WORKER_WALL_SECONDS)
628
+ idle_timeout = min(policy.idle_timeout_seconds, RUNTIME_MAX_WORKER_IDLE_SECONDS, hard_timeout)
629
+ minimum = 0 if policy.join_policy == "opportunistic" else max(1, min(worker_count, policy.min_successful_workers))
630
+ bounded = replace(policy, min_successful_workers=minimum, idle_timeout_seconds=idle_timeout, hard_timeout_seconds=hard_timeout)
631
+ if stage == "review" and modifiers.review == "strict":
632
+ bounded = replace(
633
+ bounded,
634
+ join_policy="required",
635
+ min_successful_workers=worker_count,
636
+ cancel_if_superseded=False,
637
+ cancel_stragglers_after_quorum=False,
638
+ fallback_policy="retry_review",
639
+ )
640
+ bounded.validate()
641
+ return bounded
642
+
643
+
644
+ def _canonical_task_budget(
645
+ raw: TaskBudgetPolicy,
646
+ *,
647
+ implementation_workers: int,
648
+ reviewer_workers: int,
649
+ implementation_stage: StagePolicy | None,
650
+ review_stage: StagePolicy | None,
651
+ ) -> TaskBudgetPolicy:
652
+ raw.validate()
653
+ if implementation_stage is None:
654
+ raise ValueError("delegated task budget requires implementation_stage")
655
+ if implementation_stage.maximum_work_units is None:
656
+ raise ValueError("delegated task budget requires maximum_work_units")
657
+ if raw.max_work_units != implementation_stage.maximum_work_units:
658
+ raise ValueError("task budget max_work_units must match implementation_stage maximum_work_units")
659
+ if implementation_workers > raw.max_work_units:
660
+ raise ValueError("implementation topology exceeds task budget max_work_units")
661
+ if implementation_workers > raw.max_implementation_attempts:
662
+ raise ValueError("implementation topology exceeds task budget max_implementation_attempts")
663
+ if implementation_stage.soft_timeout_seconds is not None and raw.soft_timeout_seconds < implementation_stage.soft_timeout_seconds:
664
+ raise ValueError("task soft timeout cannot precede implementation soft checkpoint budget")
665
+
666
+ if reviewer_workers <= 0:
667
+ if review_stage is not None:
668
+ raise ValueError("review_stage must be absent when reviewer_workers is zero")
669
+ return replace(raw, max_review_attempts=0)
670
+
671
+ if review_stage is None:
672
+ raise ValueError("review_stage is required when reviewer_workers is positive")
673
+ if raw.max_review_attempts < reviewer_workers:
674
+ raise ValueError("review topology exceeds task budget max_review_attempts")
675
+ completion_tail = review_stage.hard_timeout_seconds + raw.parent_finalization_seconds
676
+ canonical_hard = max(raw.hard_timeout_seconds, raw.soft_timeout_seconds + completion_tail)
677
+ return replace(raw, hard_timeout_seconds=canonical_hard)
678
+
679
+
680
+ def compile_plan(
681
+ task: TaskProfile,
682
+ *,
683
+ strategy: str = "efficient",
684
+ routing_mode: str = "adaptive",
685
+ modifiers: Modifiers | None = None,
686
+ policy: PolicySnapshot | None = None,
687
+ runtime: RuntimeState | None = None,
688
+ repo_policy: str | None = None,
689
+ efficient_reasoning: str | None = None,
690
+ ) -> ExecutionPlan:
691
+ task.validate()
692
+ modifiers = modifiers or Modifiers()
693
+ modifiers.validate()
694
+ policy = policy or _release_capability()
695
+ policy.validate()
696
+ rollout_policy = _rollout_policy_with_override(policy.reasoning_rollout, efficient_reasoning)
697
+ runtime = runtime or RuntimeState(
698
+ max_concurrent_threads=_release_int("runtime", "max_concurrent_threads", 4),
699
+ max_repair_cycles=_release_int("runtime", "max_repair_cycles", 2),
700
+ )
701
+ runtime.validate()
702
+ spec = get_strategy(strategy)
703
+ budget = spec.worker_budget(task)
704
+ budget.validate()
705
+ if routing_mode not in ROUTING_MODES:
706
+ raise ValueError(f"invalid routing mode: {routing_mode}")
707
+
708
+ route = spec.adaptive_route(task) if routing_mode == "adaptive" else routing_mode
709
+ delegated = route == "delegate"
710
+ parent_effort = _effort_max(_configured_effort(task, policy, "parent"), spec.effort(task, "parent"))
711
+ legacy_role_efforts = {
712
+ role: _effort_max(_configured_effort(task, policy, "worker"), spec.effort(task, role), _effort_next(parent_effort))
713
+ for role in ("explorer", "implementer", "reviewer")
714
+ }
715
+ role_efforts = dict(legacy_role_efforts)
716
+ reasoning_rollout: ReasoningRolloutDecision | None = None
717
+
718
+ exploration_workers = 0
719
+ implementation_workers = 0
720
+ reviewer_workers = 0
721
+ review_mode = "parent"
722
+ concurrency = 1
723
+ notes: list[str] = list(spec.notes(task))
724
+
725
+ if parent_effort == "max" and delegated:
726
+ notes.append("parent reasoning is already max; worker-role reasoning cannot exceed max and is held at max")
727
+
728
+ if delegated:
729
+ implementation_workers = 1
730
+ exploration_workers = min(_exploration_demand(task, spec.exploration_bonus(task)), budget.max_explorers, runtime.max_concurrent_threads)
731
+ proven_writable = min(task.writable_workstreams, runtime.max_concurrent_threads, budget.max_implementers)
732
+ allow_parallel_write = task.parallelism == "high" and task.write_conflict == "low" and proven_writable >= 2
733
+ if allow_parallel_write and (spec.allow_parallel_write or modifiers.fanout == "aggressive"):
734
+ implementation_workers = proven_writable
735
+ notes.append(f"parallel writable execution authorized across {implementation_workers} proven isolated workstreams")
736
+ if modifiers.fanout == "conservative":
737
+ exploration_workers = min(exploration_workers, 1)
738
+ implementation_workers = 1
739
+ elif modifiers.fanout == "aggressive" and task.parallelism == "high":
740
+ exploration_workers = min(budget.max_explorers, runtime.max_concurrent_threads, max(exploration_workers, 2))
741
+
742
+ if modifiers.review == "strict":
743
+ review_mode = "independent+parent"
744
+ elif modifiers.review == "standard":
745
+ review_mode = "parent"
746
+ elif spec.independent_review(task):
747
+ review_mode = "independent+parent"
748
+ reviewer_workers = min(_reviewer_demand(task, review_mode, spec.reviewer_bonus(task)), budget.max_reviewers, runtime.max_concurrent_threads)
749
+
750
+ if task.parallelism == "none":
751
+ exploration_workers = 0
752
+ implementation_workers = 1
753
+ reviewer_workers = min(reviewer_workers, 1)
754
+ exploration_workers, implementation_workers, reviewer_workers = _fit_total_worker_budget(
755
+ exploration_workers, implementation_workers, reviewer_workers, budget
756
+ )
757
+ concurrency = min(runtime.max_concurrent_threads, max(1, exploration_workers, implementation_workers, reviewer_workers))
758
+
759
+ if runtime.quota_pressure in {"high", "critical"}:
760
+ notes.append("quota pressure is high; speculative fan-out and repair budget are constrained before quality floors")
761
+ if spec.quota_sensitive:
762
+ exploration_workers = min(exploration_workers, 1)
763
+ implementation_workers = min(implementation_workers, 1)
764
+ reviewer_workers = min(reviewer_workers, 1)
765
+ concurrency = min(runtime.max_concurrent_threads, max(1, exploration_workers, implementation_workers, reviewer_workers))
766
+
767
+ repair_cycles = runtime.max_repair_cycles
768
+ if runtime.quota_pressure == "critical" and spec.quota_sensitive:
769
+ repair_cycles = min(repair_cycles, 1)
770
+
771
+ if delegated and spec.reasoning_rollout is not None:
772
+ decisions = {
773
+ role: spec.reasoning_rollout(task, role, rollout_policy, parent_effort, legacy_role_efforts[role])
774
+ for role in ("explorer", "implementer", "reviewer")
775
+ }
776
+ selected = {decision.selected_worker_reasoning for decision in decisions.values()}
777
+ if len(selected) != 1:
778
+ raise ValueError("strategy reasoning rollout must select one Worker effort across roles")
779
+ reasoning_rollout = next(iter(decisions.values()))
780
+ reasoning_rollout.validate()
781
+ role_efforts = {role: decisions[role].selected_worker_reasoning for role in decisions}
782
+ if reasoning_rollout.mode == "shadow":
783
+ notes.append("efficient reasoning rollout is shadow; proposed Worker effort is reported, legacy effort is selected")
784
+ elif reasoning_rollout.mode == "adaptive":
785
+ notes.append("efficient reasoning rollout is adaptive; proposed Worker effort is selected and may equal Parent")
786
+ else:
787
+ notes.append("efficient reasoning rollout is legacy; current Worker effort is selected")
788
+
789
+ if route == "direct":
790
+ exploration_workers = implementation_workers = reviewer_workers = 0
791
+ concurrency = 1
792
+ explorer_capability_policy = explorer_model = explorer_reasoning = None
793
+ implementer_capability_policy = implementer_model = implementer_reasoning = None
794
+ reviewer_capability_policy = reviewer_model = reviewer_reasoning = None
795
+ review_mode = "parent"
796
+ else:
797
+ if exploration_workers > 0:
798
+ explorer_capability_policy, explorer_model = _desired_role_capability(task, spec, policy, "explorer")
799
+ explorer_reasoning = role_efforts["explorer"]
800
+ if explorer_capability_policy != policy.worker_capability_policy:
801
+ notes.append("explorer role requests parent-class capability; use runtime override when supported")
802
+ else:
803
+ explorer_capability_policy = explorer_model = explorer_reasoning = None
804
+ implementer_capability_policy, implementer_model = _desired_role_capability(task, spec, policy, "implementer")
805
+ implementer_reasoning = role_efforts["implementer"]
806
+ if implementer_capability_policy != policy.worker_capability_policy:
807
+ notes.append("implementer role requests parent-class capability; use runtime override when supported")
808
+ if reviewer_workers > 0:
809
+ reviewer_capability_policy, reviewer_model = _desired_role_capability(task, spec, policy, "reviewer")
810
+ reviewer_reasoning = role_efforts["reviewer"]
811
+ if reviewer_capability_policy != policy.worker_capability_policy:
812
+ notes.append("reviewer role requests parent-class capability; use runtime override when supported")
813
+ else:
814
+ reviewer_capability_policy = reviewer_model = reviewer_reasoning = None
815
+
816
+ exploration_stage = _bounded_stage_policy(spec, task, "exploration", exploration_workers, modifiers)
817
+ implementation_stage = _bounded_stage_policy(spec, task, "implementation", implementation_workers, modifiers)
818
+ review_stage = _bounded_stage_policy(spec, task, "review", reviewer_workers, modifiers)
819
+
820
+ task_budget: TaskBudgetPolicy | None = None
821
+ if delegated and spec.task_budget is not None:
822
+ raw_budget = spec.task_budget(task)
823
+ if raw_budget is not None:
824
+ if not isinstance(raw_budget, TaskBudgetPolicy):
825
+ raise ValueError("strategy task_budget hook must return TaskBudgetPolicy or None")
826
+ task_budget = _canonical_task_budget(
827
+ raw_budget,
828
+ implementation_workers=implementation_workers,
829
+ reviewer_workers=reviewer_workers,
830
+ implementation_stage=implementation_stage,
831
+ review_stage=review_stage,
832
+ )
833
+ task_budget.validate()
834
+ if task_budget.hard_timeout_seconds != raw_budget.hard_timeout_seconds:
835
+ notes.append(
836
+ f"required completion extends task hard deadline to {task_budget.hard_timeout_seconds}s "
837
+ f"without shortening the {task_budget.soft_timeout_seconds}s general-work window"
838
+ )
839
+
840
+ planned_worker_count = exploration_workers + implementation_workers + reviewer_workers
841
+ return ExecutionPlan(
842
+ schema_version=11,
843
+ strategy=strategy,
844
+ routing=route,
845
+ review_modifier=modifiers.review,
846
+ fanout_modifier=modifiers.fanout,
847
+ quality_intent=task.quality_intent,
848
+ parent_capability_policy=policy.parent_capability_policy,
849
+ parent_model_floor=policy.parent_model_floor,
850
+ parent_reasoning=parent_effort,
851
+ reasoning_rollout=reasoning_rollout,
852
+ explorer_capability_policy=explorer_capability_policy,
853
+ explorer_model=explorer_model,
854
+ explorer_reasoning=explorer_reasoning,
855
+ implementer_capability_policy=implementer_capability_policy,
856
+ implementer_model=implementer_model,
857
+ implementer_reasoning=implementer_reasoning,
858
+ reviewer_capability_policy=reviewer_capability_policy,
859
+ reviewer_model=reviewer_model,
860
+ reviewer_reasoning=reviewer_reasoning,
861
+ worker_budget=budget,
862
+ task_budget=task_budget,
863
+ exploration_workers=exploration_workers,
864
+ implementation_workers=implementation_workers,
865
+ reviewer_workers=reviewer_workers,
866
+ planned_worker_count=planned_worker_count,
867
+ exploration_stage=exploration_stage,
868
+ implementation_stage=implementation_stage,
869
+ review_stage=review_stage,
870
+ review_mode=review_mode,
871
+ max_repair_cycles=repair_cycles,
872
+ max_concurrent_threads=concurrency,
873
+ escalate_on_failure=True,
874
+ quota_pressure=runtime.quota_pressure,
875
+ repo_policy=repo_policy,
876
+ notes=tuple(notes),
877
+ )
878
+
879
+
880
+ def _temporary_bypass_path() -> Path:
881
+ return CODEX_HOME / "codex-flow" / TEMPORARY_BYPASS_NAME
882
+
883
+
884
+ def arm_temporary_bypass() -> None:
885
+ path = _temporary_bypass_path()
886
+ path.parent.mkdir(parents=True, exist_ok=True)
887
+ temporary = path.with_name(f".{path.name}.{os.getpid()}.tmp")
888
+ try:
889
+ temporary.write_text("1\n", encoding="utf-8")
890
+ os.replace(temporary, path)
891
+ finally:
892
+ try:
893
+ temporary.unlink()
894
+ except FileNotFoundError:
895
+ pass
896
+
897
+
898
+ def temporary_bypass_pending() -> bool:
899
+ return _temporary_bypass_path().exists()
900
+
901
+
902
+ def consume_temporary_bypass() -> bool:
903
+ path = _temporary_bypass_path()
904
+ claim = path.with_name(f".{path.name}.{os.getpid()}.claim")
905
+ try:
906
+ os.replace(path, claim)
907
+ except FileNotFoundError:
908
+ return False
909
+ try:
910
+ return True
911
+ finally:
912
+ try:
913
+ claim.unlink()
914
+ except FileNotFoundError:
915
+ pass
916
+
917
+
918
+ def configured_strategy_enabled(path: Path) -> bool:
919
+ return _policy_bool(path, "strategy", "enabled", _release_bool("strategy", "enabled", True), strict=True)
920
+
921
+
922
+ def configured_strategy(path: Path) -> str:
923
+ return policy_value(path, "strategy", "profile", _release_value("strategy", "profile"))
924
+
925
+
926
+ def configured_routing(path: Path) -> str:
927
+ return policy_value(path, "routing", "mode", _release_value("routing", "mode"))
928
+
929
+
930
+ def _print_profiles() -> None:
931
+ for spec in all_strategy_specs():
932
+ print(f"{spec.name:10} {spec.description}")
933
+
934
+
935
+ def _task_from_args(ns: argparse.Namespace) -> TaskProfile:
936
+ return TaskProfile(
937
+ complexity=ns.complexity,
938
+ uncertainty=ns.uncertainty,
939
+ risk=ns.risk,
940
+ scope=ns.scope,
941
+ parallelism=ns.parallelism,
942
+ write_conflict=ns.write_conflict,
943
+ exploration_need=ns.exploration_need,
944
+ verification_cost=ns.verification_cost,
945
+ iteration_intensity=ns.iteration_intensity,
946
+ writable_workstreams=ns.writable_workstreams,
947
+ quality_intent=ns.quality_intent,
948
+ )
949
+
950
+
951
+ def _repo_arg(value: str | None) -> tuple[Path | None, bool]:
952
+ if not value or value == "auto":
953
+ return discover_repo_policy(), False
954
+ if value == "none":
955
+ return None, True
956
+ return Path(value), False
957
+
958
+
959
+ def build_parser() -> argparse.ArgumentParser:
960
+ parser = argparse.ArgumentParser(prog="codex-flow strategy")
961
+ parser.add_argument("--policy", type=Path, default=DEFAULT_POLICY)
962
+ sub = parser.add_subparsers(dest="command")
963
+ sub.add_parser("profiles")
964
+ show = sub.add_parser("show")
965
+ show.add_argument("--json", action="store_true")
966
+ show.add_argument("--effective", action="store_true")
967
+ show.add_argument("--repo-policy", default="auto")
968
+ sub.add_parser("enabled")
969
+ sub.add_parser("enable")
970
+ sub.add_parser("disable")
971
+ sub.add_parser("bypass-once")
972
+ sub.add_parser("bypass-pending")
973
+ sub.add_parser("consume-bypass")
974
+ set_cmd = sub.add_parser("set")
975
+ set_cmd.add_argument("profile", choices=STRATEGIES)
976
+ routing = sub.add_parser("routing")
977
+ routing.add_argument("mode", nargs="?", choices=ROUTING_MODES)
978
+ plan = sub.add_parser("plan")
979
+ plan.add_argument("--profile", choices=STRATEGIES)
980
+ plan.add_argument("--routing", choices=ROUTING_MODES)
981
+ plan.add_argument("--review", choices=REVIEW_MODES)
982
+ plan.add_argument("--fanout", choices=FANOUT_MODES)
983
+ plan.add_argument("--repo-policy", default="auto")
984
+ plan.add_argument("--complexity", choices=COMPLEXITIES, default="routine")
985
+ plan.add_argument("--uncertainty", choices=LEVELS[:3], default="medium")
986
+ plan.add_argument("--risk", choices=LEVELS, default="medium")
987
+ plan.add_argument("--scope", choices=SCOPES, default="module")
988
+ plan.add_argument("--parallelism", choices=PARALLELISM, default="limited")
989
+ plan.add_argument("--write-conflict", choices=("low", "high"), default="low")
990
+ plan.add_argument("--exploration-need", choices=LEVELS[:3], default="medium")
991
+ plan.add_argument("--verification-cost", choices=LEVELS[:3], default="medium")
992
+ plan.add_argument("--iteration-intensity", choices=ITERATION, default="iterative")
993
+ plan.add_argument("--writable-workstreams", type=int, default=1)
994
+ plan.add_argument("--quality-intent", choices=QUALITY_INTENTS, default="normal")
995
+ plan.add_argument("--efficient-reasoning", choices=REASONING_ROLLOUT_MODES)
996
+ plan.add_argument("--quota-pressure", choices=("auto", "unknown", "low", "medium", "high", "critical"), default="auto")
997
+ plan.add_argument("--max-threads", type=int)
998
+ plan.add_argument("--max-repairs", type=int)
999
+ return parser
1000
+
1001
+
1002
+ def main(argv: Iterable[str] | None = None) -> int:
1003
+ parser = build_parser()
1004
+ ns = parser.parse_args(list(argv) if argv is not None else None)
1005
+ command = ns.command or "show"
1006
+ if command == "profiles":
1007
+ _print_profiles(); return 0
1008
+ if command == "show":
1009
+ if getattr(ns, "effective", False):
1010
+ repo, _disabled = _repo_arg(getattr(ns, "repo_policy", "auto"))
1011
+ try:
1012
+ resolved = resolve_policy(ns.policy, repo)
1013
+ except ValueError as exc:
1014
+ if getattr(ns, "json", False):
1015
+ print(json.dumps({"enabled": False, "valid": False, "error": str(exc)}, ensure_ascii=False, indent=2))
1016
+ else:
1017
+ print(str(exc), file=sys.stderr)
1018
+ return 2
1019
+ result = {"enabled": resolved.enabled, "strategy": resolved.strategy, "routing": resolved.routing, "review": resolved.modifiers.review, "fanout": resolved.modifiers.fanout, "repo_policy": resolved.repo_policy, "valid": True}
1020
+ print(json.dumps(result, ensure_ascii=False, indent=2) if getattr(ns, "json", False) else f"strategy={resolved.strategy} routing={resolved.routing} review={resolved.modifiers.review} fanout={resolved.modifiers.fanout}")
1021
+ return 0
1022
+ strategy = configured_strategy(ns.policy)
1023
+ routing_mode = configured_routing(ns.policy)
1024
+ try:
1025
+ enabled = configured_strategy_enabled(ns.policy); enabled_error = None
1026
+ except ValueError as exc:
1027
+ enabled = False; enabled_error = str(exc)
1028
+ valid = enabled_error is None and strategy in STRATEGIES and routing_mode in ROUTING_MODES
1029
+ if getattr(ns, "json", False):
1030
+ result = {"enabled": enabled, "strategy": strategy, "routing": routing_mode, "valid": valid}
1031
+ if enabled_error is not None: result["error"] = enabled_error
1032
+ print(json.dumps(result, ensure_ascii=False, indent=2))
1033
+ elif enabled_error is not None:
1034
+ print(enabled_error, file=sys.stderr)
1035
+ else:
1036
+ print(f"enabled={'true' if enabled else 'false'} strategy={strategy} routing={routing_mode}")
1037
+ return 0 if valid else 2
1038
+ if command == "enabled":
1039
+ try: enabled = configured_strategy_enabled(ns.policy)
1040
+ except ValueError as exc:
1041
+ print(str(exc), file=sys.stderr); return 2
1042
+ print("true" if enabled else "false"); return 0
1043
+ if command == "bypass-once":
1044
+ try: enabled = configured_strategy_enabled(ns.policy)
1045
+ except ValueError as exc:
1046
+ print(str(exc), file=sys.stderr); return 2
1047
+ if not enabled:
1048
+ print("strategy dispatch is globally disabled; temporary bypass is unnecessary", file=sys.stderr); return 3
1049
+ arm_temporary_bypass(); print("armed=true"); return 0
1050
+ if command == "bypass-pending":
1051
+ print("true" if temporary_bypass_pending() else "false"); return 0
1052
+ if command == "consume-bypass":
1053
+ print("true" if consume_temporary_bypass() else "false"); return 0
1054
+ if command in {"enable", "disable"}:
1055
+ enabled = command == "enable"
1056
+ set_policy_value(ns.policy, "strategy", "enabled", "true" if enabled else "false", quote=False)
1057
+ print(f"enabled={'true' if enabled else 'false'}"); return 0
1058
+ if command == "set":
1059
+ set_policy_value(ns.policy, "strategy", "profile", ns.profile); print(f"strategy={ns.profile}"); return 0
1060
+ if command == "routing":
1061
+ if ns.mode is None:
1062
+ print(configured_routing(ns.policy)); return 0
1063
+ set_policy_value(ns.policy, "routing", "mode", ns.mode); print(f"routing={ns.mode}"); return 0
1064
+ if command == "plan":
1065
+ repo, _disabled = _repo_arg(ns.repo_policy)
1066
+ try: resolved = resolve_policy(ns.policy, repo)
1067
+ except ValueError as exc:
1068
+ print(str(exc), file=sys.stderr); return 3
1069
+ if not resolved.enabled:
1070
+ print("codex-flow strategy dispatch is disabled; run `codex-flow strategy enable` to re-enable it.", file=sys.stderr); return 3
1071
+ modifiers = Modifiers(review=ns.review or resolved.modifiers.review, fanout=ns.fanout or resolved.modifiers.fanout)
1072
+ max_threads = resolved.max_concurrent_threads if ns.max_threads is None else min(resolved.max_concurrent_threads, ns.max_threads)
1073
+ max_repairs = resolved.max_repair_cycles if ns.max_repairs is None else min(resolved.max_repair_cycles, ns.max_repairs)
1074
+ quota_pressure = detect_quota_pressure() if ns.quota_pressure == "auto" else ns.quota_pressure
1075
+ plan_obj = compile_plan(
1076
+ _task_from_args(ns),
1077
+ strategy=ns.profile or resolved.strategy,
1078
+ routing_mode=ns.routing or resolved.routing,
1079
+ modifiers=modifiers,
1080
+ policy=resolved.capability,
1081
+ runtime=RuntimeState(quota_pressure=quota_pressure, max_concurrent_threads=max_threads, max_repair_cycles=max_repairs),
1082
+ repo_policy=resolved.repo_policy,
1083
+ efficient_reasoning=ns.efficient_reasoning,
1084
+ )
1085
+ print(json.dumps(plan_obj.to_dict(), ensure_ascii=False, indent=2)); return 0
1086
+ parser.error(f"unsupported command: {command}")
1087
+ return 2
1088
+
1089
+
1090
+ if __name__ == "__main__":
1091
+ raise SystemExit(main())