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,421 @@
1
+ #!/usr/bin/env python3
2
+ """Deterministic validator for implementation work-unit manifests."""
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import hashlib
7
+ import json
8
+ import posixpath
9
+ import re
10
+ from collections import defaultdict
11
+ from dataclasses import dataclass
12
+ from typing import Any, Iterable
13
+
14
+ WORK_UNIT_MODES = {"single", "bounded"}
15
+ POLICY_FIELDS = {
16
+ "work_unit_mode",
17
+ "minimum_work_units",
18
+ "join_between_work_units",
19
+ "maximum_work_units",
20
+ "require_write_paths",
21
+ }
22
+
23
+
24
+ def _strict_string(value: Any, label: str) -> str:
25
+ if type(value) is not str:
26
+ raise ValueError(f"{label} must be a string")
27
+ result = value.strip()
28
+ if not result:
29
+ raise ValueError(f"{label} must be a non-empty string")
30
+ return result
31
+
32
+
33
+ def _strict_int(value: Any, label: str, *, minimum: int | None = None) -> int:
34
+ if type(value) is not int:
35
+ raise ValueError(f"{label} must be an integer")
36
+ if minimum is not None and value < minimum:
37
+ raise ValueError(f"{label} must be >= {minimum}")
38
+ return value
39
+
40
+
41
+ def _normalize_repo_path(value: Any, label: str) -> str:
42
+ path = _strict_string(value, label)
43
+ if "\x00" in path:
44
+ raise ValueError(f"{label} must not contain NUL")
45
+ if "\\" in path:
46
+ raise ValueError(f"{label} must use POSIX separators and must not contain backslashes")
47
+ if path.startswith("/"):
48
+ raise ValueError(f"{label} must be repo-relative, not absolute")
49
+ if re.match(r"^[A-Za-z]:", path):
50
+ raise ValueError(f"{label} must not use a Windows drive path")
51
+ if any(char in path for char in "*?[]{}"):
52
+ raise ValueError(f"{label} must not contain glob metacharacters")
53
+ components = path.split("/")
54
+ if any(component in {"", ".", ".."} for component in components):
55
+ raise ValueError(f"{label} must be a normalized path without empty, '.' or '..' components")
56
+ normalized = posixpath.normpath(path)
57
+ if normalized in {"", ".", ".."} or normalized != path or normalized.startswith("../"):
58
+ raise ValueError(f"{label} must be a normalized repo-relative POSIX path")
59
+ return normalized
60
+
61
+
62
+ @dataclass(frozen=True)
63
+ class WorkUnit:
64
+ unit_id: str
65
+ scope_id: str
66
+ acceptance_delta: str
67
+ write_scope_id: str
68
+ validation: tuple[str, ...]
69
+ depends_on: tuple[str, ...]
70
+ parallel_group: str | None = None
71
+ generation: int | None = None
72
+ write_paths: tuple[str, ...] = ()
73
+
74
+ @classmethod
75
+ def from_dict(cls, value: dict[str, Any]) -> "WorkUnit":
76
+ if type(value) is not dict:
77
+ raise ValueError("work unit must be an object")
78
+ required = ("unit_id", "scope_id", "acceptance_delta", "write_scope_id", "validation")
79
+ missing = [key for key in required if key not in value]
80
+ if missing:
81
+ raise ValueError(f"work unit missing fields: {missing}")
82
+ allowed = set(required) | {"depends_on", "parallel_group", "generation", "write_paths"}
83
+ unknown = sorted(set(value).difference(allowed))
84
+ if unknown:
85
+ raise ValueError(f"work unit has unknown fields: {unknown}")
86
+
87
+ unit_id = _strict_string(value["unit_id"], "unit_id")
88
+ scope_id = _strict_string(value["scope_id"], f"work unit {unit_id}: scope_id")
89
+ acceptance_delta = _strict_string(value["acceptance_delta"], f"work unit {unit_id}: acceptance_delta")
90
+ write_scope_id = _strict_string(value["write_scope_id"], f"work unit {unit_id}: write_scope_id")
91
+
92
+ raw_validation = value["validation"]
93
+ if type(raw_validation) is not list or not raw_validation:
94
+ raise ValueError(f"work unit {unit_id}: validation must be a non-empty list")
95
+ validation = tuple(_strict_string(item, f"work unit {unit_id}: validation entry") for item in raw_validation)
96
+
97
+ raw_dependencies = value.get("depends_on", [])
98
+ if type(raw_dependencies) is not list:
99
+ raise ValueError(f"work unit {unit_id}: depends_on must be a list")
100
+ depends_on = tuple(_strict_string(item, f"work unit {unit_id}: depends_on entry") for item in raw_dependencies)
101
+ if len(set(depends_on)) != len(depends_on):
102
+ raise ValueError(f"work unit {unit_id}: depends_on entries must be unique")
103
+ if unit_id in depends_on:
104
+ raise ValueError(f"work unit {unit_id}: unit cannot depend on itself")
105
+
106
+ parallel_group = None
107
+ if value.get("parallel_group") is not None:
108
+ parallel_group = _strict_string(value["parallel_group"], f"work unit {unit_id}: parallel_group")
109
+
110
+ generation = None
111
+ if "generation" in value:
112
+ generation = _strict_int(value["generation"], f"work unit {unit_id}: generation", minimum=0)
113
+
114
+ write_paths: tuple[str, ...] = ()
115
+ if "write_paths" in value:
116
+ raw_write_paths = value["write_paths"]
117
+ if type(raw_write_paths) is not list or not raw_write_paths:
118
+ raise ValueError(f"work unit {unit_id}: write_paths must be a non-empty list")
119
+ write_paths = tuple(_normalize_repo_path(item, f"work unit {unit_id}: write_paths entry") for item in raw_write_paths)
120
+ if len(set(write_paths)) != len(write_paths):
121
+ raise ValueError(f"work unit {unit_id}: write_paths entries must be unique")
122
+
123
+ return cls(
124
+ unit_id=unit_id,
125
+ scope_id=scope_id,
126
+ acceptance_delta=acceptance_delta,
127
+ write_scope_id=write_scope_id,
128
+ validation=validation,
129
+ depends_on=depends_on,
130
+ parallel_group=parallel_group,
131
+ generation=generation,
132
+ write_paths=write_paths,
133
+ )
134
+
135
+
136
+ def _load_json(raw: str, label: str) -> Any:
137
+ try:
138
+ return json.loads(raw)
139
+ except json.JSONDecodeError as exc:
140
+ raise ValueError(f"invalid {label} JSON: {exc}") from exc
141
+
142
+
143
+ def _positive_int(value: Any, label: str) -> int:
144
+ if type(value) is not int:
145
+ raise ValueError(f"{label} must be an integer")
146
+ if value < 1:
147
+ raise ValueError(f"{label} must be positive")
148
+ return value
149
+
150
+
151
+ def _policy_contract(value: dict[str, Any]) -> tuple[str, int, bool, int, bool]:
152
+ if type(value) is not dict:
153
+ raise ValueError("policy must be an object")
154
+ missing = sorted(POLICY_FIELDS.difference(value))
155
+ if missing:
156
+ raise ValueError(f"implementation work-unit policy missing fields: {missing}")
157
+
158
+ mode = value["work_unit_mode"]
159
+ if type(mode) is not str or mode not in WORK_UNIT_MODES:
160
+ raise ValueError(f"invalid work_unit_mode: {mode}")
161
+ minimum = _strict_int(value["minimum_work_units"], "minimum_work_units", minimum=1)
162
+ maximum = _strict_int(value["maximum_work_units"], "maximum_work_units", minimum=1)
163
+ join_between = value["join_between_work_units"]
164
+ require_write_paths = value["require_write_paths"]
165
+ if type(join_between) is not bool:
166
+ raise ValueError("join_between_work_units must be boolean")
167
+ if type(require_write_paths) is not bool:
168
+ raise ValueError("require_write_paths must be boolean")
169
+
170
+ if mode == "single":
171
+ if minimum != 1 or maximum != 1:
172
+ raise ValueError("single work-unit mode requires minimum_work_units=maximum_work_units=1")
173
+ if join_between:
174
+ raise ValueError("single work-unit mode cannot join between work units")
175
+ if require_write_paths:
176
+ raise ValueError("single work-unit mode must not require write_paths")
177
+ else:
178
+ if not join_between:
179
+ raise ValueError("bounded work-unit mode requires Parent joins between work units")
180
+ if maximum < minimum:
181
+ raise ValueError("bounded work-unit mode requires maximum_work_units >= minimum_work_units")
182
+ if not require_write_paths:
183
+ raise ValueError("bounded work-unit mode requires write_paths")
184
+ return mode, minimum, join_between, maximum, require_write_paths
185
+
186
+
187
+ def _check_dependencies(units: tuple[WorkUnit, ...]) -> dict[str, WorkUnit]:
188
+ by_id = {unit.unit_id: unit for unit in units}
189
+ if len(by_id) != len(units):
190
+ raise ValueError("work-unit ids must be unique")
191
+ for unit in units:
192
+ unknown = [dependency for dependency in unit.depends_on if dependency not in by_id]
193
+ if unknown:
194
+ raise ValueError(f"work unit {unit.unit_id}: unknown dependencies: {unknown}")
195
+ visiting: set[str] = set()
196
+ visited: set[str] = set()
197
+
198
+ def visit(unit_id: str) -> None:
199
+ if unit_id in visited:
200
+ return
201
+ if unit_id in visiting:
202
+ raise ValueError("work-unit dependency graph contains a cycle")
203
+ visiting.add(unit_id)
204
+ for dependency in by_id[unit_id].depends_on:
205
+ visit(dependency)
206
+ visiting.remove(unit_id)
207
+ visited.add(unit_id)
208
+
209
+ for unit_id in by_id:
210
+ visit(unit_id)
211
+ return by_id
212
+
213
+
214
+ def _depends_on_transitively(by_id: dict[str, WorkUnit], unit_id: str, dependency_id: str) -> bool:
215
+ stack = list(by_id[unit_id].depends_on)
216
+ seen: set[str] = set()
217
+ while stack:
218
+ current = stack.pop()
219
+ if current == dependency_id:
220
+ return True
221
+ if current in seen:
222
+ continue
223
+ seen.add(current)
224
+ stack.extend(by_id[current].depends_on)
225
+ return False
226
+
227
+
228
+ def _paths_overlap(left: str, right: str) -> bool:
229
+ return left == right or left.startswith(f"{right}/") or right.startswith(f"{left}/")
230
+
231
+
232
+ def _check_acceptance_deltas(units: tuple[WorkUnit, ...]) -> None:
233
+ seen: dict[str, str] = {}
234
+ for unit in units:
235
+ previous = seen.get(unit.acceptance_delta)
236
+ if previous is not None:
237
+ raise ValueError(f"work units {previous!r} and {unit.unit_id!r}: acceptance_delta must be unique")
238
+ seen[unit.acceptance_delta] = unit.unit_id
239
+
240
+
241
+ def _check_write_path_order(units: tuple[WorkUnit, ...], by_id: dict[str, WorkUnit]) -> None:
242
+ for index, left in enumerate(units):
243
+ if not left.write_paths:
244
+ continue
245
+ for right in units[index + 1 :]:
246
+ if not right.write_paths:
247
+ continue
248
+ overlaps = [
249
+ (left_path, right_path)
250
+ for left_path in left.write_paths
251
+ for right_path in right.write_paths
252
+ if _paths_overlap(left_path, right_path)
253
+ ]
254
+ if not overlaps:
255
+ continue
256
+ linked = _depends_on_transitively(by_id, left.unit_id, right.unit_id) or _depends_on_transitively(by_id, right.unit_id, left.unit_id)
257
+ if not linked:
258
+ left_path, right_path = overlaps[0]
259
+ raise ValueError(
260
+ f"work units {left.unit_id!r} and {right.unit_id!r} have overlapping write paths "
261
+ f"({left_path!r}, {right_path!r}); they must be directly or transitively dependency-linked"
262
+ )
263
+
264
+
265
+ def _check_shared_writable_scope_order(units: tuple[WorkUnit, ...]) -> None:
266
+ previous_by_scope: dict[str, WorkUnit] = {}
267
+ for unit in units:
268
+ previous = previous_by_scope.get(unit.write_scope_id)
269
+ if previous is not None and previous.unit_id not in unit.depends_on:
270
+ raise ValueError(
271
+ f"work unit {unit.unit_id}: shared write_scope_id {unit.write_scope_id!r} "
272
+ f"must directly depend on previous unit {previous.unit_id!r}"
273
+ )
274
+ previous_by_scope[unit.write_scope_id] = unit
275
+
276
+
277
+ def _check_parallel_groups(units: tuple[WorkUnit, ...], by_id: dict[str, WorkUnit], *, max_parallel_units: int) -> None:
278
+ groups: dict[str, list[WorkUnit]] = defaultdict(list)
279
+ for unit in units:
280
+ if unit.parallel_group is not None:
281
+ groups[unit.parallel_group].append(unit)
282
+ for group, members in groups.items():
283
+ if len(members) > max_parallel_units:
284
+ raise ValueError(
285
+ f"parallel group {group!r} has {len(members)} units but ExecutionPlan allows at most "
286
+ f"{max_parallel_units} concurrent implementation units"
287
+ )
288
+ seen_scopes: set[str] = set()
289
+ for member in members:
290
+ if not member.write_paths:
291
+ raise ValueError(f"parallel group {group!r} requires write_paths for every member")
292
+ if member.write_scope_id in seen_scopes:
293
+ raise ValueError(f"parallel group {group!r} contains overlapping write scope {member.write_scope_id!r}")
294
+ seen_scopes.add(member.write_scope_id)
295
+ for index, left in enumerate(members):
296
+ for right in members[index + 1 :]:
297
+ if any(_paths_overlap(lp, rp) for lp in left.write_paths for rp in right.write_paths):
298
+ raise ValueError(
299
+ f"parallel group {group!r} contains overlapping write paths between "
300
+ f"{left.unit_id!r} and {right.unit_id!r}"
301
+ )
302
+ if _depends_on_transitively(by_id, left.unit_id, right.unit_id) or _depends_on_transitively(by_id, right.unit_id, left.unit_id):
303
+ raise ValueError(
304
+ f"parallel group {group!r} contains transitively dependency-linked units; dependent units must run in later waves"
305
+ )
306
+
307
+
308
+ def _unit_fingerprint(unit: WorkUnit, *, include_generation: bool = True) -> str:
309
+ payload = {
310
+ "unit_id": unit.unit_id,
311
+ "scope_id": unit.scope_id,
312
+ "acceptance_delta": unit.acceptance_delta,
313
+ "write_scope_id": unit.write_scope_id,
314
+ "write_paths": list(unit.write_paths),
315
+ "validation": list(unit.validation),
316
+ "depends_on": list(unit.depends_on),
317
+ }
318
+ if include_generation:
319
+ payload["generation"] = unit.generation
320
+ encoded = json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")
321
+ return hashlib.sha256(encoded).hexdigest()
322
+
323
+
324
+ def validate_manifest(
325
+ policy: dict[str, Any],
326
+ manifest: dict[str, Any],
327
+ *,
328
+ implementation_workers: int = 1,
329
+ max_concurrent_threads: int = 1,
330
+ ) -> dict[str, Any]:
331
+ if type(manifest) is not dict:
332
+ raise ValueError("manifest must be an object")
333
+ implementation_workers = _positive_int(implementation_workers, "implementation_workers")
334
+ max_concurrent_threads = _positive_int(max_concurrent_threads, "max_concurrent_threads")
335
+ max_parallel_units = min(implementation_workers, max_concurrent_threads)
336
+ mode, minimum, join_between, maximum, require_write_paths = _policy_contract(policy)
337
+
338
+ raw_units = manifest.get("units")
339
+ if type(raw_units) is not list or not raw_units:
340
+ raise ValueError("manifest.units must be a non-empty list")
341
+ units = tuple(WorkUnit.from_dict(value) for value in raw_units)
342
+
343
+ if mode == "single" and len(units) != 1:
344
+ raise ValueError("single work-unit mode requires exactly one manifest unit")
345
+ if mode == "bounded" and len(units) < minimum:
346
+ raise ValueError(f"bounded implementation requires at least {minimum} work units; got {len(units)}")
347
+ if len(units) > maximum:
348
+ raise ValueError(f"manifest contains {len(units)} work units but policy maximum_work_units is {maximum}")
349
+
350
+ if mode == "bounded":
351
+ missing_generation = [unit.unit_id for raw, unit in zip(raw_units, units) if "generation" not in raw]
352
+ missing_paths = [unit.unit_id for raw, unit in zip(raw_units, units) if "write_paths" not in raw]
353
+ if missing_generation:
354
+ raise ValueError(f"bounded policy requires generation for every work unit; missing: {missing_generation}")
355
+ if missing_paths:
356
+ raise ValueError(f"bounded policy requires write_paths for every work unit; missing: {missing_paths}")
357
+ if any(unit.generation is None for unit in units):
358
+ raise AssertionError("bounded generation validation failed")
359
+ empty_paths = [unit.unit_id for unit in units if not unit.write_paths]
360
+ if empty_paths:
361
+ raise ValueError(f"bounded policy requires non-empty write_paths for every work unit; empty: {empty_paths}")
362
+
363
+ by_id = _check_dependencies(units)
364
+ _check_acceptance_deltas(units)
365
+ _check_shared_writable_scope_order(units)
366
+ _check_write_path_order(units, by_id)
367
+ _check_parallel_groups(units, by_id, max_parallel_units=max_parallel_units)
368
+
369
+ parallel_groups = sorted({unit.parallel_group for unit in units if unit.parallel_group is not None})
370
+ fingerprints = {unit.unit_id: _unit_fingerprint(unit) for unit in units}
371
+ logical_fingerprints = {unit.unit_id: _unit_fingerprint(unit, include_generation=False) for unit in units}
372
+ return {
373
+ "valid": True,
374
+ "work_unit_mode": mode,
375
+ "minimum_work_units": minimum,
376
+ "maximum_work_units": maximum,
377
+ "require_write_paths": require_write_paths,
378
+ "join_between_work_units": join_between,
379
+ "unit_count": len(units),
380
+ "unit_ids": [unit.unit_id for unit in units],
381
+ "parallel_groups": parallel_groups,
382
+ "max_parallel_units": max_parallel_units,
383
+ "unit_fingerprints": fingerprints,
384
+ "logical_unit_fingerprints": logical_fingerprints,
385
+ }
386
+
387
+
388
+ def build_parser() -> argparse.ArgumentParser:
389
+ parser = argparse.ArgumentParser(prog="work-unit-runtime")
390
+ parser.add_argument("--policy-json", required=True, help="implementation_stage JSON object")
391
+ parser.add_argument("--manifest-json", required=True, help="work-unit manifest JSON object")
392
+ parser.add_argument("--implementation-workers", required=True, type=int)
393
+ parser.add_argument("--max-concurrent-threads", required=True, type=int)
394
+ return parser
395
+
396
+
397
+ def main(argv: Iterable[str] | None = None) -> int:
398
+ ns = build_parser().parse_args(list(argv) if argv is not None else None)
399
+ policy = _load_json(ns.policy_json, "policy")
400
+ manifest = _load_json(ns.manifest_json, "manifest")
401
+ if not isinstance(policy, dict):
402
+ raise ValueError("policy must be a JSON object")
403
+ if not isinstance(manifest, dict):
404
+ raise ValueError("manifest must be a JSON object")
405
+ print(
406
+ json.dumps(
407
+ validate_manifest(
408
+ policy,
409
+ manifest,
410
+ implementation_workers=ns.implementation_workers,
411
+ max_concurrent_threads=ns.max_concurrent_threads,
412
+ ),
413
+ ensure_ascii=False,
414
+ sort_keys=True,
415
+ )
416
+ )
417
+ return 0
418
+
419
+
420
+ if __name__ == "__main__":
421
+ raise SystemExit(main())