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.
Files changed (195) hide show
  1. package/CLAUDE.md +53 -49
  2. package/_shared/pr-loop/scripts/_claude_permissions_common.py +84 -0
  3. package/_shared/pr-loop/scripts/code_rules_gate.py +4 -2
  4. package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +306 -306
  5. package/_shared/pr-loop/scripts/pr_loop_shared_constants/claude_permissions_constants.py +44 -0
  6. package/_shared/pr-loop/scripts/pr_loop_shared_constants/copilot_quota_constants.py +24 -24
  7. package/_shared/pr-loop/scripts/pr_loop_shared_constants/stale_worktree_rule_sweep_constants.py +107 -107
  8. package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +290 -48
  9. package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +42 -2
  10. package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +36 -0
  11. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +100 -1
  12. package/_shared/pr-loop/scripts/tests/test_fix_hookspath.py +497 -497
  13. package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +311 -2
  14. package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep.py +301 -301
  15. package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep_constants.py +85 -85
  16. package/_shared/pr-loop/worker-spawn.md +1 -1
  17. package/agents/CLAUDE.md +2 -1
  18. package/agents/caveman.md +0 -1
  19. package/agents/clasp-deployment-orchestrator.md +0 -1
  20. package/agents/clean-coder.md +0 -1
  21. package/agents/code-advisor.md +0 -1
  22. package/agents/code-quality-agent.md +1 -2
  23. package/agents/code-verifier.md +0 -1
  24. package/agents/deep-research.md +0 -1
  25. package/agents/docs-agent.md +0 -1
  26. package/agents/git-commit-crafter.md +0 -1
  27. package/agents/issue-tracker.md +42 -0
  28. package/agents/plan-packet-validator.md +0 -1
  29. package/agents/pr-description-writer.md +0 -1
  30. package/agents/test_agent_frontmatter.py +67 -18
  31. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +143 -141
  32. package/bin/CLAUDE.md +68 -5
  33. package/bin/ever-shipped-skills.mjs +1 -0
  34. package/bin/install-constants.mjs +88 -0
  35. package/bin/install.mjs +1138 -114
  36. package/bin/install.prune.test.mjs +869 -19
  37. package/bin/install.test.mjs +906 -2
  38. package/commands/implement.md +1 -1
  39. package/commands/right-size.md +1 -1
  40. package/docs/CLAUDE.md +1 -0
  41. package/docs/host-pool-health-monitor.md +102 -0
  42. package/docs/references/CLAUDE.md +4 -2
  43. package/docs/references/advisor-tool.md +13 -0
  44. package/docs/references/code-review-enforcement.md +10 -0
  45. package/docs/references/team-advisor-skill.md +14 -0
  46. package/hooks/blocking/CLAUDE.md +1 -0
  47. package/hooks/blocking/code_review_pr_create_gate.py +7 -3
  48. package/hooks/blocking/code_review_push_gate.py +9 -4
  49. package/hooks/blocking/code_review_stamp_directory_write_blocker.py +8 -0
  50. package/hooks/blocking/config/__init__.py +5 -5
  51. package/hooks/blocking/config/code_review_enforcement_constants.py +4 -1
  52. package/hooks/blocking/config/test_code_review_enforcement_constants.py +5 -0
  53. package/hooks/blocking/config/verified_commit_constants.py +160 -159
  54. package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +256 -0
  55. package/hooks/blocking/pre_tool_use_dispatcher.py +24 -24
  56. package/hooks/blocking/test_code_review_pr_create_gate.py +14 -0
  57. package/hooks/blocking/test_code_review_push_gate.py +16 -0
  58. package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +19 -0
  59. package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +231 -0
  60. package/hooks/blocking/test_pre_tool_use_dispatcher.py +10 -1
  61. package/hooks/blocking/test_verdict_directory_write_blocker.py +808 -808
  62. package/hooks/blocking/test_verification_verdict_store.py +54 -0
  63. package/hooks/blocking/test_verified_commit_gate.py +581 -581
  64. package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -131
  65. package/hooks/blocking/verdict_directory_write_blocker.py +687 -687
  66. package/hooks/blocking/verification_verdict_store.py +1039 -1036
  67. package/hooks/blocking/verified_commit_message_accuracy_blocker.py +167 -167
  68. package/hooks/blocking/verifier_verdict_minter.py +280 -280
  69. package/hooks/git-hooks/test_pre_push.py +25 -0
  70. package/hooks/hooks.json +10 -0
  71. package/hooks/hooks_constants/CLAUDE.md +2 -1
  72. package/hooks/hooks_constants/enter_worktree_prefetch_constants.py +18 -18
  73. package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +48 -0
  74. package/hooks/hooks_constants/ruff_integration_constants.py +16 -0
  75. package/hooks/lifecycle/enter_worktree_origin_prefetch.py +163 -146
  76. package/hooks/lifecycle/test_enter_worktree_origin_prefetch.py +185 -178
  77. package/hooks/pyproject.toml +1 -0
  78. package/hooks/validators/CLAUDE.md +1 -0
  79. package/hooks/validators/config/__init__.py +0 -0
  80. package/hooks/validators/config/directory_exemption_constants.py +183 -0
  81. package/hooks/validators/config/test_directory_exemption_constants.py +21 -0
  82. package/hooks/validators/conftest.py +4 -0
  83. package/hooks/validators/ruff_integration.py +49 -5
  84. package/hooks/validators/run_all_validators.py +206 -9
  85. package/hooks/validators/test_directory_exemption_constants.py +185 -0
  86. package/hooks/validators/test_python_antipattern_checks.py +110 -5
  87. package/hooks/validators/test_ruff_integration.py +92 -1
  88. package/hooks/validators/test_run_all_validators.py +115 -68
  89. package/hooks/validators/test_run_all_validators_pretooluse.py +159 -1
  90. package/package.json +10 -2
  91. package/rules/CLAUDE.md +1 -0
  92. package/rules/docstring-prose-matches-implementation.md +45 -44
  93. package/rules/state-what-is.md +25 -0
  94. package/rules/verified-commit-gate-skip.md +1 -1
  95. package/scripts/CLAUDE.md +1 -0
  96. package/scripts/Capture-PoolHealth.ps1 +410 -0
  97. package/scripts/_code_review_test_support.py +404 -0
  98. package/scripts/claude_chain_runner.py +141 -1
  99. package/scripts/conftest.py +16 -1
  100. package/scripts/dev_env_scripts_constants/CLAUDE.md +1 -1
  101. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +9 -0
  102. package/scripts/resolve_worker_spawn.py +626 -626
  103. package/scripts/spawn_grok_batch.py +672 -672
  104. package/scripts/test_claude_chain_runner.py +131 -0
  105. package/scripts/test_invoke_code_review_chain.py +70 -0
  106. package/scripts/test_invoke_code_review_cli.py +192 -0
  107. package/scripts/test_invoke_code_review_contract.py +256 -0
  108. package/scripts/test_invoke_code_review_git.py +123 -0
  109. package/scripts/test_invoke_code_review_mode.py +99 -0
  110. package/scripts/test_resolve_worker_spawn.py +1014 -1014
  111. package/skills/CLAUDE.md +2 -0
  112. package/skills/auditing-claude-config/SKILL.md +114 -114
  113. package/skills/autoconverge/SKILL.md +427 -427
  114. package/skills/autoconverge/reference/convergence.md +24 -3
  115. package/skills/autoconverge/workflow/CLAUDE.md +1 -0
  116. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +3 -3
  117. package/skills/autoconverge/workflow/converge.contract.test.mjs +1263 -1263
  118. package/skills/autoconverge/workflow/converge.mjs +167 -0
  119. package/skills/autoconverge/workflow/converge.p2-advance.test.mjs +202 -0
  120. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a11d903476b803493.jsonl +2 -2
  121. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a26213978adeef6fb.jsonl +2 -2
  122. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a3def0d15ed9d9110.jsonl +2 -2
  123. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a41f41b1b708ee3b7.jsonl +2 -2
  124. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a758b880abecc3ff7.jsonl +2 -2
  125. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a8897b89656b1bd16.jsonl +2 -2
  126. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-abd463d744a1437bc.jsonl +2 -2
  127. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ad19d027ae8ee1816.jsonl +2 -2
  128. package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +265 -265
  129. package/skills/closeout/SKILL.md +33 -50
  130. package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +8 -0
  131. package/skills/codex-review/scripts/run_codex_review.py +233 -1
  132. package/skills/codex-review/scripts/test_run_codex_review.py +189 -0
  133. package/skills/condensing-instructions/SKILL.md +81 -0
  134. package/skills/copilot-review/SKILL.md +119 -119
  135. package/skills/e-code-review/SKILL.md +52 -0
  136. package/skills/e-code-review/reference/fix.md +54 -0
  137. package/skills/e-code-review/reference/loop.md +43 -0
  138. package/skills/e-code-review/reference/low.md +57 -0
  139. package/skills/e-code-review/reference/medium.md +153 -0
  140. package/skills/e-code-review/reference/xhigh.md +182 -0
  141. package/skills/e-simplify/SKILL.md +97 -0
  142. package/skills/issue-tracker/SKILL.md +92 -0
  143. package/skills/issue-tracker/reference/epic-and-sub-issue-model.md +55 -0
  144. package/skills/issue-tracker/reference/handoff-schema.md +64 -0
  145. package/skills/issue-tracker/reference/operation-matrix.md +41 -0
  146. package/skills/orchestrator/SKILL.md +162 -21
  147. package/skills/orchestrator/scripts/status_gate.py +625 -0
  148. package/skills/orchestrator/scripts/status_gate_constants/__init__.py +1 -0
  149. package/skills/orchestrator/scripts/status_gate_constants/config/__init__.py +1 -0
  150. package/skills/orchestrator/scripts/status_gate_constants/config/constants.py +47 -0
  151. package/skills/orchestrator/scripts/test_status_gate.py +439 -0
  152. package/skills/orchestrator-refresh/SKILL.md +110 -35
  153. package/skills/plan-to-pr/SKILL.md +155 -0
  154. package/skills/plan-to-pr/reference/final-validation-tasks.md +15 -0
  155. package/skills/plan-to-pr/reference/model-routing.md +36 -0
  156. package/skills/plan-to-pr/reference/packet-contract.md +43 -0
  157. package/skills/plan-to-pr/reference/packet-schema.json +57 -0
  158. package/skills/plan-to-pr/reference/process-inventory.md +22 -0
  159. package/skills/plan-to-pr/reference/review-loop.md +33 -0
  160. package/skills/plan-to-pr/reference/run-record.schema.json +27 -0
  161. package/skills/plan-to-pr/reference/self-audit-tasks.md +15 -0
  162. package/skills/plan-to-pr/reference/task-seeds.md +14 -0
  163. package/skills/plan-to-pr/reference/task-ticket.md +38 -0
  164. package/skills/plan-to-pr/scripts/config/__init__.py +1 -0
  165. package/skills/plan-to-pr/scripts/config/constants.py +193 -0
  166. package/skills/plan-to-pr/scripts/create_packet.py +173 -0
  167. package/skills/plan-to-pr/scripts/test_create_packet.py +102 -0
  168. package/skills/plan-to-pr/scripts/test_validate_packet.py +256 -0
  169. package/skills/plan-to-pr/scripts/test_validate_protocol.py +135 -0
  170. package/skills/plan-to-pr/scripts/test_validate_run.py +158 -0
  171. package/skills/plan-to-pr/scripts/validate_packet.py +655 -0
  172. package/skills/plan-to-pr/scripts/validate_protocol.py +622 -0
  173. package/skills/plan-to-pr/scripts/validate_run.py +173 -0
  174. package/skills/plan-to-pr/test_skill_contract.py +207 -0
  175. package/skills/plan-to-pr/test_task_ticket_contract.py +151 -0
  176. package/skills/pr-converge/SKILL.md +472 -469
  177. package/skills/pr-converge/reference/examples.md +3 -3
  178. package/skills/pr-converge/reference/fix-protocol.md +1 -1
  179. package/skills/pr-converge/reference/ground-rules.md +7 -4
  180. package/skills/pr-converge/reference/multi-pr-orchestration.md +4 -1
  181. package/skills/pr-converge/reference/per-tick.md +5 -5
  182. package/skills/pr-converge/reference/progress-checklist.md +1 -1
  183. package/skills/pr-converge/scripts/check_convergence_gates.py +279 -279
  184. package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -507
  185. package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -84
  186. package/skills/pr-converge/test_step5_host_branch.py +1 -1
  187. package/skills/pr-fix-protocol/SKILL.md +1 -1
  188. package/skills/privacy-hygiene/SKILL.md +68 -68
  189. package/skills/prototype/workflows/promotion.md +1 -1
  190. package/skills/release-notes-html/SKILL.md +164 -0
  191. package/skills/task-build/CLAUDE.md +8 -7
  192. package/skills/task-build/SKILL.md +16 -8
  193. package/skills/task-build/reference/tool-routing.md +19 -0
  194. package/scripts/test_invoke_code_review.py +0 -966
  195. 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
- Fired by the /orchestrator loop reminder about every 45 minutes to
5
- re-assert the advisor discipline mid-run: orchestrate, route hard decisions
6
- to the shared advisor (ENDORSE / CORRECTION / PLAN / STOP — SendMessage on
7
- Claude, Claude CLI chain on a third-party host), reuse warm agents. Triggers:
8
- '/orchestrator-refresh'.
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
- 1. **Reconcile the task ledger first.** Call `TaskList` before anything else
19
- this firing. The ledger is stale when any of these holds: a running or
20
- finished executor has no `in_progress` task naming it as owner; a finished
21
- executor's task is still open (or was closed without its result merged);
22
- the next phase you will dispatch has no pending task; a `blockedBy` link
23
- contradicts the actual run order. Fix every mismatch with TaskCreate /
24
- TaskUpdate in this same firing — never defer reconciliation to "when the
25
- agent reports".
26
- 2. **You are the orchestrator.** Orchestrate and hold the user conversation;
27
- spawn executor subagents to do all the work — every code edit and build or
28
- test run.
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` and receive one of four signals ENDORSE, CORRECTION, PLAN,
32
- or STOP. The orchestrating session routes its own hard decisions the same
33
- way and keeps its tool use to orchestration and light verification reads.
34
- - **Third-party host:** the advisor is a max-tier Claude CLI bind owned by this
35
- session (`claude_chain_runner.py`, Fable high then Opus max). Do **not**
36
- spawn `session-advisor` via Agent and do **not** tell executors to
37
- SendMessage a separate advisor agent. Executors report blockers to this
38
- session; consult the Claude CLI advisor and relay ENDORSE / CORRECTION /
39
- PLAN / STOP. When the CLI bind is unreachable, fail closed and report to
40
- the user do not answer the four signals as this third-party session.
41
- 4. **Resume before you spawn.** `SendMessage` an existing *executor* agent by
42
- name or `agentId` to reuse its warm context; prefer that over a cold spawn.
43
- (On a third-party host this is executor reuse only advisor re-bind stays on the CLI
44
- chain path in the shared protocol.)
45
- 5. **Fresh spawn only for a genuine task switch.** No tool compacts or clears a
46
- subagent's context, so a clean context comes from a fresh spawn — never tell
47
- an agent to compact.
48
- 5. **Re-schedule the next refresh** (about 1200 seconds out) when the loop
49
- mechanism needs each firing to queue the following one.
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).