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
2 changes: 1 addition & 1 deletion .claude-plugin/lib/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ Tool permission evaluation for the PreToolUse hook. Provides rule-based matching

Unified workflow definitions for handsoff mode. Centralizes workflow detection, issue extraction, and continuation prompts.

**Self-contained design:** This module includes its own `_get_agentize_home()` and `_run_acw()` helpers to invoke the `acw` shell function without importing from `agentize.shell` or depending on `setup.sh`. This maintains plugin standalone capability.
**Self-contained design:** This module uses `get_agentize_home()` from `session_utils.py` for AGENTIZE_HOME resolution and includes its own `_run_acw()` helper to invoke the `acw` shell function without importing from `agentize.shell` or depending on `setup.sh`. This maintains plugin standalone capability.

**Usage:**
```python
Expand Down
6 changes: 3 additions & 3 deletions .claude-plugin/lib/logger.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import os
import datetime

from lib.session_utils import session_dir
from lib.session_utils import session_dir, get_agentize_home


def _tmp_dir():
"""Get tmp directory path using AGENTIZE_HOME fallback."""
base = os.getenv('AGENTIZE_HOME', '.')
"""Get tmp directory path using get_agentize_home()."""
base = get_agentize_home()
return os.path.join(base, '.tmp')


Expand Down
25 changes: 24 additions & 1 deletion .claude-plugin/lib/session_utils.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,28 @@ Shared utilities for session directory resolution, handsoff mode checks, and iss

## External Interface

### `get_agentize_home() -> str`

Get the AGENTIZE_HOME path for agentize repository root resolution.

**Returns:** String path to the agentize repository root

**Behavior:**
- First checks `AGENTIZE_HOME` environment variable
- If not set, derives from module location (`.claude-plugin/lib/session_utils.py` → `../../`)
- Uses `os.path.realpath()` to resolve symlinks (e.g., `.cursor/hooks/lib` → `.claude-plugin/lib`)
- Does not validate the path - caller should handle errors if expected files are missing

**Usage:**

```python
from lib.session_utils import get_agentize_home

# Get agentize repository root
agentize_home = get_agentize_home()
# Returns: "{AGENTIZE_HOME}" or derived repo root path
```

### `session_dir(makedirs: bool = False) -> str`

Get the session directory path using `AGENTIZE_HOME` fallback.
Expand Down Expand Up @@ -87,7 +109,8 @@ index_path = write_issue_index(session_id, issue_no, workflow, sess_dir=custom_d
- `.claude-plugin/hooks/user-prompt-submit.py`: Session tracking, handsoff check, issue index
- `.claude-plugin/hooks/stop.py`: Session cleanup, handsoff check
- `.claude-plugin/hooks/post-bash-issue-create.py`: Issue number persistence, issue index
- `.claude-plugin/lib/logger.py`: Log file path resolution
- `.claude-plugin/lib/logger.py`: Log file path resolution (uses `get_agentize_home()`)
- `.claude-plugin/lib/workflow.py`: acw invocation (uses `get_agentize_home()`)
- `.claude-plugin/lib/permission/determine.py`: Permission decision logging
- `.cursor/hooks/before-prompt-submit.py`: Cursor hook session tracking
- `.cursor/hooks/stop.py`: Cursor hook session cleanup
31 changes: 29 additions & 2 deletions .claude-plugin/lib/session_utils.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,40 @@
"""Session utilities for hooks and lib modules.

Provides shared session directory path resolution, handsoff mode checks,
and issue index file management used across multiple hook and library files.
AGENTIZE_HOME resolution, and issue index file management used across
multiple hook and library files.
"""

import json
import os


def get_agentize_home() -> str:
"""Get AGENTIZE_HOME path for agentize repository root resolution.

Derives the path in the following order:
1. AGENTIZE_HOME environment variable (if set)
2. Derive from session_utils.py location (.claude-plugin/lib/session_utils.py → repo root)

Returns:
Path to agentize repository root

Note:
Does not validate the path - caller should handle errors if expected files are missing.
Uses os.path.realpath to resolve symlinks (e.g., .cursor/hooks/lib -> .claude-plugin/lib).
"""
# First, check environment variable
env_home = os.getenv('AGENTIZE_HOME', '').strip()
if env_home:
return env_home

# Derive from session_utils.py location: .claude-plugin/lib/session_utils.py → ../../
# Use realpath to resolve symlinks (e.g., .cursor/hooks/lib -> .claude-plugin/lib)
module_dir = os.path.dirname(os.path.realpath(__file__))
repo_root = os.path.dirname(os.path.dirname(module_dir))
return repo_root


def is_handsoff_enabled() -> bool:
"""Check if handsoff mode is enabled via environment variable.

Expand Down Expand Up @@ -61,7 +88,7 @@ def session_dir(makedirs: bool = False) -> str:
Returns:
String path to the session directory (.tmp/hooked-sessions under base).
"""
base = os.getenv('AGENTIZE_HOME', '.')
base = get_agentize_home()
path = os.path.join(base, '.tmp', 'hooked-sessions')

if makedirs:
Expand Down
32 changes: 5 additions & 27 deletions .claude-plugin/lib/workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
- /sync-master: Sync local main/master with upstream

Self-contained design:
- This module provides its own `_get_agentize_home()` and `_run_acw()` helpers
- These invoke the `acw` shell function by sourcing `src/cli/acw.sh` directly
- Uses `get_agentize_home()` from `session_utils.py` for AGENTIZE_HOME resolution
- Provides `_run_acw()` helper that invokes `acw` by sourcing `src/cli/acw.sh`
- No imports from `agentize.shell` or dependency on `setup.sh`
- Maintains plugin standalone capability for handsoff supervisor workflows
"""
Expand All @@ -26,6 +26,8 @@
from typing import Optional
from datetime import datetime

from lib.session_utils import get_agentize_home

# ============================================================
# Workflow name constants
# ============================================================
Expand Down Expand Up @@ -142,30 +144,6 @@ def _get_supervisor_flags() -> str:
# Self-contained acw invocation helpers
# ============================================================

def _get_agentize_home() -> str:
"""Get AGENTIZE_HOME path for acw invocation.

Derives the path in the following order:
1. AGENTIZE_HOME environment variable (if set)
2. Derive from workflow.py location (.claude-plugin/lib/workflow.py → repo root)

Returns:
Path to agentize repository root

Note:
Does not validate the path - caller should handle errors if acw.sh is missing.
"""
# First, check environment variable
env_home = os.getenv('AGENTIZE_HOME', '').strip()
if env_home:
return env_home

# Derive from workflow.py location: .claude-plugin/lib/workflow.py → ../../
module_dir = os.path.dirname(os.path.abspath(__file__))
repo_root = os.path.dirname(os.path.dirname(module_dir))
return repo_root


def _run_acw(provider: str, model: str, input_file: str, output_file: str,
extra_flags: list, timeout: int = 900) -> subprocess.CompletedProcess:
"""Run acw shell function by sourcing acw.sh directly.
Expand All @@ -184,7 +162,7 @@ def _run_acw(provider: str, model: str, input_file: str, output_file: str,
Returns:
subprocess.CompletedProcess result
"""
agentize_home = _get_agentize_home()
agentize_home = get_agentize_home()
acw_script = os.path.join(agentize_home, 'src', 'cli', 'acw.sh')

# Build the bash command to source acw.sh and invoke acw function
Expand Down
13 changes: 9 additions & 4 deletions tests/cli/test-cursor-hook-before-prompt-submit.sh
Original file line number Diff line number Diff line change
Expand Up @@ -53,18 +53,23 @@ STATE_FILE_2="$CENTRAL_HOME/.tmp/hooked-sessions/$SESSION_ID_2.json"
ISSUE_NO_2=$(jq -r '.issue_no' "$STATE_FILE_2")
[ "$ISSUE_NO_2" = "42" ] || test_fail "Expected issue_no=42, got '$ISSUE_NO_2'"

# Test 3: Without AGENTIZE_HOME, session file created in local .tmp/
test_info "Test 3: AGENTIZE_HOME unset → local session file"
# Test 3: Without AGENTIZE_HOME, session file created at repo root (derived from module location)
test_info "Test 3: AGENTIZE_HOME unset → session file at repo root (derived from session_utils.py)"
SESSION_ID_3="test-session-local-3"
run_hook "/issue-to-impl 99" "$SESSION_ID_3" ""

STATE_FILE_3="$LOCAL_HOME/.tmp/hooked-sessions/$SESSION_ID_3.json"
[ -f "$STATE_FILE_3" ] || test_fail "Session file not created at local path: $STATE_FILE_3"
# When AGENTIZE_HOME is unset, get_agentize_home() derives from session_utils.py location
# which resolves to the repo root, not the current working directory
STATE_FILE_3="$PROJECT_ROOT/.tmp/hooked-sessions/$SESSION_ID_3.json"
[ -f "$STATE_FILE_3" ] || test_fail "Session file not created at repo root path: $STATE_FILE_3"

# Verify issue_no is extracted
ISSUE_NO_3=$(jq -r '.issue_no' "$STATE_FILE_3")
[ "$ISSUE_NO_3" = "99" ] || test_fail "Expected issue_no=99, got '$ISSUE_NO_3'"

# Clean up the session file from repo root
rm -f "$STATE_FILE_3"

# Test 4: /ultra-planner with --refine <issue> extracts issue_no
test_info "Test 4: /ultra-planner --refine 123 → issue_no=123"
SESSION_ID_4="test-session-refine-4"
Expand Down
13 changes: 9 additions & 4 deletions tests/cli/test-handsoff-session-path.sh
Original file line number Diff line number Diff line change
Expand Up @@ -44,18 +44,23 @@ STATE_FILE_1="$CENTRAL_HOME/.tmp/hooked-sessions/$SESSION_ID_1.json"
ISSUE_NO_1=$(jq -r '.issue_no' "$STATE_FILE_1")
[ "$ISSUE_NO_1" = "42" ] || test_fail "Expected issue_no=42, got '$ISSUE_NO_1'"

# Test 2: Without AGENTIZE_HOME, session file created in local .tmp/
test_info "Test 2: AGENTIZE_HOME unset → local session file"
# Test 2: Without AGENTIZE_HOME, session file created at repo root (derived from module location)
test_info "Test 2: AGENTIZE_HOME unset → session file at repo root (derived from session_utils.py)"
SESSION_ID_2="test-session-local-2"
run_hook "/issue-to-impl 99" "$SESSION_ID_2" ""

STATE_FILE_2="$LOCAL_HOME/.tmp/hooked-sessions/$SESSION_ID_2.json"
[ -f "$STATE_FILE_2" ] || test_fail "Session file not created at local path: $STATE_FILE_2"
# When AGENTIZE_HOME is unset, get_agentize_home() derives from session_utils.py location
# which resolves to the repo root, not the current working directory
STATE_FILE_2="$PROJECT_ROOT/.tmp/hooked-sessions/$SESSION_ID_2.json"
[ -f "$STATE_FILE_2" ] || test_fail "Session file not created at repo root path: $STATE_FILE_2"

# Verify issue_no is extracted
ISSUE_NO_2=$(jq -r '.issue_no' "$STATE_FILE_2")
[ "$ISSUE_NO_2" = "99" ] || test_fail "Expected issue_no=99, got '$ISSUE_NO_2'"

# Clean up the session file from repo root
rm -f "$STATE_FILE_2"

# Test 3: /ultra-planner with --refine <issue> extracts issue_no
test_info "Test 3: /ultra-planner --refine 123 → issue_no=123"
SESSION_ID_3="test-session-refine-3"
Expand Down
20 changes: 10 additions & 10 deletions tests/cli/test-workflow-module.sh
Original file line number Diff line number Diff line change
Expand Up @@ -289,33 +289,33 @@ print('EMPTY' if flags == '' else flags)
[ "$RESULT" = "EMPTY" ] || test_fail "Expected empty string, got '$RESULT'"

# ============================================================
# Test _get_agentize_home() and _run_acw() helpers
# Test get_agentize_home() (session_utils) and _run_acw() helpers
# ============================================================

test_info "Test 40: _get_agentize_home() reads from AGENTIZE_HOME env var"
test_info "Test 40: get_agentize_home() reads from AGENTIZE_HOME env var"
RESULT=$(run_workflow_python_env "AGENTIZE_HOME=/custom/path" "
from lib.workflow import _get_agentize_home
home = _get_agentize_home()
from lib.session_utils import get_agentize_home
home = get_agentize_home()
print(home)
")
[ "$RESULT" = "/custom/path" ] || test_fail "Expected '/custom/path', got '$RESULT'"

test_info "Test 41: _get_agentize_home() derives from workflow.py location when env var not set"
test_info "Test 41: get_agentize_home() derives from session_utils.py location when env var not set"
RESULT=$(run_workflow_python_env "AGENTIZE_HOME=" "
from lib.workflow import _get_agentize_home
from lib.session_utils import get_agentize_home
import os
home = _get_agentize_home()
home = get_agentize_home()
# Should derive to repo root where Makefile exists
makefile = os.path.join(home, 'Makefile')
print('VALID' if os.path.isfile(makefile) else 'INVALID')
")
[ "$RESULT" = "VALID" ] || test_fail "Expected derived path to be valid repo root, got '$RESULT'"

test_info "Test 42: _get_agentize_home() returns correct repo root structure"
test_info "Test 42: get_agentize_home() returns correct repo root structure"
RESULT=$(run_workflow_python_env "AGENTIZE_HOME=" "
from lib.workflow import _get_agentize_home
from lib.session_utils import get_agentize_home
import os
home = _get_agentize_home()
home = get_agentize_home()
# Verify expected files exist
acw_sh = os.path.join(home, 'src', 'cli', 'acw.sh')
print('ACW_OK' if os.path.isfile(acw_sh) else 'ACW_MISSING')
Expand Down