Skip to content
52 changes: 52 additions & 0 deletions FORK.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# OpenClaw Fork Notes

This repository is an **OpenClaw-adapted fork** of [HKUDS/ClawTeam](https://github.com/HKUDS/ClawTeam).

Its purpose is to keep ClawTeam aligned with upstream while carrying a small, explicit downstream layer needed for OpenClaw integration.

## What differs from upstream

Compared with upstream `HKUDS/ClawTeam`, this fork carries a focused OpenClaw delta:

- **OpenClaw skill support** for using ClawTeam from OpenClaw-driven workflows
- **OpenClaw install/bootstrap flow** for easier setup in OpenClaw environments
- **Session isolation and adapter support** needed to run OpenClaw-integrated agent sessions cleanly and predictably

The goal is to keep this delta small, reviewable, and clearly separated from the upstream engine wherever possible.

## Sync strategy

This fork follows an **upstream-first** maintenance model:

- **Biweekly upstream merge:** regularly merge the latest `HKUDS/ClawTeam` changes into this fork
- **Cherry-pick OpenClaw delta:** replay or maintain OpenClaw-specific changes as small, auditable downstream patches instead of drifting into a long-lived fork rewrite

In practice:

1. merge upstream on a biweekly cadence (or sooner when important upstream changes land)
2. keep OpenClaw-specific behavior in a narrow integration layer
3. prefer cherry-picking or replaying downstream-only changes instead of merging legacy downstream history wholesale

## Acknowledgements

The OpenClaw integration work in this fork was **directly inspired by and partially derived from** [win4r/ClawTeam-OpenClaw](https://github.com/win4r/ClawTeam-OpenClaw). Their fork was the first to adapt ClawTeam for OpenClaw environments, covering areas such as:

- OpenClaw as a first-class spawn target
- Install scripts and exec approval bootstrapping
- Session isolation for OpenClaw agents
- Skill documentation for OpenClaw workflows

We cherry-picked and reviewed their downstream delta as the starting point for our own integration layer. Where their approach and ours diverge (e.g., we restructured session isolation into the canonical adapter pattern rather than patching engine files directly), we rewrote the implementation — but the direction and many of the ideas originated from their work.

Thank you to the win4r team for blazing the trail.

## Reference docs

For the current fork policy and delta inventory, see:

- `workflow/UPSTREAM_SYNC.md`
- `workflow/reports/win4r-delta-filelist-2026-03-22.md`

## Maintainer

- **dtzp555-max**
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
> **Fork notice:** This `openclaw-integration` branch is an OpenClaw-adapted fork of `HKUDS/ClawTeam`. It carries a small downstream delta for OpenClaw skill support, install/bootstrap flow, and session-isolation/adapter support. See [FORK.md](FORK.md), `workflow/UPSTREAM_SYNC.md`, and `workflow/reports/win4r-delta-filelist-2026-03-22.md` for details.

<h1 align="center"><img src="assets/icon.png" alt="" width="64" style="vertical-align: middle;">&nbsp; ClawTeam: Agent Swarm Intelligence</h1>

<p align="center">
Expand Down
51 changes: 48 additions & 3 deletions clawteam/cli/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from pathlib import Path
from typing import Optional

import click
import typer
from rich.console import Console
from rich.table import Table
Expand Down Expand Up @@ -1551,7 +1552,21 @@ def team_snapshot_delete(
# Inbox Commands
# ============================================================================

inbox_app = typer.Typer(help="Inbox / messaging commands")

class InboxTyperGroup(typer.core.TyperGroup):
"""Inbox subcommands with a clearer error for the common missing-subcommand case."""

def resolve_command(self, ctx: click.Context, args: list[str]):
try:
return super().resolve_command(ctx, args)
except click.UsageError as exc:
if args and args[0] not in self.commands and not args[0].startswith("-"):
hint = "Use `clawteam inbox send <team> <recipient> <message>`."
raise click.UsageError(f"{exc.message} {hint}", ctx=ctx) from exc
raise


inbox_app = typer.Typer(help="Inbox / messaging commands", cls=InboxTyperGroup)
app.add_typer(inbox_app, name="inbox")


Expand All @@ -1569,9 +1584,24 @@ def inbox_send(
from clawteam.team.mailbox import MailboxManager
from clawteam.team.models import MessageType

if not content or not content.strip():
_output(
{"error": "Message content cannot be empty."},
lambda d: console.print(f"[red]{d['error']}[/red]"),
)
raise typer.Exit(1)

sender = from_agent or AgentIdentity.from_env().agent_name
mailbox = MailboxManager(team)
mt = MessageType(msg_type)
try:
mt = MessageType(msg_type)
except ValueError:
valid = ", ".join(t.value for t in MessageType)
_output(
{"error": f"Invalid message type '{msg_type}'. Valid types: {valid}"},
lambda d: console.print(f"[red]{d['error']}[/red]"),
)
raise typer.Exit(1)
msg = mailbox.send(
from_agent=sender,
to=to,
Expand All @@ -1596,9 +1626,24 @@ def inbox_broadcast(
from clawteam.team.mailbox import MailboxManager
from clawteam.team.models import MessageType

if not content or not content.strip():
_output(
{"error": "Broadcast content cannot be empty."},
lambda d: console.print(f"[red]{d['error']}[/red]"),
)
raise typer.Exit(1)

sender = from_agent or AgentIdentity.from_env().agent_name
mailbox = MailboxManager(team)
mt = MessageType(msg_type)
try:
mt = MessageType(msg_type)
except ValueError:
valid = ", ".join(t.value for t in MessageType)
_output(
{"error": f"Invalid message type '{msg_type}'. Valid types: {valid}"},
lambda d: console.print(f"[red]{d['error']}[/red]"),
)
raise typer.Exit(1)
messages = mailbox.broadcast(
from_agent=sender,
content=content,
Expand Down
11 changes: 6 additions & 5 deletions clawteam/spawn/adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,18 +57,19 @@ def prepare_command(
if prompt:
final_command.extend(["-m", prompt])
elif is_openclaw_command(normalized_command):
if "agent" in normalized_command:
# OpenClaw uses subcommands: 'tui' (interactive) or 'agent' (non-interactive)
if "tui" not in final_command and "agent" not in final_command:
final_command.insert(1, "tui" if interactive else "agent")
if "agent" in final_command:
if "--local" not in normalized_command:
final_command.append("--local")
if agent_name and "--session-id" not in normalized_command:
final_command.extend(["--session-id", agent_name])
if prompt:
final_command.extend(["--message", prompt])
else:
if agent_name and "--session" not in normalized_command:
final_command.extend(["--session", agent_name])
if prompt:
final_command.extend(["--message", prompt])
if prompt:
final_command.extend(["--message", prompt])
elif prompt:
if interactive and is_claude_command(normalized_command):
post_launch_prompt = prompt
Expand Down
63 changes: 63 additions & 0 deletions clawteam/spawn/prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,56 @@

from __future__ import annotations

import re
from pathlib import Path


def _extract_path_references(text: str) -> list[str]:
"""Extract file path references from task text.

Looks for absolute paths (starting with /) and common relative paths
that look like file references (containing a dot extension or ending with /).
"""
# Match paths like /foo/bar.py, src/main.rs, ./config.json, etc.
# Excludes URLs (http://, https://) and common non-path patterns
pattern = r'(?<!\w)(?:\.?/[\w./-]+|[\w][\w/-]*\.[\w]+)'
candidates = re.findall(pattern, text)
# Filter out things that look like URLs, URL path components, or version numbers
# Also remove anything preceded by :// in the original text
url_pattern = re.compile(r'https?://\S+')
url_spans = [(m.start(), m.end()) for m in url_pattern.finditer(text)]
result = []
for p in candidates:
if p.startswith("http") or ".." in p:
continue
# Check if this match falls within a URL span
idx = text.find(p)
in_url = any(start <= idx < end for start, end in url_spans)
if in_url:
continue
result.append(p)
return result


def _check_path_references(task: str, workspace_dir: str) -> list[str]:
"""Check task text for file path references that don't exist.

Returns list of warning strings for missing paths.
"""
if not workspace_dir:
return []
refs = _extract_path_references(task)
warnings = []
ws = Path(workspace_dir)
for ref in refs:
if ref.startswith("/"):
full = Path(ref)
else:
full = ws / ref
if not full.exists():
warnings.append(f"Referenced path not found: {ref}")
return warnings


def _build_context_block(team_name: str, agent_name: str, repo: str | None = None) -> str:
"""Build a context awareness block from the workspace context layer.
Expand Down Expand Up @@ -36,6 +86,7 @@ def build_agent_prompt(
workspace_branch: str = "",
isolated_workspace: bool = False,
repo_path: str | None = None,
data_dir: str = "",
) -> str:
"""Build agent prompt: identity + task + context + coordination."""
lines = [
Expand Down Expand Up @@ -70,6 +121,18 @@ def build_agent_prompt(
task,
])

# Check for referenced paths that don't exist
path_warnings = _check_path_references(task, workspace_dir)
if path_warnings:
lines.extend([
"",
"## Path Warnings\n",
"The following paths referenced in the task were not found:",
])
for w in path_warnings:
lines.append(f"- {w}")
lines.append("- Verify these paths before starting work.")

# Inject cross-agent context awareness
context_block = _build_context_block(team_name, agent_name, repo_path)
if context_block:
Expand Down
12 changes: 11 additions & 1 deletion clawteam/spawn/subprocess_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import shlex
import subprocess

from clawteam.spawn.adapters import NativeCliAdapter
from clawteam.spawn.adapters import NativeCliAdapter, is_openclaw_command
from clawteam.spawn.base import SpawnBackend
from clawteam.spawn.cli_env import build_spawn_path, resolve_clawteam_executable
from clawteam.spawn.command_validation import validate_spawn_command
Expand All @@ -31,6 +31,8 @@ def spawn(
cwd: str | None = None,
skip_permissions: bool = False,
) -> str:
from clawteam.team.models import get_data_dir

spawn_env = os.environ.copy()
clawteam_bin = resolve_clawteam_executable()
spawn_env.update({
Expand All @@ -40,6 +42,9 @@ def spawn(
"CLAWTEAM_TEAM_NAME": team_name,
"CLAWTEAM_AGENT_LEADER": "0",
})
# Propagate resolved data dir so spawned agents find the right
# task/inbox storage even when the leader resolved it via config.
spawn_env.setdefault("CLAWTEAM_DATA_DIR", str(get_data_dir()))
# Propagate user if set
user = os.environ.get("CLAWTEAM_USER", "")
if user:
Expand Down Expand Up @@ -68,6 +73,11 @@ def spawn(
validation_command = normalized_command
final_command = list(prepared.final_command)

# Isolate OpenClaw agents in per-agent sessions
if is_openclaw_command(normalized_command):
session_key = f"clawteam-{team_name}-{agent_name}"
final_command.extend(["--session-id", session_key])

command_error = validate_spawn_command(validation_command, path=spawn_env["PATH"], cwd=cwd)
if command_error:
return command_error
Expand Down
15 changes: 15 additions & 0 deletions clawteam/spawn/tmux_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
is_gemini_command,
is_kimi_command,
is_nanobot_command,
is_openclaw_command,
is_opencode_command,
is_qwen_command,
)
Expand Down Expand Up @@ -55,6 +56,8 @@ def spawn(

session_name = f"clawteam-{team_name}"
clawteam_bin = resolve_clawteam_executable()
from clawteam.team.models import get_data_dir

env_vars = os.environ.copy()
# Interactive CLIs like Codex refuse to start when TERM=dumb is inherited
# from a non-interactive shell. tmux provides a real terminal, so we
Expand All @@ -68,6 +71,9 @@ def spawn(
"CLAWTEAM_TEAM_NAME": team_name,
"CLAWTEAM_AGENT_LEADER": "0",
})
# Propagate resolved data dir so spawned agents find the right
# task/inbox storage even when the leader resolved it via config.
env_vars.setdefault("CLAWTEAM_DATA_DIR", str(get_data_dir()))
if cwd:
env_vars["CLAWTEAM_WORKSPACE_DIR"] = cwd
# Inject context awareness flags
Expand All @@ -91,6 +97,15 @@ def spawn(
final_command = list(prepared.final_command)
post_launch_prompt = prepared.post_launch_prompt

# Isolate OpenClaw agents in per-agent sessions
if is_openclaw_command(normalized_command):
session_key = f"clawteam-{team_name}-{agent_name}"
# tui mode uses --session; agent mode uses --session-id
if "tui" in final_command:
final_command.extend(["--session", session_key])
elif "agent" in final_command:
final_command.extend(["--session-id", session_key])

command_error = validate_spawn_command(validation_command, path=env_vars["PATH"], cwd=cwd)
if command_error:
return command_error
Expand Down
13 changes: 10 additions & 3 deletions clawteam/team/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ def _now_iso() -> str:
return datetime.now(timezone.utc).isoformat()


def _normalize_owner(owner: str | None) -> str | None:
if owner is None:
return None
return owner.strip()


class TaskStore:
"""File-based task store with dependency tracking.

Expand Down Expand Up @@ -70,7 +76,7 @@ def create(
task = TaskItem(
subject=subject,
description=description,
owner=owner,
owner=_normalize_owner(owner) or "",
priority=priority or TaskPriority.medium,
blocks=blocks or [],
blocked_by=blocked_by or [],
Expand Down Expand Up @@ -138,7 +144,7 @@ def update(
if status is not None:
task.status = status
if owner is not None:
task.owner = owner
task.owner = _normalize_owner(owner) or ""
if subject is not None:
task.subject = subject
if description is not None:
Expand Down Expand Up @@ -222,6 +228,7 @@ def _list_tasks_unlocked(
priority: TaskPriority | None = None,
sort_by_priority: bool = False,
) -> list[TaskItem]:
owner = _normalize_owner(owner)
root = _tasks_root(self.team_name)
tasks = []
for f in sorted(root.glob("task-*.json")):
Expand All @@ -230,7 +237,7 @@ def _list_tasks_unlocked(
task = TaskItem.model_validate(data)
if status and task.status != status:
continue
if owner and task.owner != owner:
if owner and task.owner.lower() != owner.lower():
continue
if priority and task.priority != priority:
continue
Expand Down
Loading