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.
- package/CLAUDE.md +26 -59
- package/_shared/pr-loop/scripts/_claude_permissions_common.py +84 -0
- package/_shared/pr-loop/scripts/code_rules_gate.py +6 -3
- package/_shared/pr-loop/scripts/code_rules_gate_parts/CLAUDE.md +12 -2
- package/_shared/pr-loop/scripts/code_rules_gate_parts/baseline_import_isolation.py +309 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_regression.py +540 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_running.py +206 -70
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/__init__.py +1 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/_repo_test_helpers.py +76 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_baseline_import_isolation.py +248 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_regression.py +309 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_running.py +91 -58
- package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +306 -306
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/claude_permissions_constants.py +44 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +202 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/copilot_quota_constants.py +24 -24
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/stale_worktree_rule_sweep_constants.py +107 -107
- package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +290 -48
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +42 -2
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +36 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +100 -1
- package/_shared/pr-loop/scripts/tests/test_fix_hookspath.py +497 -497
- package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +311 -2
- package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep.py +301 -301
- package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep_constants.py +85 -85
- package/_shared/pr-loop/worker-spawn.md +1 -1
- package/agents/CLAUDE.md +3 -2
- package/agents/caveman.md +0 -1
- package/agents/clasp-deployment-orchestrator.md +0 -1
- package/agents/clean-coder.md +0 -1
- package/agents/code-advisor.md +0 -1
- package/agents/code-quality-agent.md +1 -2
- package/agents/code-verifier.md +36 -8
- package/agents/deep-research.md +0 -1
- package/agents/docs-agent.md +0 -1
- package/agents/git-commit-crafter.md +0 -1
- package/agents/issue-tracker.md +42 -0
- package/agents/plan-packet-validator.md +0 -1
- package/agents/pr-description-writer.md +0 -1
- package/agents/test_agent_frontmatter.py +67 -18
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +143 -141
- package/bin/CLAUDE.md +68 -5
- package/bin/codex-compat.mjs +104 -0
- package/bin/codex-compat.test.mjs +51 -0
- package/bin/ever-shipped-skills.mjs +1 -0
- package/bin/install-constants.mjs +88 -0
- package/bin/install.mjs +1138 -114
- package/bin/install.prune.test.mjs +869 -19
- package/bin/install.test.mjs +906 -2
- package/codex-capability-map.json +13 -0
- package/commands/implement.md +1 -1
- package/commands/right-size.md +1 -1
- package/docs/CLAUDE.md +1 -0
- package/docs/CODE_RULES.md +2 -0
- package/docs/codex-compatibility.md +25 -0
- package/docs/host-pool-health-monitor.md +102 -0
- package/docs/nas-ssh-invocation.md +96 -12
- package/docs/references/CLAUDE.md +4 -2
- package/docs/references/advisor-tool.md +13 -0
- package/docs/references/code-review-enforcement.md +35 -0
- package/docs/references/team-advisor-skill.md +14 -0
- package/hooks/blocking/CLAUDE.md +4 -0
- package/hooks/blocking/code_review_pr_create_gate.py +7 -3
- package/hooks/blocking/code_review_push_gate.py +9 -4
- package/hooks/blocking/code_review_stamp_directory_write_blocker.py +8 -0
- package/hooks/blocking/config/__init__.py +5 -5
- package/hooks/blocking/config/code_review_enforcement_constants.py +40 -7
- package/hooks/blocking/config/test_code_review_enforcement_constants.py +58 -0
- package/hooks/blocking/config/verified_commit_constants.py +160 -159
- package/hooks/blocking/eli11_reply_enforcer.py +479 -0
- package/hooks/blocking/gh_body_arg_blocker.py +1 -1
- package/hooks/blocking/nas_ssh_binary_enforcer.py +8 -46
- package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +256 -0
- package/hooks/blocking/pre_tool_use_dispatcher.py +24 -24
- package/hooks/blocking/shell_substitution_blocker.py +129 -0
- package/hooks/blocking/state_description_blocker.py +1 -1
- package/hooks/blocking/stop_dispatcher.py +1 -1
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +2 -3
- package/hooks/blocking/test_code_review_pr_create_gate.py +14 -0
- package/hooks/blocking/test_code_review_push_gate.py +16 -0
- package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +19 -0
- package/hooks/blocking/test_eli11_reply_enforcer.py +457 -0
- package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +231 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +10 -1
- package/hooks/blocking/test_shell_substitution_blocker.py +124 -0
- package/hooks/blocking/test_stop_dispatcher.py +23 -0
- package/hooks/blocking/test_unscoped_search_blocker.py +102 -0
- package/hooks/blocking/test_verdict_directory_write_blocker.py +804 -808
- package/hooks/blocking/test_verification_verdict_store.py +54 -0
- package/hooks/blocking/test_verified_commit_gate.py +581 -581
- package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -131
- package/hooks/blocking/unscoped_search_blocker.py +391 -0
- package/hooks/blocking/verdict_directory_write_blocker.py +687 -687
- package/hooks/blocking/verification_verdict_store.py +1039 -1036
- package/hooks/blocking/verified_commit_message_accuracy_blocker.py +167 -167
- package/hooks/blocking/verifier_verdict_minter.py +280 -280
- package/hooks/git-hooks/CLAUDE.md +3 -0
- package/hooks/git-hooks/conftest.py +30 -0
- package/hooks/git-hooks/gate_utils.py +2 -2
- package/hooks/git-hooks/git_hooks_constants/__init__.py +41 -2
- package/hooks/git-hooks/pre_push.py +75 -4
- package/hooks/git-hooks/pre_push_base_reference.py +166 -0
- package/hooks/git-hooks/test_config.py +0 -15
- package/hooks/git-hooks/test_gate_utils.py +3 -15
- package/hooks/git-hooks/test_pre_commit.py +1 -15
- package/hooks/git-hooks/test_pre_push.py +257 -23
- package/hooks/git-hooks/test_pre_push_base_reference.py +339 -0
- package/hooks/hooks.json +10 -12
- package/hooks/hooks_constants/CLAUDE.md +7 -2
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +4 -4
- package/hooks/hooks_constants/eli11_reply_enforcer_constants.py +101 -0
- package/hooks/hooks_constants/enter_worktree_prefetch_constants.py +18 -18
- package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +2 -8
- package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +48 -0
- package/hooks/hooks_constants/ruff_integration_constants.py +16 -0
- package/hooks/hooks_constants/shell_command_segments.py +82 -0
- package/hooks/hooks_constants/shell_substitution_blocker_constants.py +67 -0
- package/hooks/hooks_constants/stop_dispatcher_constants.py +1 -0
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +5 -6
- package/hooks/hooks_constants/test_stop_dispatcher_constants.py +1 -0
- package/hooks/hooks_constants/unscoped_search_blocker_constants.py +153 -0
- package/hooks/lifecycle/enter_worktree_origin_prefetch.py +163 -146
- package/hooks/lifecycle/test_enter_worktree_origin_prefetch.py +185 -178
- package/hooks/pyproject.toml +1 -0
- package/hooks/validators/CLAUDE.md +1 -0
- package/hooks/validators/config/__init__.py +0 -0
- package/hooks/validators/config/directory_exemption_constants.py +183 -0
- package/hooks/validators/config/test_directory_exemption_constants.py +21 -0
- package/hooks/validators/conftest.py +4 -0
- package/hooks/validators/ruff_integration.py +49 -5
- package/hooks/validators/run_all_validators.py +206 -9
- package/hooks/validators/test_directory_exemption_constants.py +185 -0
- package/hooks/validators/test_python_antipattern_checks.py +110 -5
- package/hooks/validators/test_ruff_integration.py +92 -1
- package/hooks/validators/test_run_all_validators.py +115 -68
- package/hooks/validators/test_run_all_validators_pretooluse.py +159 -1
- package/package.json +13 -3
- package/rules/CLAUDE.md +17 -22
- package/rules/agent-spawn-protocol.md +6 -6
- package/rules/anti-corollary-tests.md +1 -1
- package/rules/bdd.md +1 -1
- package/rules/cleanup-temp-files.md +10 -4
- package/rules/code-standards.md +7 -0
- package/rules/conservative-action.md +1 -5
- package/rules/context7.md +0 -4
- package/rules/destructive-commands.md +47 -0
- package/rules/doc-inventory-integrity.md +48 -0
- package/rules/doc-prose-cuts.md +58 -0
- package/rules/docstring-prose-matches-implementation.md +53 -44
- package/rules/durable-post-artifacts.md +0 -4
- package/rules/eli11-replies.md +31 -0
- package/rules/explore-thoroughly.md +4 -4
- package/rules/falsify-before-green.md +68 -0
- package/rules/file-global-constants.md +1 -1
- package/rules/filesystem-search.md +51 -0
- package/rules/gh-cli-conventions.md +27 -0
- package/rules/git-workflow.md +26 -0
- package/rules/hedging-claims.md +9 -0
- package/rules/long-horizon-autonomy.md +0 -4
- package/rules/measurement-denominators.md +48 -0
- package/rules/nas-ssh-invocation.md +23 -5
- package/rules/parallel-tools.md +2 -2
- package/rules/plain-illustrative-docstrings.md +3 -7
- package/rules/plain-language.md +2 -0
- package/rules/proof-of-work-pr-comments.md +0 -4
- package/rules/re-stage-before-commit.md +2 -0
- package/rules/research-mode.md +10 -0
- package/rules/shell-invocation.md +21 -0
- package/rules/testing.md +4 -0
- package/rules/verified-commit-gate-skip.md +3 -27
- package/rules/verify-before-asking.md +5 -0
- package/rules/windows-filesystem-safe.md +1 -1
- package/rules/workers-done-before-complete.md +4 -0
- package/scripts/CLAUDE.md +1 -0
- package/scripts/Capture-PoolHealth.ps1 +410 -0
- package/scripts/Migrate-ShellPolicy.ps1 +1 -1
- package/scripts/_code_review_test_support.py +404 -0
- package/scripts/claude_chain_runner.py +141 -1
- package/scripts/codex_capability_bridge.py +171 -0
- package/scripts/codex_compat_materializer.py +1087 -0
- package/scripts/codex_compat_watcher.py +502 -0
- package/scripts/conftest.py +16 -1
- package/scripts/dev_env_scripts_constants/CLAUDE.md +1 -1
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +9 -0
- package/scripts/dev_env_scripts_constants/code_review_constants.py +37 -0
- package/scripts/invoke_code_review.py +11 -4
- package/scripts/resolve_worker_spawn.py +626 -626
- package/scripts/spawn_grok_batch.py +672 -672
- package/scripts/sync_to_cursor/rules.py +0 -10
- package/scripts/test_claude_chain_runner.py +131 -0
- package/scripts/test_invoke_code_review.py +85 -908
- package/scripts/test_invoke_code_review_chain.py +70 -0
- package/scripts/test_invoke_code_review_cli.py +192 -0
- package/scripts/test_invoke_code_review_contract.py +256 -0
- package/scripts/test_invoke_code_review_git.py +123 -0
- package/scripts/test_invoke_code_review_mode.py +99 -0
- package/scripts/test_resolve_worker_spawn.py +1014 -1014
- package/scripts/tests/test_code_review_constants.py +80 -0
- package/scripts/tests/test_codex_capability_bridge.py +91 -0
- package/scripts/tests/test_codex_compat_materializer.py +632 -0
- package/scripts/tests/test_codex_compat_watcher.py +599 -0
- package/scripts/tests/test_sync_to_cursor.py +0 -1
- package/skills/CLAUDE.md +2 -0
- package/skills/auditing-claude-config/SKILL.md +114 -114
- package/skills/autoconverge/SKILL.md +427 -427
- package/skills/autoconverge/reference/convergence.md +24 -3
- package/skills/autoconverge/workflow/CLAUDE.md +1 -0
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +3 -3
- package/skills/autoconverge/workflow/converge.contract.test.mjs +1263 -1263
- package/skills/autoconverge/workflow/converge.mjs +168 -1
- package/skills/autoconverge/workflow/converge.p2-advance.test.mjs +202 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a11d903476b803493.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a26213978adeef6fb.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a3def0d15ed9d9110.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a41f41b1b708ee3b7.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a758b880abecc3ff7.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a8897b89656b1bd16.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-abd463d744a1437bc.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ad19d027ae8ee1816.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +265 -265
- package/skills/bugteam/reference/copilot-gap-analysis.md +1 -1
- package/skills/closeout/SKILL.md +33 -50
- package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +8 -0
- package/skills/codex-review/scripts/run_codex_review.py +233 -1
- package/skills/codex-review/scripts/test_run_codex_review.py +189 -0
- package/skills/condensing-instructions/SKILL.md +72 -0
- package/skills/copilot-review/SKILL.md +119 -119
- package/skills/e-code-review/SKILL.md +52 -0
- package/skills/e-code-review/reference/fix.md +54 -0
- package/skills/e-code-review/reference/loop.md +43 -0
- package/skills/e-code-review/reference/low.md +57 -0
- package/skills/e-code-review/reference/medium.md +153 -0
- package/skills/e-code-review/reference/xhigh.md +182 -0
- package/skills/e-simplify/SKILL.md +97 -0
- package/skills/fresh-branch/CLAUDE.md +1 -1
- package/skills/fresh-branch/SKILL.md +5 -6
- package/skills/fresh-branch/scripts/create_fresh_branch.py +42 -24
- package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +1 -3
- package/skills/fresh-branch/scripts/test_create_fresh_branch.py +30 -126
- package/skills/issue-tracker/SKILL.md +92 -0
- package/skills/issue-tracker/reference/epic-and-sub-issue-model.md +55 -0
- package/skills/issue-tracker/reference/handoff-schema.md +64 -0
- package/skills/issue-tracker/reference/operation-matrix.md +41 -0
- package/skills/orchestrator/SKILL.md +177 -22
- package/skills/orchestrator/scripts/status_gate.py +625 -0
- package/skills/orchestrator/scripts/status_gate_constants/__init__.py +1 -0
- package/skills/orchestrator/scripts/status_gate_constants/config/__init__.py +1 -0
- package/skills/orchestrator/scripts/status_gate_constants/config/constants.py +47 -0
- package/skills/orchestrator/scripts/test_status_gate.py +439 -0
- package/skills/orchestrator-refresh/SKILL.md +129 -35
- package/skills/plan-to-pr/SKILL.md +155 -0
- package/skills/plan-to-pr/reference/final-validation-tasks.md +15 -0
- package/skills/plan-to-pr/reference/model-routing.md +36 -0
- package/skills/plan-to-pr/reference/packet-contract.md +43 -0
- package/skills/plan-to-pr/reference/packet-schema.json +57 -0
- package/skills/plan-to-pr/reference/process-inventory.md +22 -0
- package/skills/plan-to-pr/reference/review-loop.md +33 -0
- package/skills/plan-to-pr/reference/run-record.schema.json +27 -0
- package/skills/plan-to-pr/reference/self-audit-tasks.md +15 -0
- package/skills/plan-to-pr/reference/task-seeds.md +14 -0
- package/skills/plan-to-pr/reference/task-ticket.md +38 -0
- package/skills/plan-to-pr/scripts/config/__init__.py +1 -0
- package/skills/plan-to-pr/scripts/config/constants.py +193 -0
- package/skills/plan-to-pr/scripts/create_packet.py +173 -0
- package/skills/plan-to-pr/scripts/test_create_packet.py +102 -0
- package/skills/plan-to-pr/scripts/test_validate_packet.py +256 -0
- package/skills/plan-to-pr/scripts/test_validate_protocol.py +135 -0
- package/skills/plan-to-pr/scripts/test_validate_run.py +158 -0
- package/skills/plan-to-pr/scripts/validate_packet.py +655 -0
- package/skills/plan-to-pr/scripts/validate_protocol.py +622 -0
- package/skills/plan-to-pr/scripts/validate_run.py +173 -0
- package/skills/plan-to-pr/test_skill_contract.py +207 -0
- package/skills/plan-to-pr/test_task_ticket_contract.py +151 -0
- package/skills/pr-converge/SKILL.md +472 -469
- package/skills/pr-converge/reference/examples.md +3 -3
- package/skills/pr-converge/reference/fix-protocol.md +1 -1
- package/skills/pr-converge/reference/ground-rules.md +7 -4
- package/skills/pr-converge/reference/multi-pr-orchestration.md +4 -1
- package/skills/pr-converge/reference/per-tick.md +5 -5
- package/skills/pr-converge/reference/progress-checklist.md +1 -1
- package/skills/pr-converge/scripts/check_convergence_gates.py +279 -279
- package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -507
- package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -84
- package/skills/pr-converge/test_step5_host_branch.py +1 -1
- package/skills/pr-fix-protocol/SKILL.md +1 -1
- package/skills/privacy-hygiene/SKILL.md +68 -68
- package/skills/privacy-hygiene/reference/sweep-procedure.md +1 -1
- package/skills/prototype/workflows/promotion.md +1 -1
- package/skills/release-notes-html/SKILL.md +164 -0
- package/skills/session-log/SKILL.md +1 -1
- package/skills/task-build/CLAUDE.md +8 -7
- package/skills/task-build/SKILL.md +16 -8
- package/skills/task-build/reference/tool-routing.md +19 -0
- package/rules/claude-md-orphan-file.md +0 -28
- package/rules/cleanup-command-forms.md +0 -23
- package/rules/code-reviews.md +0 -11
- package/rules/env-var-table-code-drift.md +0 -10
- package/rules/gh-body-file.md +0 -5
- package/rules/gh-paginate.md +0 -3
- package/rules/hook-prose-matches-detector.md +0 -15
- package/rules/no-historical-clutter.md +0 -26
- package/rules/no-inline-destructive-literals.md +0 -9
- package/rules/no-justification-noise.md +0 -61
- package/rules/package-inventory-stale-entry.md +0 -25
- package/rules/right-sized-engineering.md +0 -28
- package/rules/self-contained-docs.md +0 -17
- package/rules/shell-invocation-policy.md +0 -5
- package/rules/tdd.md +0 -7
- 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"
|