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,590 @@
1
+ #!/usr/bin/env python3
2
+ """Deterministic Worker lifecycle evaluator for FlowPilot."""
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import math
8
+ from dataclasses import asdict, dataclass
9
+ from typing import Any, Iterable
10
+
11
+ FALLBACK_ACTIONS = {
12
+ "continue_partial": "continue_partial",
13
+ "parent_delta": "parent_delta",
14
+ "replan": "replan",
15
+ "retry_review": "retry_review",
16
+ "fail": "fail",
17
+ }
18
+ JOIN_POLICIES = {"opportunistic", "quorum", "required"}
19
+ WRITER_FALLBACKS = {"parent_delta", "replan"}
20
+ EPOCH_MILLISECONDS_THRESHOLD = 100_000_000_000
21
+ CHECKPOINT_STATUSES = {"not_requested", "requested", "received", "harvested"}
22
+
23
+
24
+ def _strict_timeout(value: Any, label: str) -> float:
25
+ if type(value) not in (int, float):
26
+ raise ValueError(f"{label} must be a number")
27
+ result = float(value)
28
+ if not math.isfinite(result):
29
+ raise ValueError(f"{label} must be finite")
30
+ return result
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class LifecyclePolicy:
35
+ join_policy: str
36
+ min_successful_workers: int
37
+ idle_timeout_seconds: float
38
+ hard_timeout_seconds: float
39
+ cancel_if_superseded: bool
40
+ cancel_stragglers_after_quorum: bool
41
+ fallback_policy: str
42
+ soft_timeout_seconds: float | None = None
43
+ checkpoint_rearm_seconds: float | None = None
44
+
45
+ @classmethod
46
+ def from_dict(cls, value: dict[str, Any]) -> "LifecyclePolicy":
47
+ if type(value) is not dict:
48
+ raise ValueError("stage policy must be an object")
49
+ required = {
50
+ "join_policy",
51
+ "min_successful_workers",
52
+ "idle_timeout_seconds",
53
+ "hard_timeout_seconds",
54
+ "cancel_if_superseded",
55
+ "cancel_stragglers_after_quorum",
56
+ "fallback_policy",
57
+ }
58
+ missing = required.difference(value)
59
+ if missing:
60
+ raise ValueError(f"stage policy missing fields: {sorted(missing)}")
61
+ for key in ("cancel_if_superseded", "cancel_stragglers_after_quorum"):
62
+ if type(value[key]) is not bool:
63
+ raise ValueError(f"{key} must be boolean")
64
+ if type(value["join_policy"]) is not str or type(value["fallback_policy"]) is not str:
65
+ raise ValueError("join_policy and fallback_policy must be strings")
66
+ if type(value["min_successful_workers"]) is not int:
67
+ raise ValueError("min_successful_workers must be an integer")
68
+ raw_soft = value.get("soft_timeout_seconds")
69
+ raw_rearm = value.get("checkpoint_rearm_seconds")
70
+ policy = cls(
71
+ join_policy=value["join_policy"],
72
+ min_successful_workers=value["min_successful_workers"],
73
+ idle_timeout_seconds=_strict_timeout(value["idle_timeout_seconds"], "idle_timeout_seconds"),
74
+ hard_timeout_seconds=_strict_timeout(value["hard_timeout_seconds"], "hard_timeout_seconds"),
75
+ cancel_if_superseded=value["cancel_if_superseded"],
76
+ cancel_stragglers_after_quorum=value["cancel_stragglers_after_quorum"],
77
+ fallback_policy=value["fallback_policy"],
78
+ soft_timeout_seconds=None if raw_soft is None else _strict_timeout(raw_soft, "soft_timeout_seconds"),
79
+ checkpoint_rearm_seconds=None if raw_rearm is None else _strict_timeout(raw_rearm, "checkpoint_rearm_seconds"),
80
+ )
81
+ policy.validate()
82
+ return policy
83
+
84
+ def validate(self) -> None:
85
+ if self.join_policy not in JOIN_POLICIES:
86
+ raise ValueError(f"invalid join policy: {self.join_policy}")
87
+ if self.fallback_policy not in FALLBACK_ACTIONS:
88
+ raise ValueError(f"invalid fallback policy: {self.fallback_policy}")
89
+ if type(self.min_successful_workers) is not int or self.min_successful_workers < 0:
90
+ raise ValueError("min_successful_workers must be a non-negative integer")
91
+ if self.join_policy == "opportunistic" and self.min_successful_workers != 0:
92
+ raise ValueError("opportunistic stage must use min_successful_workers=0")
93
+ if self.join_policy != "opportunistic" and self.min_successful_workers < 1:
94
+ raise ValueError(f"{self.join_policy} stage requires at least one successful worker")
95
+ for label, value in (
96
+ ("idle_timeout_seconds", self.idle_timeout_seconds),
97
+ ("hard_timeout_seconds", self.hard_timeout_seconds),
98
+ ):
99
+ if type(value) not in (int, float) or not math.isfinite(float(value)) or value <= 0:
100
+ raise ValueError(f"{label} must be a positive finite number")
101
+ if self.hard_timeout_seconds < self.idle_timeout_seconds:
102
+ raise ValueError("hard_timeout_seconds must be >= idle_timeout_seconds")
103
+ if self.soft_timeout_seconds is not None:
104
+ if type(self.soft_timeout_seconds) not in (int, float) or not math.isfinite(float(self.soft_timeout_seconds)):
105
+ raise ValueError("soft_timeout_seconds must be finite")
106
+ if self.soft_timeout_seconds <= 0 or self.soft_timeout_seconds >= self.hard_timeout_seconds:
107
+ raise ValueError("soft_timeout_seconds must be positive and lower than hard_timeout_seconds")
108
+ if self.checkpoint_rearm_seconds is not None:
109
+ if type(self.checkpoint_rearm_seconds) not in (int, float) or not math.isfinite(float(self.checkpoint_rearm_seconds)):
110
+ raise ValueError("checkpoint_rearm_seconds must be finite")
111
+ if self.checkpoint_rearm_seconds <= 0 or self.checkpoint_rearm_seconds >= self.hard_timeout_seconds:
112
+ raise ValueError("checkpoint_rearm_seconds must be positive and lower than hard_timeout_seconds")
113
+ if self.soft_timeout_seconds is None:
114
+ raise ValueError("checkpoint_rearm_seconds requires soft_timeout_seconds")
115
+ if self.soft_timeout_seconds + self.checkpoint_rearm_seconds >= self.hard_timeout_seconds:
116
+ raise ValueError("checkpoint_rearm_seconds must leave time for a second checkpoint before hard_timeout_seconds")
117
+
118
+
119
+ @dataclass(frozen=True)
120
+ class CheckpointRecord:
121
+ sequence: int
122
+ generation: int
123
+ requested_at: float
124
+ received_at: float | None = None
125
+ harvested_at: float | None = None
126
+
127
+ @classmethod
128
+ def from_dict(cls, value: dict[str, Any]) -> "CheckpointRecord":
129
+ if type(value) is not dict:
130
+ raise ValueError("checkpoint record must be an object")
131
+ allowed = {"sequence", "generation", "requested_at", "received_at", "harvested_at"}
132
+ unknown = sorted(set(value).difference(allowed))
133
+ if unknown:
134
+ raise ValueError(f"checkpoint record has unknown fields: {unknown}")
135
+ for key in ("sequence", "generation"):
136
+ if type(value.get(key)) is not int:
137
+ raise ValueError(f"checkpoint {key} must be an integer")
138
+ if "requested_at" not in value:
139
+ raise ValueError("checkpoint record missing requested_at")
140
+ return cls(
141
+ sequence=value["sequence"],
142
+ generation=value["generation"],
143
+ requested_at=_strict_timeout(value["requested_at"], "checkpoint requested_at"),
144
+ received_at=None if value.get("received_at") is None else _strict_timeout(value["received_at"], "checkpoint received_at"),
145
+ harvested_at=None if value.get("harvested_at") is None else _strict_timeout(value["harvested_at"], "checkpoint harvested_at"),
146
+ )
147
+
148
+ def validate(self, *, started_at: float, now: float) -> None:
149
+ if self.sequence < 1 or self.generation < 0:
150
+ raise ValueError("checkpoint sequence must be positive and generation non-negative")
151
+ for label, value in (
152
+ ("requested_at", self.requested_at),
153
+ ("received_at", self.received_at),
154
+ ("harvested_at", self.harvested_at),
155
+ ):
156
+ if value is None:
157
+ continue
158
+ if type(value) not in (int, float) or not math.isfinite(float(value)):
159
+ raise ValueError(f"checkpoint {label} must be finite")
160
+ if value > EPOCH_MILLISECONDS_THRESHOLD:
161
+ raise ValueError(f"checkpoint {label} must use Unix seconds")
162
+ if value < started_at or value > now:
163
+ raise ValueError(f"checkpoint {label} must be between started_at and now")
164
+ if self.received_at is not None and self.received_at < self.requested_at:
165
+ raise ValueError("checkpoint received_at cannot precede requested_at")
166
+ if self.harvested_at is not None:
167
+ if self.received_at is None:
168
+ raise ValueError("checkpoint harvested_at requires received_at")
169
+ if self.harvested_at < self.received_at:
170
+ raise ValueError("checkpoint harvested_at cannot precede received_at")
171
+
172
+
173
+ @dataclass(frozen=True)
174
+ class WorkerObservation:
175
+ scope_id: str
176
+ stage: str
177
+ started_at: float
178
+ last_progress_at: float
179
+ now: float
180
+ last_meaningful_progress_at: float | None = None
181
+ writable: bool = False
182
+ in_flight: bool = False
183
+ terminal_success: bool = False
184
+ terminal_failure: bool = False
185
+ scope_superseded: bool = False
186
+ cancel_confirmed: bool = False
187
+ replacement_isolated: bool = False
188
+ generation: int = 0
189
+ checkpoint_sequence: tuple[CheckpointRecord, ...] = ()
190
+
191
+ def meaningful_progress_at(self) -> float:
192
+ return self.last_progress_at if self.last_meaningful_progress_at is None else self.last_meaningful_progress_at
193
+
194
+ def checkpoint_records(self) -> tuple[CheckpointRecord, ...]:
195
+ return self.checkpoint_sequence
196
+
197
+ def latest_checkpoint(self) -> CheckpointRecord | None:
198
+ return self.checkpoint_sequence[-1] if self.checkpoint_sequence else None
199
+
200
+ def latest_harvested_checkpoint(self) -> CheckpointRecord | None:
201
+ harvested = [record for record in self.checkpoint_sequence if record.harvested_at is not None]
202
+ return harvested[-1] if harvested else None
203
+
204
+ def checkpoint_status(self) -> str:
205
+ latest = self.latest_checkpoint()
206
+ if latest is None:
207
+ return "not_requested"
208
+ if latest.harvested_at is not None:
209
+ return "harvested"
210
+ if latest.received_at is not None:
211
+ return "received"
212
+ return "requested"
213
+
214
+ def validate(self) -> None:
215
+ if not self.scope_id:
216
+ raise ValueError("scope_id is required")
217
+ if self.stage not in {"exploration", "implementation", "review"}:
218
+ raise ValueError(f"invalid stage: {self.stage}")
219
+ if type(self.generation) is not int or self.generation < 0:
220
+ raise ValueError("generation must be a non-negative integer")
221
+ if type(self.checkpoint_sequence) is not tuple:
222
+ raise ValueError("checkpoint_sequence must be a tuple")
223
+ if self.stage != "implementation" and self.checkpoint_sequence:
224
+ raise ValueError("checkpoint_sequence is only valid for implementation stage")
225
+
226
+ timestamps = [self.started_at, self.last_progress_at, self.now]
227
+ if self.last_meaningful_progress_at is not None:
228
+ timestamps.append(self.last_meaningful_progress_at)
229
+ if any(type(v) not in (int, float) or not math.isfinite(float(v)) or v < 0 for v in timestamps):
230
+ raise ValueError("timestamps must be finite non-negative seconds")
231
+ if any(v > EPOCH_MILLISECONDS_THRESHOLD for v in timestamps):
232
+ raise ValueError("timestamps must use Unix seconds")
233
+ if self.now < self.started_at:
234
+ raise ValueError("now cannot precede started_at")
235
+ if self.last_progress_at < self.started_at or self.last_progress_at > self.now:
236
+ raise ValueError("last_progress_at must be between started_at and now")
237
+ meaningful = self.meaningful_progress_at()
238
+ if meaningful < self.started_at or meaningful > self.last_progress_at:
239
+ raise ValueError("last_meaningful_progress_at must be between started_at and last_progress_at")
240
+
241
+ for index, record in enumerate(self.checkpoint_sequence, start=1):
242
+ if record.sequence != index:
243
+ raise ValueError("checkpoint sequences must be contiguous starting at 1")
244
+ if record.generation != self.generation:
245
+ raise ValueError("checkpoint generation must equal observation generation")
246
+ record.validate(started_at=self.started_at, now=self.now)
247
+ if index < len(self.checkpoint_sequence) and record.harvested_at is None:
248
+ raise ValueError("every checkpoint except the latest must be harvested")
249
+ if index > 1:
250
+ previous = self.checkpoint_sequence[index - 2]
251
+ if previous.harvested_at is None:
252
+ raise ValueError("a new checkpoint cannot start before the previous one is harvested")
253
+ if record.requested_at < previous.harvested_at:
254
+ raise ValueError("checkpoint requested_at cannot precede previous harvest")
255
+ if self.checkpoint_status() not in CHECKPOINT_STATUSES:
256
+ raise AssertionError("invalid checkpoint status")
257
+ if self.terminal_success and self.terminal_failure:
258
+ raise ValueError("worker cannot be both terminal-success and terminal-failure")
259
+ if self.cancel_confirmed and (self.terminal_success or self.terminal_failure):
260
+ raise ValueError("cancel_confirmed cannot be combined with another terminal state")
261
+
262
+
263
+ @dataclass(frozen=True)
264
+ class LifecycleDecision:
265
+ state: str
266
+ action: str
267
+ reason: str
268
+ cancel_required: bool
269
+ replacement_allowed: bool
270
+ fence_required: bool
271
+ idle_seconds: float
272
+ meaningful_idle_seconds: float
273
+ progress_quality: str
274
+ checkpoint_status: str
275
+ replan_scope: str | None
276
+ checkpoint_reuse_mode: str | None
277
+ wall_seconds: float
278
+ fallback_policy: str | None
279
+ checkpoint_generation: int
280
+ checkpoint_sequence: int
281
+ next_checkpoint_sequence: int | None
282
+ harvested_checkpoint_sequence: int
283
+ checkpoint_rearm_at: float | None
284
+ checkpoint_rearm_remaining_seconds: float | None
285
+
286
+ def to_dict(self) -> dict[str, Any]:
287
+ return asdict(self)
288
+
289
+
290
+ def _progress_metrics(policy: LifecyclePolicy, observation: WorkerObservation) -> tuple[float, float, float, str]:
291
+ idle = max(0.0, observation.now - observation.last_progress_at)
292
+ wall = max(0.0, observation.now - observation.started_at)
293
+ meaningful_at = observation.meaningful_progress_at()
294
+ meaningful_idle = max(0.0, observation.now - meaningful_at)
295
+ if meaningful_at > observation.started_at and meaningful_idle < policy.idle_timeout_seconds:
296
+ quality = "meaningful"
297
+ elif observation.in_flight or idle < policy.idle_timeout_seconds:
298
+ quality = "activity_only"
299
+ else:
300
+ quality = "none"
301
+ return idle, meaningful_idle, wall, quality
302
+
303
+
304
+ def _replan_contract(observation: WorkerObservation, fallback_policy: str | None) -> tuple[str | None, str | None]:
305
+ if fallback_policy != "replan":
306
+ return None, None
307
+ if observation.latest_harvested_checkpoint() is None:
308
+ return "uncovered_scope", None
309
+ return (
310
+ "checkpoint_remaining_delta",
311
+ "harvested_snapshot_only" if observation.replacement_isolated else "retained_workspace",
312
+ )
313
+
314
+
315
+ def _decision(
316
+ policy: LifecyclePolicy,
317
+ observation: WorkerObservation,
318
+ *,
319
+ state: str,
320
+ action: str,
321
+ reason: str,
322
+ cancel_required: bool,
323
+ replacement_allowed: bool,
324
+ fence_required: bool,
325
+ fallback_policy: str | None,
326
+ ) -> LifecycleDecision:
327
+ idle, meaningful_idle, wall, quality = _progress_metrics(policy, observation)
328
+ replan_scope, reuse = _replan_contract(observation, fallback_policy)
329
+ latest = observation.latest_checkpoint()
330
+ latest_harvested = observation.latest_harvested_checkpoint()
331
+ latest_sequence = latest.sequence if latest is not None else 0
332
+ if latest_harvested is not None and policy.checkpoint_rearm_seconds is not None:
333
+ rearm_at = latest_harvested.harvested_at + policy.checkpoint_rearm_seconds
334
+ rearm_remaining = max(0.0, rearm_at - observation.now)
335
+ else:
336
+ rearm_at = None
337
+ rearm_remaining = None
338
+ return LifecycleDecision(
339
+ state=state,
340
+ action=action,
341
+ reason=reason,
342
+ cancel_required=cancel_required,
343
+ replacement_allowed=replacement_allowed,
344
+ fence_required=fence_required,
345
+ idle_seconds=idle,
346
+ meaningful_idle_seconds=meaningful_idle,
347
+ progress_quality=quality,
348
+ checkpoint_status=observation.checkpoint_status(),
349
+ replan_scope=replan_scope,
350
+ checkpoint_reuse_mode=reuse,
351
+ wall_seconds=wall,
352
+ fallback_policy=fallback_policy,
353
+ checkpoint_generation=observation.generation,
354
+ checkpoint_sequence=latest_sequence,
355
+ next_checkpoint_sequence=latest_sequence + 1 if action == "request_checkpoint" else None,
356
+ harvested_checkpoint_sequence=latest_harvested.sequence if latest_harvested is not None else 0,
357
+ checkpoint_rearm_at=rearm_at,
358
+ checkpoint_rearm_remaining_seconds=rearm_remaining,
359
+ )
360
+
361
+
362
+ def _checkpoint_harvest_decision(policy: LifecyclePolicy, observation: WorkerObservation) -> LifecycleDecision:
363
+ return _decision(
364
+ policy,
365
+ observation,
366
+ state="progressing" if observation.last_progress_at > observation.started_at or observation.in_flight else "running",
367
+ action="harvest_checkpoint",
368
+ reason="worker checkpoint is available; harvest durable work before any fallback",
369
+ cancel_required=False,
370
+ replacement_allowed=False,
371
+ fence_required=False,
372
+ fallback_policy=None,
373
+ )
374
+
375
+
376
+ def _fallback_decision(
377
+ policy: LifecyclePolicy,
378
+ observation: WorkerObservation,
379
+ *,
380
+ state: str,
381
+ reason: str,
382
+ terminal: bool,
383
+ ) -> LifecycleDecision:
384
+ if policy.fallback_policy == "retry_review" and observation.stage != "review":
385
+ raise ValueError("retry_review fallback is only valid for review stage")
386
+ fallback = FALLBACK_ACTIONS[policy.fallback_policy]
387
+ cancel_required = not terminal
388
+ fallback_creates_writer = policy.fallback_policy in WRITER_FALLBACKS
389
+ fence_required = observation.stage == "implementation" and observation.writable and fallback_creates_writer
390
+
391
+ if policy.fallback_policy == "retry_review":
392
+ action = "retry_review"
393
+ replacement_allowed = True
394
+ reason += "; replacement is read-only and must consume a review_attempt reservation"
395
+ elif fence_required and not terminal and not observation.replacement_isolated:
396
+ action = "request_cancel"
397
+ replacement_allowed = False
398
+ else:
399
+ action = fallback
400
+ replacement_allowed = fallback == "replan" and (not fence_required or terminal or observation.replacement_isolated)
401
+ if fence_required and observation.replacement_isolated and not terminal:
402
+ reason += "; downstream writer is isolated and old output is fenced"
403
+
404
+ if policy.fallback_policy == "replan" and observation.latest_harvested_checkpoint() is not None:
405
+ reason += "; replan is restricted to harvested remaining_delta"
406
+ if cancel_required:
407
+ reason += "; non-terminal Worker cancellation is required"
408
+ return _decision(
409
+ policy,
410
+ observation,
411
+ state=state,
412
+ action=action,
413
+ reason=reason,
414
+ cancel_required=cancel_required,
415
+ replacement_allowed=replacement_allowed,
416
+ fence_required=fence_required,
417
+ fallback_policy=policy.fallback_policy,
418
+ )
419
+
420
+
421
+ def _superseded_decision(policy: LifecyclePolicy, observation: WorkerObservation) -> LifecycleDecision:
422
+ terminal = observation.terminal_failure or observation.cancel_confirmed
423
+ return _decision(
424
+ policy,
425
+ observation,
426
+ state="superseded",
427
+ action="continue" if terminal else "request_cancel",
428
+ reason="scope is already covered; no fallback work is required",
429
+ cancel_required=not terminal,
430
+ replacement_allowed=False,
431
+ fence_required=False,
432
+ fallback_policy=None,
433
+ )
434
+
435
+
436
+ def _soft_budget_decision(policy: LifecyclePolicy, observation: WorkerObservation) -> LifecycleDecision:
437
+ _idle, _meaningful_idle, _wall, quality = _progress_metrics(policy, observation)
438
+ state = "progressing" if observation.last_progress_at > observation.started_at or observation.in_flight else "running"
439
+ checkpoint_status = observation.checkpoint_status()
440
+ if checkpoint_status == "not_requested":
441
+ action, suffix = "request_checkpoint", "request a non-terminal checkpoint"
442
+ elif checkpoint_status == "requested":
443
+ action, suffix = "await_checkpoint", "checkpoint is already requested"
444
+ elif checkpoint_status == "received":
445
+ return _checkpoint_harvest_decision(policy, observation)
446
+ else:
447
+ latest = observation.latest_harvested_checkpoint()
448
+ assert latest is not None
449
+ rearm_at = latest.harvested_at + policy.checkpoint_rearm_seconds
450
+ explicit = observation.last_meaningful_progress_at
451
+ has_delta = explicit is not None and explicit > latest.harvested_at
452
+ cooldown = observation.now >= rearm_at
453
+ if has_delta and cooldown:
454
+ action, suffix = "request_checkpoint", "explicit meaningful progress and cooldown re-armed checkpointing"
455
+ elif explicit is None:
456
+ action, suffix = "continue", "last_progress_at cannot re-arm checkpointing; explicit meaningful progress is required"
457
+ elif not has_delta:
458
+ action, suffix = "continue", "harvested checkpoint remains current; no new meaningful delta"
459
+ else:
460
+ action, suffix = "continue", "harvested checkpoint remains current; checkpoint rearm cooldown has not elapsed"
461
+ return _decision(
462
+ policy,
463
+ observation,
464
+ state=state,
465
+ action=action,
466
+ reason=f"soft worker execution budget reached; progress={quality}; {suffix}",
467
+ cancel_required=False,
468
+ replacement_allowed=False,
469
+ fence_required=False,
470
+ fallback_policy=None,
471
+ )
472
+
473
+
474
+ def evaluate_worker(policy: LifecyclePolicy, observation: WorkerObservation) -> LifecycleDecision:
475
+ policy.validate()
476
+ observation.validate()
477
+ if observation.stage == "implementation":
478
+ if policy.soft_timeout_seconds is None:
479
+ raise ValueError("implementation lifecycle requires soft_timeout_seconds")
480
+ if policy.checkpoint_rearm_seconds is None:
481
+ raise ValueError("implementation lifecycle requires checkpoint_rearm_seconds")
482
+ idle, _meaningful_idle, wall, _quality = _progress_metrics(policy, observation)
483
+
484
+ if observation.checkpoint_status() == "received":
485
+ return _checkpoint_harvest_decision(policy, observation)
486
+ if observation.terminal_success:
487
+ return _decision(
488
+ policy,
489
+ observation,
490
+ state="completed",
491
+ action="consume_result",
492
+ reason="worker reported terminal success",
493
+ cancel_required=False,
494
+ replacement_allowed=False,
495
+ fence_required=False,
496
+ fallback_policy=None,
497
+ )
498
+ if observation.scope_superseded and policy.cancel_if_superseded:
499
+ return _superseded_decision(policy, observation)
500
+ if observation.terminal_failure:
501
+ return _fallback_decision(policy, observation, state="failed", reason="worker reported terminal failure", terminal=True)
502
+ if observation.cancel_confirmed:
503
+ return _fallback_decision(policy, observation, state="cancelled", reason="worker cancellation/termination confirmed", terminal=True)
504
+ if wall >= policy.hard_timeout_seconds:
505
+ return _fallback_decision(policy, observation, state="stalled", reason="hard worker wall-clock ceiling reached", terminal=False)
506
+ if not observation.in_flight and idle >= policy.idle_timeout_seconds:
507
+ return _fallback_decision(policy, observation, state="stalled", reason="idle progress lease expired with no visible in-flight work", terminal=False)
508
+ if policy.soft_timeout_seconds is not None and wall >= policy.soft_timeout_seconds:
509
+ return _soft_budget_decision(policy, observation)
510
+ return _decision(
511
+ policy,
512
+ observation,
513
+ state="progressing" if observation.last_progress_at > observation.started_at or observation.in_flight else "running",
514
+ action="continue",
515
+ reason="worker remains non-terminal with an active liveness lease",
516
+ cancel_required=False,
517
+ replacement_allowed=False,
518
+ fence_required=False,
519
+ fallback_policy=None,
520
+ )
521
+
522
+
523
+ def _load_json_object(raw: str, label: str) -> dict[str, Any]:
524
+ try:
525
+ value = json.loads(raw)
526
+ except json.JSONDecodeError as exc:
527
+ raise ValueError(f"invalid {label} JSON: {exc}") from exc
528
+ if not isinstance(value, dict):
529
+ raise ValueError(f"{label} must be a JSON object")
530
+ return value
531
+
532
+
533
+ def _load_checkpoint_sequence(raw: str) -> tuple[CheckpointRecord, ...]:
534
+ try:
535
+ value = json.loads(raw)
536
+ except json.JSONDecodeError as exc:
537
+ raise ValueError(f"invalid checkpoint sequence JSON: {exc}") from exc
538
+ if type(value) is not list:
539
+ raise ValueError("checkpoint sequence must be a JSON array")
540
+ return tuple(CheckpointRecord.from_dict(item) for item in value)
541
+
542
+
543
+ def build_parser() -> argparse.ArgumentParser:
544
+ parser = argparse.ArgumentParser(prog="worker-lifecycle")
545
+ parser.add_argument("--policy-json", required=True)
546
+ parser.add_argument("--scope-id", required=True)
547
+ parser.add_argument("--stage", choices=("exploration", "implementation", "review"), required=True)
548
+ parser.add_argument("--started-at", type=float, required=True)
549
+ parser.add_argument("--last-progress-at", type=float, required=True)
550
+ parser.add_argument("--last-meaningful-progress-at", type=float)
551
+ parser.add_argument("--now", type=float, required=True)
552
+ parser.add_argument("--writable", action="store_true")
553
+ parser.add_argument("--in-flight", action="store_true")
554
+ parser.add_argument("--terminal-success", action="store_true")
555
+ parser.add_argument("--terminal-failure", action="store_true")
556
+ parser.add_argument("--scope-superseded", action="store_true")
557
+ parser.add_argument("--cancel-confirmed", action="store_true")
558
+ parser.add_argument("--replacement-isolated", action="store_true")
559
+ parser.add_argument("--generation", type=int, default=0)
560
+ parser.add_argument("--checkpoint-sequence-json")
561
+ return parser
562
+
563
+
564
+ def main(argv: Iterable[str] | None = None) -> int:
565
+ ns = build_parser().parse_args(list(argv) if argv is not None else None)
566
+ policy = LifecyclePolicy.from_dict(_load_json_object(ns.policy_json, "policy"))
567
+ sequence = () if ns.checkpoint_sequence_json is None else _load_checkpoint_sequence(ns.checkpoint_sequence_json)
568
+ observation = WorkerObservation(
569
+ scope_id=ns.scope_id,
570
+ stage=ns.stage,
571
+ started_at=ns.started_at,
572
+ last_progress_at=ns.last_progress_at,
573
+ now=ns.now,
574
+ last_meaningful_progress_at=ns.last_meaningful_progress_at,
575
+ writable=ns.writable,
576
+ in_flight=ns.in_flight,
577
+ terminal_success=ns.terminal_success,
578
+ terminal_failure=ns.terminal_failure,
579
+ scope_superseded=ns.scope_superseded,
580
+ cancel_confirmed=ns.cancel_confirmed,
581
+ replacement_isolated=ns.replacement_isolated,
582
+ generation=ns.generation,
583
+ checkpoint_sequence=sequence,
584
+ )
585
+ print(json.dumps(evaluate_worker(policy, observation).to_dict(), ensure_ascii=False, sort_keys=True))
586
+ return 0
587
+
588
+
589
+ if __name__ == "__main__":
590
+ raise SystemExit(main())