Skip to content

feat(session): session lifecycle hooks, CLAUDE_PLUGIN_DATA state root, --json output - #18

Merged
freema merged 2 commits into
mainfrom
feat/session-lifecycle
Jul 28, 2026
Merged

feat(session): session lifecycle hooks, CLAUDE_PLUGIN_DATA state root, --json output#18
freema merged 2 commits into
mainfrom
feat/session-lifecycle

Conversation

@freema

@freema freema commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Summary

Second of four PRs porting the high-value patterns from openai/codex-plugin-cc (see analysis in the session that produced #17). This one adds session tracking:

  • hooks/hooks.json + scripts/session-hook.mjs — SessionStart exports the Claude session id (and CLAUDE_PLUGIN_DATA) into CLAUDE_ENV_FILE; SessionEnd cancels the session's still-running jobs. A closed Claude session no longer leaves detached workers + cursor-agent running unattended. Jobs from other sessions, or without a session stamp, are never touched — intentionally long-running jobs can still be started outside the hook's reach.
  • Jobs are stamped with the owning session (sessionId, stamped centrally in createJob).
  • /cursor:status default view is session-scoped (own + unattributed jobs, 10-row cap, hint line when rows are hidden); --all lifts both.
  • --json on /cursor:status / /cursor:result / /cursor:setup — raw job records / structured doctor report, for scripting and for the upcoming stop-review-gate hook (phase 3).
  • State root honours CLAUDE_PLUGIN_DATA for fresh installs (…/state/jobs/<repo-hash>/, cleaned up with the plugin). An existing ~/.cursor-plugin-cc always wins, so no install ever loses job history; CURSOR_PLUGIN_CC_HOME still overrides everything. Layout under the root is unchanged.

Notes

  • hooks/hooks.json is a new top-level plugin dir (same layout as codex-plugin-cc); session-hook.mjs is the one non-command script under scripts/ — both documented in AGENTS.md.
  • Zero new dependencies; no build step.
  • Independent of fix(jobs): kill the whole process group on /cursor:cancel #17 (only tests/helpers.mjs overlaps, with identical content — merges clean either order).

Test plan

  • New tests/session.test.mjs: session stamping from env, filterJobsForSession, SessionStart env-file write (incl. quote escaping), SessionEnd cancels only the session's running jobs, no-op paths.
  • tests/paths.test.mjs: state-root precedence (env override / legacy dir wins / plugin-data fresh install).
  • npm test: 99/99 green. npm run lint: clean.

…, --json output

Modelled on openai/codex-plugin-cc's session tracking, adapted to the
zero-deps runtime:

- hooks/hooks.json + scripts/session-hook.mjs: SessionStart exports the
  Claude session id (and CLAUDE_PLUGIN_DATA) into CLAUDE_ENV_FILE;
  SessionEnd cancels the session's still-running jobs so a closed session
  no longer leaves detached workers and cursor-agent running unattended.
  Other sessions' jobs and unattributed jobs are never touched.
- createJob stamps sessionId from CURSOR_PLUGIN_CC_SESSION_ID
- /cursor:status default view scoped to the current session (plus
  unattributed jobs) with a hidden-rows hint; --all lifts scope and cap;
  new lib/jobs.mjs#filterJobsForSession
- --json on status/result/setup for scripting (raw job records / doctor
  report); setup's doctor split into gather + render
- pluginHome(): CURSOR_PLUGIN_CC_HOME > existing ~/.cursor-plugin-cc >
  CLAUDE_PLUGIN_DATA/state > ~/.cursor-plugin-cc; jobs/<repo-hash>/
  layout unchanged
- tests: session stamping, session filter, hook handlers (env-file write,
  selective cancel), state-root precedence
- docs: README session-lifecycle section, AGENTS.md state-root rule,
  CHANGELOG

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@freema freema left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

CodeForge Review

Verdict: approve | Score: 8/10

Clean, well-scoped feature PR adding session lifecycle hooks (stamp jobs with the owning Claude session, cancel them on SessionEnd) plus a --json output mode across status/result/setup and a CLAUDE_PLUGIN_DATA-aware state root with legacy-dir fallback. Logic is sound, docs/CHANGELOG are updated consistently with the code, and the new session-hook/paths test suites are solid (all 99 tests pass, eslint clean). Only minor gaps: no direct tests for the new --json branches in status.mjs/result.mjs/setup.mjs, and SessionEnd only cancels jobs in the repo resolved from the hook's cwd, so a session that touched multiple git repos would leave jobs in the other repos running.


Reviewed by CodeForge

const rows = flags['all']
? all
: filterJobsForSession(all, process.env[SESSION_ID_ENV]).slice(0, 10);
if (asJson) {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

[MINOR] The new --json flag (also added to result.mjs and setup.mjs) has no direct unit test coverage, even though the CHANGELOG/README frame it as the primary scripting-facing feature of this release. Only the underlying data shapes (filterJobsForSession, doctor checks) are tested, not the CLI flag wiring itself.

Suggestion: Add a small test per script asserting main(['--json']) (or with a job id) writes valid JSON matching the expected job/doctor record shape to stdout.

(typeof input.session_id === 'string' && input.session_id) || process.env[SESSION_ID_ENV];
if (!sessionId) return 0;
const cwd = typeof input.cwd === 'string' && input.cwd ? input.cwd : process.cwd();
const root = await repoRoot(cwd);

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

[SUGGESTION] handleSessionEnd resolves only one repo (via repoRoot(cwd) at hook-invocation time) and cancels running jobs there. If the same Claude Code session delegated work in more than one git repo (e.g. the user cd'd into a different project mid-session), jobs left running in those other repos are never cancelled when the session ends.

Suggestion: Either document this as a known limitation (single-repo-per-session assumption) in README's Session lifecycle section, or track touched repos per session if multi-repo sessions are a supported use case.

@freema
freema merged commit 14960ec into main Jul 28, 2026
6 checks passed
@freema
freema deleted the feat/session-lifecycle branch July 28, 2026 13:55

@freema freema left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

CodeForge Review

Verdict: approve | Score: 8/10

Well-scoped, well-tested feature: session-scoped job tracking via new SessionStart/SessionEnd hooks, a migration-safe state-root change (CLAUDE_PLUGIN_DATA with legacy-dir fallback), and --json output on three commands. Shell-quoting for the env-file export is handled correctly, cancellation only touches the calling session's own jobs, and docs/CHANGELOG/README are updated in lockstep with the code. Typecheck, lint, and the new tests all pass. Main gaps are missing unit tests for the new --json branches in status/result/setup, and a narrow edge case where SessionEnd only cancels jobs in the repo matching the hook's cwd.


Reviewed by CodeForge

}
// Default view is scoped to the current Claude session (plus unattributed
// jobs); --all lifts both the session scope and the 10-row cap.
const all = listJobs(root);

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

[MINOR] The new --json branches for /cursor:status, /cursor:result, and /cursor:setup --json have no unit tests (session.test.mjs and paths.test.mjs only cover the session-hook/state-root logic). These are simple branches but they are also the ones future hooks/scripts will script against, so a schema regression would be silent.

Suggestion: Add a small test per command asserting the emitted JSON has the expected shape (e.g. for status: array of job records; for result: single record; for setup: {checks, allOk, ...}).

const cwd = typeof input.cwd === 'string' && input.cwd ? input.cwd : process.cwd();
const root = await repoRoot(cwd);
const mine = listJobs(root).filter((j) => j.status === 'running' && j.sessionId === sessionId);
await Promise.all(mine.map((j) => cancelJob(root, j.id, 3_000)));

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

[MINOR] handleSessionEnd resolves repoRoot from the hook's cwd at session end and only cancels running jobs whose repoPath hashes to that same root. If a session delegates a job in one repo and the session's cwd differs at SessionEnd time (or the hook cwd doesn't match the repo the job was created against), that job is silently never cancelled despite carrying this session's id.

Suggestion: Consider scanning across all repo-hash directories under the state root for jobs matching the session id, or document this single-repo assumption explicitly in AGENTS.md/CHANGELOG so it's a known limitation rather than a surprise.

// Existing installs keep their state where it already lives — the harness
// starting to provide a data dir must never strand previous job history.
const legacy = join(homedir(), '.cursor-plugin-cc');
if (existsSync(legacy)) return legacy;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

[SUGGESTION] pluginHome() calls existsSync(legacy) on every invocation, and it's invoked frequently (jobsDir/logsDir/ensureDir call it repeatedly per command run). Not a correctness issue, just a small avoidable stat() per call.

Suggestion: Not worth blocking on — could memoize per-process if it ever shows up as a hot path.

freema added a commit that referenced this pull request Jul 28, 2026
…roup cancel (#22)

Cut the 0.5.0 changelog section covering #17/#18/#20/#21 and bump the
version in package.json, package-lock.json, and plugin.json.
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.

1 participant