claude-dev-env 2.4.0 → 2.5.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 +53 -49
- package/_shared/pr-loop/scripts/_claude_permissions_common.py +84 -0
- package/_shared/pr-loop/scripts/code_rules_gate.py +4 -2
- 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/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 +2 -1
- 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 +0 -1
- 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/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/commands/implement.md +1 -1
- package/commands/right-size.md +1 -1
- package/docs/CLAUDE.md +1 -0
- package/docs/host-pool-health-monitor.md +102 -0
- package/docs/references/CLAUDE.md +4 -2
- package/docs/references/advisor-tool.md +13 -0
- package/docs/references/code-review-enforcement.md +10 -0
- package/docs/references/team-advisor-skill.md +14 -0
- package/hooks/blocking/CLAUDE.md +1 -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 +4 -1
- package/hooks/blocking/config/test_code_review_enforcement_constants.py +5 -0
- package/hooks/blocking/config/verified_commit_constants.py +160 -159
- package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +256 -0
- package/hooks/blocking/pre_tool_use_dispatcher.py +24 -24
- 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_orchestrator_refresh_reschedule_gate.py +231 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +10 -1
- package/hooks/blocking/test_verdict_directory_write_blocker.py +808 -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/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/test_pre_push.py +25 -0
- package/hooks/hooks.json +10 -0
- package/hooks/hooks_constants/CLAUDE.md +2 -1
- package/hooks/hooks_constants/enter_worktree_prefetch_constants.py +18 -18
- package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +48 -0
- package/hooks/hooks_constants/ruff_integration_constants.py +16 -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 +10 -2
- package/rules/CLAUDE.md +1 -0
- package/rules/docstring-prose-matches-implementation.md +45 -44
- package/rules/state-what-is.md +25 -0
- package/rules/verified-commit-gate-skip.md +1 -1
- package/scripts/CLAUDE.md +1 -0
- package/scripts/Capture-PoolHealth.ps1 +410 -0
- package/scripts/_code_review_test_support.py +404 -0
- package/scripts/claude_chain_runner.py +141 -1
- 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/resolve_worker_spawn.py +626 -626
- package/scripts/spawn_grok_batch.py +672 -672
- package/scripts/test_claude_chain_runner.py +131 -0
- 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/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 +167 -0
- 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/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 +81 -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/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 +162 -21
- 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 +110 -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/prototype/workflows/promotion.md +1 -1
- package/skills/release-notes-html/SKILL.md +164 -0
- 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/scripts/test_invoke_code_review.py +0 -966
- package/skills/closeout/reference/issue-body-templates.md +0 -108
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
"""Tests for orchestrator status_gate (status file + single-pending re-arm)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib.util
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from types import ModuleType
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
SCRIPTS_DIRECTORY = Path(__file__).resolve().parent
|
|
14
|
+
if str(SCRIPTS_DIRECTORY) not in sys.path:
|
|
15
|
+
sys.path.insert(0, str(SCRIPTS_DIRECTORY))
|
|
16
|
+
|
|
17
|
+
from status_gate_constants.config.constants import ( # noqa: E402
|
|
18
|
+
ALL_DEFAULT_STATUS_DIRECTORY_PARTS,
|
|
19
|
+
EXIT_CODE_STOP,
|
|
20
|
+
EXIT_CODE_SUCCESS,
|
|
21
|
+
REARM_PENDING_FIELD_NAME,
|
|
22
|
+
REASON_ACTIVE,
|
|
23
|
+
REASON_MISSING_STATUS_FILE,
|
|
24
|
+
REASON_REARM_ALREADY_PENDING,
|
|
25
|
+
REASON_STATUS_NOT_ACTIVE,
|
|
26
|
+
RUN_STATUS_ACTIVE,
|
|
27
|
+
RUN_STATUS_DONE,
|
|
28
|
+
STATUS_FIELD_NAME,
|
|
29
|
+
STATUS_FILE_ENV_VAR,
|
|
30
|
+
STATUS_FILE_NAME,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def load_status_gate_module() -> ModuleType:
|
|
35
|
+
module_path = SCRIPTS_DIRECTORY / "status_gate.py"
|
|
36
|
+
spec = importlib.util.spec_from_file_location("status_gate", module_path)
|
|
37
|
+
assert spec is not None
|
|
38
|
+
assert spec.loader is not None
|
|
39
|
+
status_gate_module = importlib.util.module_from_spec(spec)
|
|
40
|
+
sys.modules[spec.name] = status_gate_module
|
|
41
|
+
spec.loader.exec_module(status_gate_module)
|
|
42
|
+
return status_gate_module
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class TestWriteAndDecide:
|
|
46
|
+
def should_allow_reschedule_when_status_is_active(
|
|
47
|
+
self, temporary_directory: Path
|
|
48
|
+
) -> None:
|
|
49
|
+
status_gate = load_status_gate_module()
|
|
50
|
+
status_file_path = temporary_directory / "run-status.json"
|
|
51
|
+
status_gate.write_status_file(
|
|
52
|
+
status_file_path, RUN_STATUS_ACTIVE, "", is_rearm_pending=False
|
|
53
|
+
)
|
|
54
|
+
is_allowed, reason_code = status_gate.decide_should_reschedule(status_file_path)
|
|
55
|
+
assert is_allowed is True
|
|
56
|
+
assert reason_code == REASON_ACTIVE
|
|
57
|
+
|
|
58
|
+
def should_stop_when_status_is_done(self, temporary_directory: Path) -> None:
|
|
59
|
+
status_gate = load_status_gate_module()
|
|
60
|
+
status_file_path = temporary_directory / "run-status.json"
|
|
61
|
+
status_gate.write_status_file(
|
|
62
|
+
status_file_path, RUN_STATUS_DONE, "", is_rearm_pending=False
|
|
63
|
+
)
|
|
64
|
+
is_allowed, reason_code = status_gate.decide_should_reschedule(status_file_path)
|
|
65
|
+
assert is_allowed is False
|
|
66
|
+
assert reason_code == REASON_STATUS_NOT_ACTIVE
|
|
67
|
+
|
|
68
|
+
def should_fail_closed_when_status_file_missing(
|
|
69
|
+
self, temporary_directory: Path
|
|
70
|
+
) -> None:
|
|
71
|
+
status_gate = load_status_gate_module()
|
|
72
|
+
status_file_path = temporary_directory / "missing.json"
|
|
73
|
+
is_allowed, reason_code = status_gate.decide_should_reschedule(status_file_path)
|
|
74
|
+
assert is_allowed is False
|
|
75
|
+
assert reason_code == REASON_MISSING_STATUS_FILE
|
|
76
|
+
|
|
77
|
+
def should_fail_closed_when_status_file_is_invalid_json(
|
|
78
|
+
self, temporary_directory: Path
|
|
79
|
+
) -> None:
|
|
80
|
+
status_gate = load_status_gate_module()
|
|
81
|
+
status_file_path = temporary_directory / "bad.json"
|
|
82
|
+
status_file_path.write_text("{not-json", encoding="utf-8")
|
|
83
|
+
is_allowed, reason_code = status_gate.decide_should_reschedule(status_file_path)
|
|
84
|
+
assert is_allowed is False
|
|
85
|
+
|
|
86
|
+
def should_deny_reschedule_when_rearm_already_pending(
|
|
87
|
+
self, temporary_directory: Path
|
|
88
|
+
) -> None:
|
|
89
|
+
status_gate = load_status_gate_module()
|
|
90
|
+
status_file_path = temporary_directory / "run-status.json"
|
|
91
|
+
status_gate.write_status_file(
|
|
92
|
+
status_file_path, RUN_STATUS_ACTIVE, "", is_rearm_pending=True
|
|
93
|
+
)
|
|
94
|
+
is_allowed, reason_code = status_gate.decide_should_reschedule(status_file_path)
|
|
95
|
+
assert is_allowed is False
|
|
96
|
+
assert reason_code == REASON_REARM_ALREADY_PENDING
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class TestSetPreservesPending:
|
|
100
|
+
def should_preserve_rearm_pending_when_reasserting_active(
|
|
101
|
+
self, temporary_directory: Path, monkeypatch: pytest.MonkeyPatch
|
|
102
|
+
) -> None:
|
|
103
|
+
status_gate = load_status_gate_module()
|
|
104
|
+
status_file_path = temporary_directory / "status.json"
|
|
105
|
+
status_gate.write_status_file(
|
|
106
|
+
status_file_path, RUN_STATUS_ACTIVE, "demo", is_rearm_pending=False
|
|
107
|
+
)
|
|
108
|
+
assert status_gate.claim_rearm_slot(status_file_path, "demo")[0] is True
|
|
109
|
+
monkeypatch.setattr(
|
|
110
|
+
sys,
|
|
111
|
+
"argv",
|
|
112
|
+
[
|
|
113
|
+
"status_gate.py",
|
|
114
|
+
"set",
|
|
115
|
+
"--status-file",
|
|
116
|
+
str(status_file_path),
|
|
117
|
+
"--status",
|
|
118
|
+
RUN_STATUS_ACTIVE,
|
|
119
|
+
"--run-slug",
|
|
120
|
+
"demo",
|
|
121
|
+
],
|
|
122
|
+
)
|
|
123
|
+
assert status_gate.main() == EXIT_CODE_SUCCESS
|
|
124
|
+
payload = json.loads(status_file_path.read_text(encoding="utf-8"))
|
|
125
|
+
assert payload[REARM_PENDING_FIELD_NAME] is True
|
|
126
|
+
is_allowed, reason_code = status_gate.decide_should_reschedule(
|
|
127
|
+
status_file_path
|
|
128
|
+
)
|
|
129
|
+
assert is_allowed is False
|
|
130
|
+
assert reason_code == REASON_REARM_ALREADY_PENDING
|
|
131
|
+
|
|
132
|
+
def should_clear_rearm_pending_when_setting_done(
|
|
133
|
+
self, temporary_directory: Path, monkeypatch: pytest.MonkeyPatch
|
|
134
|
+
) -> None:
|
|
135
|
+
status_gate = load_status_gate_module()
|
|
136
|
+
status_file_path = temporary_directory / "status.json"
|
|
137
|
+
status_gate.write_status_file(
|
|
138
|
+
status_file_path, RUN_STATUS_ACTIVE, "", is_rearm_pending=True
|
|
139
|
+
)
|
|
140
|
+
monkeypatch.setattr(
|
|
141
|
+
sys,
|
|
142
|
+
"argv",
|
|
143
|
+
[
|
|
144
|
+
"status_gate.py",
|
|
145
|
+
"set",
|
|
146
|
+
"--status-file",
|
|
147
|
+
str(status_file_path),
|
|
148
|
+
"--status",
|
|
149
|
+
RUN_STATUS_DONE,
|
|
150
|
+
],
|
|
151
|
+
)
|
|
152
|
+
assert status_gate.main() == EXIT_CODE_SUCCESS
|
|
153
|
+
payload = json.loads(status_file_path.read_text(encoding="utf-8"))
|
|
154
|
+
assert payload[STATUS_FIELD_NAME] == RUN_STATUS_DONE
|
|
155
|
+
assert payload[REARM_PENDING_FIELD_NAME] is False
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class TestBeginClaimRelease:
|
|
159
|
+
def should_claim_rearm_only_once_until_begin_firing(
|
|
160
|
+
self, temporary_directory: Path
|
|
161
|
+
) -> None:
|
|
162
|
+
status_gate = load_status_gate_module()
|
|
163
|
+
status_file_path = temporary_directory / "run-status.json"
|
|
164
|
+
status_gate.write_status_file(
|
|
165
|
+
status_file_path, RUN_STATUS_ACTIVE, "demo", is_rearm_pending=False
|
|
166
|
+
)
|
|
167
|
+
is_claimed, reason_code, payload = status_gate.claim_rearm_slot(
|
|
168
|
+
status_file_path, "demo"
|
|
169
|
+
)
|
|
170
|
+
assert is_claimed is True
|
|
171
|
+
assert payload is not None
|
|
172
|
+
assert payload[REARM_PENDING_FIELD_NAME] is True
|
|
173
|
+
|
|
174
|
+
is_claimed_again, second_reason, _second_payload = status_gate.claim_rearm_slot(
|
|
175
|
+
status_file_path, "demo"
|
|
176
|
+
)
|
|
177
|
+
assert is_claimed_again is False
|
|
178
|
+
assert second_reason == REASON_REARM_ALREADY_PENDING
|
|
179
|
+
|
|
180
|
+
is_firing, firing_reason, firing_payload = status_gate.begin_firing(
|
|
181
|
+
status_file_path, "demo"
|
|
182
|
+
)
|
|
183
|
+
assert is_firing is True
|
|
184
|
+
assert firing_payload is not None
|
|
185
|
+
assert firing_payload[REARM_PENDING_FIELD_NAME] is False
|
|
186
|
+
assert firing_reason
|
|
187
|
+
|
|
188
|
+
is_claimed_after_firing, _claim_reason, after_payload = (
|
|
189
|
+
status_gate.claim_rearm_slot(status_file_path, "demo")
|
|
190
|
+
)
|
|
191
|
+
assert is_claimed_after_firing is True
|
|
192
|
+
assert after_payload is not None
|
|
193
|
+
assert after_payload[REARM_PENDING_FIELD_NAME] is True
|
|
194
|
+
|
|
195
|
+
def should_release_rearm_after_failed_schedule(
|
|
196
|
+
self, temporary_directory: Path
|
|
197
|
+
) -> None:
|
|
198
|
+
status_gate = load_status_gate_module()
|
|
199
|
+
status_file_path = temporary_directory / "run-status.json"
|
|
200
|
+
status_gate.write_status_file(
|
|
201
|
+
status_file_path, RUN_STATUS_ACTIVE, "", is_rearm_pending=False
|
|
202
|
+
)
|
|
203
|
+
assert status_gate.claim_rearm_slot(status_file_path, "")[0] is True
|
|
204
|
+
is_released, reason_code, payload = status_gate.release_rearm_slot(
|
|
205
|
+
status_file_path, ""
|
|
206
|
+
)
|
|
207
|
+
assert is_released is True
|
|
208
|
+
assert payload is not None
|
|
209
|
+
assert payload[REARM_PENDING_FIELD_NAME] is False
|
|
210
|
+
assert reason_code
|
|
211
|
+
is_allowed, allow_reason = status_gate.decide_should_reschedule(
|
|
212
|
+
status_file_path
|
|
213
|
+
)
|
|
214
|
+
assert is_allowed is True
|
|
215
|
+
assert allow_reason == REASON_ACTIVE
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class TestResolveAndCli:
|
|
219
|
+
def should_resolve_explicit_status_file_path(
|
|
220
|
+
self, temporary_directory: Path
|
|
221
|
+
) -> None:
|
|
222
|
+
status_gate = load_status_gate_module()
|
|
223
|
+
target_path = temporary_directory / "explicit.json"
|
|
224
|
+
resolved_path = status_gate.resolve_status_file_path(
|
|
225
|
+
str(target_path), None, ""
|
|
226
|
+
)
|
|
227
|
+
assert resolved_path == target_path.resolve()
|
|
228
|
+
|
|
229
|
+
def should_resolve_default_path_under_base_directory(
|
|
230
|
+
self, temporary_directory: Path
|
|
231
|
+
) -> None:
|
|
232
|
+
status_gate = load_status_gate_module()
|
|
233
|
+
resolved_path = status_gate.resolve_status_file_path(
|
|
234
|
+
None, temporary_directory, ""
|
|
235
|
+
)
|
|
236
|
+
expected_path = temporary_directory.joinpath(
|
|
237
|
+
*ALL_DEFAULT_STATUS_DIRECTORY_PARTS, STATUS_FILE_NAME
|
|
238
|
+
).resolve()
|
|
239
|
+
assert resolved_path == expected_path
|
|
240
|
+
|
|
241
|
+
def should_scope_default_path_by_run_slug(
|
|
242
|
+
self, temporary_directory: Path
|
|
243
|
+
) -> None:
|
|
244
|
+
status_gate = load_status_gate_module()
|
|
245
|
+
run_slug = "demo-run"
|
|
246
|
+
resolved_path = status_gate.resolve_status_file_path(
|
|
247
|
+
None, temporary_directory, run_slug
|
|
248
|
+
)
|
|
249
|
+
expected_path = temporary_directory.joinpath(
|
|
250
|
+
*ALL_DEFAULT_STATUS_DIRECTORY_PARTS, run_slug, STATUS_FILE_NAME
|
|
251
|
+
).resolve()
|
|
252
|
+
assert resolved_path == expected_path
|
|
253
|
+
|
|
254
|
+
def should_set_and_gate_via_main(
|
|
255
|
+
self,
|
|
256
|
+
temporary_directory: Path,
|
|
257
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
258
|
+
) -> None:
|
|
259
|
+
status_gate = load_status_gate_module()
|
|
260
|
+
status_file_path = temporary_directory / "status.json"
|
|
261
|
+
monkeypatch.setattr(
|
|
262
|
+
sys,
|
|
263
|
+
"argv",
|
|
264
|
+
[
|
|
265
|
+
"status_gate.py",
|
|
266
|
+
"set",
|
|
267
|
+
"--status-file",
|
|
268
|
+
str(status_file_path),
|
|
269
|
+
"--status",
|
|
270
|
+
RUN_STATUS_ACTIVE,
|
|
271
|
+
"--run-slug",
|
|
272
|
+
"demo-run",
|
|
273
|
+
],
|
|
274
|
+
)
|
|
275
|
+
assert status_gate.main() == EXIT_CODE_SUCCESS
|
|
276
|
+
payload = json.loads(status_file_path.read_text(encoding="utf-8"))
|
|
277
|
+
assert payload[STATUS_FIELD_NAME] == RUN_STATUS_ACTIVE
|
|
278
|
+
assert payload[REARM_PENDING_FIELD_NAME] is False
|
|
279
|
+
|
|
280
|
+
monkeypatch.setattr(
|
|
281
|
+
sys,
|
|
282
|
+
"argv",
|
|
283
|
+
[
|
|
284
|
+
"status_gate.py",
|
|
285
|
+
"should-reschedule",
|
|
286
|
+
"--status-file",
|
|
287
|
+
str(status_file_path),
|
|
288
|
+
],
|
|
289
|
+
)
|
|
290
|
+
assert status_gate.main() == EXIT_CODE_SUCCESS
|
|
291
|
+
|
|
292
|
+
monkeypatch.setattr(
|
|
293
|
+
sys,
|
|
294
|
+
"argv",
|
|
295
|
+
[
|
|
296
|
+
"status_gate.py",
|
|
297
|
+
"claim-rearm",
|
|
298
|
+
"--status-file",
|
|
299
|
+
str(status_file_path),
|
|
300
|
+
],
|
|
301
|
+
)
|
|
302
|
+
assert status_gate.main() == EXIT_CODE_SUCCESS
|
|
303
|
+
monkeypatch.setattr(
|
|
304
|
+
sys,
|
|
305
|
+
"argv",
|
|
306
|
+
[
|
|
307
|
+
"status_gate.py",
|
|
308
|
+
"claim-rearm",
|
|
309
|
+
"--status-file",
|
|
310
|
+
str(status_file_path),
|
|
311
|
+
],
|
|
312
|
+
)
|
|
313
|
+
assert status_gate.main() == EXIT_CODE_STOP
|
|
314
|
+
monkeypatch.setattr(
|
|
315
|
+
sys,
|
|
316
|
+
"argv",
|
|
317
|
+
[
|
|
318
|
+
"status_gate.py",
|
|
319
|
+
"should-reschedule",
|
|
320
|
+
"--status-file",
|
|
321
|
+
str(status_file_path),
|
|
322
|
+
],
|
|
323
|
+
)
|
|
324
|
+
assert status_gate.main() == EXIT_CODE_STOP
|
|
325
|
+
|
|
326
|
+
monkeypatch.setattr(
|
|
327
|
+
sys,
|
|
328
|
+
"argv",
|
|
329
|
+
[
|
|
330
|
+
"status_gate.py",
|
|
331
|
+
"begin-firing",
|
|
332
|
+
"--status-file",
|
|
333
|
+
str(status_file_path),
|
|
334
|
+
],
|
|
335
|
+
)
|
|
336
|
+
assert status_gate.main() == EXIT_CODE_SUCCESS
|
|
337
|
+
monkeypatch.setattr(
|
|
338
|
+
sys,
|
|
339
|
+
"argv",
|
|
340
|
+
[
|
|
341
|
+
"status_gate.py",
|
|
342
|
+
"should-reschedule",
|
|
343
|
+
"--status-file",
|
|
344
|
+
str(status_file_path),
|
|
345
|
+
],
|
|
346
|
+
)
|
|
347
|
+
assert status_gate.main() == EXIT_CODE_SUCCESS
|
|
348
|
+
|
|
349
|
+
monkeypatch.setattr(
|
|
350
|
+
sys,
|
|
351
|
+
"argv",
|
|
352
|
+
[
|
|
353
|
+
"status_gate.py",
|
|
354
|
+
"set",
|
|
355
|
+
"--status-file",
|
|
356
|
+
str(status_file_path),
|
|
357
|
+
"--status",
|
|
358
|
+
RUN_STATUS_DONE,
|
|
359
|
+
],
|
|
360
|
+
)
|
|
361
|
+
assert status_gate.main() == EXIT_CODE_SUCCESS
|
|
362
|
+
monkeypatch.setattr(
|
|
363
|
+
sys,
|
|
364
|
+
"argv",
|
|
365
|
+
[
|
|
366
|
+
"status_gate.py",
|
|
367
|
+
"should-reschedule",
|
|
368
|
+
"--status-file",
|
|
369
|
+
str(status_file_path),
|
|
370
|
+
],
|
|
371
|
+
)
|
|
372
|
+
assert status_gate.main() == EXIT_CODE_STOP
|
|
373
|
+
|
|
374
|
+
def should_resolve_status_file_from_environment(
|
|
375
|
+
self, temporary_directory: Path, monkeypatch: pytest.MonkeyPatch
|
|
376
|
+
) -> None:
|
|
377
|
+
status_gate = load_status_gate_module()
|
|
378
|
+
status_file_path = temporary_directory / "env-status.json"
|
|
379
|
+
monkeypatch.setenv(STATUS_FILE_ENV_VAR, str(status_file_path))
|
|
380
|
+
monkeypatch.setattr(
|
|
381
|
+
sys,
|
|
382
|
+
"argv",
|
|
383
|
+
["status_gate.py", "set", "--status", RUN_STATUS_ACTIVE],
|
|
384
|
+
)
|
|
385
|
+
assert status_gate.main() == EXIT_CODE_SUCCESS
|
|
386
|
+
assert status_file_path.is_file()
|
|
387
|
+
is_allowed, _reason = status_gate.decide_should_reschedule(status_file_path)
|
|
388
|
+
assert is_allowed is True
|
|
389
|
+
|
|
390
|
+
def should_scope_set_and_should_reschedule_with_matching_run_slug(
|
|
391
|
+
self, temporary_directory: Path, monkeypatch: pytest.MonkeyPatch
|
|
392
|
+
) -> None:
|
|
393
|
+
status_gate = load_status_gate_module()
|
|
394
|
+
monkeypatch.chdir(temporary_directory)
|
|
395
|
+
monkeypatch.delenv(STATUS_FILE_ENV_VAR, raising=False)
|
|
396
|
+
run_slug = "demo-run"
|
|
397
|
+
monkeypatch.setattr(
|
|
398
|
+
sys,
|
|
399
|
+
"argv",
|
|
400
|
+
[
|
|
401
|
+
"status_gate.py",
|
|
402
|
+
"set",
|
|
403
|
+
"--status",
|
|
404
|
+
RUN_STATUS_ACTIVE,
|
|
405
|
+
"--run-slug",
|
|
406
|
+
run_slug,
|
|
407
|
+
],
|
|
408
|
+
)
|
|
409
|
+
assert status_gate.main() == EXIT_CODE_SUCCESS
|
|
410
|
+
slug_path = status_gate.resolve_status_file_path(
|
|
411
|
+
None, temporary_directory, run_slug
|
|
412
|
+
)
|
|
413
|
+
default_path = status_gate.resolve_status_file_path(
|
|
414
|
+
None, temporary_directory, ""
|
|
415
|
+
)
|
|
416
|
+
assert slug_path.is_file()
|
|
417
|
+
assert not default_path.is_file()
|
|
418
|
+
monkeypatch.setattr(
|
|
419
|
+
sys,
|
|
420
|
+
"argv",
|
|
421
|
+
[
|
|
422
|
+
"status_gate.py",
|
|
423
|
+
"should-reschedule",
|
|
424
|
+
"--run-slug",
|
|
425
|
+
run_slug,
|
|
426
|
+
],
|
|
427
|
+
)
|
|
428
|
+
assert status_gate.main() == EXIT_CODE_SUCCESS
|
|
429
|
+
monkeypatch.setattr(
|
|
430
|
+
sys,
|
|
431
|
+
"argv",
|
|
432
|
+
["status_gate.py", "should-reschedule"],
|
|
433
|
+
)
|
|
434
|
+
assert status_gate.main() == EXIT_CODE_STOP
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
@pytest.fixture
|
|
438
|
+
def temporary_directory(tmp_path: Path) -> Path:
|
|
439
|
+
return tmp_path
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: orchestrator-refresh
|
|
3
3
|
description: >-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
Re-assert orchestrator discipline on a one-shot delayed wake: ledger
|
|
5
|
+
reconcile, advisor routing, warm executor reuse, single-pending re-arm
|
|
6
|
+
via status_gate. Terminates when the gate says stop. Triggers:
|
|
7
|
+
'/orchestrator-refresh', orchestrator-refresh, refresh the orchestrator
|
|
8
|
+
loop, re-arm orchestrator.
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
# Orchestrator Refresh
|
|
@@ -15,35 +15,110 @@ Detect the host profile first (see Host profiles in
|
|
|
15
15
|
Re-assert the discipline for that host only — do not invent an Agent-tool
|
|
16
16
|
Claude `session-advisor` spawn on a third-party host.
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
18
|
+
## 0. status_gate first (deterministic)
|
|
19
|
+
|
|
20
|
+
Script home (prefer install path, else package checkout):
|
|
21
|
+
|
|
22
|
+
- `%USERPROFILE%/.claude/skills/orchestrator/scripts/status_gate.py`
|
|
23
|
+
- `~/.claude/skills/orchestrator/scripts/status_gate.py`
|
|
24
|
+
- `skills/orchestrator/scripts/status_gate.py`
|
|
25
|
+
|
|
26
|
+
Pass the same `--run-slug` used at activate, if any.
|
|
27
|
+
|
|
28
|
+
### 0a. begin-firing (consume prior re-arm)
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
python <status_gate.py> begin-firing [--run-slug SLUG]
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
| Exit | Action |
|
|
35
|
+
|---|---|
|
|
36
|
+
| **1** | Stop. Cancel matching host schedules for `/orchestrator-refresh` if the host allows. Report inactive/done. **Do not** re-arm. Do not spawn. |
|
|
37
|
+
| **0** | Latch cleared. Continue with steps 1–6. |
|
|
38
|
+
|
|
39
|
+
### 0b. Done after ledger (step 1)
|
|
40
|
+
|
|
41
|
+
After ledger reconcile: if every task is completed/cancelled and no
|
|
42
|
+
executor is running:
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
python <status_gate.py> set --status done [--run-slug SLUG]
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Cancel matching host schedules; stop without re-arming.
|
|
49
|
+
|
|
50
|
+
## Discipline steps
|
|
51
|
+
|
|
52
|
+
1. **Reconcile the task ledger first.** Call `TaskList` after the gate.
|
|
53
|
+
The ledger is stale when any of these holds: a running or finished
|
|
54
|
+
executor has no `in_progress` task naming it as owner; a finished
|
|
55
|
+
executor's task is still open (or was closed without its result
|
|
56
|
+
merged); the next phase you will dispatch has no pending task; a
|
|
57
|
+
`blockedBy` link contradicts the actual run order. Fix every mismatch
|
|
58
|
+
with TaskCreate / TaskUpdate in this same firing — never defer.
|
|
59
|
+
2. **You are the orchestrator.** Orchestrate and hold the user
|
|
60
|
+
conversation; spawn executor subagents for every code edit and build
|
|
61
|
+
or test run.
|
|
62
|
+
- **Worker model.** Every coding spawn sets `model: sonnet` on a
|
|
63
|
+
Claude host, or the resolver-printed sonnet-equivalent id on a
|
|
64
|
+
third-party host, and fails closed — see Workflow Agent Routing in
|
|
65
|
+
[`skills/orchestrator/SKILL.md`](../orchestrator/SKILL.md#workflow-agent-routing).
|
|
66
|
+
- **Focused tickets.** One mechanical done-check per ticket; resume a
|
|
67
|
+
warm agent with a thin next-slice ticket rather than a fresh cold
|
|
68
|
+
spawn, and keep thick context in the assignment file.
|
|
29
69
|
3. **Hard decisions go to the shared advisor.**
|
|
30
70
|
- **Claude host:** executors consult the warm `session-advisor` via
|
|
31
|
-
`SendMessage`
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
- **Third-party host:**
|
|
35
|
-
session (`claude_chain_runner.py`, Fable high then Opus
|
|
36
|
-
spawn `session-advisor` via Agent
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
71
|
+
`SendMessage` (ENDORSE / CORRECTION / PLAN / STOP). This session
|
|
72
|
+
routes the same way; keep tool use to orchestration and light
|
|
73
|
+
verification reads.
|
|
74
|
+
- **Third-party host:** advisor is a max-tier Claude CLI bind owned
|
|
75
|
+
by this session (`claude_chain_runner.py`, Fable high then Opus
|
|
76
|
+
max). Do **not** spawn `session-advisor` via Agent. Executors
|
|
77
|
+
report blockers here; consult the CLI advisor and relay signals.
|
|
78
|
+
If the CLI bind is unreachable, fail closed — do not answer the
|
|
79
|
+
four signals as this third-party session.
|
|
80
|
+
4. **Resume before you spawn.** `SendMessage` an existing *executor* by
|
|
81
|
+
name or `agentId` before a cold spawn. (Third-party: executor reuse
|
|
82
|
+
only — advisor stays on the CLI chain.)
|
|
83
|
+
5. **Fresh spawn only for a genuine task switch.** Never tell an agent
|
|
84
|
+
to compact for a clean context.
|
|
85
|
+
6. **Single-pending re-arm only.** Same protocol as the orchestrator
|
|
86
|
+
skill (host-agnostic; **create then claim**):
|
|
87
|
+
|
|
88
|
+
1. Cancel matching schedules only when the host can list/cancel by
|
|
89
|
+
prompt (`/orchestrator-refresh` + `--run-slug` if used). On Claude,
|
|
90
|
+
skip selective cancel — the latch is the sole stacking enforcement.
|
|
91
|
+
2. `python <status_gate.py> should-reschedule [--run-slug SLUG]`
|
|
92
|
+
- Exit **1** → stop; do not schedule.
|
|
93
|
+
- Exit **0** → continue.
|
|
94
|
+
3. Create **exactly one** non-recurring delayed wake (~1200–2700s)
|
|
95
|
+
with prompt `/orchestrator-refresh` (plus `--run-slug` when used).
|
|
96
|
+
Host one-shot tool only (on Claude: `ScheduleWakeup`). Never
|
|
97
|
+
recurring / never cadence / never a second create this firing.
|
|
98
|
+
4. `python <status_gate.py> claim-rearm [--run-slug SLUG]` right
|
|
99
|
+
after a successful create. Exit 1 → cancel that schedule and stop.
|
|
100
|
+
5. If create fails: do not claim; stop or retry once from cancel.
|
|
101
|
+
|
|
102
|
+
## Gotchas
|
|
103
|
+
|
|
104
|
+
- **Stacking loops.** A second schedule create while one is already
|
|
105
|
+
queued multiplies firings. Cancel → `should-reschedule` → one create
|
|
106
|
+
→ `claim-rearm`. Never claim before create on Claude (PreToolUse
|
|
107
|
+
denies `ScheduleWakeup` when pending).
|
|
108
|
+
- **Skipping `begin-firing`.** Prior `rearm_pending` stays set; re-arm
|
|
109
|
+
stays denied. Always run step 0a first.
|
|
110
|
+
- **Create without claim.** Skip claim after create and a second create
|
|
111
|
+
can stack. Claim immediately after success.
|
|
112
|
+
|
|
113
|
+
## File Index
|
|
114
|
+
|
|
115
|
+
| File | Purpose |
|
|
116
|
+
|---|---|
|
|
117
|
+
| `SKILL.md` | Refresh firing steps; points at orchestrator `status_gate.py`. |
|
|
118
|
+
|
|
119
|
+
## Folder Map
|
|
120
|
+
|
|
121
|
+
- `SKILL.md` — this skill (thin); gate implementation lives under
|
|
122
|
+
`skills/orchestrator/scripts/`.
|
|
123
|
+
- Advisor policy:
|
|
124
|
+
[`_shared/advisor/advisor-protocol.md`](../../_shared/advisor/advisor-protocol.md).
|