claude-dev-env 2.4.0 → 2.7.0

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 (309) hide show
  1. package/CLAUDE.md +26 -59
  2. package/_shared/pr-loop/scripts/_claude_permissions_common.py +84 -0
  3. package/_shared/pr-loop/scripts/code_rules_gate.py +6 -3
  4. package/_shared/pr-loop/scripts/code_rules_gate_parts/CLAUDE.md +12 -2
  5. package/_shared/pr-loop/scripts/code_rules_gate_parts/baseline_import_isolation.py +309 -0
  6. package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_regression.py +540 -0
  7. package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_running.py +206 -70
  8. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/__init__.py +1 -0
  9. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/_repo_test_helpers.py +76 -0
  10. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_baseline_import_isolation.py +248 -0
  11. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_regression.py +309 -0
  12. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_running.py +91 -58
  13. package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +306 -306
  14. package/_shared/pr-loop/scripts/pr_loop_shared_constants/claude_permissions_constants.py +44 -0
  15. package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +202 -0
  16. package/_shared/pr-loop/scripts/pr_loop_shared_constants/copilot_quota_constants.py +24 -24
  17. package/_shared/pr-loop/scripts/pr_loop_shared_constants/stale_worktree_rule_sweep_constants.py +107 -107
  18. package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +290 -48
  19. package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +42 -2
  20. package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +36 -0
  21. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +100 -1
  22. package/_shared/pr-loop/scripts/tests/test_fix_hookspath.py +497 -497
  23. package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +311 -2
  24. package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep.py +301 -301
  25. package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep_constants.py +85 -85
  26. package/_shared/pr-loop/worker-spawn.md +1 -1
  27. package/agents/CLAUDE.md +3 -2
  28. package/agents/caveman.md +0 -1
  29. package/agents/clasp-deployment-orchestrator.md +0 -1
  30. package/agents/clean-coder.md +0 -1
  31. package/agents/code-advisor.md +0 -1
  32. package/agents/code-quality-agent.md +1 -2
  33. package/agents/code-verifier.md +36 -8
  34. package/agents/deep-research.md +0 -1
  35. package/agents/docs-agent.md +0 -1
  36. package/agents/git-commit-crafter.md +0 -1
  37. package/agents/issue-tracker.md +42 -0
  38. package/agents/plan-packet-validator.md +0 -1
  39. package/agents/pr-description-writer.md +0 -1
  40. package/agents/test_agent_frontmatter.py +67 -18
  41. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +143 -141
  42. package/bin/CLAUDE.md +68 -5
  43. package/bin/codex-compat.mjs +104 -0
  44. package/bin/codex-compat.test.mjs +51 -0
  45. package/bin/ever-shipped-skills.mjs +1 -0
  46. package/bin/install-constants.mjs +88 -0
  47. package/bin/install.mjs +1138 -114
  48. package/bin/install.prune.test.mjs +869 -19
  49. package/bin/install.test.mjs +906 -2
  50. package/codex-capability-map.json +13 -0
  51. package/commands/implement.md +1 -1
  52. package/commands/right-size.md +1 -1
  53. package/docs/CLAUDE.md +1 -0
  54. package/docs/CODE_RULES.md +2 -0
  55. package/docs/codex-compatibility.md +25 -0
  56. package/docs/host-pool-health-monitor.md +102 -0
  57. package/docs/nas-ssh-invocation.md +96 -12
  58. package/docs/references/CLAUDE.md +4 -2
  59. package/docs/references/advisor-tool.md +13 -0
  60. package/docs/references/code-review-enforcement.md +35 -0
  61. package/docs/references/team-advisor-skill.md +14 -0
  62. package/hooks/blocking/CLAUDE.md +4 -0
  63. package/hooks/blocking/code_review_pr_create_gate.py +7 -3
  64. package/hooks/blocking/code_review_push_gate.py +9 -4
  65. package/hooks/blocking/code_review_stamp_directory_write_blocker.py +8 -0
  66. package/hooks/blocking/config/__init__.py +5 -5
  67. package/hooks/blocking/config/code_review_enforcement_constants.py +40 -7
  68. package/hooks/blocking/config/test_code_review_enforcement_constants.py +58 -0
  69. package/hooks/blocking/config/verified_commit_constants.py +160 -159
  70. package/hooks/blocking/eli11_reply_enforcer.py +479 -0
  71. package/hooks/blocking/gh_body_arg_blocker.py +1 -1
  72. package/hooks/blocking/nas_ssh_binary_enforcer.py +8 -46
  73. package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +256 -0
  74. package/hooks/blocking/pre_tool_use_dispatcher.py +24 -24
  75. package/hooks/blocking/shell_substitution_blocker.py +129 -0
  76. package/hooks/blocking/state_description_blocker.py +1 -1
  77. package/hooks/blocking/stop_dispatcher.py +1 -1
  78. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +2 -3
  79. package/hooks/blocking/test_code_review_pr_create_gate.py +14 -0
  80. package/hooks/blocking/test_code_review_push_gate.py +16 -0
  81. package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +19 -0
  82. package/hooks/blocking/test_eli11_reply_enforcer.py +457 -0
  83. package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +231 -0
  84. package/hooks/blocking/test_pre_tool_use_dispatcher.py +10 -1
  85. package/hooks/blocking/test_shell_substitution_blocker.py +124 -0
  86. package/hooks/blocking/test_stop_dispatcher.py +23 -0
  87. package/hooks/blocking/test_unscoped_search_blocker.py +102 -0
  88. package/hooks/blocking/test_verdict_directory_write_blocker.py +804 -808
  89. package/hooks/blocking/test_verification_verdict_store.py +54 -0
  90. package/hooks/blocking/test_verified_commit_gate.py +581 -581
  91. package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -131
  92. package/hooks/blocking/unscoped_search_blocker.py +391 -0
  93. package/hooks/blocking/verdict_directory_write_blocker.py +687 -687
  94. package/hooks/blocking/verification_verdict_store.py +1039 -1036
  95. package/hooks/blocking/verified_commit_message_accuracy_blocker.py +167 -167
  96. package/hooks/blocking/verifier_verdict_minter.py +280 -280
  97. package/hooks/git-hooks/CLAUDE.md +3 -0
  98. package/hooks/git-hooks/conftest.py +30 -0
  99. package/hooks/git-hooks/gate_utils.py +2 -2
  100. package/hooks/git-hooks/git_hooks_constants/__init__.py +41 -2
  101. package/hooks/git-hooks/pre_push.py +75 -4
  102. package/hooks/git-hooks/pre_push_base_reference.py +166 -0
  103. package/hooks/git-hooks/test_config.py +0 -15
  104. package/hooks/git-hooks/test_gate_utils.py +3 -15
  105. package/hooks/git-hooks/test_pre_commit.py +1 -15
  106. package/hooks/git-hooks/test_pre_push.py +257 -23
  107. package/hooks/git-hooks/test_pre_push_base_reference.py +339 -0
  108. package/hooks/hooks.json +10 -12
  109. package/hooks/hooks_constants/CLAUDE.md +7 -2
  110. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +4 -4
  111. package/hooks/hooks_constants/eli11_reply_enforcer_constants.py +101 -0
  112. package/hooks/hooks_constants/enter_worktree_prefetch_constants.py +18 -18
  113. package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +2 -8
  114. package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +48 -0
  115. package/hooks/hooks_constants/ruff_integration_constants.py +16 -0
  116. package/hooks/hooks_constants/shell_command_segments.py +82 -0
  117. package/hooks/hooks_constants/shell_substitution_blocker_constants.py +67 -0
  118. package/hooks/hooks_constants/stop_dispatcher_constants.py +1 -0
  119. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +5 -6
  120. package/hooks/hooks_constants/test_stop_dispatcher_constants.py +1 -0
  121. package/hooks/hooks_constants/unscoped_search_blocker_constants.py +153 -0
  122. package/hooks/lifecycle/enter_worktree_origin_prefetch.py +163 -146
  123. package/hooks/lifecycle/test_enter_worktree_origin_prefetch.py +185 -178
  124. package/hooks/pyproject.toml +1 -0
  125. package/hooks/validators/CLAUDE.md +1 -0
  126. package/hooks/validators/config/__init__.py +0 -0
  127. package/hooks/validators/config/directory_exemption_constants.py +183 -0
  128. package/hooks/validators/config/test_directory_exemption_constants.py +21 -0
  129. package/hooks/validators/conftest.py +4 -0
  130. package/hooks/validators/ruff_integration.py +49 -5
  131. package/hooks/validators/run_all_validators.py +206 -9
  132. package/hooks/validators/test_directory_exemption_constants.py +185 -0
  133. package/hooks/validators/test_python_antipattern_checks.py +110 -5
  134. package/hooks/validators/test_ruff_integration.py +92 -1
  135. package/hooks/validators/test_run_all_validators.py +115 -68
  136. package/hooks/validators/test_run_all_validators_pretooluse.py +159 -1
  137. package/package.json +13 -3
  138. package/rules/CLAUDE.md +17 -22
  139. package/rules/agent-spawn-protocol.md +6 -6
  140. package/rules/anti-corollary-tests.md +1 -1
  141. package/rules/bdd.md +1 -1
  142. package/rules/cleanup-temp-files.md +10 -4
  143. package/rules/code-standards.md +7 -0
  144. package/rules/conservative-action.md +1 -5
  145. package/rules/context7.md +0 -4
  146. package/rules/destructive-commands.md +47 -0
  147. package/rules/doc-inventory-integrity.md +48 -0
  148. package/rules/doc-prose-cuts.md +58 -0
  149. package/rules/docstring-prose-matches-implementation.md +53 -44
  150. package/rules/durable-post-artifacts.md +0 -4
  151. package/rules/eli11-replies.md +31 -0
  152. package/rules/explore-thoroughly.md +4 -4
  153. package/rules/falsify-before-green.md +68 -0
  154. package/rules/file-global-constants.md +1 -1
  155. package/rules/filesystem-search.md +51 -0
  156. package/rules/gh-cli-conventions.md +27 -0
  157. package/rules/git-workflow.md +26 -0
  158. package/rules/hedging-claims.md +9 -0
  159. package/rules/long-horizon-autonomy.md +0 -4
  160. package/rules/measurement-denominators.md +48 -0
  161. package/rules/nas-ssh-invocation.md +23 -5
  162. package/rules/parallel-tools.md +2 -2
  163. package/rules/plain-illustrative-docstrings.md +3 -7
  164. package/rules/plain-language.md +2 -0
  165. package/rules/proof-of-work-pr-comments.md +0 -4
  166. package/rules/re-stage-before-commit.md +2 -0
  167. package/rules/research-mode.md +10 -0
  168. package/rules/shell-invocation.md +21 -0
  169. package/rules/testing.md +4 -0
  170. package/rules/verified-commit-gate-skip.md +3 -27
  171. package/rules/verify-before-asking.md +5 -0
  172. package/rules/windows-filesystem-safe.md +1 -1
  173. package/rules/workers-done-before-complete.md +4 -0
  174. package/scripts/CLAUDE.md +1 -0
  175. package/scripts/Capture-PoolHealth.ps1 +410 -0
  176. package/scripts/Migrate-ShellPolicy.ps1 +1 -1
  177. package/scripts/_code_review_test_support.py +404 -0
  178. package/scripts/claude_chain_runner.py +141 -1
  179. package/scripts/codex_capability_bridge.py +171 -0
  180. package/scripts/codex_compat_materializer.py +1087 -0
  181. package/scripts/codex_compat_watcher.py +502 -0
  182. package/scripts/conftest.py +16 -1
  183. package/scripts/dev_env_scripts_constants/CLAUDE.md +1 -1
  184. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +9 -0
  185. package/scripts/dev_env_scripts_constants/code_review_constants.py +37 -0
  186. package/scripts/invoke_code_review.py +11 -4
  187. package/scripts/resolve_worker_spawn.py +626 -626
  188. package/scripts/spawn_grok_batch.py +672 -672
  189. package/scripts/sync_to_cursor/rules.py +0 -10
  190. package/scripts/test_claude_chain_runner.py +131 -0
  191. package/scripts/test_invoke_code_review.py +85 -908
  192. package/scripts/test_invoke_code_review_chain.py +70 -0
  193. package/scripts/test_invoke_code_review_cli.py +192 -0
  194. package/scripts/test_invoke_code_review_contract.py +256 -0
  195. package/scripts/test_invoke_code_review_git.py +123 -0
  196. package/scripts/test_invoke_code_review_mode.py +99 -0
  197. package/scripts/test_resolve_worker_spawn.py +1014 -1014
  198. package/scripts/tests/test_code_review_constants.py +80 -0
  199. package/scripts/tests/test_codex_capability_bridge.py +91 -0
  200. package/scripts/tests/test_codex_compat_materializer.py +632 -0
  201. package/scripts/tests/test_codex_compat_watcher.py +599 -0
  202. package/scripts/tests/test_sync_to_cursor.py +0 -1
  203. package/skills/CLAUDE.md +2 -0
  204. package/skills/auditing-claude-config/SKILL.md +114 -114
  205. package/skills/autoconverge/SKILL.md +427 -427
  206. package/skills/autoconverge/reference/convergence.md +24 -3
  207. package/skills/autoconverge/workflow/CLAUDE.md +1 -0
  208. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +3 -3
  209. package/skills/autoconverge/workflow/converge.contract.test.mjs +1263 -1263
  210. package/skills/autoconverge/workflow/converge.mjs +168 -1
  211. package/skills/autoconverge/workflow/converge.p2-advance.test.mjs +202 -0
  212. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a11d903476b803493.jsonl +2 -2
  213. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a26213978adeef6fb.jsonl +2 -2
  214. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a3def0d15ed9d9110.jsonl +2 -2
  215. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a41f41b1b708ee3b7.jsonl +2 -2
  216. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a758b880abecc3ff7.jsonl +2 -2
  217. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a8897b89656b1bd16.jsonl +2 -2
  218. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-abd463d744a1437bc.jsonl +2 -2
  219. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ad19d027ae8ee1816.jsonl +2 -2
  220. package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +265 -265
  221. package/skills/bugteam/reference/copilot-gap-analysis.md +1 -1
  222. package/skills/closeout/SKILL.md +33 -50
  223. package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +8 -0
  224. package/skills/codex-review/scripts/run_codex_review.py +233 -1
  225. package/skills/codex-review/scripts/test_run_codex_review.py +189 -0
  226. package/skills/condensing-instructions/SKILL.md +72 -0
  227. package/skills/copilot-review/SKILL.md +119 -119
  228. package/skills/e-code-review/SKILL.md +52 -0
  229. package/skills/e-code-review/reference/fix.md +54 -0
  230. package/skills/e-code-review/reference/loop.md +43 -0
  231. package/skills/e-code-review/reference/low.md +57 -0
  232. package/skills/e-code-review/reference/medium.md +153 -0
  233. package/skills/e-code-review/reference/xhigh.md +182 -0
  234. package/skills/e-simplify/SKILL.md +97 -0
  235. package/skills/fresh-branch/CLAUDE.md +1 -1
  236. package/skills/fresh-branch/SKILL.md +5 -6
  237. package/skills/fresh-branch/scripts/create_fresh_branch.py +42 -24
  238. package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +1 -3
  239. package/skills/fresh-branch/scripts/test_create_fresh_branch.py +30 -126
  240. package/skills/issue-tracker/SKILL.md +92 -0
  241. package/skills/issue-tracker/reference/epic-and-sub-issue-model.md +55 -0
  242. package/skills/issue-tracker/reference/handoff-schema.md +64 -0
  243. package/skills/issue-tracker/reference/operation-matrix.md +41 -0
  244. package/skills/orchestrator/SKILL.md +177 -22
  245. package/skills/orchestrator/scripts/status_gate.py +625 -0
  246. package/skills/orchestrator/scripts/status_gate_constants/__init__.py +1 -0
  247. package/skills/orchestrator/scripts/status_gate_constants/config/__init__.py +1 -0
  248. package/skills/orchestrator/scripts/status_gate_constants/config/constants.py +47 -0
  249. package/skills/orchestrator/scripts/test_status_gate.py +439 -0
  250. package/skills/orchestrator-refresh/SKILL.md +129 -35
  251. package/skills/plan-to-pr/SKILL.md +155 -0
  252. package/skills/plan-to-pr/reference/final-validation-tasks.md +15 -0
  253. package/skills/plan-to-pr/reference/model-routing.md +36 -0
  254. package/skills/plan-to-pr/reference/packet-contract.md +43 -0
  255. package/skills/plan-to-pr/reference/packet-schema.json +57 -0
  256. package/skills/plan-to-pr/reference/process-inventory.md +22 -0
  257. package/skills/plan-to-pr/reference/review-loop.md +33 -0
  258. package/skills/plan-to-pr/reference/run-record.schema.json +27 -0
  259. package/skills/plan-to-pr/reference/self-audit-tasks.md +15 -0
  260. package/skills/plan-to-pr/reference/task-seeds.md +14 -0
  261. package/skills/plan-to-pr/reference/task-ticket.md +38 -0
  262. package/skills/plan-to-pr/scripts/config/__init__.py +1 -0
  263. package/skills/plan-to-pr/scripts/config/constants.py +193 -0
  264. package/skills/plan-to-pr/scripts/create_packet.py +173 -0
  265. package/skills/plan-to-pr/scripts/test_create_packet.py +102 -0
  266. package/skills/plan-to-pr/scripts/test_validate_packet.py +256 -0
  267. package/skills/plan-to-pr/scripts/test_validate_protocol.py +135 -0
  268. package/skills/plan-to-pr/scripts/test_validate_run.py +158 -0
  269. package/skills/plan-to-pr/scripts/validate_packet.py +655 -0
  270. package/skills/plan-to-pr/scripts/validate_protocol.py +622 -0
  271. package/skills/plan-to-pr/scripts/validate_run.py +173 -0
  272. package/skills/plan-to-pr/test_skill_contract.py +207 -0
  273. package/skills/plan-to-pr/test_task_ticket_contract.py +151 -0
  274. package/skills/pr-converge/SKILL.md +472 -469
  275. package/skills/pr-converge/reference/examples.md +3 -3
  276. package/skills/pr-converge/reference/fix-protocol.md +1 -1
  277. package/skills/pr-converge/reference/ground-rules.md +7 -4
  278. package/skills/pr-converge/reference/multi-pr-orchestration.md +4 -1
  279. package/skills/pr-converge/reference/per-tick.md +5 -5
  280. package/skills/pr-converge/reference/progress-checklist.md +1 -1
  281. package/skills/pr-converge/scripts/check_convergence_gates.py +279 -279
  282. package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -507
  283. package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -84
  284. package/skills/pr-converge/test_step5_host_branch.py +1 -1
  285. package/skills/pr-fix-protocol/SKILL.md +1 -1
  286. package/skills/privacy-hygiene/SKILL.md +68 -68
  287. package/skills/privacy-hygiene/reference/sweep-procedure.md +1 -1
  288. package/skills/prototype/workflows/promotion.md +1 -1
  289. package/skills/release-notes-html/SKILL.md +164 -0
  290. package/skills/session-log/SKILL.md +1 -1
  291. package/skills/task-build/CLAUDE.md +8 -7
  292. package/skills/task-build/SKILL.md +16 -8
  293. package/skills/task-build/reference/tool-routing.md +19 -0
  294. package/rules/claude-md-orphan-file.md +0 -28
  295. package/rules/cleanup-command-forms.md +0 -23
  296. package/rules/code-reviews.md +0 -11
  297. package/rules/env-var-table-code-drift.md +0 -10
  298. package/rules/gh-body-file.md +0 -5
  299. package/rules/gh-paginate.md +0 -3
  300. package/rules/hook-prose-matches-detector.md +0 -15
  301. package/rules/no-historical-clutter.md +0 -26
  302. package/rules/no-inline-destructive-literals.md +0 -9
  303. package/rules/no-justification-noise.md +0 -61
  304. package/rules/package-inventory-stale-entry.md +0 -25
  305. package/rules/right-sized-engineering.md +0 -28
  306. package/rules/self-contained-docs.md +0 -17
  307. package/rules/shell-invocation-policy.md +0 -5
  308. package/rules/tdd.md +0 -7
  309. package/skills/closeout/reference/issue-body-templates.md +0 -108
@@ -0,0 +1,625 @@
1
+ #!/usr/bin/env python3
2
+ """Deterministic orchestrator status gate: status JSON and reschedule exit codes.
3
+
4
+ ::
5
+
6
+ python status_gate.py set --status active|done
7
+ python status_gate.py begin-firing
8
+ python status_gate.py should-reschedule
9
+ python status_gate.py claim-rearm
10
+ python status_gate.py release-rearm
11
+
12
+ ``should-reschedule`` and ``claim-rearm`` exit 0 only when status is
13
+ ``active`` and no re-arm slot is already pending. Host re-arm order is
14
+ create-then-claim: ``should-reschedule``, one host schedule create, then
15
+ ``claim-rearm`` (which sets the pending latch). ``begin-firing`` clears
16
+ the latch at the start of a refresh firing. Missing or invalid status
17
+ files fail closed.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import argparse
23
+ import json
24
+ import os
25
+ import sys
26
+ from collections.abc import Callable
27
+ from datetime import datetime, timezone
28
+ from pathlib import Path
29
+ from typing import TypedDict
30
+
31
+ from status_gate_constants.config.constants import (
32
+ ALL_DEFAULT_STATUS_DIRECTORY_PARTS,
33
+ ALL_VALID_RUN_STATUSES,
34
+ COMMAND_BEGIN_FIRING,
35
+ COMMAND_CLAIM_REARM,
36
+ COMMAND_RELEASE_REARM,
37
+ COMMAND_SET,
38
+ COMMAND_SHOULD_RESCHEDULE,
39
+ EXIT_CODE_STOP,
40
+ EXIT_CODE_SUCCESS,
41
+ JSON_INDENT_SPACES,
42
+ REARM_PENDING_FIELD_NAME,
43
+ REASON_ACTIVE,
44
+ REASON_FIELD_NAME,
45
+ REASON_FIRING_STARTED,
46
+ REASON_INVALID_STATUS_FILE,
47
+ REASON_MISSING_STATUS_FILE,
48
+ REASON_REARM_ALREADY_PENDING,
49
+ REASON_REARM_SLOT_CLAIMED,
50
+ REASON_REARM_SLOT_RELEASED,
51
+ REASON_STATUS_NOT_ACTIVE,
52
+ RESCHEDULE_FIELD_NAME,
53
+ RUN_SLUG_ENV_VAR,
54
+ RUN_SLUG_FIELD_NAME,
55
+ RUN_STATUS_ACTIVE,
56
+ STATUS_FIELD_NAME,
57
+ STATUS_FILE_ENV_VAR,
58
+ STATUS_FILE_FIELD_NAME,
59
+ STATUS_FILE_NAME,
60
+ STATUS_FILE_TEMPORARY_SUFFIX,
61
+ UPDATED_AT_FIELD_NAME,
62
+ UTF8_ENCODING,
63
+ )
64
+
65
+
66
+ class StatusFilePayload(TypedDict, total=False):
67
+ """On-disk orchestrator run status JSON shape."""
68
+
69
+ status: str
70
+ updated_at: str
71
+ rearm_pending: bool
72
+ run_slug: str
73
+
74
+
75
+ RearmMutation = Callable[[Path, str], tuple[bool, str, StatusFilePayload | None]]
76
+
77
+
78
+ def _encode_status_file_payload(
79
+ run_status: str,
80
+ run_slug: str,
81
+ is_rearm_pending: bool,
82
+ ) -> StatusFilePayload:
83
+ """Build a status payload for disk write.
84
+
85
+ Args:
86
+ run_status: ``active`` or ``done``.
87
+ run_slug: Run slug stored when non-empty; pass empty string to omit.
88
+ is_rearm_pending: Whether a one-shot re-arm is already reserved.
89
+
90
+ Returns:
91
+ Typed status payload ready for serialization.
92
+ """
93
+ all_status_fields: StatusFilePayload = {
94
+ STATUS_FIELD_NAME: run_status,
95
+ UPDATED_AT_FIELD_NAME: datetime.now(timezone.utc).isoformat(),
96
+ REARM_PENDING_FIELD_NAME: is_rearm_pending,
97
+ }
98
+ if run_slug != "":
99
+ all_status_fields[RUN_SLUG_FIELD_NAME] = run_slug
100
+ return all_status_fields
101
+
102
+
103
+ def _decode_status_file_payload(
104
+ all_loaded_fields: dict[object, object],
105
+ ) -> StatusFilePayload | None:
106
+ """Parse a raw JSON object into StatusFilePayload, or None if invalid.
107
+
108
+ Args:
109
+ all_loaded_fields: Decoded JSON object.
110
+
111
+ Returns:
112
+ Typed status payload, or None when the shape is wrong.
113
+ """
114
+ all_status_fields: StatusFilePayload = {}
115
+ for each_field_name, each_field_entry in all_loaded_fields.items():
116
+ if not isinstance(each_field_name, str):
117
+ return None
118
+ if each_field_name == STATUS_FIELD_NAME:
119
+ if not isinstance(each_field_entry, str):
120
+ return None
121
+ all_status_fields[STATUS_FIELD_NAME] = each_field_entry
122
+ continue
123
+ if each_field_name == UPDATED_AT_FIELD_NAME:
124
+ if not isinstance(each_field_entry, str):
125
+ return None
126
+ all_status_fields[UPDATED_AT_FIELD_NAME] = each_field_entry
127
+ continue
128
+ if each_field_name == RUN_SLUG_FIELD_NAME:
129
+ if not isinstance(each_field_entry, str):
130
+ return None
131
+ all_status_fields[RUN_SLUG_FIELD_NAME] = each_field_entry
132
+ continue
133
+ if each_field_name == REARM_PENDING_FIELD_NAME:
134
+ if not isinstance(each_field_entry, bool):
135
+ return None
136
+ all_status_fields[REARM_PENDING_FIELD_NAME] = each_field_entry
137
+ continue
138
+ return None
139
+ return all_status_fields
140
+
141
+
142
+ def resolve_status_file_path(
143
+ explicit_path: str | None,
144
+ base_directory: Path | None,
145
+ run_slug: str,
146
+ ) -> Path:
147
+ """Resolve status file from CLI flag, env var, or default under base_directory.
148
+
149
+ Args:
150
+ explicit_path: ``--status-file`` value, or None.
151
+ base_directory: Directory that owns the default relative path when no
152
+ flag or env is set. When None, uses the process current directory.
153
+ run_slug: When non-empty (and no explicit/env path), place the status
154
+ file under ``docs/plans/<run_slug>/`` so concurrent runs do not
155
+ share one file.
156
+
157
+ Returns:
158
+ Absolute path to the status file.
159
+ """
160
+ if explicit_path:
161
+ return Path(explicit_path).expanduser().resolve()
162
+ environment_path = os.environ.get(STATUS_FILE_ENV_VAR)
163
+ if environment_path:
164
+ return Path(environment_path).expanduser().resolve()
165
+ root_directory = base_directory if base_directory is not None else Path.cwd()
166
+ resolved_slug = run_slug
167
+ if resolved_slug == "":
168
+ environment_slug = os.environ.get(RUN_SLUG_ENV_VAR)
169
+ if environment_slug:
170
+ resolved_slug = environment_slug
171
+ plans_directory = root_directory.joinpath(*ALL_DEFAULT_STATUS_DIRECTORY_PARTS)
172
+ if resolved_slug != "":
173
+ return (plans_directory / resolved_slug / STATUS_FILE_NAME).resolve()
174
+ return (plans_directory / STATUS_FILE_NAME).resolve()
175
+
176
+
177
+ def write_status_file(
178
+ status_file_path: Path,
179
+ run_status: str,
180
+ run_slug: str,
181
+ is_rearm_pending: bool,
182
+ ) -> StatusFilePayload:
183
+ """Write the run status file, creating parent directories as needed.
184
+
185
+ Args:
186
+ status_file_path: Destination path.
187
+ run_status: ``active`` or ``done``.
188
+ run_slug: Run slug stored when non-empty; pass empty string to omit.
189
+ is_rearm_pending: Whether a one-shot re-arm is already reserved.
190
+
191
+ Returns:
192
+ The payload written to disk.
193
+
194
+ Raises:
195
+ ValueError: When ``run_status`` is not a valid status token.
196
+ """
197
+ if run_status not in ALL_VALID_RUN_STATUSES:
198
+ raise ValueError(f"invalid status: {run_status}")
199
+ all_status_fields = _encode_status_file_payload(
200
+ run_status=run_status,
201
+ run_slug=run_slug,
202
+ is_rearm_pending=is_rearm_pending,
203
+ )
204
+ return _atomic_write_payload(status_file_path, all_status_fields)
205
+
206
+
207
+ def _atomic_write_payload(
208
+ status_file_path: Path,
209
+ all_status_fields: StatusFilePayload,
210
+ ) -> StatusFilePayload:
211
+ """Write a status payload atomically.
212
+
213
+ Args:
214
+ status_file_path: Destination path.
215
+ all_status_fields: JSON-serializable status mapping.
216
+
217
+ Returns:
218
+ The same payload.
219
+ """
220
+ status_file_path.parent.mkdir(parents=True, exist_ok=True)
221
+ serialized_payload = json.dumps(all_status_fields, indent=JSON_INDENT_SPACES) + "\n"
222
+ temporary_path = status_file_path.with_name(
223
+ status_file_path.name + STATUS_FILE_TEMPORARY_SUFFIX
224
+ )
225
+ temporary_path.write_text(serialized_payload, encoding=UTF8_ENCODING)
226
+ temporary_path.replace(status_file_path)
227
+ return all_status_fields
228
+
229
+
230
+ def _load_status_payload(
231
+ status_file_path: Path,
232
+ ) -> tuple[StatusFilePayload | None, str | None]:
233
+ """Load and validate the status file payload.
234
+
235
+ Args:
236
+ status_file_path: Path to the run status file.
237
+
238
+ Returns:
239
+ ``(payload, None)`` on success, or ``(None, reason_code)`` on failure.
240
+ """
241
+ if not status_file_path.is_file():
242
+ return None, REASON_MISSING_STATUS_FILE
243
+ try:
244
+ loaded_payload = json.loads(status_file_path.read_text(encoding=UTF8_ENCODING))
245
+ except (OSError, json.JSONDecodeError):
246
+ return None, REASON_INVALID_STATUS_FILE
247
+ if not isinstance(loaded_payload, dict):
248
+ return None, REASON_INVALID_STATUS_FILE
249
+ all_status_fields = _decode_status_file_payload(loaded_payload)
250
+ if all_status_fields is None:
251
+ return None, REASON_INVALID_STATUS_FILE
252
+ return all_status_fields, None
253
+
254
+
255
+ def _is_rearm_pending(all_status_fields: StatusFilePayload) -> bool:
256
+ """Return whether the payload marks a re-arm slot as pending.
257
+
258
+ Args:
259
+ all_status_fields: Loaded status mapping.
260
+
261
+ Returns:
262
+ True when ``rearm_pending`` is truthy.
263
+ """
264
+ return bool(all_status_fields.get(REARM_PENDING_FIELD_NAME, False))
265
+
266
+
267
+ def _status_file_run_slug(
268
+ all_status_fields: StatusFilePayload,
269
+ fallback_run_slug: str,
270
+ ) -> str:
271
+ """Prefer the slug stored in the payload when present.
272
+
273
+ Args:
274
+ all_status_fields: Loaded status mapping.
275
+ fallback_run_slug: CLI or empty slug.
276
+
277
+ Returns:
278
+ Slug string for rewrites.
279
+ """
280
+ stored_slug = all_status_fields.get(RUN_SLUG_FIELD_NAME)
281
+ if isinstance(stored_slug, str) and stored_slug != "":
282
+ return stored_slug
283
+ return fallback_run_slug
284
+
285
+
286
+ def _load_active_status(
287
+ status_file_path: Path,
288
+ ) -> tuple[StatusFilePayload | None, str | None]:
289
+ """Load the status payload and require an active run.
290
+
291
+ Args:
292
+ status_file_path: Path to the run status file.
293
+
294
+ Returns:
295
+ ``(payload, None)`` when the file loads and status is ``active``;
296
+ ``(None, reason_code)`` on a missing/invalid file or inactive status.
297
+ """
298
+ all_status_fields, load_failure_reason = _load_status_payload(status_file_path)
299
+ if all_status_fields is None:
300
+ return None, load_failure_reason
301
+ if all_status_fields.get(STATUS_FIELD_NAME) != RUN_STATUS_ACTIVE:
302
+ return None, REASON_STATUS_NOT_ACTIVE
303
+ return all_status_fields, None
304
+
305
+
306
+ def decide_should_reschedule(status_file_path: Path) -> tuple[bool, str]:
307
+ """Decide whether the orchestrator refresh loop may re-arm.
308
+
309
+ Args:
310
+ status_file_path: Path to the run status file.
311
+
312
+ Returns:
313
+ ``(is_reschedule_allowed, reason_code)``. Fail closed on missing/invalid
314
+ status, inactive status, or an already-pending re-arm slot.
315
+ """
316
+ all_status_fields, load_failure_reason = _load_active_status(status_file_path)
317
+ if all_status_fields is None:
318
+ assert load_failure_reason is not None
319
+ return False, load_failure_reason
320
+ if _is_rearm_pending(all_status_fields):
321
+ return False, REASON_REARM_ALREADY_PENDING
322
+ return True, REASON_ACTIVE
323
+
324
+
325
+ def _apply_rearm_latch(
326
+ status_file_path: Path,
327
+ run_slug: str,
328
+ is_rearm_pending: bool,
329
+ success_reason: str,
330
+ ) -> tuple[bool, str, StatusFilePayload | None]:
331
+ """Rewrite the re-arm latch of an active run and report the outcome.
332
+
333
+ Setting the latch (``is_rearm_pending`` True) is a claim: it fails with
334
+ ``REASON_REARM_ALREADY_PENDING`` when a slot is already reserved. Clearing
335
+ the latch (False) always succeeds while the run is active.
336
+
337
+ Args:
338
+ status_file_path: Path to the run status file.
339
+ run_slug: Fallback slug when the file has none.
340
+ is_rearm_pending: Latch value to write.
341
+ success_reason: Reason code returned when the rewrite lands.
342
+
343
+ Returns:
344
+ ``(is_applied, reason_code, payload_or_none)``.
345
+ """
346
+ all_status_fields, load_failure_reason = _load_active_status(status_file_path)
347
+ if all_status_fields is None:
348
+ assert load_failure_reason is not None
349
+ return False, load_failure_reason, None
350
+ if is_rearm_pending and _is_rearm_pending(all_status_fields):
351
+ return False, REASON_REARM_ALREADY_PENDING, None
352
+ written_status = write_status_file(
353
+ status_file_path=status_file_path,
354
+ run_status=RUN_STATUS_ACTIVE,
355
+ run_slug=_status_file_run_slug(all_status_fields, run_slug),
356
+ is_rearm_pending=is_rearm_pending,
357
+ )
358
+ return True, success_reason, written_status
359
+
360
+
361
+ def begin_firing(
362
+ status_file_path: Path,
363
+ run_slug: str,
364
+ ) -> tuple[bool, str, StatusFilePayload | None]:
365
+ """Clear the re-arm latch at the start of a refresh firing.
366
+
367
+ Args:
368
+ status_file_path: Path to the run status file.
369
+ run_slug: Fallback slug when the file has none.
370
+
371
+ Returns:
372
+ ``(is_allowed, reason_code, payload_or_none)``.
373
+ """
374
+ return _apply_rearm_latch(
375
+ status_file_path,
376
+ run_slug,
377
+ is_rearm_pending=False,
378
+ success_reason=REASON_FIRING_STARTED,
379
+ )
380
+
381
+
382
+ def claim_rearm_slot(
383
+ status_file_path: Path,
384
+ run_slug: str,
385
+ ) -> tuple[bool, str, StatusFilePayload | None]:
386
+ """Latch the single re-arm slot after a successful host schedule create.
387
+
388
+ Args:
389
+ status_file_path: Path to the run status file.
390
+ run_slug: Fallback slug when the file has none.
391
+
392
+ Returns:
393
+ ``(is_claimed, reason_code, payload_or_none)``.
394
+ """
395
+ return _apply_rearm_latch(
396
+ status_file_path,
397
+ run_slug,
398
+ is_rearm_pending=True,
399
+ success_reason=REASON_REARM_SLOT_CLAIMED,
400
+ )
401
+
402
+
403
+ def release_rearm_slot(
404
+ status_file_path: Path,
405
+ run_slug: str,
406
+ ) -> tuple[bool, str, StatusFilePayload | None]:
407
+ """Clear a reserved re-arm slot after a failed host schedule create.
408
+
409
+ Args:
410
+ status_file_path: Path to the run status file.
411
+ run_slug: Fallback slug when the file has none.
412
+
413
+ Returns:
414
+ ``(is_released, reason_code, payload_or_none)``.
415
+ """
416
+ return _apply_rearm_latch(
417
+ status_file_path,
418
+ run_slug,
419
+ is_rearm_pending=False,
420
+ success_reason=REASON_REARM_SLOT_RELEASED,
421
+ )
422
+
423
+
424
+ def _build_argument_parser() -> argparse.ArgumentParser:
425
+ """Build the status-gate CLI parser.
426
+
427
+ Returns:
428
+ Configured argument parser.
429
+ """
430
+ parser = argparse.ArgumentParser(
431
+ description="Orchestrator run status and single-pending re-arm gate.",
432
+ )
433
+ subparsers = parser.add_subparsers(dest="command", required=True)
434
+
435
+ set_parser = subparsers.add_parser(
436
+ COMMAND_SET,
437
+ help=(
438
+ "Write run status (active or done). "
439
+ "done clears rearm_pending; re-asserting active preserves it."
440
+ ),
441
+ )
442
+ set_parser.add_argument("--status-file", default=None)
443
+ set_parser.add_argument(
444
+ "--status",
445
+ required=True,
446
+ choices=sorted(ALL_VALID_RUN_STATUSES),
447
+ )
448
+ set_parser.add_argument("--run-slug", default=None)
449
+
450
+ for each_command, each_help in (
451
+ (
452
+ COMMAND_SHOULD_RESCHEDULE,
453
+ "Exit 0 when active and rearm_pending is false (read-only).",
454
+ ),
455
+ (
456
+ COMMAND_BEGIN_FIRING,
457
+ "Clear rearm_pending at refresh start; exit 0 when active.",
458
+ ),
459
+ (
460
+ COMMAND_CLAIM_REARM,
461
+ "Reserve the re-arm slot (active and not pending); exit 0 on claim.",
462
+ ),
463
+ (
464
+ COMMAND_RELEASE_REARM,
465
+ "Clear rearm_pending after a failed host schedule create.",
466
+ ),
467
+ ):
468
+ each_parser = subparsers.add_parser(each_command, help=each_help)
469
+ each_parser.add_argument("--status-file", default=None)
470
+ each_parser.add_argument("--run-slug", default=None)
471
+ return parser
472
+
473
+
474
+ def _print_gate_report(
475
+ is_reschedule_allowed: bool,
476
+ reason_code: str,
477
+ status_file_path: Path,
478
+ ) -> None:
479
+ """Print the standard JSON gate report to stdout.
480
+
481
+ Args:
482
+ is_reschedule_allowed: Whether the caller may proceed.
483
+ reason_code: Machine reason string.
484
+ status_file_path: Resolved status path.
485
+ """
486
+ report = {
487
+ RESCHEDULE_FIELD_NAME: is_reschedule_allowed,
488
+ REASON_FIELD_NAME: reason_code,
489
+ STATUS_FILE_FIELD_NAME: str(status_file_path),
490
+ }
491
+ print(json.dumps(report, indent=JSON_INDENT_SPACES))
492
+
493
+
494
+ def _run_set_command(
495
+ status_file: str | None,
496
+ run_status: str,
497
+ run_slug: str,
498
+ ) -> int:
499
+ """Write status JSON for the set subcommand.
500
+
501
+ Re-asserting ``active`` when the file is already ``active`` preserves
502
+ ``rearm_pending`` so a second ``/orchestrator`` cannot clear a latched
503
+ one-shot. ``done`` always clears the latch.
504
+
505
+ Args:
506
+ status_file: Optional explicit status path.
507
+ run_status: ``active`` or ``done``.
508
+ run_slug: Run slug for path scoping and payload.
509
+
510
+ Returns:
511
+ Process exit code.
512
+ """
513
+ status_file_path = resolve_status_file_path(status_file, None, run_slug)
514
+ is_rearm_pending = False
515
+ if run_status == RUN_STATUS_ACTIVE:
516
+ all_existing_fields, _load_failure_reason = _load_status_payload(
517
+ status_file_path
518
+ )
519
+ if (
520
+ all_existing_fields is not None
521
+ and all_existing_fields.get(STATUS_FIELD_NAME) == RUN_STATUS_ACTIVE
522
+ ):
523
+ is_rearm_pending = _is_rearm_pending(all_existing_fields)
524
+ written_status = write_status_file(
525
+ status_file_path=status_file_path,
526
+ run_status=run_status,
527
+ run_slug=run_slug,
528
+ is_rearm_pending=is_rearm_pending,
529
+ )
530
+ print(json.dumps(written_status, indent=JSON_INDENT_SPACES))
531
+ return EXIT_CODE_SUCCESS
532
+
533
+
534
+ def _report_and_exit(
535
+ is_allowed: bool,
536
+ reason_code: str,
537
+ status_file_path: Path,
538
+ ) -> int:
539
+ """Print the JSON gate report and map the decision to an exit code.
540
+
541
+ Args:
542
+ is_allowed: Whether the caller may proceed.
543
+ reason_code: Machine reason string.
544
+ status_file_path: Resolved status path.
545
+
546
+ Returns:
547
+ ``EXIT_CODE_SUCCESS`` when allowed, else ``EXIT_CODE_STOP``.
548
+ """
549
+ _print_gate_report(is_allowed, reason_code, status_file_path)
550
+ if is_allowed:
551
+ return EXIT_CODE_SUCCESS
552
+ return EXIT_CODE_STOP
553
+
554
+
555
+ def _run_should_reschedule_command(
556
+ status_file: str | None,
557
+ run_slug: str,
558
+ ) -> int:
559
+ """Evaluate whether a one-shot re-arm is allowed (read-only).
560
+
561
+ Args:
562
+ status_file: Optional explicit status path.
563
+ run_slug: Run slug for path scoping.
564
+
565
+ Returns:
566
+ Exit 0 when active and free; exit 1 when the loop must not re-arm.
567
+ """
568
+ status_file_path = resolve_status_file_path(status_file, None, run_slug)
569
+ is_reschedule_allowed, reason_code = decide_should_reschedule(status_file_path)
570
+ return _report_and_exit(is_reschedule_allowed, reason_code, status_file_path)
571
+
572
+
573
+ def _run_rearm_mutation_command(
574
+ mutation: RearmMutation,
575
+ status_file: str | None,
576
+ run_slug: str,
577
+ ) -> int:
578
+ """Run a re-arm latch mutation and map its decision to an exit code.
579
+
580
+ Args:
581
+ mutation: begin_firing, claim_rearm_slot, or release_rearm_slot.
582
+ status_file: Optional explicit status path.
583
+ run_slug: Run slug for path scoping.
584
+
585
+ Returns:
586
+ Exit 0 when the mutation lands; exit 1 otherwise.
587
+ """
588
+ status_file_path = resolve_status_file_path(status_file, None, run_slug)
589
+ is_applied, reason_code, _payload = mutation(status_file_path, run_slug)
590
+ return _report_and_exit(is_applied, reason_code, status_file_path)
591
+
592
+
593
+ def main() -> int:
594
+ """CLI entrypoint for status and re-arm latch commands.
595
+
596
+ Returns:
597
+ Process exit code.
598
+ """
599
+ parsed_arguments = _build_argument_parser().parse_args()
600
+ selected_run_slug = parsed_arguments.run_slug or ""
601
+ if parsed_arguments.command == COMMAND_SET:
602
+ return _run_set_command(
603
+ status_file=parsed_arguments.status_file,
604
+ run_status=parsed_arguments.status,
605
+ run_slug=selected_run_slug,
606
+ )
607
+ if parsed_arguments.command == COMMAND_SHOULD_RESCHEDULE:
608
+ return _run_should_reschedule_command(
609
+ status_file=parsed_arguments.status_file,
610
+ run_slug=selected_run_slug,
611
+ )
612
+ rearm_mutations: dict[str, RearmMutation] = {
613
+ COMMAND_BEGIN_FIRING: begin_firing,
614
+ COMMAND_CLAIM_REARM: claim_rearm_slot,
615
+ COMMAND_RELEASE_REARM: release_rearm_slot,
616
+ }
617
+ return _run_rearm_mutation_command(
618
+ rearm_mutations[parsed_arguments.command],
619
+ status_file=parsed_arguments.status_file,
620
+ run_slug=selected_run_slug,
621
+ )
622
+
623
+
624
+ if __name__ == "__main__":
625
+ sys.exit(main())
@@ -0,0 +1 @@
1
+ """Named constants for orchestrator status_gate scripts."""
@@ -0,0 +1 @@
1
+ """Config package for status_gate constants."""
@@ -0,0 +1,47 @@
1
+ """Config constants for orchestrator run-status gate scripts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ RUN_STATUS_ACTIVE = "active"
6
+ RUN_STATUS_DONE = "done"
7
+ ALL_VALID_RUN_STATUSES: frozenset[str] = frozenset(
8
+ {RUN_STATUS_ACTIVE, RUN_STATUS_DONE}
9
+ )
10
+
11
+ STATUS_FIELD_NAME = "status"
12
+ UPDATED_AT_FIELD_NAME = "updated_at"
13
+ RUN_SLUG_FIELD_NAME = "run_slug"
14
+ REARM_PENDING_FIELD_NAME = "rearm_pending"
15
+ REASON_FIELD_NAME = "reason"
16
+ RESCHEDULE_FIELD_NAME = "reschedule"
17
+ STATUS_FILE_FIELD_NAME = "status_file"
18
+
19
+ STATUS_FILE_ENV_VAR = "ORCHESTRATOR_RUN_STATUS_FILE"
20
+ RUN_SLUG_ENV_VAR = "ORCHESTRATOR_RUN_SLUG"
21
+ ALL_DEFAULT_STATUS_DIRECTORY_PARTS: tuple[str, ...] = (
22
+ "docs",
23
+ "plans",
24
+ )
25
+ STATUS_FILE_NAME = ".orchestrator-run-status.json"
26
+ STATUS_FILE_TEMPORARY_SUFFIX = ".tmp"
27
+
28
+ COMMAND_SET = "set"
29
+ COMMAND_SHOULD_RESCHEDULE = "should-reschedule"
30
+ COMMAND_BEGIN_FIRING = "begin-firing"
31
+ COMMAND_CLAIM_REARM = "claim-rearm"
32
+ COMMAND_RELEASE_REARM = "release-rearm"
33
+
34
+ EXIT_CODE_SUCCESS = 0
35
+ EXIT_CODE_STOP = 1
36
+
37
+ REASON_MISSING_STATUS_FILE = "missing_status_file"
38
+ REASON_INVALID_STATUS_FILE = "invalid_status_file"
39
+ REASON_STATUS_NOT_ACTIVE = "status_not_active"
40
+ REASON_ACTIVE = "active"
41
+ REASON_REARM_ALREADY_PENDING = "rearm_already_pending"
42
+ REASON_REARM_SLOT_CLAIMED = "rearm_slot_claimed"
43
+ REASON_REARM_SLOT_RELEASED = "rearm_slot_released"
44
+ REASON_FIRING_STARTED = "firing_started"
45
+
46
+ JSON_INDENT_SPACES = 2
47
+ UTF8_ENCODING = "utf-8"