claude-dev-env 8.59.0 → 8.60.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.
@@ -3,8 +3,9 @@ name: orchestrator
3
3
  description: >-
4
4
  Coordinate user goals, parent tasks, workers, evidence, and recovery.
5
5
  Triggers: /orchestrator, orchestrate, operate like a coordinator,
6
- track my goals, coordinate workers, retain goals across compaction.
7
- disable-model-invocation: true
6
+ track my goals, coordinate workers, retain goals across compaction,
7
+ before spawning a subagent, thread, or session, or starting work that
8
+ spans several turns.
8
9
  ---
9
10
 
10
11
  # Orchestrator
@@ -46,6 +47,7 @@ If a run is already active, retain its follow-up entry and answer without replac
46
47
  Create durable run state when work spans turns, has several goals, delegates, or waits on an external result.
47
48
  An explicit advisor-only request can restrict execution to workers for that run.
48
49
  For Claude Projects facts or reported coordinator mechanisms, read [platform evidence](reference/platform-evidence.md).
50
+ Before spawning a subagent, thread, or session, read [oversee delegated work](reference/oversee-delegated-work.md).
49
51
 
50
52
  ## Process
51
53
 
@@ -135,6 +137,7 @@ Report the result, evidence, and any remaining limit.
135
137
  | `.claude/CLAUDE.md` | Claude instruction import. |
136
138
  | `reference/run-state.md` | Goal records, task authority, follow list, and active-root registry. |
137
139
  | `reference/recovery.md` | Cold-start and compaction recovery. |
140
+ | `reference/oversee-delegated-work.md` | Spawner review of delegated work before it reaches the user. |
138
141
  | `reference/platform-evidence.md` | Official Projects sources and coordinator report boundaries. |
139
142
  | `reference/scheduling.md` | Optional existing gate commands and owned wake lifecycle. |
140
143
  | `reference/consult-the-orchestrator.md` | Executor consults and four-signal replies. |
@@ -0,0 +1,12 @@
1
+ # Oversee delegated work
2
+
3
+ Every agent that spawns a subagent, thread, or session owns that work until it reaches the user.
4
+ Agents make mistakes. The spawner is a fresh second set of eyes that catches and corrects them before they reach the user.
5
+ The `spawn_oversight_hook` adds this duty to every spawn, at every level of the tree.
6
+
7
+ - Brief each agent with the user's own words, the standards that apply, and the acceptance check.
8
+ - While it runs, read its progress. Wake a quiet agent with one concrete next step.
9
+ - Before its output reaches the user, check it against the user's words and standards yourself.
10
+ - Send the agent a correction when it misses, and check the corrected output the same way.
11
+ - Give each piece of a multi-piece task its own reviewer or helper.
12
+ - Show the user you are involved: say what you checked and what you corrected.
@@ -124,9 +124,10 @@ test('seeds one editable policy and one native Codex routing hook', () => {
124
124
  group => group.matcher === 'multi_agent_v1__spawn_agent',
125
125
  );
126
126
  assert.equal(firstRoutingGroups.length, 1);
127
- assert.equal(firstRoutingGroups[0].hooks.length, 2);
127
+ assert.equal(firstRoutingGroups[0].hooks.length, 3);
128
128
  assert.match(firstRoutingGroups[0].hooks[0].command, /subagent_model_routing\.mjs/);
129
129
  assert.match(firstRoutingGroups[0].hooks[1].command, /spawn_readiness_hook\.py/);
130
+ assert.match(firstRoutingGroups[0].hooks[2].command, /spawn_oversight_hook\.py/);
130
131
  const firstSpawnPromptGroups = firstCodexHooks.hooks.PreToolUse.filter(
131
132
  group => group.matcher === 'Agent|Task',
132
133
  );
@@ -1,10 +1,11 @@
1
1
  # Spawn routing
2
2
 
3
- This family reminds the agent to gather context before a spawn, adjusts thread requests when usage exceeds pace, and selects an allowed subagent model.
3
+ This family reminds the agent to gather context before a spawn, tells it that it owns the spawned work, adjusts thread requests when usage exceeds pace, and selects an allowed subagent model.
4
4
 
5
5
  ## Checks
6
6
 
7
7
  - `routing/spawn_readiness_hook.py` adds context to a spawn that comes before a read step, or before an answered question or a settled-scope line. The spawn still runs.
8
+ - `routing/spawn_oversight_hook.py` adds the orchestrator oversight directive to every spawn: watch the agent, check its output against the user's words and standards, and correct it before the output reaches the user. The spawn still runs.
8
9
  - `routing/thread_spawn_pace_hook.py` reshapes a thread request when usage is over pace or unreadable.
9
10
  - `routing/subagent_model_pin_hook.py` moves every Agent and Task subagent to Opus. A spawn that names Opus or Fable runs unchanged.
10
11
  - `routing/subagent_model_routing.mjs` allows a selected Luna model, remaps eligible requests, and denies unsupported model routing.
@@ -12,6 +13,7 @@ This family reminds the agent to gather context before a spawn, adjusts thread r
12
13
  ## When it fires
13
14
 
14
15
  - `routing/spawn_readiness_hook.py` runs on `PreToolUse` with matchers `Agent|Task`, `multi_agent_v1__spawn_agent`, `Workflow|mcp__github__actions_run_trigger`, and `mcp__hearthbot__start_thread_session`, each at `10` seconds in `hooks.json`. A workflow dispatch counts only when its inputs carry a `prompt`.
16
+ - `routing/spawn_oversight_hook.py` runs on `PreToolUse` with matchers `Agent|Task`, `multi_agent_v1__spawn_agent`, `Workflow|mcp__github__actions_run_trigger`, `mcp__hearthbot__start_thread_session`, and `mcp__hearthbot__start_rc_session`, each at `10` seconds in `hooks.json`. A workflow dispatch counts only when its inputs carry a `prompt`.
15
17
  - `routing/thread_spawn_pace_hook.py` runs on `PreToolUse`, matcher `mcp__hearthbot__start_thread_session`, timeout `30` seconds in `hooks.json`.
16
18
  - `routing/subagent_model_pin_hook.py` runs on `PreToolUse`, matcher `Agent|Task`, timeout `10` seconds in `hooks.json`.
17
19
  - `routing/subagent_model_routing.mjs` runs on `PreToolUse`, matcher `multi_agent_v1__spawn_agent`, timeout `10` seconds through the quoted Node command in `hooks.json`.
@@ -23,6 +25,7 @@ Preconditions:
23
25
  - Run each command from the repository root. Python and pytest cover the two Python scripts; Node covers the model router.
24
26
 
25
27
  - **Spawn readiness.** Input is a spawn after a read, a question, and its answer. Run `python -m pytest packages/claude-dev-env/hooks/routing/test_spawn_readiness_hook.py -q`. The adjacent test observes no output for a ready spawn and reminder context for each missing step.
28
+ - **Spawn oversight.** Input is a spawn from each registered tool, a subagent's own spawn, and a non-spawn call. Run `python -m pytest packages/claude-dev-env/hooks/routing/test_spawn_oversight_hook.py -q`. The adjacent test observes the directive for every spawn and no output for the other calls.
26
29
  - **Thread pace.** Input is a thread spawn with usage over pace. Run `python -m pytest packages/claude-dev-env/hooks/routing/test_thread_spawn_pace_hook.py -q`. The adjacent test observes updated tool input with the selected model, effort, and advisor line.
27
30
  - **Model pin.** Input is an Agent spawn that names a Sonnet model. Run `python -m pytest packages/claude-dev-env/hooks/routing/test_subagent_model_pin_hook.py -q`. The adjacent test observes updated tool input with model `opus`.
28
31
  - **Model routing.** Input is a `multi_agent_v1__spawn_agent` payload with a model choice. Run `node --test packages/claude-dev-env/hooks/routing/subagent_model_routing.test.mjs`. The adjacent test observes an allow or deny decision with the routed model.
@@ -30,5 +33,6 @@ Preconditions:
30
33
  ## Gotchas
31
34
 
32
35
  - `routing/spawn_readiness_hook.py` also runs after the pace hook on thread spawns. A reshaped request still gets the readiness check.
36
+ - The oversight hook fires inside subagents too, so each level of the tree oversees the level below it. It emits `additionalContext` only and never blocks.
33
37
  - The readiness hook emits `additionalContext` only, so it never blocks. It passes subagent calls, read-only helper types, a non-object tool input, and unreadable transcripts.
34
38
  - The model router allows an advisor flag to bypass the Luna restriction. Its ordinary path denies unresolved or unsupported models.
package/hooks/hooks.json CHANGED
@@ -19,6 +19,11 @@
19
19
  "type": "command",
20
20
  "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/routing/subagent_model_pin_hook.py",
21
21
  "timeout": 10
22
+ },
23
+ {
24
+ "type": "command",
25
+ "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/routing/spawn_oversight_hook.py",
26
+ "timeout": 10
22
27
  }
23
28
  ]
24
29
  },
@@ -34,6 +39,11 @@
34
39
  "type": "command",
35
40
  "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/routing/spawn_readiness_hook.py",
36
41
  "timeout": 10
42
+ },
43
+ {
44
+ "type": "command",
45
+ "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/routing/spawn_oversight_hook.py",
46
+ "timeout": 10
37
47
  }
38
48
  ]
39
49
  },
@@ -44,6 +54,11 @@
44
54
  "type": "command",
45
55
  "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/routing/spawn_readiness_hook.py",
46
56
  "timeout": 10
57
+ },
58
+ {
59
+ "type": "command",
60
+ "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/routing/spawn_oversight_hook.py",
61
+ "timeout": 10
47
62
  }
48
63
  ]
49
64
  },
@@ -159,6 +174,21 @@
159
174
  "type": "command",
160
175
  "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/routing/spawn_readiness_hook.py",
161
176
  "timeout": 10
177
+ },
178
+ {
179
+ "type": "command",
180
+ "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/routing/spawn_oversight_hook.py",
181
+ "timeout": 10
182
+ }
183
+ ]
184
+ },
185
+ {
186
+ "matcher": "mcp__hearthbot__start_rc_session",
187
+ "hooks": [
188
+ {
189
+ "type": "command",
190
+ "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/routing/spawn_oversight_hook.py",
191
+ "timeout": 10
162
192
  }
163
193
  ]
164
194
  },
@@ -0,0 +1,38 @@
1
+ """Shared main loop for PreToolUse hooks that only add context.
2
+
3
+ Reads the payload from stdin, asks the hook's decide function for an output,
4
+ and writes it as JSON. Exit status is always 0, so the tool call runs; a
5
+ decide function that raises leaves the call with no output.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import sys
12
+ from collections.abc import Callable
13
+
14
+ from hooks_constants.pre_tool_use_stdin import read_hook_input_dictionary_from_stdin
15
+
16
+
17
+ def run_context_hook(
18
+ decide_hook_output: Callable[[dict[str, object]], dict[str, object] | None],
19
+ ) -> int:
20
+ """Print the decided output for the stdin payload, or nothing.
21
+
22
+ Args:
23
+ decide_hook_output: Maps the parsed payload to an output or None.
24
+
25
+ Returns:
26
+ 0 in every case; the context travels in the JSON output.
27
+ """
28
+ hook_payload = read_hook_input_dictionary_from_stdin()
29
+ if hook_payload is None:
30
+ return 0
31
+ try:
32
+ hook_output = decide_hook_output(hook_payload)
33
+ except Exception:
34
+ return 0
35
+ if hook_output is not None:
36
+ sys.stdout.write(json.dumps(hook_output))
37
+ sys.stdout.flush()
38
+ return 0
@@ -0,0 +1,40 @@
1
+ """Constants for the spawn oversight PreToolUse hook.
2
+
3
+ Groups: the spawn tools it answers, the workflow dispatch that carries an
4
+ agent prompt, the payload keys it reads, and the
5
+ oversight directive it adds to every spawn.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ TOOL_NAME_KEY = "tool_name"
11
+ TOOL_INPUT_KEY = "tool_input"
12
+ PRE_TOOL_USE_EVENT_NAME = "PreToolUse"
13
+ ADDITIONAL_CONTEXT_KEY = "additionalContext"
14
+
15
+ WORKFLOW_DISPATCH_TOOL_NAME = "mcp__github__actions_run_trigger"
16
+ DISPATCH_METHOD_INPUT_KEY = "method"
17
+ DISPATCH_RUN_WORKFLOW_METHOD = "run_workflow"
18
+ DISPATCH_INPUTS_KEY = "inputs"
19
+ DISPATCH_PROMPT_INPUT_KEY = "prompt"
20
+
21
+ ALL_SPAWN_TOOL_NAMES = frozenset(
22
+ {
23
+ "Agent",
24
+ "Task",
25
+ "Workflow",
26
+ "multi_agent_v1__spawn_agent",
27
+ "mcp__hearthbot__start_thread_session",
28
+ "mcp__hearthbot__start_rc_session",
29
+ }
30
+ )
31
+
32
+ SPAWN_OVERSIGHT_DIRECTIVE = (
33
+ "You own the work this spawn starts. Load the orchestrator skill and follow "
34
+ "its oversee delegated work reference. While the agent runs, read its progress "
35
+ "and wake it with one next step when it goes quiet. Before its output "
36
+ "reaches the user, check it against the user's own words and standards, and "
37
+ "send the agent a correction when it misses. Give each piece of a "
38
+ "multi-piece task its own reviewer or helper. Tell the user you are "
39
+ "checking the work."
40
+ )
@@ -0,0 +1,47 @@
1
+ import io
2
+ import json
3
+ import sys
4
+
5
+ import pytest
6
+
7
+ from hooks_constants.pre_tool_use_context_runner import run_context_hook
8
+
9
+
10
+ def _feed_stdin(monkeypatch: pytest.MonkeyPatch, text: str) -> None:
11
+ monkeypatch.setattr(sys, "stdin", io.TextIOWrapper(io.BytesIO(text.encode("utf-8"))))
12
+
13
+
14
+ def test_prints_the_decided_output(
15
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
16
+ ) -> None:
17
+ _feed_stdin(monkeypatch, json.dumps({"tool_name": "Agent"}))
18
+ exit_code = run_context_hook(lambda payload: {"seen": payload["tool_name"]})
19
+ assert exit_code == 0
20
+ assert json.loads(capsys.readouterr().out) == {"seen": "Agent"}
21
+
22
+
23
+ def test_prints_nothing_when_the_decision_is_none(
24
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
25
+ ) -> None:
26
+ _feed_stdin(monkeypatch, json.dumps({"tool_name": "Bash"}))
27
+ assert run_context_hook(lambda payload: None) == 0
28
+ assert capsys.readouterr().out == ""
29
+
30
+
31
+ def test_prints_nothing_for_empty_stdin(
32
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
33
+ ) -> None:
34
+ _feed_stdin(monkeypatch, "")
35
+ assert run_context_hook(lambda payload: {"never": True}) == 0
36
+ assert capsys.readouterr().out == ""
37
+
38
+
39
+ def test_prints_nothing_and_exits_zero_when_the_decision_raises(
40
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
41
+ ) -> None:
42
+ def _raise(payload: dict[str, object]) -> dict[str, object] | None:
43
+ raise KeyError(payload["missing"])
44
+
45
+ _feed_stdin(monkeypatch, json.dumps({"tool_name": "Agent"}))
46
+ assert run_context_hook(_raise) == 0
47
+ assert capsys.readouterr().out == ""
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env python3
2
+ """PreToolUse hook: tell the spawning session it owns the work it delegates.
3
+
4
+ Registered on every tool that starts another agent: ``Agent``, ``Task``,
5
+ ``Workflow``, ``multi_agent_v1__spawn_agent``,
6
+ ``mcp__hearthbot__start_thread_session``, and
7
+ ``mcp__hearthbot__start_rc_session``, plus a workflow dispatch whose inputs
8
+ carry a ``prompt`` for an agent. Each spawn gets the oversight directive
9
+ as ``additionalContext``, so the spawn runs and keeps its normal permission
10
+ flow. A subagent that spawns its own agents gets the same directive, so every
11
+ level of the tree oversees the level below it.
12
+
13
+ ::
14
+
15
+ Agent {"prompt": "Do X."} -> additionalContext: the directive
16
+ actions trigger {"method": "run_workflow",
17
+ "inputs": {"prompt": "Do X."}}
18
+ -> additionalContext: the directive
19
+ actions trigger {"method": "cancel_workflow_run"}
20
+ -> no output
21
+ Bash {"command": "ls"} -> no output
22
+ Agent "not an object" -> no output
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import sys
28
+ from pathlib import Path
29
+
30
+ hooks_root_directory = str(Path(__file__).resolve().parent.parent)
31
+ if hooks_root_directory not in sys.path:
32
+ sys.path.insert(0, hooks_root_directory)
33
+
34
+ from hooks_constants.hook_specific_output_keys import (
35
+ HOOK_EVENT_NAME_KEY,
36
+ HOOK_SPECIFIC_OUTPUT_KEY,
37
+ )
38
+ from hooks_constants.pre_tool_use_context_runner import run_context_hook
39
+ from hooks_constants.spawn_oversight_hook_constants import (
40
+ ADDITIONAL_CONTEXT_KEY,
41
+ ALL_SPAWN_TOOL_NAMES,
42
+ DISPATCH_INPUTS_KEY,
43
+ DISPATCH_METHOD_INPUT_KEY,
44
+ DISPATCH_PROMPT_INPUT_KEY,
45
+ DISPATCH_RUN_WORKFLOW_METHOD,
46
+ PRE_TOOL_USE_EVENT_NAME,
47
+ SPAWN_OVERSIGHT_DIRECTIVE,
48
+ TOOL_INPUT_KEY,
49
+ TOOL_NAME_KEY,
50
+ WORKFLOW_DISPATCH_TOOL_NAME,
51
+ )
52
+
53
+
54
+ def starts_an_agent(tool_name: object, all_tool_input_fields: dict[str, object]) -> bool:
55
+ """Return True when the call starts an agent: a spawn tool or a prompt dispatch.
56
+
57
+ Args:
58
+ tool_name: The tool the session called.
59
+ all_tool_input_fields: The tool input.
60
+ """
61
+ if tool_name in ALL_SPAWN_TOOL_NAMES:
62
+ return True
63
+ if tool_name != WORKFLOW_DISPATCH_TOOL_NAME:
64
+ return False
65
+ if all_tool_input_fields.get(DISPATCH_METHOD_INPUT_KEY) != DISPATCH_RUN_WORKFLOW_METHOD:
66
+ return False
67
+ workflow_inputs = all_tool_input_fields.get(DISPATCH_INPUTS_KEY)
68
+ return isinstance(workflow_inputs, dict) and isinstance(
69
+ workflow_inputs.get(DISPATCH_PROMPT_INPUT_KEY), str
70
+ )
71
+
72
+
73
+ def decide_hook_output(all_hook_fields: dict[str, object]) -> dict[str, object] | None:
74
+ """Return the oversight directive for a spawn call, else None.
75
+
76
+ Args:
77
+ all_hook_fields: The parsed PreToolUse payload.
78
+ """
79
+ all_tool_input_fields = all_hook_fields.get(TOOL_INPUT_KEY)
80
+ if not isinstance(all_tool_input_fields, dict):
81
+ return None
82
+ if not starts_an_agent(all_hook_fields.get(TOOL_NAME_KEY), all_tool_input_fields):
83
+ return None
84
+ return {
85
+ HOOK_SPECIFIC_OUTPUT_KEY: {
86
+ HOOK_EVENT_NAME_KEY: PRE_TOOL_USE_EVENT_NAME,
87
+ ADDITIONAL_CONTEXT_KEY: SPAWN_OVERSIGHT_DIRECTIVE,
88
+ }
89
+ }
90
+
91
+
92
+ def main() -> int:
93
+ """Run the hook on the stdin payload; always 0."""
94
+ return run_context_hook(decide_hook_output)
95
+
96
+
97
+ if __name__ == "__main__":
98
+ sys.exit(main())
@@ -35,7 +35,7 @@ routing_directory = str(Path(__file__).resolve().parent)
35
35
  if routing_directory not in sys.path:
36
36
  sys.path.insert(0, routing_directory)
37
37
 
38
- from hooks_constants.pre_tool_use_stdin import read_hook_input_dictionary_from_stdin
38
+ from hooks_constants.pre_tool_use_context_runner import run_context_hook
39
39
  from hooks_constants.spawn_readiness_hook_constants import (
40
40
  ADDITIONAL_CONTEXT_KEY,
41
41
  AGENT_ID_KEY,
@@ -250,19 +250,8 @@ def decide_hook_output(all_hook_fields: dict[str, object]) -> dict[str, object]
250
250
 
251
251
 
252
252
  def main() -> int:
253
- """Read the PreToolUse payload and print the reminder when the spawn is not ready.
254
-
255
- Returns:
256
- 0 in every case; the reminder travels in the JSON output.
257
- """
258
- hook_payload = read_hook_input_dictionary_from_stdin()
259
- if hook_payload is None:
260
- return 0
261
- hook_output = decide_hook_output(hook_payload)
262
- if hook_output is not None:
263
- sys.stdout.write(json.dumps(hook_output))
264
- sys.stdout.flush()
265
- return 0
253
+ """Run the hook on the stdin payload; always 0."""
254
+ return run_context_hook(decide_hook_output)
266
255
 
267
256
 
268
257
  if __name__ == "__main__":
@@ -0,0 +1,104 @@
1
+ import json
2
+ import subprocess
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ import pytest
7
+
8
+ import spawn_oversight_hook
9
+ from hooks_constants.spawn_oversight_hook_constants import SPAWN_OVERSIGHT_DIRECTIVE
10
+
11
+ HOOK_SCRIPT = Path(__file__).resolve().parent / "spawn_oversight_hook.py"
12
+
13
+
14
+ def _expected_output() -> dict[str, object]:
15
+ return {
16
+ "hookSpecificOutput": {
17
+ "hookEventName": "PreToolUse",
18
+ "additionalContext": SPAWN_OVERSIGHT_DIRECTIVE,
19
+ }
20
+ }
21
+
22
+
23
+ @pytest.mark.parametrize(
24
+ "tool_name",
25
+ [
26
+ "Agent",
27
+ "Task",
28
+ "Workflow",
29
+ "multi_agent_v1__spawn_agent",
30
+ "mcp__hearthbot__start_thread_session",
31
+ "mcp__hearthbot__start_rc_session",
32
+ ],
33
+ )
34
+ def test_every_spawn_tool_gets_the_oversight_directive(tool_name: str) -> None:
35
+ payload = {"tool_name": tool_name, "tool_input": {"prompt": "Do X."}}
36
+ assert spawn_oversight_hook.decide_hook_output(payload) == _expected_output()
37
+
38
+
39
+ def test_a_subagent_spawning_its_own_agent_gets_the_directive() -> None:
40
+ payload = {
41
+ "tool_name": "Agent",
42
+ "tool_input": {"prompt": "Do X."},
43
+ "agent_id": "agent_child",
44
+ }
45
+ assert spawn_oversight_hook.decide_hook_output(payload) == _expected_output()
46
+
47
+
48
+ def test_a_read_only_helper_spawn_gets_the_directive() -> None:
49
+ payload = {
50
+ "tool_name": "Agent",
51
+ "tool_input": {"subagent_type": "Explore", "prompt": "Find X."},
52
+ }
53
+ assert spawn_oversight_hook.decide_hook_output(payload) == _expected_output()
54
+
55
+
56
+ def test_a_workflow_dispatch_with_an_agent_prompt_gets_the_directive() -> None:
57
+ payload = {
58
+ "tool_name": "mcp__github__actions_run_trigger",
59
+ "tool_input": {"method": "run_workflow", "inputs": {"prompt": "Do X."}},
60
+ }
61
+ assert spawn_oversight_hook.decide_hook_output(payload) == _expected_output()
62
+
63
+
64
+ @pytest.mark.parametrize(
65
+ "payload",
66
+ [
67
+ {
68
+ "tool_name": "mcp__github__actions_run_trigger",
69
+ "tool_input": {"method": "cancel_workflow_run", "run_id": 1},
70
+ },
71
+ {
72
+ "tool_name": "mcp__github__actions_run_trigger",
73
+ "tool_input": {"method": "run_workflow", "inputs": {"head": "branch"}},
74
+ },
75
+ {"tool_name": "Bash", "tool_input": {"command": "ls"}},
76
+ {"tool_name": "mcp__hearthbot__reply", "tool_input": {"text": "hi"}},
77
+ {"tool_name": "Agent", "tool_input": "not an object"},
78
+ {"tool_input": {"prompt": "Do X."}},
79
+ ],
80
+ )
81
+ def test_other_calls_get_no_output(payload: dict[str, object]) -> None:
82
+ assert spawn_oversight_hook.decide_hook_output(payload) is None
83
+
84
+
85
+ def test_script_prints_the_directive_for_a_spawn() -> None:
86
+ completed = subprocess.run(
87
+ [sys.executable, str(HOOK_SCRIPT)],
88
+ input=json.dumps({"tool_name": "Agent", "tool_input": {"prompt": "Do X."}}),
89
+ capture_output=True,
90
+ text=True,
91
+ check=True,
92
+ )
93
+ assert json.loads(completed.stdout) == _expected_output()
94
+
95
+
96
+ def test_script_prints_nothing_for_empty_stdin() -> None:
97
+ completed = subprocess.run(
98
+ [sys.executable, str(HOOK_SCRIPT)],
99
+ input="",
100
+ capture_output=True,
101
+ text=True,
102
+ check=True,
103
+ )
104
+ assert completed.stdout == ""
@@ -368,3 +368,15 @@ def test_should_quiet_a_codex_spawn_whose_brief_settles_scope(tmp_path: Path) ->
368
368
  assert stdout == ""
369
369
  [log_record] = _decision_log(tmp_path)
370
370
  assert log_record["outcome"] == "scope_settled"
371
+
372
+
373
+ def test_should_print_nothing_for_empty_stdin(tmp_path: Path) -> None:
374
+ completed = subprocess.run(
375
+ [sys.executable, str(HOOK_SCRIPT)],
376
+ input="",
377
+ capture_output=True,
378
+ text=True,
379
+ env={**os.environ, "HOME": str(tmp_path)},
380
+ check=True,
381
+ )
382
+ assert completed.stdout == ""
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "8.59.0",
3
+ "version": "8.60.0",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {