Persist SessionEnd cleanup intent before hook return, move deferred effects into fenced recoverable workers, and retain the generated shipping closure used by scripts/run.cjs.
Fixes#3477
Move update availability text out of SessionStart additionalContext and into the hook systemMessage output so the model does not carry persistent update prompt context. Preserve passive autoUpgradePrompt=false wording and cover both installed script paths.\n\nConstraint: issue #3155 requires user-visible update notices without permanent model additionalContext.\nRejected: stderr-only notices | successful SessionStart stderr is not a sufficiently contract-safe user-visible channel.\nConfidence: high\nScope-risk: narrow\nDirective: Keep update notices out of hookSpecificOutput.additionalContext; use user-visible hook output for future startup notices.\nTested: npm test -- --run src/installer/__tests__/session-start-template.test.ts src/__tests__/session-start-script-context.test.ts; npx tsc --noEmit; npm run lint -- src/installer/__tests__/session-start-template.test.ts src/__tests__/session-start-script-context.test.ts; git diff --check; node --check scripts/session-start.mjs; node --check templates/hooks/session-start.mjs\nNot-tested: Manual Claude Code UI rendering of SessionStart systemMessage
Co-authored-by: Codex Review <codex-review@example.com>
Addresses Codex review on #3130.
P1 (validateCwd): cwd is commonly a subdirectory of the repo/workspace root.
The previous direct-only check rejected subdirs and made session-start skip
all restoration. Now walk up from cwd to find a .git or .omc-workspace,
stopping at $HOME so a stray marker/repo in $HOME cannot validate an unrelated
dir. Applied to both scripts/session-start.mjs and templates/hooks/session-start.mjs.
P2 (findWorkspaceRoot): the home-boundary check now runs before the marker
check, so a stray ~/.omc-workspace no longer collapses unrelated repos under
home into one shared state root.
Added a regression test: session-start accepts a nested subdir of a .git repo.
Confidence: high
Scope-risk: narrow
Resolve conflicts after dev advanced past the merge base:
- package.json / plugin.json / marketplace.json / docs/CLAUDE.md: keep dev's 4.14.4 version (feature PR carries no version bump)
- scripts/keyword-detector.mjs: combine dev's added 'ultragoal' clear target with branch's omcRoot arg
- src/hud/render.ts: combine branch's local-build "L" suffix with dev's updateNotification gate
- src/__tests__/doctor-conflicts.test.ts: keep both new imports (checkWorkspaceMarker + checkWindowsUnsafePluginHooks)
- dist/ and bridge/: regenerated via npm run build (generated artifacts)
Confidence: high
Scope-risk: moderate
* Keep update-check cache under Claude config
Route HUD and hook update-cache access through the same CLAUDE_CONFIG_DIR-aware helper so Windows and custom config roots do not split writer and reader state.
Constraint: Scope limited to update-check path consistency for issue #3109.
Rejected: Moving the cache to ~/.omc | That would preserve the HUD-only path and diverge from existing CLAUDE_CONFIG_DIR-aware updater/config storage.
Confidence: high
Scope-risk: narrow
Directive: Keep future update-check readers and writers on getUpdateCheckCachePath().
Tested: npm test -- --run src/__tests__/update-check-path.test.ts src/__tests__/config-dir.test.ts; npx eslint src/utils/config-dir.ts src/hud/index.ts src/__tests__/update-check-path.test.ts; npm run build
Not-tested: Full test suite
* Keep session-start template helper in sync
Expose the update-check cache helper from the template-local config-dir module so direct template execution has the same helper that session-start imports.
Constraint: Scope limited to the update-check path/template regression from PR #3112 CI.
Rejected: Changing session-start error handling | The failure was missing helper propagation, not restore-context behavior.
Confidence: high
Scope-risk: narrow
Directive: Keep templates/hooks/lib/config-dir.mjs aligned with update-check helpers used by templates/hooks/session-start.mjs.
Tested: npm test -- --run src/installer/__tests__/session-start-template.test.ts src/__tests__/update-check-path.test.ts src/__tests__/config-dir.test.ts; npm run build; git diff --check
Not-tested: Full test suite
---------
Co-authored-by: Codex Review <codex-review@example.com>
Adapt OMC state, hooks, HUD, and session tracking to operate correctly
across multiple git repos that share one logical workspace, instead of
assuming "single repo == single state root".
Core:
- `.omc-workspace` marker anchors `.omc/` at a non-git parent dir.
Resolution order: `OMC_STATE_DIR` > `.omc-workspace` > git root > cwd
(src/lib/worktree-paths.ts).
- Session-scoped state via `resolveSessionStatePaths(name, sessionId, dir)`
returning branded `ReadPath`/`WritePath`, so a read path can never be
silently used as a write path. An ESLint `no-restricted-syntax` rule bans
casting these brands outside worktree-paths.ts.
- `resolveSessionId()` centralizes session-id resolution (hook payload wins
for hooks, env wins for CLI) in src/lib/session-id.ts.
- Opt-in legacy state migration via `OMC_MIGRATE_LEGACY_STATE=1`; nothing is
copied by default.
Concurrency and isolation:
- session-start hook uses PID-aware liveness: a dead owner session no longer
suppresses state restore.
- subagent-tracker migrated to session-scoped paths plus `withFileLockSync`,
keeping the hot-path lock scope minimal (read, update, write). Perf bench
records Windows p99 around 3.0 to 3.7 ms, under the Linux budget.
- New concurrent ralph-session and project-memory integration tests.
HUD:
- Multi-repo workspace chip showing subrepo and active-session counts
(src/hud/elements/multi-repo.ts).
- "L" suffix on the OMC version when running from a local fork; local installs
are detected even when the fork is copied (not symlinked) into the plugin
cache.
Other:
- ultragoal artifacts made workspace-aware.
- CI guard (scripts/ci/check-multirepo-paths.mjs wired into ci.yml) plus audit
and smoke scripts that keep new code on the canonical path helpers.
Constraint: dist/ and bridge/ are tracked in this repo, so artifacts were
rebuilt against dev and committed.
Directive: session-scoped state must go through resolveSessionStatePaths();
do not cast ReadPath/WritePath.
Confidence: high
Scope-risk: broad
SessionEnd remains the normal cleanup path. SessionStart now writes a narrow session-scoped marker and only reconciles prior marked sessions when the marker owns its session directory, no SessionEnd summary exists, and PPID/boot evidence shows the owner is gone. The backstop clears OMC mode/mission state only and deliberately avoids team runtime shutdown or tmux PID killing.
Constraint: Issue #2816 requires SessionEnd to stay primary and SessionStart cleanup to fail closed on ambiguous ownership
Rejected: Global stale sweep by age | could erase active concurrent sessions without explicit ownership
Rejected: Process-name dedupe or tmux PID cleanup | explicitly disallowed and unsafe for SessionStart
Confidence: high
Scope-risk: moderate
Directive: Do not broaden SessionStart reconciliation without explicit owner evidence and active-session safety tests
Tested: npm test -- --run src/hooks/__tests__/bridge-routing.test.ts src/hooks/session-end/__tests__/mode-state-cleanup.test.ts
Tested: npx tsc --noEmit
Tested: npm run build
Tested: npm run lint (passes with pre-existing warnings)
Standalone and template SessionStart hooks already prioritized the model-routing override marker, but only the TypeScript bridge emitted the actual provider-safety guidance. This preserves the Bedrock/Vertex/proxy force-inherit reminder across shipped hook surfaces and adds budget-pressure parity tests for both standalone paths.
Constraint: SessionStart scripts/templates must remain standalone and cannot import the TypeScript config loader.
Rejected: Rely on bridge-only coverage | standalone/template hooks are shipped surfaces and can miss provider-safety guidance.
Confidence: high
Scope-risk: narrow
Directive: Keep provider detection and SessionStart priority rules aligned across bridge, scripts, and templates until hook generation is centralized.
Tested: 178 targeted Vitest tests; npm run build; npx tsc --noEmit --pretty false; targeted ESLint; node --check on changed SessionStart scripts; git diff --check; code-review APPROVE; architect WATCH with prior BLOCK resolved
Not-tested: Full GitHub CI on follow-up branch pending PR creation
Large prompt payloads were entering through several independent hook paths: keyword-triggered full skill injection, learned-skill body injection, root AGENTS startup loading, and multi-file context loading. This change converts skill paths to compact path-backed guidance, caps learned-skill descriptors, compacts OMC startup guidance, and enforces aggregate budgets at SessionStart and context-file loading boundaries.
Constraint: Hook templates and standalone scripts must remain runnable without importing the TypeScript runtime.
Rejected: Inline full SKILL.md bodies on keyword detection | this caused the hottest UserPromptSubmit token blowups.
Rejected: Per-file AGENTS truncation only | multi-file AGENTS/CLAUDE accumulation could still exceed budget.
Confidence: high
Scope-risk: moderate
Directive: Keep budget constants aligned across source, scripts, and templates until hook template generation is centralized.
Tested: 584 targeted Vitest tests; npx tsc --noEmit --pretty false; targeted ESLint on changed TS/test files; node --check on changed mjs hooks/scripts; npm run build; git diff --check; code-review APPROVE; architect CLEAR
Not-tested: Full repo lint is still blocked by 10 pre-existing warnings outside touched paths
If stalePath's parent directory was removed (deleted config tree),
symlinkSync throws ENOENT and the repair is silently skipped.
Create the parent directory recursively before creating the temp symlink.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
If renameSync fails after tmpLink creation (e.g., replacing an existing
junction on Windows), the fallback path recreates the symlink at stalePath.
Without unlinking first, symlinkSync throws EEXIST on a pre-existing
dangling link and the error is swallowed — leaving the stale root broken.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
POSIX symlink target was relative (just version name), resolving from
symlink parent — wrong when stalePath isn't under cacheBase.
Always use absolute path so the symlink works regardless of parent.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Trailing / or \ in CLAUDE_PLUGIN_ROOT causes tmpLink to be placed
inside the non-existent version directory, making symlinkSync fail with
ENOENT. Strip trailing separators before deriving paths.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Upgrade removes old version directories but running sessions still have
the old CLAUDE_PLUGIN_ROOT in their environment, causing "Plugin directory
does not exist" errors on every hook invocation.
Guard detects when CLAUDE_PLUGIN_ROOT points to a deleted version
directory and creates a symlink from the stale path to the latest version,
so subsequent run.cjs invocations resolve correctly.
Constraint: Must not block or delay session-start
Constraint: Atomic symlink to avoid race conditions on concurrent upgrades
Rejected: Error suppression only | sessions keep failing until restart
Rejected: Skip grace period | could delete in-use cache dirs
Confidence: high
Scope-risk: narrow
Directive: Grace period (24h) prevents removing dirs still referenced by long-running sessions
Not-tested: Concurrent upgrade race condition (requires multi-process test harness)
scripts/persistent-mode.cjs and .mjs hardcoded join(directory, '.omc', 'state'),
bypassing OMC_STATE_DIR entirely. session-start.mjs had the same bug across all
state-restore paths (ultrawork, ralph, notepad, todos).
Add scripts/lib/state-root.{mjs,cjs} — thin async wrappers that delegate to
getOmcRoot() from dist/lib/worktree-paths.js (the canonical resolver) when
CLAUDE_PLUGIN_ROOT is set, with an inline OMC_STATE_DIR + simplified-hash
fallback when dist is unavailable (dev/first-run only).
Wire the helper into all three hook entrypoints so every state read/write
goes through a single resolver.
Add src/__tests__/state-root-resolution.test.ts — 7 subprocess regression tests
covering:
- Default .omc path when OMC_STATE_DIR is unset
- session-start restores ralph/ultrawork from centralized dir
- session-start does NOT restore when state is only in default .omc
- Stop hook blocks when active ralph state is in default dir (baseline)
- Stop hook blocks when active ralph state is in centralized dir
- Stop hook does NOT block when state is only in default dir + OMC_STATE_DIR set
Note: stop-hook fixtures require fresh started_at/last_checked_at timestamps
because persistent-mode.cjs treats state older than 2h as stale and ignores it.
Constraint: scripts/persistent-mode.cjs is CJS; dist/lib/worktree-paths.js is ESM
— resolved via dynamic import() inside the async helper.
Rejected: inline OMC_STATE_DIR check in each script | duplicates project-identifier
logic and drifts over time.
Confidence: high
Scope-risk: narrow
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Several code paths hardcoded ~/.claude instead of resolving the active config directory. Introduce
getClaudeConfigDir() as a shared helper (with ESM/CJS/shell mirrors in scripts/lib/) and update
all call sites:
- transcript validation: allow paths under the active config directory
- transcript fallback resolution: build projects/ lookups from the active config directory instead
of inline env-var fallback
- rules discovery: use getClaudeConfigDir() instead of hardcoded path
- rules injector: remove dead homeDir parameter from findRuleFiles and USER_RULE_DIR export;
breaking change to the public surface (no npm dependents)
- uninstall: preserve existing CLAUDE_CONFIG_DIR env var
- CLI: use getClaudeConfigDir() for runtime paths and config directory descriptions
(default: ~/.claude/)
- orchestrator: allow absolute paths under the active config directory while preserving
project-root and relative-path checks
- skills tools: reference the active config directory in runtime output and help text
- shared memory: read .omc-config.json from the active config directory
- session history search: resolve transcript roots from the active config directory
- auto-slash-command executor: replace hardcoded ~/.claude in error message output
- factcheck guard: add ${CLAUDE_CONFIG_DIR} token to forbidden_path_prefixes defaults and token
expander, replacing the hardcoded ${HOME}/.claude prefix
- config-dir helper: implement full getClaudeConfigDir() in config-dir.ts with tilde expansion
and trailing-slash normalization, replacing the trivial getConfigDir() wrapper and the re-export
from paths.ts
- runtime Node scripts: use shared config-dir helpers (ESM and CJS) instead of repeating
~/.claude fallbacks
- standalone hook templates: add templates/hooks/lib/config-dir.mjs shared helper (mirrors
scripts/lib/config-dir.mjs) so template scripts resolve the config directory consistently
- HUD wrapper: import the shared lib/config-dir helper instead of embedding duplicate
config-directory resolution logic
- persistent-mode scripts: resolve native tasks/todos from [$CLAUDE_CONFIG_DIR|~/.claude]
- shell scripts: use a shared config-dir.sh helper with ~ expansion and trailing-slash
normalization
- installer: copy config-dir helpers alongside standalone hooks and the HUD find-node.sh helper
- plugin setup: move nodeBin to module scope so both settings.json and hooks.json patching blocks
can access it (pre-existing const scoping bug where hooks.json patch always failed silently)
- skill snippets: replace $HOME/.claude with ${CLAUDE_CONFIG_DIR:-$HOME/.claude} in executable
shell snippets across 8 skill files
- skill prose instructions: replace hardcoded ~/.claude in LLM-actionable directives (Read/Write
tool targets, bash code blocks, path references in step instructions) across 9 skill files so
an LLM executing the skill resolves the active config directory
- HUD usage API: document and test that Keychain service names hash the exact CLAUDE_CONFIG_DIR
string, preserving distinct service-name mapping for ~-prefixed vs expanded inputs
- test files: align mock paths from paths.js to config-dir.js; fix vi.mock hoisting in
doctor-conflicts.test.ts; resolve macOS /var symlink mismatch in bridge-integration,
team-status, and edge-cases tests; update string-pattern assertions in hud-windows.test.ts
and mingw-escape.test.ts
- test script: source lib/config-dir.sh in scripts/test-pr25.sh for post-install path
verification
Adds focused regression coverage in:
- src/__tests__/auto-update.test.ts
- src/__tests__/cli-config-stop-callback.test.ts
- src/__tests__/config-dir.test.ts
- src/__tests__/delegation-enforcement-levels.test.ts
- src/__tests__/doctor-conflicts.test.ts
- src/__tests__/hud-api-key-source.test.ts
- src/__tests__/hud-marketplace-resolution.test.ts
- src/__tests__/hud-windows.test.ts
- src/__tests__/hud/cli-diagnostic.test.ts
- src/__tests__/hud/usage-api-lock.test.ts
- src/__tests__/hud/usage-api-stale.test.ts
- src/__tests__/hud/usage-api.test.ts
- src/__tests__/installer.test.ts
- src/__tests__/purge-stale-cache.test.ts
- src/__tests__/session-history-search.test.ts
- src/__tests__/setup-claude-md-script.test.ts
- src/__tests__/shared-memory.test.ts
- src/hooks/factcheck/__tests__/factcheck.test.ts
- src/installer/__tests__/standalone-hook-reconcile.test.ts
- src/notifications/__tests__/config-merge.test.ts
- src/notifications/__tests__/profiles.test.ts
- src/openclaw/__tests__/config.test.ts
- src/skills/__tests__/mingw-escape.test.ts
- src/team/__tests__/bridge-integration.test.ts
- src/team/__tests__/edge-cases.test.ts
- src/team/__tests__/inbox-outbox.test.ts
- src/team/__tests__/message-router.test.ts
- src/team/__tests__/outbox-reader.test.ts
- src/team/__tests__/team-registration.test.ts
- src/team/__tests__/team-status.test.ts
Move clearTimeout from try body to finally block in both session-start
scripts to prevent timer leaks on fetch errors.
Constraint: Fetch abort controller requires timeout to be cleared
Confidence: high
Scope-risk: narrow
When running as a Claude Code plugin (CLAUDE_PLUGIN_ROOT is set), the
silentAutoUpdate config option has no effect because session-start.mjs
never calls initSilentAutoUpdate() and performUpdate() blocks on
isRunningAsPlugin(). Add a session-start warning when both conditions
are detected, directing users to use the correct update method.
Fixes#1773
Co-authored-by: sspark-kisane <woo47380@gmail.com>
The installed SessionStart hook runs in its own Node process, so the
existing project-memory registration path never reached the startup
additionalContext consumed by Claude Code. Load and, when needed,
refresh persisted project memory directly from the plugin runtime
inside the session-start script, then append the formatted summary
without relying on process-local collector state. Keep the legacy
helper aligned with plugin-root-aware imports.
Constraint: The installed SessionStart runtime is `scripts/session-start.mjs`, not the in-process bridge path
Constraint: Session-start hooks must remain failure-tolerant and continue on missing dist/runtime artifacts
Rejected: Refactor all session-start assembly into shared bridge/script runtime | broader change than needed for the bug
Rejected: Preserve collector-only registration across processes | process isolation makes that design ineffective here
Confidence: high
Scope-risk: narrow
Directive: Do not rely on in-memory contextCollector state for subprocess hook injection paths without a persisted or returned handoff
Tested: node --check scripts/session-start.mjs scripts/project-memory-session.mjs
Tested: Manual A/B subprocess reproduction against pre-fix session-start script vs patched script with persisted project-memory.json
Tested: Added focused regression test in src/__tests__/session-start-script-context.test.ts
Not-tested: Full Vitest run in this worktree (local shared node_modules does not expose a runnable repo-local vitest binary)
Related: issue #1779
* fix(notifications): fix 3 reply listener bugs preventing Telegram reply injection
Bug 1: session-start.mjs now calls startReplyListener() after notify block
so the reply listener daemon is started automatically on session start.
Bug 2: injectReply() replaces confidence-based analyzePaneContent() check
with simple empty-pane check, fixing false negatives on non-English panes.
Bug 3: sendToPane() splits tmux send-keys into two commands - text with
-l flag (literal) then Enter separately - fixing submit in Claude Code TUI.
Fixes#1097
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(notifications): remove unused analyzePaneContent import and dead code
Clean up dead code from the confidence check removal: remove unused
analyzePaneContent import and update doc comment.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: gaebal-gajae <gaebal-gajae@layofflabs.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When a plugin update replaces an old version directory, sessions still
referencing the old CLAUDE_PLUGIN_ROOT fail with MODULE_NOT_FOUND,
stopping execution. This was intermittent and depended on timing of
cache cleanup vs hook invocation.
Changes:
- run.cjs: validate target exists before spawning; fall back to latest
available plugin cache version with the same script; exit 0 gracefully
when no valid target is found (prevents hook errors from blocking execution)
- session-start.mjs: use atomic temp-symlink + rename for symlink updates
to eliminate the race window between rmSync and symlinkSync
- Add regression tests for run.cjs graceful fallback scenarios
Fixes#1007
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
On Windows, directory symlinks require Developer Mode or admin
privileges. Junction type works without either, making the cache
symlink approach reliable across all platforms.
- Handle EEXIST race in symlinkSync when concurrent sessions create
the same symlink simultaneously
- Add comment linking staleStats default to STALE_THRESHOLD_MS constant
- Add test for updating existing symlink pointing to non-latest target
Stop hooks fail with "Cannot find module" when a plugin is updated
mid-session because session-start.mjs aggressively deletes old cache
directories while running sessions still reference them via the
resolved CLAUDE_PLUGIN_ROOT environment variable.
Changes:
- session-start.mjs: Replace rmSync with symlinkSync for old versions
(beyond latest 2), so stale CLAUDE_PLUGIN_ROOT paths follow the
symlink to the current version's scripts
- paths.ts: Increase purgeStalePluginCacheVersions grace period from
1 hour to 24 hours to avoid premature deletion during long sessions
- Add integration tests for the symlink cleanup behavior
- Update existing purge-stale-cache test staleStats default to match
the new 24-hour threshold
Closes#970
- Remove duplicate `const configDir` declaration in session-start.mjs
that caused a SyntaxError killing the hook on every startup
- Add Telegram env var fallbacks (OMC_TELEGRAM_NOTIFIER_BOT_TOKEN,
OMC_TELEGRAM_NOTIFIER_UID) to notification config
- Wire up notify('session-start') in bridge.ts processSessionStart
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
All hardcoded ~/.claude paths now respect the CLAUDE_CONFIG_DIR
environment variable when set, enabling custom config directory
locations. Added src/utils/config-dir.ts as the canonical source
and updated getClaudeConfigDir() in paths.ts to delegate to it.
Closes#559
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implement a full notification pipeline for session-start, session-stop, session-end, and ask-user-question events with tmux/context metadata and env-based auto-configuration.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use pathToFileURL for dynamic imports instead of raw file paths, which
fixes "hook error" on Windows where backslash paths break ESM imports.
Add suppressOutput: true to empty hook responses to mitigate the known
Claude Code "hook error" display bug (#10936).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When multiple Claude Code sessions run in the same directory, mode state
(ultrawork, ralph, autopilot, etc.) leaked between sessions because state
files fell back to legacy shared paths. This change enforces session-scoped
state isolation: when a session_id is known, legacy state is invisible.
Key changes:
- persistent-mode.cjs: add isSessionMatch() helper, remove legacy fallback
in readStateFileWithSession() when sessionId present, apply to all 8 modes
- session-start.mjs: restore only from session-scoped paths when session_id
exists, legacy paths only when session_id absent
- ralph/loop.ts: readRalphState() and isUltraQAActive() no legacy fallback
- ultraqa/index.ts: readUltraQAState() no legacy fallback
- autopilot/state.ts: readAutopilotState() session-scoped only, thread
sessionId through cancel.ts, validation.ts, enforcement.ts; fix missing
sessionId propagation in transitionRalphToUltraQA -> updateExecution
- state-tools.ts: warnings added for missing session_id, schema descriptions
updated
- Tests updated to match session-first behavior
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When two Claude Code instances run concurrently in the same repository,
they share state files at .omc/state/{mode}-state.json, causing write
clobbering, context leakage, and race conditions.
This commit namespaces state file paths by session ID so each session
gets its own isolated directory at:
.omc/state/sessions/{session_id}/{mode}-state.json
Key changes:
- Add session path resolution layer (worktree-paths.ts)
- Update mode-registry with session-aware detection and cross-session scanning
- Add session_id parameter to all 5 MCP state tools
- Thread sessionId through all 5 mode modules (ultrawork, autopilot, ralph, ultraqa, ultrapilot)
- Update persistent-mode hook to pass sessionId through all state operations
- Update 4 hook scripts (keyword-detector, persistent-mode, session-start)
- Add 44 tests covering session isolation, backward compatibility, and security
Legacy state files at the old shared path continue to work as a read
fallback. Swarm (SQLite-based) is excluded as it has built-in concurrency.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude Code sends hook input with snake_case field names (tool_name,
tool_input, tool_response, session_id, cwd) but all OMC hook scripts
were reading camelCase (toolName, toolInput, toolOutput, sessionId,
directory). This caused hooks to silently receive empty values.
Changes:
- All 11 hook scripts updated to read snake_case first with camelCase fallback
- PostToolUse hooks return suppressOutput:true when no meaningful message
to reduce "hook error" noise (Claude Code display bug #10936)
- template hooks updated to match
- Version bumped to 4.0.4
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Detect version drift between plugin root, npm package, and CLAUDE.md
- Notify user to run 'omc update' when components are out of sync
- Add npm registry update check with 24h cache and 2s timeout
- Clean up old plugin cache versions on session start (keep latest 2)
- Fix HUD wrapper to filter for built versions before selecting latest
- Add <!-- OMC:VERSION:X.Y.Z --> marker to CLAUDE.md for version tracking
- Update mergeClaudeMd() to inject version marker dynamically
- Fix update notification to recommend 'omc update' instead of '/plugin install'
Fixes from code review:
- Return 'unknown' when CLAUDE.md exists but lacks version marker
- Report legacy users as "needs migration" instead of silently skipping
- Use string equality for drift detection (more reliable than semverCompare)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
PR #354 only fixed pre-tool-enforcer.mjs but missed both
session-start.mjs files that also scanned ~/.claude/todos/,
causing phantom task counts in fresh sessions.
Closes#358
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Ultrawork's persistent session hooks were not checking session_id before
blocking stops or restoring state, causing the original task to re-execute
in new sessions that inherited stale ultrawork state.
Changes:
- keyword-detector: store session_id in state files on activation
- session-start: only restore ultrawork if session_id matches current session
- persistent-mode: only block stops for matching session_id
- Add session isolation tests for persistent-mode
Closes#311
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
All hook scripts were using a `message` field in their JSON output which is
not part of the Claude Code hooks API. This caused hook messages to be silently
dropped — keyword detection, skill injection, session restore, pre-tool
reminders, and post-tool verification were all non-functional.
Fixed 5 scripts to use the documented `hookSpecificOutput.additionalContext`:
- keyword-detector.mjs (UserPromptSubmit) - 16 instances
- skill-injector.mjs (UserPromptSubmit) - 1 instance
- session-start.mjs (SessionStart) - 1 instance
- pre-tool-enforcer.mjs (PreToolUse) - 1 instance
- post-tool-verifier.mjs (PostToolUse) - 2 instances
Also added RULE 5 (Documentation-First Development) to CLAUDE.md to prevent
future assumptions about SDK/API behavior without consulting official docs.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>