Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ DOCTOR_ARGS ?=
PREFERRED_PYTHON := $(shell $(PYTHON) src/clawops/platform_compat.py --field preferred_project_python_version 2>/dev/null)
UV_SYNC := $(UV) sync $(if $(PREFERRED_PYTHON),--python $(PREFERRED_PYTHON),)

.PHONY: help install setup doctor dev fmt lint imports typecheck actionlint shellcheck precommit dev-check test test-unit test-integration test-contracts test-framework test-e2e test-hypermemory test-qdrant test-all test-governance compile start-sidecars stop-sidecars render-config verify context-index run-harness backup
.PHONY: help install setup doctor dev dev-shell fmt lint imports typecheck actionlint shellcheck precommit dev-check test test-unit test-integration test-contracts test-framework test-e2e test-hypermemory test-qdrant test-all test-governance compile start-sidecars stop-sidecars render-config verify context-index run-harness backup

help: ## Show available targets.
@awk 'BEGIN {FS = ":.*## "}; /^[a-zA-Z0-9_.-]+:.*## / {printf "%-16s %s\n", $$1, $$2}' $(MAKEFILE_LIST)
Expand All @@ -34,6 +34,9 @@ dev: ## Sync the locked dev environment and install pre-commit hooks.
$(UV_SYNC) $(DEV_SYNC_FLAGS)
$(PRE_COMMIT) install --install-hooks

dev-shell: install ## Open an interactive dev shell with repo-backed StrongClaw assets enabled.
@bash -lc 'source scripts/dev-env.sh && exec "$${SHELL:-/bin/bash}" -i'

fmt: ## Apply import sorting, lint autofixes, and formatting.
$(RUN) isort src tests
$(RUN) ruff check --fix src tests
Expand Down Expand Up @@ -103,7 +106,7 @@ compile: ## Compile source and tests in the managed dev environment.
$(RUN) python -m compileall -q src tests

render-config: ## Render the OpenClaw config bundle.
$(RUN) clawops render-openclaw-config --repo-root .
$(RUN) clawops render-openclaw-config --asset-root .

start-sidecars: ## Launch the sidecar services.
$(RUN) clawops ops sidecars up
Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,19 @@ Boundary override flags are now literal:
- use `--source-root` for source-tree-only verification such as `clawops baseline`
- use `--repo-root` only for repo-contract tooling such as `clawops repo`, `clawops worktree`, and `clawops supply-chain`

Package-safe runtime commands now default to the packaged asset bundle even
when you run them from inside a StrongClaw source checkout. To opt into
repo-backed assets for local development, export `STRONGCLAW_ASSET_ROOT` or use
the repo-local developer flow:

```bash
source scripts/dev-env.sh
clawops-dev render-openclaw-config
```

`make dev-shell` opens an interactive shell with the same repo-backed asset
override and managed virtualenv activated.

By default, StrongClaw now renders and provisions the
`hypermemory` stack. Set one embedding model name before you run
the no-arg setup path:
Expand Down
14 changes: 14 additions & 0 deletions bin/clawops-dev
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/usr/bin/env sh
set -eu

script_dir=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd)
repo_root=$(CDPATH='' cd -- "$script_dir/.." && pwd)

if [ ! -f "$repo_root/pyproject.toml" ] || [ ! -d "$repo_root/src/clawops" ]; then
printf '%s\n' "clawops-dev must live inside a StrongClaw source checkout." >&2
exit 1
fi

export STRONGCLAW_ASSET_ROOT="${STRONGCLAW_ASSET_ROOT:-$repo_root}"

exec uv run --project "$repo_root" clawops "$@"
9 changes: 9 additions & 0 deletions docs/testing/operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,15 @@
- Safe timeout wrapper:
`uv run python -m tests.utils.scripts.pytest_safe --timeout 600 -q -m integration`

## Contributor Dev Shell

- Source the repo-local developer environment:
`source scripts/dev-env.sh`
- Or open a prepared shell in one step:
`make dev-shell`
- After either flow, use `clawops-dev ...` to run the repo checkout against
repo-backed assets without changing the default installed/runtime behavior.

## Common Triage

- If the monkeypatch governance contract fails, migrate the test to
Expand Down
31 changes: 31 additions & 0 deletions scripts/dev-env.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#!/usr/bin/env sh

# shellcheck disable=SC2317
_strongclaw_dev_env_return_or_exit() {
code="$1"
return "$code" 2>/dev/null || exit "$code"
}

if [ ! -f "pyproject.toml" ] || [ ! -d "src/clawops" ]; then
printf '%s\n' "source scripts/dev-env.sh from the StrongClaw repository root." >&2
_strongclaw_dev_env_return_or_exit 1
fi

repo_root=$(pwd -P)
venv_activate="$repo_root/.venv/bin/activate"

if [ ! -f "$venv_activate" ]; then
printf '%s\n' "Managed environment not found at $venv_activate. Run \`uv sync --locked\` first." >&2
_strongclaw_dev_env_return_or_exit 1
fi

case ":$PATH:" in
*":$repo_root/bin:"*) ;;
*) PATH="$repo_root/bin:$PATH" ;;
esac

export PATH
export STRONGCLAW_ASSET_ROOT="$repo_root"

# shellcheck source=/dev/null
. "$venv_activate"
4 changes: 3 additions & 1 deletion src/clawops/cli_roots.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from typing import Final

from clawops.root_detection import resolve_project_root, resolve_strongclaw_repo_root
from clawops.runtime_assets import resolve_asset_root
from clawops.runtime_assets import require_asset_root, resolve_asset_root

DEPRECATED_REPO_ROOT_FLAG: Final[str] = "--repo-root"

Expand Down Expand Up @@ -101,6 +101,8 @@ def resolve_asset_root_argument(
command_name=command_name,
canonical_flag="--asset-root",
)
if candidate is not None:
return require_asset_root(candidate)
return resolve_asset_root(candidate)


Expand Down
46 changes: 30 additions & 16 deletions src/clawops/runtime_assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

import dataclasses
import os
import pathlib
import shutil
from typing import Final
Expand All @@ -16,12 +17,10 @@
strongclaw_workspace_dir,
strongclaw_worktrees_dir,
)
from clawops.root_detection import (
STRONGCLAW_REPO_MARKERS,
discover_strongclaw_repo_root,
)
from clawops.root_detection import STRONGCLAW_REPO_MARKERS

PACKAGED_ASSET_ROOT: Final[pathlib.Path] = pathlib.Path(__file__).resolve().parent / "assets"
ASSET_ROOT_ENV_VAR: Final[str] = "STRONGCLAW_ASSET_ROOT"
PLATFORM_DIR_NAME: Final[str] = "platform"
MEMORY_CONFIG_RELATIVE_DIR: Final[pathlib.Path] = pathlib.Path("platform/configs/memory")
VARLOCK_CONFIG_RELATIVE_DIR: Final[pathlib.Path] = pathlib.Path("platform/configs/varlock")
Expand All @@ -48,7 +47,7 @@ class RuntimeLayout:
@property
def uses_packaged_assets(self) -> bool:
"""Return whether runtime assets come from the installed package bundle."""
return self.source_checkout_root is None
return self.asset_root == PACKAGED_ASSET_ROOT


def _resolve_path(value: pathlib.Path | str) -> pathlib.Path:
Expand All @@ -70,26 +69,41 @@ def _matches_source_checkout(root: pathlib.Path) -> bool:
return all((root / marker).exists() for marker in STRONGCLAW_REPO_MARKERS)


def resolve_asset_root(repo_root: pathlib.Path | str | None = None) -> pathlib.Path:
"""Return the effective runtime asset root."""
def _configured_asset_root_override(
repo_root: pathlib.Path | str | None = None,
) -> pathlib.Path | None:
"""Return the explicit asset-root override when one was supplied."""
if repo_root is not None:
return _resolve_path(repo_root)
source_root = discover_strongclaw_repo_root()
if source_root is not None:
return _require_platform_root(source_root)
configured = os.environ.get(ASSET_ROOT_ENV_VAR, "").strip()
if not configured:
return None
return _require_platform_root(_resolve_path(configured))


def require_asset_root(root: pathlib.Path | str) -> pathlib.Path:
"""Resolve and validate one explicit runtime asset-root override."""
return _require_platform_root(_resolve_path(root))


def resolve_asset_root(repo_root: pathlib.Path | str | None = None) -> pathlib.Path:
"""Return the effective runtime asset root."""
override = _configured_asset_root_override(repo_root)
if override is not None:
return override
return _require_platform_root(PACKAGED_ASSET_ROOT)


def resolve_source_checkout_root(
repo_root: pathlib.Path | str | None = None,
) -> pathlib.Path | None:
"""Return the active StrongClaw source checkout when one exists."""
if repo_root is not None:
candidate = _resolve_path(repo_root)
if _matches_source_checkout(candidate):
return candidate
"""Return the active explicit StrongClaw source checkout when one exists."""
override = _configured_asset_root_override(repo_root)
if override is None:
return None
return discover_strongclaw_repo_root()
if _matches_source_checkout(override):
return override
return None


def resolve_runtime_layout(
Expand Down
9 changes: 6 additions & 3 deletions src/clawops/workflow_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
)
from clawops.policy_engine import PolicyEngine
from clawops.process_runner import run_command
from clawops.root_detection import DEFAULT_SOURCE_REPO_ROOT
from clawops.runtime_assets import resolve_asset_path
from clawops.typed_values import (
as_bool,
Expand Down Expand Up @@ -105,9 +106,11 @@ def _default_context_pack_output(*, base_dir: pathlib.Path, step_name: str) -> p
return scoped_state_dir(base_dir, category="context-packs") / f"{_safe_step_slug(step_name)}.md"


TRUSTED_WORKFLOW_ROOTS: tuple[pathlib.Path, ...] = (
resolve_asset_path("platform/configs/workflows"),
)
_trusted_workflow_roots: list[pathlib.Path] = [resolve_asset_path("platform/configs/workflows")]
_source_workflow_root = (DEFAULT_SOURCE_REPO_ROOT / "platform" / "configs" / "workflows").resolve()
if _source_workflow_root.is_dir():
_trusted_workflow_roots.append(_source_workflow_root)
TRUSTED_WORKFLOW_ROOTS: tuple[pathlib.Path, ...] = tuple(dict.fromkeys(_trusted_workflow_roots))

ALLOWED_WORKFLOW_KINDS = frozenset(
{
Expand Down
15 changes: 14 additions & 1 deletion tests/suites/contracts/repo/test_scripts_migration_surfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,26 @@ def test_makefile_uses_python_native_operational_targets() -> None:
makefile = (REPO_ROOT / "Makefile").read_text(encoding="utf-8")

assert "preferred_python.sh" not in makefile
assert "./scripts/" not in makefile
assert "scripts/ops/" not in makefile
assert "PYTHONPATH=src" not in makefile
assert "--extra dev" not in makefile
assert "$(UV) run --locked pytest" in makefile
assert "dev-shell: install" in makefile
assert "$(RUN) clawops ops sidecars up" in makefile
assert "$(RUN) clawops baseline verify" in makefile
assert "$(RUN) clawops recovery backup-create" in makefile
assert "$(RUN) clawops render-openclaw-config --asset-root ." in makefile


def test_repo_dev_entrypoints_enable_explicit_repo_asset_mode() -> None:
dev_env = (REPO_ROOT / "scripts" / "dev-env.sh").read_text(encoding="utf-8")
wrapper = (REPO_ROOT / "bin" / "clawops-dev").read_text(encoding="utf-8")

assert 'export STRONGCLAW_ASSET_ROOT="$repo_root"' in dev_env
assert 'PATH="$repo_root/bin:$PATH"' in dev_env
assert '. "$venv_activate"' in dev_env
assert 'export STRONGCLAW_ASSET_ROOT="${STRONGCLAW_ASSET_ROOT:-$repo_root}"' in wrapper
assert 'exec uv run --project "$repo_root" clawops "$@"' in wrapper


def test_service_templates_call_repo_venv_python() -> None:
Expand Down
83 changes: 83 additions & 0 deletions tests/suites/unit/clawops/assets/test_dev_entrypoints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"""Unit tests for repo-local developer entrypoints."""

from __future__ import annotations

import json
import os
import pathlib
import subprocess

from tests.utils.helpers.repo import REPO_ROOT


def _write_fake_uv(fake_bin: pathlib.Path) -> None:
"""Write one fake `uv` executable that records argv and asset-root env."""
fake_bin.mkdir()
fake_uv = fake_bin / "uv"
fake_uv.write_text(
"\n".join(
(
"#!/usr/bin/env python3",
"from __future__ import annotations",
"import json",
"import os",
"import pathlib",
"import sys",
"pathlib.Path(os.environ['TEST_CAPTURE_PATH']).write_text(",
" json.dumps({'argv': sys.argv[1:], 'asset_root': os.environ.get('STRONGCLAW_ASSET_ROOT')}),",
" encoding='utf-8',",
")",
)
)
+ "\n",
encoding="utf-8",
)
fake_uv.chmod(0o755)


def test_clawops_dev_wrapper_exports_repo_asset_root_and_forwards_args(
tmp_path: pathlib.Path,
) -> None:
fake_bin = tmp_path / "fake-bin"
capture_path = tmp_path / "capture.json"
_write_fake_uv(fake_bin)
wrapper_path = REPO_ROOT / "bin" / "clawops-dev"
env = os.environ.copy()
env["PATH"] = f"{fake_bin}{os.pathsep}{env.get('PATH', '')}"
env["TEST_CAPTURE_PATH"] = str(capture_path)

subprocess.run(
[str(wrapper_path), "doctor", "--json"],
check=True,
cwd=tmp_path,
env=env,
)

payload = json.loads(capture_path.read_text(encoding="utf-8"))
assert payload == {
"argv": ["run", "--project", str(REPO_ROOT), "clawops", "doctor", "--json"],
"asset_root": str(REPO_ROOT),
}


def test_clawops_dev_wrapper_respects_preconfigured_asset_root(tmp_path: pathlib.Path) -> None:
fake_bin = tmp_path / "fake-bin"
capture_path = tmp_path / "capture.json"
_write_fake_uv(fake_bin)
wrapper_path = REPO_ROOT / "bin" / "clawops-dev"
configured_asset_root = tmp_path / "configured-assets"
env = os.environ.copy()
env["PATH"] = f"{fake_bin}{os.pathsep}{env.get('PATH', '')}"
env["STRONGCLAW_ASSET_ROOT"] = str(configured_asset_root)
env["TEST_CAPTURE_PATH"] = str(capture_path)

subprocess.run(
[str(wrapper_path), "config"],
check=True,
cwd=tmp_path,
env=env,
)

payload = json.loads(capture_path.read_text(encoding="utf-8"))
assert payload["argv"] == ["run", "--project", str(REPO_ROOT), "clawops", "config"]
assert payload["asset_root"] == str(configured_asset_root)
Loading
Loading