claude-dev-env 2.7.1 → 2.8.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/_shared/CLAUDE.md +1 -0
- package/_shared/advisor/advisor-protocol.md +19 -9
- package/_shared/pr-loop/audit-contract.md +4 -4
- package/_shared/pr-loop/precatch-rubric.md +2 -2
- package/_shared/process-tree/CLAUDE.md +41 -0
- package/_shared/process-tree/scripts/config/process_tree_scripts_constants/__init__.py +1 -0
- package/_shared/process-tree/scripts/config/process_tree_scripts_constants/process_tree_kill_constants.py +27 -0
- package/_shared/process-tree/scripts/process_tree_kill.py +141 -0
- package/_shared/process-tree/scripts/pyproject.toml +16 -0
- package/_shared/process-tree/scripts/test_process_tree_kill.py +278 -0
- package/agents/code-quality-agent.md +6 -5
- package/agents/deep-research.md +7 -24
- package/agents/docs-agent.md +1 -27
- package/agents/issue-tracker.md +1 -7
- package/agents/skill-writer-agent.md +1 -2
- package/agents/test_agent_frontmatter.py +309 -12
- package/hooks/blocking/CLAUDE.md +2 -0
- package/hooks/blocking/fable_spawn_gate.py +187 -0
- package/hooks/blocking/piped_pytest_blocker.py +1223 -0
- package/hooks/blocking/plain_language_blocker.py +287 -15
- package/hooks/blocking/test_fable_spawn_gate.py +374 -0
- package/hooks/blocking/test_piped_pytest_blocker.py +587 -0
- package/hooks/blocking/test_plain_language_blocker.py +277 -2
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +34 -2
- package/hooks/git-hooks/CLAUDE.md +2 -2
- package/hooks/git-hooks/git_hooks_constants/__init__.py +28 -0
- package/hooks/git-hooks/pre_push.py +343 -54
- package/hooks/git-hooks/test_pre_push.py +852 -6
- package/hooks/hooks.json +9 -19
- package/hooks/hooks_constants/CLAUDE.md +2 -0
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +1 -0
- package/hooks/hooks_constants/fable_spawn_gate_constants.py +62 -0
- package/hooks/hooks_constants/piped_pytest_blocker_constants.py +360 -0
- package/hooks/hooks_constants/plain_language_blocker_constants.py +64 -1
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +10 -0
- package/hooks/hooks_constants/shell_command_segments.py +1 -1
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +1 -0
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +27 -0
- package/hooks/hooks_constants/test_prose_metrics_parity.py +124 -0
- package/package.json +1 -1
- package/rules/CLAUDE.md +1 -0
- package/rules/ask-user-question-required.md +26 -0
- package/rules/claims-as-quotes.md +65 -0
- package/scripts/CLAUDE.md +1 -1
- package/scripts/_code_review_test_support.py +6 -0
- package/scripts/check.ps1 +18 -5
- package/scripts/claude_chain_runner.py +203 -31
- package/scripts/codec_forwarding_test_support.py +2 -0
- package/scripts/dev_env_scripts_constants/CLAUDE.md +4 -4
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +38 -0
- package/scripts/dev_env_scripts_constants/code_review_constants.py +403 -2
- package/scripts/dev_env_scripts_constants/grok_worker_constants.py +28 -24
- package/scripts/grok_headless_runner.py +16 -83
- package/scripts/spawn_grok_batch.py +38 -1
- package/scripts/test_claude_chain_runner.py +358 -0
- package/scripts/test_grok_headless_runner.py +18 -63
- package/scripts/test_invoke_code_review.py +298 -0
- package/scripts/test_resolve_worker_spawn.py +6 -0
- package/scripts/test_spawn_grok_batch.py +101 -0
- package/scripts/tests/CLAUDE.md +1 -0
- package/scripts/tests/test_grok_worker_constants.py +59 -0
- package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +46 -0
- package/skills/autoconverge/workflow/converge.contract.test.mjs +105 -2
- package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +34 -1
- package/skills/autoconverge/workflow/converge.mjs +66 -33
- package/skills/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +0 -4
- package/skills/codex-review/scripts/codex_usage_probe.py +20 -33
- package/skills/codex-review/scripts/run_codex_review.py +16 -64
- package/skills/codex-review/scripts/test_codex_usage_probe.py +46 -41
- package/skills/codex-review/scripts/test_run_codex_review.py +1 -33
- package/skills/e-code-review/SKILL.md +9 -8
- package/skills/e-code-review/reference/fix.md +29 -7
- package/skills/e-code-review/reference/loop.md +230 -14
- package/skills/e-code-review/reference/low.md +33 -15
- package/skills/e-code-review/reference/medium.md +55 -21
- package/skills/e-code-review/reference/xhigh.md +30 -12
- package/skills/fresh-branch/CLAUDE.md +5 -5
- package/skills/fresh-branch/SKILL.md +14 -6
- package/skills/fresh-branch/scripts/create_fresh_branch.py +122 -39
- package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +10 -3
- package/skills/fresh-branch/scripts/test_create_fresh_branch.py +251 -0
- package/skills/grok-spawn/SKILL.md +5 -0
- package/skills/orchestrator/SKILL.md +4 -1
- package/skills/orchestrator-refresh/SKILL.md +5 -1
- package/skills/team-advisor/SKILL.md +4 -1
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
"""Unit tests for the fable_spawn_gate PreToolUse hook.
|
|
2
|
+
|
|
3
|
+
Covers the decision table. A spawn at model ``fable`` whose prompt lacks the
|
|
4
|
+
authorization marker is denied, and the deny reason points at the
|
|
5
|
+
advisor-protocol document while carrying no copy of the token — one row holds
|
|
6
|
+
the reason token-free, and its anti-corollary feeds that reason back as a
|
|
7
|
+
retry prompt and expects a second denial. The same spawn carrying the marker
|
|
8
|
+
is allowed. A spawn at any other model tier, and a spawn with no ``model``
|
|
9
|
+
field, pass whether or not the marker is present. One test reads
|
|
10
|
+
``hooks.json`` and asserts the gate is registered on a matcher covering both
|
|
11
|
+
spawn tool names.
|
|
12
|
+
|
|
13
|
+
Token pins read the warm-up section and each consuming skill. Two doc-gate
|
|
14
|
+
agreement tests assemble a spawn prompt out of the advisor-protocol wording a
|
|
15
|
+
warm-up bind and a drift re-spawn follow, and run each through the gate, so
|
|
16
|
+
wording that stops naming the marker fails here. Two deny-path tests read the
|
|
17
|
+
preview the gate hands the block logger and hold it bounded and scoped to the
|
|
18
|
+
model field.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import importlib.util
|
|
24
|
+
import io
|
|
25
|
+
import json
|
|
26
|
+
import pathlib
|
|
27
|
+
import sys
|
|
28
|
+
from typing import Any
|
|
29
|
+
from unittest import mock
|
|
30
|
+
|
|
31
|
+
import pytest
|
|
32
|
+
|
|
33
|
+
_HOOK_DIR = pathlib.Path(__file__).parent
|
|
34
|
+
_HOOKS_TREE = _HOOK_DIR.parent
|
|
35
|
+
for each_path in (str(_HOOK_DIR), str(_HOOKS_TREE)):
|
|
36
|
+
if each_path not in sys.path:
|
|
37
|
+
sys.path.insert(0, each_path)
|
|
38
|
+
|
|
39
|
+
hook_spec = importlib.util.spec_from_file_location(
|
|
40
|
+
"fable_spawn_gate",
|
|
41
|
+
_HOOK_DIR / "fable_spawn_gate.py",
|
|
42
|
+
)
|
|
43
|
+
assert hook_spec is not None
|
|
44
|
+
assert hook_spec.loader is not None
|
|
45
|
+
hook_module = importlib.util.module_from_spec(hook_spec)
|
|
46
|
+
hook_spec.loader.exec_module(hook_module)
|
|
47
|
+
|
|
48
|
+
from hooks_constants.fable_spawn_gate_constants import (
|
|
49
|
+
ADVISOR_PROTOCOL_DOCUMENT_PATH,
|
|
50
|
+
AGENT_TOOL_NAME,
|
|
51
|
+
CALLING_HOOK_NAME,
|
|
52
|
+
DENY_ADDITIONAL_CONTEXT,
|
|
53
|
+
FABLE_MODEL_ALIAS,
|
|
54
|
+
FABLE_SPAWN_AUTHORIZATION_MARKER,
|
|
55
|
+
TASK_TOOL_NAME,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
_ADVISOR_SUBAGENT_TYPE = "session-advisor"
|
|
59
|
+
_OPUS_MODEL_ALIAS = "opus"
|
|
60
|
+
_SONNET_MODEL_ALIAS = "sonnet"
|
|
61
|
+
_TITLE_CASE_FABLE_MODEL = "Fable"
|
|
62
|
+
_FULL_FABLE_MODEL_ID = "claude-fable-5"
|
|
63
|
+
_FULL_SONNET_MODEL_ID = "claude-sonnet-4-5"
|
|
64
|
+
_PACKAGE_ROOT = _HOOKS_TREE.parent
|
|
65
|
+
_ADVISOR_PROTOCOL_PATH = _PACKAGE_ROOT / "_shared" / "advisor" / "advisor-protocol.md"
|
|
66
|
+
_ADVISOR_PROTOCOL_TEXT = _ADVISOR_PROTOCOL_PATH.read_text(encoding="utf-8")
|
|
67
|
+
_ALL_CONSUMING_SKILL_NAMES = ("team-advisor", "orchestrator", "orchestrator-refresh")
|
|
68
|
+
_ALL_CONSUMING_SKILL_PATHS = tuple(
|
|
69
|
+
_PACKAGE_ROOT / "skills" / each_skill_name / "SKILL.md"
|
|
70
|
+
for each_skill_name in _ALL_CONSUMING_SKILL_NAMES
|
|
71
|
+
)
|
|
72
|
+
_skill_pin_ids = _ALL_CONSUMING_SKILL_NAMES
|
|
73
|
+
_BIND_SECTION_HEADING = "## Warm-up (once per session)"
|
|
74
|
+
_MARKDOWN_SECTION_HEADING_PREFIX = "\n## "
|
|
75
|
+
_RESPAWN_PARAGRAPH_MARKER = "**Re-spawn on drift.**"
|
|
76
|
+
_WARM_UP_PROMPT_BULLET_MARKER = "- `prompt`: the charter below."
|
|
77
|
+
_PARAGRAPH_SEPARATOR = "\n\n"
|
|
78
|
+
_MULTI_KILOBYTE_PROMPT = "Audit the changed lines and report each finding. " * 200
|
|
79
|
+
_MAXIMUM_DENY_PREVIEW_LENGTH = 120
|
|
80
|
+
_MARKER_STATE_FIELD_NAME = "marker_present"
|
|
81
|
+
_PROMPT_BODY_SAMPLE = "Audit the changed lines"
|
|
82
|
+
_UNMARKED_PROMPT = "Audit the changed lines and report each finding with its path and line."
|
|
83
|
+
_ADVISOR_BIND_PROMPT = (
|
|
84
|
+
"You are the standing session advisor for this run. "
|
|
85
|
+
f"{FABLE_SPAWN_AUTHORIZATION_MARKER} "
|
|
86
|
+
"Answer every consult with ENDORSE, CORRECTION, PLAN, or STOP."
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _spawn_payload(
|
|
91
|
+
*,
|
|
92
|
+
tool_name: str = AGENT_TOOL_NAME,
|
|
93
|
+
model: str | None = FABLE_MODEL_ALIAS,
|
|
94
|
+
prompt: str = _UNMARKED_PROMPT,
|
|
95
|
+
) -> dict[str, Any]:
|
|
96
|
+
"""Build a PreToolUse spawn payload for the decision table.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
tool_name: The spawning tool name.
|
|
100
|
+
model: The model alias, or None to leave the field out entirely.
|
|
101
|
+
prompt: The spawn prompt text.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
The payload mapping a hook reads from stdin.
|
|
105
|
+
"""
|
|
106
|
+
all_tool_input: dict[str, Any] = {
|
|
107
|
+
"subagent_type": _ADVISOR_SUBAGENT_TYPE,
|
|
108
|
+
"description": "Bind the session advisor",
|
|
109
|
+
"prompt": prompt,
|
|
110
|
+
}
|
|
111
|
+
if model is not None:
|
|
112
|
+
all_tool_input["model"] = model
|
|
113
|
+
return {"tool_name": tool_name, "tool_input": all_tool_input}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _run_main_with_io(input_text: str) -> str:
|
|
117
|
+
"""Run the gate's main against stdin text and return its stdout.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
input_text: The raw stdin text the gate reads.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Everything the gate wrote to stdout — empty on an allow.
|
|
124
|
+
"""
|
|
125
|
+
with mock.patch("sys.stdin", io.StringIO(input_text)):
|
|
126
|
+
with mock.patch("sys.stdout", new_callable=io.StringIO) as captured_stdout:
|
|
127
|
+
try:
|
|
128
|
+
hook_module.main()
|
|
129
|
+
except SystemExit:
|
|
130
|
+
pass
|
|
131
|
+
return captured_stdout.getvalue()
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _decision_for(payload: dict[str, Any]) -> str:
|
|
135
|
+
"""Return the gate's permissionDecision for one spawn payload.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
payload: The PreToolUse payload to feed the gate.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
The decision string from the deny payload.
|
|
142
|
+
"""
|
|
143
|
+
captured_output = _run_main_with_io(json.dumps(payload))
|
|
144
|
+
return json.loads(captured_output)["hookSpecificOutput"]["permissionDecision"]
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def test_should_deny_fable_spawn_without_the_marker() -> None:
|
|
148
|
+
assert _decision_for(_spawn_payload()) == "deny"
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def test_should_allow_fable_spawn_carrying_the_marker() -> None:
|
|
152
|
+
payload = _spawn_payload(prompt=_ADVISOR_BIND_PROMPT)
|
|
153
|
+
assert _run_main_with_io(json.dumps(payload)) == ""
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def test_should_allow_opus_spawn_without_the_marker() -> None:
|
|
157
|
+
payload = _spawn_payload(model=_OPUS_MODEL_ALIAS)
|
|
158
|
+
assert _run_main_with_io(json.dumps(payload)) == ""
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def test_should_allow_sonnet_spawn_carrying_the_marker() -> None:
|
|
162
|
+
payload = _spawn_payload(model=_SONNET_MODEL_ALIAS, prompt=_ADVISOR_BIND_PROMPT)
|
|
163
|
+
assert _run_main_with_io(json.dumps(payload)) == ""
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def test_should_allow_spawn_with_no_model_field() -> None:
|
|
167
|
+
payload = _spawn_payload(model=None)
|
|
168
|
+
assert _run_main_with_io(json.dumps(payload)) == ""
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def test_should_deny_title_case_fable_spawn_without_the_marker() -> None:
|
|
172
|
+
assert _decision_for(_spawn_payload(model=_TITLE_CASE_FABLE_MODEL)) == "deny"
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def test_should_deny_full_model_id_fable_spawn_without_the_marker() -> None:
|
|
176
|
+
assert _decision_for(_spawn_payload(model=_FULL_FABLE_MODEL_ID)) == "deny"
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def test_should_allow_full_model_id_fable_spawn_carrying_the_marker() -> None:
|
|
180
|
+
payload = _spawn_payload(model=_FULL_FABLE_MODEL_ID, prompt=_ADVISOR_BIND_PROMPT)
|
|
181
|
+
assert _run_main_with_io(json.dumps(payload)) == ""
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def test_should_allow_full_model_id_sonnet_spawn_without_the_marker() -> None:
|
|
185
|
+
payload = _spawn_payload(model=_FULL_SONNET_MODEL_ID)
|
|
186
|
+
assert _run_main_with_io(json.dumps(payload)) == ""
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def test_should_deny_fable_task_spawn_without_the_marker() -> None:
|
|
190
|
+
assert _decision_for(_spawn_payload(tool_name=TASK_TOOL_NAME)) == "deny"
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def test_should_deny_fable_spawn_whose_prompt_field_is_absent() -> None:
|
|
194
|
+
payload: dict[str, Any] = {
|
|
195
|
+
"tool_name": AGENT_TOOL_NAME,
|
|
196
|
+
"tool_input": {
|
|
197
|
+
"subagent_type": _ADVISOR_SUBAGENT_TYPE,
|
|
198
|
+
"model": FABLE_MODEL_ALIAS,
|
|
199
|
+
},
|
|
200
|
+
}
|
|
201
|
+
assert _decision_for(payload) == "deny"
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _deny_reason_for(payload: dict[str, Any]) -> str:
|
|
205
|
+
"""Return the permissionDecisionReason the gate writes for one payload.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
payload: The PreToolUse payload the gate denies.
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
The deny reason text.
|
|
212
|
+
"""
|
|
213
|
+
captured_output = _run_main_with_io(json.dumps(payload))
|
|
214
|
+
return str(
|
|
215
|
+
json.loads(captured_output)["hookSpecificOutput"]["permissionDecisionReason"]
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def test_deny_reason_names_the_document_without_the_token() -> None:
|
|
220
|
+
deny_reason = _deny_reason_for(_spawn_payload())
|
|
221
|
+
assert ADVISOR_PROTOCOL_DOCUMENT_PATH in deny_reason
|
|
222
|
+
assert FABLE_SPAWN_AUTHORIZATION_MARKER not in deny_reason
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _additional_context_for(payload: dict[str, Any]) -> str:
|
|
226
|
+
"""Return hookSpecificOutput.additionalContext for a denied spawn."""
|
|
227
|
+
captured_output = _run_main_with_io(json.dumps(payload))
|
|
228
|
+
return str(json.loads(captured_output)["hookSpecificOutput"]["additionalContext"])
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def test_deny_payload_carries_additional_context_for_the_spawner() -> None:
|
|
232
|
+
"""A denial injects recovery steps into additionalContext for the spawner."""
|
|
233
|
+
additional_context = _additional_context_for(_spawn_payload())
|
|
234
|
+
assert additional_context == DENY_ADDITIONAL_CONTEXT
|
|
235
|
+
assert "fable-spawn-gate" in additional_context
|
|
236
|
+
assert "opus" in additional_context.lower()
|
|
237
|
+
assert "opus-equivalent" in additional_context.lower()
|
|
238
|
+
assert ADVISOR_PROTOCOL_DOCUMENT_PATH in additional_context
|
|
239
|
+
assert FABLE_SPAWN_AUTHORIZATION_MARKER not in additional_context
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def test_spawn_prompt_carrying_the_deny_text_still_denies() -> None:
|
|
243
|
+
deny_reason = _deny_reason_for(_spawn_payload())
|
|
244
|
+
assert _decision_for(_spawn_payload(prompt=deny_reason)) == "deny"
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def test_should_allow_when_the_payload_is_malformed_json() -> None:
|
|
248
|
+
assert _run_main_with_io("not valid json {{{") == ""
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _denial_preview_for(payload: dict[str, Any]) -> str:
|
|
252
|
+
"""Return the offending-input preview the gate hands the block logger.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
payload: The PreToolUse payload the gate denies.
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
The ``offending_input_preview`` argument of the logged block.
|
|
259
|
+
"""
|
|
260
|
+
with mock.patch.object(hook_module, "log_hook_block") as recorded_block:
|
|
261
|
+
_run_main_with_io(json.dumps(payload))
|
|
262
|
+
return str(recorded_block.call_args.kwargs["offending_input_preview"])
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def test_deny_preview_stays_bounded_on_a_multi_kilobyte_prompt() -> None:
|
|
266
|
+
preview = _denial_preview_for(_spawn_payload(prompt=_MULTI_KILOBYTE_PROMPT))
|
|
267
|
+
assert len(preview) <= _MAXIMUM_DENY_PREVIEW_LENGTH
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def test_deny_preview_carries_the_model_and_marker_state_without_the_prompt() -> None:
|
|
271
|
+
preview = _denial_preview_for(_spawn_payload(prompt=_MULTI_KILOBYTE_PROMPT))
|
|
272
|
+
assert FABLE_MODEL_ALIAS in preview
|
|
273
|
+
assert _MARKER_STATE_FIELD_NAME in preview
|
|
274
|
+
assert _PROMPT_BODY_SAMPLE not in preview
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _matchers_registering_the_gate() -> list[str]:
|
|
278
|
+
"""Return every PreToolUse matcher whose group runs this gate.
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
The matcher strings of the registration groups naming the gate script.
|
|
282
|
+
"""
|
|
283
|
+
manifest_record = json.loads(
|
|
284
|
+
(_HOOKS_TREE / "hooks.json").read_text(encoding="utf-8")
|
|
285
|
+
)
|
|
286
|
+
all_matchers: list[str] = []
|
|
287
|
+
for each_group in manifest_record["hooks"]["PreToolUse"]:
|
|
288
|
+
for each_hook in each_group.get("hooks", []):
|
|
289
|
+
if CALLING_HOOK_NAME in each_hook.get("command", ""):
|
|
290
|
+
all_matchers.append(each_group.get("matcher", ""))
|
|
291
|
+
return all_matchers
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def test_gate_is_registered_on_a_matcher_covering_both_spawn_tools() -> None:
|
|
295
|
+
all_matchers = _matchers_registering_the_gate()
|
|
296
|
+
assert len(all_matchers) == 1
|
|
297
|
+
assert AGENT_TOOL_NAME in all_matchers[0]
|
|
298
|
+
assert TASK_TOOL_NAME in all_matchers[0]
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _bind_section_text() -> str:
|
|
302
|
+
"""Return the advisor-protocol warm-up section that documents the bind.
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
The section text running from its heading to the next heading.
|
|
306
|
+
"""
|
|
307
|
+
section_start = _ADVISOR_PROTOCOL_TEXT.index(_BIND_SECTION_HEADING)
|
|
308
|
+
section_body = _ADVISOR_PROTOCOL_TEXT[section_start + len(_BIND_SECTION_HEADING) :]
|
|
309
|
+
next_heading_start = section_body.find(_MARKDOWN_SECTION_HEADING_PREFIX)
|
|
310
|
+
if next_heading_start < 0:
|
|
311
|
+
return section_body
|
|
312
|
+
return section_body[:next_heading_start]
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def test_advisor_protocol_bind_section_names_the_marker_token() -> None:
|
|
316
|
+
assert FABLE_SPAWN_AUTHORIZATION_MARKER in _bind_section_text()
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
@pytest.mark.parametrize("skill_path", _ALL_CONSUMING_SKILL_PATHS, ids=_skill_pin_ids)
|
|
320
|
+
def test_consuming_skill_names_the_marker_token(skill_path: pathlib.Path) -> None:
|
|
321
|
+
assert FABLE_SPAWN_AUTHORIZATION_MARKER in skill_path.read_text(encoding="utf-8")
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _paragraph_starting_at(paragraph_marker: str) -> str:
|
|
325
|
+
"""Return the advisor-protocol paragraph that opens with a marker.
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
paragraph_marker: The literal text opening the paragraph.
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
The paragraph text, running from that marker to the blank line that
|
|
332
|
+
closes it.
|
|
333
|
+
"""
|
|
334
|
+
paragraph_body = _ADVISOR_PROTOCOL_TEXT[
|
|
335
|
+
_ADVISOR_PROTOCOL_TEXT.index(paragraph_marker) :
|
|
336
|
+
]
|
|
337
|
+
paragraph_end = paragraph_body.find(_PARAGRAPH_SEPARATOR)
|
|
338
|
+
if paragraph_end < 0:
|
|
339
|
+
return paragraph_body
|
|
340
|
+
return paragraph_body[:paragraph_end]
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _respawn_spawn_prompt() -> str:
|
|
344
|
+
"""Assemble the spawn prompt a drift re-spawn writes from the protocol.
|
|
345
|
+
|
|
346
|
+
The prompt comes from the re-spawn paragraph alone, so the gate reads
|
|
347
|
+
what that one paragraph tells a session to send.
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
The spawn prompt text a session following that paragraph sends.
|
|
351
|
+
"""
|
|
352
|
+
return _paragraph_starting_at(_RESPAWN_PARAGRAPH_MARKER)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def test_respawn_paragraph_prompt_passes_the_gate_at_the_fable_tier() -> None:
|
|
356
|
+
payload = _spawn_payload(prompt=_respawn_spawn_prompt())
|
|
357
|
+
assert _run_main_with_io(json.dumps(payload)) == ""
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _warm_up_spawn_prompt() -> str:
|
|
361
|
+
"""Assemble the spawn prompt a warm-up bind writes from the protocol.
|
|
362
|
+
|
|
363
|
+
The prompt comes from the spawn-field prompt bullet alone, so the gate
|
|
364
|
+
reads what that one bullet tells a session to send.
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
The spawn prompt text a session following that bullet sends.
|
|
368
|
+
"""
|
|
369
|
+
return _paragraph_starting_at(_WARM_UP_PROMPT_BULLET_MARKER)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def test_warm_up_prompt_bullet_passes_the_gate_at_the_fable_tier() -> None:
|
|
373
|
+
payload = _spawn_payload(prompt=_warm_up_spawn_prompt())
|
|
374
|
+
assert _run_main_with_io(json.dumps(payload)) == ""
|