feat: add harness skill with hooks install/uninstall support (#156)

Add multi-session autonomous agent harness with progress checkpointing,
failure recovery, task dependencies, and post-completion self-reflection.

- Add harness module to config.json (copy_dir with hooks.json)
- Add 7 hook scripts: stop, sessionstart, teammateidle, subagentstop,
  claim, renew, self-reflect-stop + shared _harness_common.py
- Fix self-reflect-stop: only triggers when harness was initialized
  (checks harness-tasks.json existence), not on every session
- Add unmerge_hooks_from_settings() to uninstall.py for clean hook removal
- Add unit tests (57 tests) and E2E test (100 tasks + 5 self-reflect)

Generated with SWE-Agent.ai

Co-Authored-By: SWE-Agent.ai <noreply@swe-agent.ai>
This commit is contained in:
cexll
2026-03-01 22:14:16 +08:00
parent 62309d1429
commit 683409464c
14 changed files with 3051 additions and 10 deletions

View File

@@ -12,6 +12,7 @@ from pathlib import Path
from typing import Any, Dict, List, Optional, Set
DEFAULT_INSTALL_DIR = "~/.claude"
SETTINGS_FILE = "settings.json"
# Files created by installer itself (not by modules)
INSTALLER_FILES = ["install.log", "installed_modules.json", "installed_modules.json.bak"]
@@ -80,6 +81,42 @@ def load_config(install_dir: Path) -> Dict[str, Any]:
return {}
def unmerge_hooks_from_settings(module_name: str, install_dir: Path) -> bool:
"""Remove hooks tagged with __module__=module_name from settings.json."""
settings_path = install_dir / SETTINGS_FILE
if not settings_path.exists():
return False
try:
with settings_path.open("r", encoding="utf-8") as f:
settings = json.load(f)
except (json.JSONDecodeError, OSError):
return False
if "hooks" not in settings:
return False
modified = False
for hook_type in list(settings["hooks"].keys()):
original_len = len(settings["hooks"][hook_type])
settings["hooks"][hook_type] = [
entry for entry in settings["hooks"][hook_type]
if entry.get("__module__") != module_name
]
if len(settings["hooks"][hook_type]) < original_len:
modified = True
# Remove empty hook type arrays
if not settings["hooks"][hook_type]:
del settings["hooks"][hook_type]
if modified:
with settings_path.open("w", encoding="utf-8") as f:
json.dump(settings, f, indent=2, ensure_ascii=False)
f.write("\n")
return modified
def get_module_files(module_name: str, config: Dict[str, Any]) -> Set[str]:
"""Extract files/dirs that a module installs based on config.json operations."""
files: Set[str] = set()
@@ -261,6 +298,11 @@ def main(argv: Optional[List[str]] = None) -> int:
except OSError as e:
print(f" ✗ Failed to remove {item}: {e}", file=sys.stderr)
# Remove module hooks from settings.json
for m in selected:
if unmerge_hooks_from_settings(m, install_dir):
print(f" ✓ Removed hooks for module '{m}' from settings.json")
# Update installed_modules.json
status_file = install_dir / "installed_modules.json"
if status_file.exists() and selected != list(installed_modules.keys()):