Skip to content

fix(learn): resolve saved-trajectory path via shared get_trajectories_dir#247

Merged
visahak merged 4 commits into
AgentToolkit:mainfrom
vinodmut:stop-hook-bug
May 4, 2026
Merged

fix(learn): resolve saved-trajectory path via shared get_trajectories_dir#247
visahak merged 4 commits into
AgentToolkit:mainfrom
vinodmut:stop-hook-bug

Conversation

@vinodmut
Copy link
Copy Markdown
Contributor

@vinodmut vinodmut commented May 4, 2026

Summary

  • learn/scripts/on_stop.py hardcoded .evolve/trajectories/claude-transcript_<id>.jsonl while save-trajectory/scripts/on_stop.py resolved the same path from EVOLVE_DIR. When EVOLVE_DIR was set, the two hooks disagreed — the learn skill read a non-existent file and silently produced zero guidelines.
  • Add get_trajectories_dir() in lib/entity_io.py returning get_evolve_dir() / "trajectories" (absolute). Both Stop hooks now share it; learn's hook emits the resolved absolute path in its reason field so the skill can Read it directly.
  • Update learn/SKILL.md so the skill no longer assumes a relative path.

Fixes #246.

Test plan

  • New unit + integration tests in tests/platform_integrations/test_stop_hooks_path_resolution.py — 9 tests covering EVOLVE_DIR-set and default scenarios for each hook plus a parity check. All pass.
  • Existing tests/platform_integrations/test_entity_io.py — 11 tests, no regressions.
  • E2e tests/e2e/test_sandbox_learn_recall.py::test_learn_then_recall_flow — passed in 4m18s against the Docker sandbox.

Summary by CodeRabbit

  • Refactor

    • Centralized trajectory directory handling so all skills save transcripts under EVOLVE_DIR (or .evolve) in a resolved trajectories/ directory with consistent permissions.
  • Documentation

    • Clarified saved trajectory path: use the absolute resolved path EVOLVE_DIR/trajectories/claude-transcript_.jsonl and attach that exact path in entities.
  • Tests

    • Added integration tests to ensure both stop hooks resolve and report the same trajectory path across EVOLVE_DIR and default scenarios.

…_dir

The learn Stop hook hardcoded `.evolve/trajectories/claude-transcript_<id>.jsonl`
while the save-trajectory Stop hook resolved the same path from env. When
EVOLVE_DIR was set the two hooks disagreed and the learn skill read a
non-existent file, silently producing zero guidelines.

Add get_trajectories_dir() to lib/entity_io.py that returns
`get_evolve_dir() / "trajectories"` (absolute). Both Stop hooks now share
this helper; learn's on_stop emits the resolved absolute path in its
reason field. Update SKILL.md so the skill no longer assumes a relative
path. Add platform-integration tests covering EVOLVE_DIR-set and default
scenarios plus a parity check between the two hooks.

Fixes AgentToolkit#246
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 4, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0cda7c9d-3b13-4b2b-818f-61f7a340f183

📥 Commits

Reviewing files that changed from the base of the PR and between 940210d and 95ccbcf.

📒 Files selected for processing (2)
  • platform-integrations/claude/plugins/evolve-lite/skills/learn/SKILL.md
  • tests/platform_integrations/test_stop_hooks_path_resolution.py
✅ Files skipped from review due to trivial changes (1)
  • platform-integrations/claude/plugins/evolve-lite/skills/learn/SKILL.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/platform_integrations/test_stop_hooks_path_resolution.py

📝 Walkthrough

Walkthrough

This PR adds a shared get_trajectories_dir() helper in lib/entity_io.py, updates both the learn and save-trajectory stop hooks to import and use that helper (removing a duplicated local implementation), updates SKILL.md to reference the resolved absolute saved-path, and adds integration tests verifying both hooks resolve the same transcript path under different EVOLVE_DIR configurations.

Changes

Trajectory Path Resolution Unification

Layer / File(s) Summary
Shared Library Helper
platform-integrations/claude/plugins/evolve-lite/lib/entity_io.py
New get_trajectories_dir() creates/returns the absolute trajectories/ directory under the evolve root (get_evolve_dir()), using mkdir(..., mode=0o700) and Path.resolve().
Hook Integration
platform-integrations/claude/plugins/evolve-lite/skills/learn/scripts/on_stop.py, platform-integrations/claude/plugins/evolve-lite/skills/save-trajectory/scripts/on_stop.py
Both stop hooks now sys.path-import get_trajectories_dir from the shared lib and use it to build the saved transcript path; save-trajectory removes its former in-file helper.
Skill Documentation
platform-integrations/claude/plugins/evolve-lite/skills/learn/SKILL.md
Step 0 clarified: saved transcript is under $EVOLVE_DIR/trajectories/ (or .evolve/trajectories/ when unset) as claude-transcript_<session-id>.jsonl; instructs to use the resolved absolute path as saved_trajectory_path and attach it to entities.
Integration Tests
tests/platform_integrations/test_stop_hooks_path_resolution.py
New tests run both hooks as subprocesses with/without EVOLVE_DIR set, assert the hooks emit/write the expected resolved filename under the correct trajectories directory, and assert parity between the two hooks' reported paths.

Sequence Diagram

sequenceDiagram
    autonumber
    participant SaveHook as save-trajectory on_stop
    participant LearnHook as learn on_stop
    participant EntityIO as lib/entity_io.get_trajectories_dir
    participant FS as Filesystem

    SaveHook->>EntityIO: request trajectories dir for session
    EntityIO->>FS: ensure <EVOLVE_DIR|.evolve>/trajectories exists (mode 0o700)
    EntityIO-->>SaveHook: return resolved trajectories path
    SaveHook->>FS: copy transcript -> {resolved}/claude-transcript_<session>.jsonl
    SaveHook-->>Tester: "Trajectory saved: <resolved path>"

    LearnHook->>EntityIO: request trajectories dir for session
    EntityIO->>FS: ensure dir exists (if needed)
    EntityIO-->>LearnHook: return resolved trajectories path
    LearnHook-->>Tester: emit reason with "The saved trajectory path is: <resolved path>"
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • visahak
  • illeatmyhat

Poem

🐰 I hopped through paths both lost and found,

Shared helpers now keep routes sound.
One directory, two hooks in tune,
Trajectories saved beneath the moon.
Hooray — no more paths out of tune! ✨

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR addresses the core issue #246 but introduces a regression: save-trajectory no longer respects CLAUDE_PROJECT_ROOT when EVOLVE_DIR is unset, breaking the intended resolution order. Update get_trajectories_dir() in entity_io.py to restore the full resolution order: EVOLVE_DIR → CLAUDE_PROJECT_ROOT/.evolve → .evolve fallback, matching the original save-trajectory logic.
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: extracting and using a shared get_trajectories_dir helper to resolve paths consistently across hooks.
Out of Scope Changes check ✅ Passed All changes are directly related to unifying trajectory path resolution and testing, with no unrelated modifications.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

Comment @coderabbitai help to get the list of available commands and usage tips.

Fixes failing CI check: check-formatting (3.12)
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
tests/platform_integrations/test_stop_hooks_path_resolution.py (2)

38-46: ⚡ Quick win

Add timeout to subprocess.run to prevent indefinite CI hangs.

If a hook regression causes it to stall (e.g., waiting for additional stdin), the test suite hangs with no recovery. A few seconds is more than enough for these lightweight scripts.

⏱️ Proposed fix
     return subprocess.run(
         [sys.executable, str(script)],
         input=json.dumps(payload),
         capture_output=True,
         text=True,
         cwd=str(cwd),
         env=env,
         check=True,
+        timeout=30,
     )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/platform_integrations/test_stop_hooks_path_resolution.py` around lines
38 - 46, The subprocess invocation in the test currently calls
subprocess.run([...], input=json.dumps(payload), capture_output=True, text=True,
cwd=str(cwd), env=env, check=True) without a timeout, which can hang CI; add a
timeout parameter (e.g., timeout=5 or another small number) to the
subprocess.run call in this test (the call that uses variables script, payload,
cwd, env) so the test fails quickly instead of blocking indefinitely.

66-72: ⚡ Quick win

Use temp_project_dir instead of tmp_path for file operations in this subprocess-spawning test.

The coding guidelines require temp_project_dir for all file operations in platform integration tests (tests/platform_integrations/), with tmp_path only permitted for pure-Python tests that spawn no subprocesses. Since _run_hook calls subprocess.run, the exception does not apply here. The cwd passed to each subprocess should also use temp_project_dir so the process's .evolve/ resolution is fully within the managed sandbox.

♻️ Proposed refactor
 `@pytest.fixture`
-def fake_transcript(tmp_path):
+def fake_transcript(temp_project_dir):
     """Create a fake live transcript file matching Claude Code's layout."""
-    src = tmp_path / "projects" / "fake-project" / f"{_SESSION_ID}.jsonl"
+    src = temp_project_dir / "projects" / "fake-project" / f"{_SESSION_ID}.jsonl"
     src.parent.mkdir(parents=True)
     src.write_text('{"type":"assistant","content":"hi"}\n')
     return src

And update each test to pass temp_project_dir as cwd:

-def test_learn_uses_evolve_dir(tmp_path, fake_transcript):
-    custom = tmp_path / "my-evolve"
+def test_learn_uses_evolve_dir(temp_project_dir, fake_transcript):
+    custom = temp_project_dir / "my-evolve"
     result = _run_hook(
         LEARN_ON_STOP,
-        cwd=tmp_path,
+        cwd=temp_project_dir,
         ...
     )

(Apply the same temp_project_dir substitution across all test functions and the parity test.)

As per coding guidelines: "All file operations in platform integration tests must use temp_project_dir fixture - never touch real files."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/platform_integrations/test_stop_hooks_path_resolution.py` around lines
66 - 72, Replace tmp_path with the temp_project_dir fixture for all file
operations in this test: update the fake_transcript fixture to accept
temp_project_dir instead of tmp_path and create the transcript file under
temp_project_dir so the file and its parent dirs are inside the sandbox; also
ensure calls to _run_hook (and any subprocess.run invocations) pass
cwd=temp_project_dir so subprocesses resolve .evolve inside the managed
temp_project_dir; apply the same substitution across all related test functions
and parity checks referencing fake_transcript or tmp_path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/platform_integrations/test_stop_hooks_path_resolution.py`:
- Around line 38-46: The subprocess invocation in the test currently calls
subprocess.run([...], input=json.dumps(payload), capture_output=True, text=True,
cwd=str(cwd), env=env, check=True) without a timeout, which can hang CI; add a
timeout parameter (e.g., timeout=5 or another small number) to the
subprocess.run call in this test (the call that uses variables script, payload,
cwd, env) so the test fails quickly instead of blocking indefinitely.
- Around line 66-72: Replace tmp_path with the temp_project_dir fixture for all
file operations in this test: update the fake_transcript fixture to accept
temp_project_dir instead of tmp_path and create the transcript file under
temp_project_dir so the file and its parent dirs are inside the sandbox; also
ensure calls to _run_hook (and any subprocess.run invocations) pass
cwd=temp_project_dir so subprocesses resolve .evolve inside the managed
temp_project_dir; apply the same substitution across all related test functions
and parity checks referencing fake_transcript or tmp_path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b8ca6880-03e4-4ae3-890f-ff9de1753153

📥 Commits

Reviewing files that changed from the base of the PR and between a2ab28c and 940210d.

📒 Files selected for processing (5)
  • platform-integrations/claude/plugins/evolve-lite/lib/entity_io.py
  • platform-integrations/claude/plugins/evolve-lite/skills/learn/SKILL.md
  • platform-integrations/claude/plugins/evolve-lite/skills/learn/scripts/on_stop.py
  • platform-integrations/claude/plugins/evolve-lite/skills/save-trajectory/scripts/on_stop.py
  • tests/platform_integrations/test_stop_hooks_path_resolution.py

@vinodmut vinodmut requested review from visahak and removed request for visahak May 4, 2026 18:55
@visahak visahak self-requested a review May 4, 2026 19:13
@visahak
Copy link
Copy Markdown
Collaborator

visahak commented May 4, 2026

Summary

This PR hardens the filesystem backend against interrupted writes, aligns several sharing/sync stderr expectations, and tries to unify Claude stop-hook trajectory path
resolution through a shared helper. The backend changes and the expanded tests look solid overall, and the full e2e suite passed locally in .venv. I found one regression in
the stop-hook path change that should be fixed before merge.

Findings

  1. save-trajectory no longer respects CLAUDE_PROJECT_ROOT when EVOLVE_DIR is unset (confidence: 100/100)
    • Why it matters: the Claude stop hook can now save the copied transcript under the process working directory instead of the actual project root, so the learn flow can miss
      the saved trajectory whenever the hook is launched outside the repo root.
    • Evidence: platform-integrations/claude/plugins/evolve-lite/skills/save-trajectory/scripts/on_stop.py:64 now delegates to platform-integrations/claude/plugins/evolve- lite/lib/entity_io.py:100, and that helper only checks EVOLVE_DIR or .evolve in the current cwd. The existing save helper still documents and implements the older
      resolution order including CLAUDE_PROJECT_ROOT in platform-integrations/claude/plugins/evolve-lite/skills/save-trajectory/scripts/save_trajectory.py:46.
    • Example: I reproduced this by running the current on_stop.py from a temp runner/ directory with CLAUDE_PROJECT_ROOT pointed at a separate project/ directory. It
      wrote claude-transcript_abc-123.jsonl to runner/.evolve/trajectories/…, while project/.evolve/trajectories/… was not created.

Testing

  • uv run pytest -m e2e -v: 196 passed, 544 deselected, 2 warnings in 579.43s
  • Failure details: none
  • Additional validation: confirmed pytest used .venv/bin/python; manually reproduced the CLAUDE_PROJECT_ROOT regression described above

@vinodmut
Copy link
Copy Markdown
Contributor Author

vinodmut commented May 4, 2026

@visahak Thanks for the reproducer.

I'd prefer to keep CLAUDE_PROJECT_ROOT out of the shared get_trajectories_dir(). It lives in lib/entity_io.py, which every platform consumes (bob, claude, codex, claw-code), and teaching the shared helper about a Claude-specific env var pulls platform-specific logic into the common layer. That runs counter to the direction in #235 (plugin-source unification).

For the "hook launched outside the repo root" case, I'd rather rely on the Claude Stop hook's cwd contract (which is project root today), or set EVOLVE_DIR explicitly in the plugin's hook config if we need a path independent of cwd.

Happy to open a follow-up if the cwd assumption breaks in the field.

Copy link
Copy Markdown
Collaborator

@visahak visahak left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@visahak visahak merged commit a584af6 into AgentToolkit:main May 4, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(learn-hook): on_stop ignores EVOLVE_DIR when pointing learn skill at saved transcript

2 participants