From 4d7706286ce51df452e3005fe3d40736c1589c40 Mon Sep 17 00:00:00 2001 From: Jian Weng Date: Fri, 23 Jan 2026 20:39:19 +0300 Subject: [PATCH] [#592][refactor] Consolidate _get_agentize_home() into session_utils.py .claude-plugin/lib/session_utils.py: Add get_agentize_home() as public function with symlink-aware path resolution using os.path.realpath(). Update session_dir() to use it. .claude-plugin/lib/workflow.py: Remove _get_agentize_home(), import from session_utils. Update module docstring to reflect the dependency on session_utils. .claude-plugin/lib/logger.py: Update _tmp_dir() to use get_agentize_home() instead of direct os.getenv() call. .claude-plugin/lib/session_utils.md: Document get_agentize_home() interface and behavior. .claude-plugin/lib/README.md: Update workflow.py description. tests/cli/test-workflow-module.sh: Update tests 40-42 to import from session_utils. tests/cli/test-handsoff-session-path.sh: Update test 2 for new path derivation behavior. tests/cli/test-cursor-hook-before-prompt-submit.sh: Update test 3 for new behavior. This eliminates AGENTIZE_HOME path resolution duplication across workflow.py, session_utils.py, and logger.py by centralizing the robust derivation logic. Co-Authored-By: Claude Opus 4.5 --- .claude-plugin/lib/README.md | 2 +- .claude-plugin/lib/logger.py | 6 ++-- .claude-plugin/lib/session_utils.md | 25 ++++++++++++++- .claude-plugin/lib/session_utils.py | 31 ++++++++++++++++-- .claude-plugin/lib/workflow.py | 32 +++---------------- .../test-cursor-hook-before-prompt-submit.sh | 13 +++++--- tests/cli/test-handsoff-session-path.sh | 13 +++++--- tests/cli/test-workflow-module.sh | 20 ++++++------ 8 files changed, 90 insertions(+), 52 deletions(-) diff --git a/.claude-plugin/lib/README.md b/.claude-plugin/lib/README.md index f1d58ce..fc5118c 100644 --- a/.claude-plugin/lib/README.md +++ b/.claude-plugin/lib/README.md @@ -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 diff --git a/.claude-plugin/lib/logger.py b/.claude-plugin/lib/logger.py index 406d724..adcc61c 100644 --- a/.claude-plugin/lib/logger.py +++ b/.claude-plugin/lib/logger.py @@ -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') diff --git a/.claude-plugin/lib/session_utils.md b/.claude-plugin/lib/session_utils.md index 5800ee6..846fb05 100644 --- a/.claude-plugin/lib/session_utils.md +++ b/.claude-plugin/lib/session_utils.md @@ -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. @@ -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 diff --git a/.claude-plugin/lib/session_utils.py b/.claude-plugin/lib/session_utils.py index a0a2e4b..fbf5cd6 100644 --- a/.claude-plugin/lib/session_utils.py +++ b/.claude-plugin/lib/session_utils.py @@ -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. @@ -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: diff --git a/.claude-plugin/lib/workflow.py b/.claude-plugin/lib/workflow.py index 9d76fc0..f5127d1 100644 --- a/.claude-plugin/lib/workflow.py +++ b/.claude-plugin/lib/workflow.py @@ -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 """ @@ -26,6 +26,8 @@ from typing import Optional from datetime import datetime +from lib.session_utils import get_agentize_home + # ============================================================ # Workflow name constants # ============================================================ @@ -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. @@ -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 diff --git a/tests/cli/test-cursor-hook-before-prompt-submit.sh b/tests/cli/test-cursor-hook-before-prompt-submit.sh index 65777b2..8b51e1a 100755 --- a/tests/cli/test-cursor-hook-before-prompt-submit.sh +++ b/tests/cli/test-cursor-hook-before-prompt-submit.sh @@ -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 extracts issue_no test_info "Test 4: /ultra-planner --refine 123 → issue_no=123" SESSION_ID_4="test-session-refine-4" diff --git a/tests/cli/test-handsoff-session-path.sh b/tests/cli/test-handsoff-session-path.sh index 134ea66..590a0cb 100755 --- a/tests/cli/test-handsoff-session-path.sh +++ b/tests/cli/test-handsoff-session-path.sh @@ -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 extracts issue_no test_info "Test 3: /ultra-planner --refine 123 → issue_no=123" SESSION_ID_3="test-session-refine-3" diff --git a/tests/cli/test-workflow-module.sh b/tests/cli/test-workflow-module.sh index e1fe39c..797daee 100755 --- a/tests/cli/test-workflow-module.sh +++ b/tests/cli/test-workflow-module.sh @@ -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')