Skip to content

Commit 18ea6d1

Browse files
emmahydeclaude
andcommitted
feat(orchestrator): move state to .claude/orchestrator, disallow TaskOutput
- Relocate orchestrator state from .orchestrator/ to .claude/orchestrator/ so write scope hook doesn't block normal interactive sessions - Add disallowedTools: ["TaskOutput"] to orchestrator frontmatter - Hook only enforces write restrictions when .claude/orchestrator/ dir exists - Update all hook scripts and build_dispatch_prompt.py for new path Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a0bef29 commit 18ea6d1

File tree

7 files changed

+25
-18
lines changed

7 files changed

+25
-18
lines changed

agents/orchestrator.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ description: >
3434
3535
model: inherit
3636
color: yellow
37-
tools: ["Read", "Write", "Edit", "Bash", "Grep", "Glob", "Task", "TaskCreate", "TaskGet", "TaskUpdate", "TaskList", "TaskOutput", "TaskStop", "AskUserQuestion", "WebSearch", "WebFetch", "LSP", "Skill", "ToolSearch", "EnterPlanMode", "ExitPlanMode", "ListMcpResourcesTool", "ReadMcpResourceTool"]
37+
tools: ["Read", "Write", "Edit", "Bash", "Grep", "Glob", "Task", "TaskCreate", "TaskGet", "TaskUpdate", "TaskList", "TaskStop", "AskUserQuestion", "WebSearch", "WebFetch", "LSP", "Skill", "ToolSearch", "EnterPlanMode", "ExitPlanMode", "ListMcpResourcesTool", "ReadMcpResourceTool"]
38+
disallowedTools: ["TaskOutput"]
3839
---
3940

4041
You are ORCHESTRATOR-AGENT. Follow this contract exactly.
@@ -56,13 +57,14 @@ You are ORCHESTRATOR-AGENT. Follow this contract exactly.
5657
[ROLE]
5758
You are the sole planner and coordinator.
5859
You own decomposition, assignment, lock management, context packaging, validation, and integration decisions.
60+
You HAVE the Task tool — use it to dispatch subagents. Do not claim otherwise.
5961

6062
[INITIALIZATION]
61-
Before any other action, create `.orchestrator/active_locks.json` with your own lock entry:
63+
Before any other action, create `.claude/orchestrator/active_locks.json` with your own lock entry:
6264
```bash
63-
mkdir -p .orchestrator
64-
cat > .orchestrator/active_locks.json << 'LOCKS'
65-
[{"task_id":"orchestrator","owner":"orchestrator","resource":[".claude/","docs/",".orchestrator/"],"active":true}]
65+
mkdir -p .claude/orchestrator
66+
cat > .claude/orchestrator/active_locks.json << 'LOCKS'
67+
[{"task_id":"orchestrator","owner":"orchestrator","resource":[".claude/","docs/"],"active":true}]
6668
LOCKS
6769
```
6870
This registers the orchestrator's own write scope. All subsequent lock entries for subagents are appended to this file. The PreToolUse hook enforces these scopes on every Write/Edit call.

hooks/scripts/post_task_dispatch_marker.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def deduplicate_locks(locks: list[dict]) -> list[dict]:
6262

6363
def merge_and_update_locks(cwd: Path, packet: dict):
6464
try:
65-
locks_file = cwd / '.orchestrator' / 'active_locks.json'
65+
locks_file = cwd / '.claude/orchestrator' / 'active_locks.json'
6666

6767
existing_locks = []
6868
if locks_file.is_file():
@@ -103,7 +103,7 @@ def main():
103103
sys.exit(0)
104104

105105
cwd = Path(cwd_str)
106-
orchestrator_dir = cwd / '.orchestrator'
106+
orchestrator_dir = cwd / '.claude/orchestrator'
107107
orchestrator_dir.mkdir(exist_ok=True)
108108

109109
active_dispatch_marker = orchestrator_dir / '.active_dispatch'

hooks/scripts/pre_task_validate_tools.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ def main() -> None:
104104
f"{', '.join(missing)}. The subagent contract requires these for "
105105
f"writing task reports and worklogs."
106106
)
107-
orchestrated = (cwd / ".orchestrator").is_dir()
107+
orchestrated = (cwd / '.claude/orchestrator').is_dir()
108108

109109
if orchestrated:
110110
print(

hooks/scripts/pre_write_scope_check.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
def is_always_allowed(rel_path: str) -> bool:
1515
"""Check if path is in always-allowed directories."""
1616
return (
17-
rel_path.startswith(".orchestrator/outputs/") or
17+
rel_path.startswith(".claude/orchestrator/outputs/") or
1818
rel_path.startswith("worklogs/")
1919
)
2020

@@ -86,10 +86,15 @@ def main():
8686
if is_always_allowed(rel_path):
8787
sys.exit(0)
8888

89-
locks_file = Path(cwd) / ".orchestrator" / "active_locks.json"
89+
orchestrator_dir = Path(cwd) / ".claude" / "orchestrator"
90+
locks_file = orchestrator_dir / "active_locks.json"
91+
92+
# No .claude/orchestrator/ dir = normal interactive session, no restrictions
93+
if not orchestrator_dir.exists():
94+
sys.exit(0)
95+
96+
# Dir exists but no lock file = orchestrator hasn't initialized yet
9097
if not locks_file.exists():
91-
# No lock file = orchestrator or main session context
92-
# Restrict writes to allowed directories only
9398
if any(rel_path.startswith(p) for p in ORCHESTRATOR_ALLOWED):
9499
sys.exit(0)
95100
dirs = ", ".join(ORCHESTRATOR_ALLOWED)

hooks/scripts/subagent_stop_gate.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ def has_recent_task_report(outputs_dir: Path, seconds: int = 60) -> bool:
2424

2525
def is_orchestrated_subagent(cwd: Path) -> bool:
2626
"""Check if this is an orchestrated subagent context."""
27-
outputs_dir = cwd / ".orchestrator" / "outputs"
28-
active_dispatch = cwd / ".orchestrator" / ".active_dispatch"
27+
outputs_dir = cwd / '.claude/orchestrator' / "outputs"
28+
active_dispatch = cwd / '.claude/orchestrator' / ".active_dispatch"
2929

3030
return outputs_dir.exists() and active_dispatch.exists()
3131

@@ -45,7 +45,7 @@ def main():
4545
if not is_orchestrated_subagent(cwd):
4646
sys.exit(0)
4747

48-
outputs_dir = cwd / ".orchestrator" / "outputs"
48+
outputs_dir = cwd / '.claude/orchestrator' / "outputs"
4949
if not has_recent_task_report(outputs_dir, seconds=60):
5050
error_response = {
5151
"decision": "block",

hooks/scripts/subagent_stop_summary.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ def main():
170170
if not cwd:
171171
sys.exit(0)
172172

173-
outputs_dir = Path(cwd) / '.orchestrator' / 'outputs'
173+
outputs_dir = Path(cwd) / '.claude/orchestrator' / 'outputs'
174174
if not outputs_dir.is_dir():
175175
sys.exit(0)
176176

@@ -190,7 +190,7 @@ def main():
190190
task_id = frontmatter.get('task_id', '')
191191
status = frontmatter.get('status', '')
192192
report_basename = report.name
193-
report_relpath = f'.orchestrator/outputs/{report_basename}'
193+
report_relpath = f'.claude/orchestrator/outputs/{report_basename}'
194194

195195
files_touched = frontmatter.get('files_touched', [])
196196
acceptance_check = frontmatter.get('acceptance_check', [])

skills/multi-agent-operator-guide/scripts/build_dispatch_prompt.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ def main() -> int:
7878
# Convert title to snake_case descriptor
7979
descriptor = "_".join(title.lower().split())
8080
descriptor = re.sub(r"[^a-z0-9_]", "", descriptor)
81-
task["report_path"] = f".orchestrator/outputs/task__{descriptor}.md"
81+
task["report_path"] = f".claude/orchestrator/outputs/task__{descriptor}.md"
8282
packet["task"] = task
8383

8484
# Build prompt

0 commit comments

Comments
 (0)