The deployed scripts/keyword-detector.mjs still listed "autonomous" in the
autopilot trigger regex, even though src/hooks/keyword-detector/index.ts
and templates/hooks/keyword-detector.mjs both removed it some time ago.
Because "autonomous" is extremely common in technical/research prose
("autonomous driving", "autonomous agent", "autonomous system"), the
UserPromptSubmit hook was silently creating autopilot-state.json on
completely unrelated prompts. The Stop hook (persistent-mode.mjs) then
treated autopilot as active and forced the assistant into a
"[AUTOPILOT - Phase: unspecified] Autopilot not complete. Continue working."
block loop that the user could not easily escape — state_clear from the
cancel skill did not target the per-project state path and the Stop hook
kept refreshing it.
Reproduction (before this patch):
echo '{"hook_event_name":"UserPromptSubmit","cwd":"'"$(pwd)"'",
"session_id":"x","prompt":"DriveVLA-W0: World Models Amplify Data
Scaling Law in Autonomous Driving"}' | node scripts/keyword-detector.mjs
-> emits [MAGIC KEYWORD: AUTOPILOT] and writes
.omc/state/sessions/x/autopilot-state.json
After the patch the same prompt returns
{"continue":true,"suppressOutput":true} and no state file is created,
while an explicit "autopilot build a todo CLI" prompt still triggers
autopilot as expected.
Changes:
- scripts/keyword-detector.mjs: drop "autonomous" from the autopilot
regex; add a comment explaining why, pointing at the already-aligned
TS source and templates mjs.
- src/__tests__/keyword-detector-script.test.ts: add two regression
tests — one negative ("Autonomous Driving" paper title must not
activate autopilot state) and one positive control (explicit
"autopilot ..." still writes state and emits the magic keyword).
The HUD wrapper needs npm root discovery to keep global installs reachable when cache and marketplace paths are absent. Node 20.12+ rejects direct npm.cmd execFileSync calls on Windows unless shell execution is enabled, so limit shell:true to the Windows npm.cmd probe while preserving the existing non-Windows npm exec behavior.
Constraint: Node 20.12+ requires shell execution for Windows .cmd/.bat child_process calls.
Rejected: Use execSync('npm root -g') everywhere | broader behavior change for non-Windows wrapper runtime.
Confidence: high
Scope-risk: narrow
Tested: npm test -- --run src/installer/__tests__/hud-wrapper-env.test.ts src/__tests__/hud-windows.test.ts src/__tests__/hud-wrapper-template-sync.test.ts src/__tests__/hud-marketplace-resolution.test.ts
Tested: npx tsc
OMX deep-interview and ralplan flows can write approved artifacts and state
under .omx, but the post-ralplan shortcut path only recognized .omc plan
artifacts, only parsed omc launch hints, and returned early before short
follow-up prompts like "team" could reach the approved execution gate.
This narrows the fix to the state-transition detection path: planning artifact
resolution now reads both .omc and .omx plan roots, launch hints accept OMX
commands, and keyword-detector checks approved follow-ups before the no-match
fast path while recognizing OMX ralplan state files.
Constraint: Team keyword auto-detection remains disabled to avoid worker respawn loops
Rejected: Re-enable generic team keyword detection | would broaden behavior beyond the broken post-ralplan transition
Rejected: Rewrite ralplan state storage globally to .omx only | wider migration than needed for issue #2714
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep approved follow-up detection aligned with both OMC and OMX artifact/state namespaces unless runtime ownership is fully unified
Tested: npx vitest run src/planning/__tests__/artifacts.test.ts src/team/__tests__/followup-planner.test.ts src/__tests__/keyword-detector-script.test.ts
Not-tested: Full repository test suite
Related: #2714
ScheduleWakeup-style resume events are not real persistence work, but the Stop hook treated them like ordinary idle stops and could inject continuation text that tells Claude to run /oh-my-claudecode:cancel. That let scheduled /loop turns cancel themselves before the scheduled task executed.
This change adds a narrow scheduled-wakeup bypass alongside the existing non-reinforceable stop guards and mirrors the same detector in the shipped script/template hook surfaces so installed runtime behavior stays aligned with the shared TypeScript path.
Constraint: Installed Stop hook behavior is shipped via script/template surfaces as well as shared TypeScript logic
Rejected: Tighten global persistent-mode freshness rules | broader behavioral change across legitimate long-running sessions
Confidence: medium
Scope-risk: narrow
Directive: Keep scheduled-resume stop marker detection aligned across shared logic and script/template mirrors if Claude Code changes its native wakeup payloads
Tested: ./node_modules/.bin/vitest run src/hooks/todo-continuation/__tests__/isRateLimitStop.test.ts src/hooks/persistent-mode/__tests__/rate-limit-stop.test.ts src/hooks/persistent-mode/stop-hook-blocking.test.ts
Tested: ./node_modules/.bin/eslint src/hooks/todo-continuation/index.ts src/hooks/persistent-mode/index.ts src/hooks/todo-continuation/__tests__/isRateLimitStop.test.ts src/hooks/persistent-mode/__tests__/rate-limit-stop.test.ts src/hooks/persistent-mode/stop-hook-blocking.test.ts
Tested: ./node_modules/.bin/tsc --noEmit --pretty false
Not-tested: Live Claude Code /loop ScheduleWakeup reproduction on macOS
Re-reviewed in OMX after follow-up commits. Original blocker was contract drift between hook-side alias acceptance and end-to-end routing proof. The updated PR narrows the routing proof to CC-native envs, excludes OMC_MODEL_* from alias proof, and keeps OMC-owned vars on the stricter safety path. Targeted hook/model-routing tests passed locally and CI is fully green.
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)
* Keep explicit ralplan startup from stalling before planning
Explicit /oh-my-claudecode:ralplan invokes were not guaranteed to arm state and emit startup context early enough at UserPromptSubmit, which left startup behavior dependent on later heuristics. This change makes the startup contract deterministic in both the standalone keyword-detector script and the TypeScript bridge path, while preserving the stop hook's awaiting-confirmation bypass so startup does not get choked before the planning workflow begins.
Constraint: Explicit slash invoke must initialize ralplan state during UserPromptSubmit, not later via Skill-tool side effects
Constraint: Stop-hook startup path must stay non-blocking while ralplan is awaiting confirmation
Rejected: Rely on existing task-size/keyword heuristics | explicit slash invoke is an owner-defined contract, not a best-effort detection path
Rejected: Change stop-hook terminal logic only | would not guarantee init context/state is present at startup
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep explicit /ralplan startup handling aligned between scripts/keyword-detector.mjs and src/hooks/bridge.ts so standalone and bridge paths cannot drift again
Tested: npm test -- --run src/__tests__/keyword-detector-script.test.ts src/hooks/__tests__/bridge-routing.test.ts src/hooks/persistent-mode/stop-hook-blocking.test.ts; npm run build; manual hook replay for /oh-my-claudecode:ralplan startup + Stop bypass
Not-tested: Full live Claude Code slash-command session outside the local hook harness
* Preserve explicit /ralplan startup context in bridge hook output
The explicit /ralplan UserPromptSubmit path in the bridge returned a top-level message while the real hook contract expects hookSpecificOutput.additionalContext. That mismatch let source tests pass in some paths while leaving the built bridge artifact stale for the serialized hook path.
This narrows the fix to the explicit startup branch, adds regression coverage around serialization and routing, and rebuilds the shipped bridge outputs so runtime behavior matches the source contract.
Constraint: Keep explicit /ralplan state-init and stop-hook awaiting-confirmation behavior unchanged
Rejected: Fix only scripts/keyword-detector.mjs | explicit bridge/runtime path would remain inconsistent
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: UserPromptSubmit bridge handlers must emit injectable context through hookSpecificOutput.additionalContext, not top-level message
Tested: vitest targeted bridge/keyword-detector/persistent-mode suite; tsc; npm run build:cli; manual scripts/keyword-detector.mjs explicit /ralplan output check
Not-tested: full repository test suite; installed external Claude hook environment end-to-end
Related: PR #2624
---------
Co-authored-by: Codex Review <codex-review@example.com>
scripts/pre-tool-enforcer.mjs still hardcoded `join(directory, '.omc', 'state')`
in seven places, bypassing OMC_STATE_DIR entirely. PR #2532 centralized the
resolver for session-start.mjs and persistent-mode but explicitly scoped itself
to those entrypoints — pre-tool-enforcer slipped through. When users ran
Claude Code with OMC_STATE_DIR pointing to a relocated state directory, the
PreToolUse hook still read and wrote from `<project>/.omc/state/…`, so:
- team-routing enforcement (`getActiveTeamState`) never saw centralized
team-state.json → Task calls under an active team silently bypassed the
[TEAM ROUTING REQUIRED] redirect.
- skill protection (`writeSkillActiveState`) wrote `skill-active-state.json`
into the project tree while sibling hooks looked for it in the centralized
dir → the Stop hook could terminate a session mid-skill.
- subagent tracking, canonical team state, and ralph/ultrawork confirmation
flag clearing all read/wrote the wrong location.
Adopt the same pattern PR #2532 used for persistent-mode:
- Import `resolveOmcStateRoot` from `./lib/state-root.mjs`.
- Resolve `omcRoot` + `stateDir` once at the top of `main()` and thread
`stateDir` through every helper (`getAgentTrackingInfo`, `hasActiveMode`,
`readCanonicalActiveTeamState`, `getActiveTeamState`,
`generateAgentSpawnMessage`, `writeSkillActiveState`,
`clearAwaitingConfirmationFlag`, `confirmSkillModeStates`).
- Helper signatures change from `(directory, …)` to `(stateDir, …)` —
nothing else inside those functions used `directory`, so the rename is
purely mechanical.
Scope notes:
- `.omc/todos.json` (pre-tool-enforcer.mjs:189) is left alone. That path
is also hardcoded in persistent-mode.{cjs,mjs} and two templates; it
deserves its own follow-up that covers all five call sites atomically
rather than a half-fix here.
- `.omc-config.json` / `.omc/config.json` reads (lines 494–495) are
config, not ephemeral state, and follow a project-wide convention
(40+ files) that is out of scope for a state-root PR.
Tests extend `src/__tests__/state-root-resolution.test.ts` with four
scenarios mirroring the persistent-mode coverage:
1. baseline — team-state in default `.omc/state` still triggers
`[TEAM ROUTING REQUIRED]`.
2. centralized — team-state only in `OMC_STATE_DIR` still triggers it.
3. mismatch — team-state in default `.omc/state` is invisible when
`OMC_STATE_DIR` is set (asserts the old hardcode really is gone).
4. write — Skill invocation writes `skill-active-state.json` into the
centralized dir, not the project tree.
All 11 tests pass locally (7 existing + 4 new). Full suite: 8336 pass,
1 pre-existing flaky tmux test in `src/team/__tests__/scaling.test.ts`
(passes in isolation; unrelated to this change).
Constraint: must not regress default `.omc/state` behavior when OMC_STATE_DIR is unset — covered by existing baseline tests plus new baseline.
Constraint: helper signatures are called from one file only; safe to rename `directory` → `stateDir` in-place.
Rejected: make each helper async and call `resolveOmcStateRoot` internally | spreads async through the call graph for no benefit over a single top-level resolve.
Rejected: bundle the `.omc/todos.json` fix here | cross-cuts 5 files across two hook entrypoints — cleaner as a separate PR.
Confidence: high
Scope-risk: narrow
Directive: when adding new `.omc/state/…` paths to pre-tool-enforcer.mjs, use the pre-resolved `stateDir` — never reintroduce `join(directory, '.omc', 'state', …)`.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Prevent mention-only ralplan prose from arming stop-hook
The real UserPromptSubmit activation path runs through the standalone
keyword-detector hook script, which was still treating several
question and reference prompts as ralplan invocations and writing
ralplan state before the Stop hook checked it.
This narrows ralplan auto-activation to explicit invocation contexts:
direct command-style prefixes or nearby activation phrasing. Shared
TypeScript detection, the standalone script, and the template copy now
use the same intent rule, and regression tests cover both mention-only
prose and true ralplan task entrypoints.
Constraint: UserPromptSubmit state activation happens in the standalone hook script, not only the bridge path
Rejected: Remove ralplan keyword auto-activation entirely | would break legitimate planning invocations
Rejected: Broaden generic informational filtering for every mode | higher regression risk outside ralplan
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep TypeScript detector, standalone hook script, and template hook logic aligned for keyword intent changes
Tested: vitest targeted keyword-detector suites; npm run build; manual script repro for mention-vs-invocation state writes
Not-tested: full external host end-to-end hook install flow
* Preserve ralplan natural-language invocation without arming mention-only stops
The keyword detector already distinguished explicit ralplan requests from
informational mentions, but it missed common natural-language invocation
phrases such as 'please ralplan this issue' and 'let's ralplan...'.
This narrows the detector by adding conversational invocation patterns
only for ralplan, while leaving question-only and mention-only prose
non-actionable. Regression tests cover both the restored invocation
contract and the guarantee that keyword-only mentions do not create
stop-hook-blocking state.
Constraint: Keep the fix narrow to the existing ralplan detector path
Rejected: Loosen all skill keyword matching | would widen false positives across other modes
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Extend invocation phrasing only with tests that prove mention-only prose stays inert
Tested: npx vitest run src/hooks/keyword-detector/__tests__/index.test.ts src/hooks/__tests__/bridge-routing.test.ts src/hooks/persistent-mode/__tests__/team-ralplan-stop.test.ts; npm run build
Not-tested: Full repository test suite
---------
Co-authored-by: Codex Review <codex-review@example.com>
The post-tool verifier was treating quoted error-like strings in edited test code as edit failures and classifying pytest red runs as bash tool failures. This narrows both heuristics without weakening real tool-level failure detection, and locks the behavior with direct detector and hook-level regressions.
Constraint: Keep the hook advisory during normal TDD red-phase runs without suppressing real shell/tool failures
Rejected: Disable bash failure detection for all non-zero exits | would hide genuine command failures outside test workflows
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Preserve pytest-session detection as a narrow allowlist so other failing CLI workflows still surface actionable hook guidance
Tested: npx vitest run src/__tests__/post-tool-verifier.test.mjs; npx tsc --noEmit --pretty false
Not-tested: Full repo test suite and CI matrix
Co-authored-by: Codex Review <codex-review@example.com>
Narrow hardening only: suppress pure Permission denied fallout from broad AGENTS-style parent scans in the post-tool failure hook, filter clean diagnostic/search residue from tmux alert parsing, and exclude non-active PSM review/fix sessions from cleanup scans so completed sessions stop churning stale alerts.
Constraint: Keep issue #2606 scope limited to alert/failure-noise surfaces without changing core review flow behavior
Rejected: Broader failure-hook heuristics across all Bash errors | too risky for unrelated real failures
Rejected: Reworking PSM session lifecycle state model | unnecessary for stale cleanup-query churn
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Preserve actionable runtime failures after stripping scan/query residue; do not widen suppression beyond pure noise-only cases without new regression tests
Tested: npm test -- --run src/__tests__/post-tool-use-failure.test.ts src/notifications/__tests__/formatter.test.ts src/__tests__/project-session-manager-session-filter.test.ts; npm run build
Not-tested: Full end-to-end live review notification flow with real tmux panes
Co-authored-by: Codex Review <codex-review@example.com>
Review and deep-interview startup can probe optional OMX state/memory reads before
those MCP surfaces are available. Treating those Method not found responses like
real tool failures injected noisy continuation guidance into the pane. Narrow the
suppression to specific optional read tools with that exact failure signature so
real transport and runtime errors still surface and get recorded.
Constraint: Suppression must not hide non-optional tool failures or non-method-not-found errors
Rejected: Suppress all MCP Method not found failures | would mask real unsupported-tool mistakes
Rejected: Silence failures at broader review/deep-interview call sites | wider blast radius than needed
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep this suppression limited to startup-only optional reads unless a new false-positive path is verified
Tested: npm test -- --run src/__tests__/post-tool-use-failure.test.ts
Not-tested: Full interactive review/deep-interview startup pane flow
Three independent bugs caused context window growth across turns:
Bug 1 – skill-injector fallback used an in-memory Map that reset on every
process spawn (UserPromptSubmit fires a new Node.js process each turn).
Fixed: fallback dedup state persisted to
.omc/state/skill-sessions-fallback.json with a 1-hour TTL.
Bug 2 – createRulesInjectorHook existed in
src/hooks/rules-injector/index.ts but was never wired into hooks.json.
Fixed: new scripts/post-tool-rules-injector.mjs added as a PostToolUse
hook that calls the injector and emits hookSpecificOutput.
Bug 3 – In a git worktree nested inside the parent repo, rules from the
parent could bleed in when cwd was the parent repo.
Fixed: processToolExecution calls findProjectRoot(filePath) — using the
accessed file's path, not cwd — so a .git FILE at the worktree root stops
the upward walk before reaching the parent repo.
Regression tests added in src/__tests__/context-bloat-2577.test.ts
(9 new tests; full suite 8106/8113 pass, no regressions).
Constraint: hooks.json pre-tool node path was patched machine-locally by OMC init; only our new PostToolUse entry is included here
Rejected: patching the existing hook commands in hooks.json | machine-local absolute path should not be committed to repo
Confidence: high
Scope-risk: narrow
Not-tested: skill-injector bridge path (file-based dedup already existed there); rules injector with non-alwaysApply glob patterns
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
run.cjs delegated hook execution without applying hooks.json timeouts, so a slow stop hook could stall far past its configured 5s/10s budget. The fix resolves the target hook timeout from hooks.json and enforces it in the runner, while context-guard-stop now skips git worktree probes unless a local .git marker exists and applies a short timeout to the remaining rev-parse calls. Focused regressions cover both the runner timeout enforcement and the non-git guard path.
Constraint: Stop hooks must fail open rather than deadlock session shutdown
Constraint: Keep the fix narrow to runner enforcement and non-git probing
Rejected: Refactor persistent-mode idle path in this patch | wider behavior change than needed for the timeout bug
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep run.cjs timeout resolution aligned with hooks/hooks.json if stop-hook timeout values change
Tested: vitest run src/__tests__/run-cjs-graceful-fallback.test.ts src/__tests__/context-guard-stop.test.ts
Tested: eslint src/__tests__/run-cjs-graceful-fallback.test.ts src/__tests__/context-guard-stop.test.ts
Tested: tsc --noEmit
Tested: node --check scripts/run.cjs
Tested: node --check scripts/context-guard-stop.mjs
Not-tested: End-to-end Claude hook runtime outside local reproduction/targeted tests
spawnSync inherits the parent's stdin by default. When omc ask is
invoked from Claude Code's Bash tool (or any piped environment),
Codex detects a pipe on stdin and blocks forever waiting for EOF.
Explicitly set stdio to ['ignore', 'pipe', 'pipe'] when not piping
the prompt via stdin.
Constraint: spawnSync with no stdio option inherits parent file descriptors
Constraint: Codex reads from stdin when it detects a pipe (not a TTY)
Rejected: Always pipe prompt via stdin | breaks providers that don't support '-' arg
Confidence: high
Scope-risk: narrow
Not-tested: Windows + piped environment (existing win32 tests cover stdin piping path)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The post-tool verifier was still treating quoted literals like "error" and
zero-error metadata summaries as actionable failure signals. Strip quoted spans
from the remaining bash output after removing non-actionable metadata so real
command failures still trip alerts while inert diagnostic payloads stay quiet.
Constraint: Keep the fix narrow to issue #2558 target files and existing hook semantics
Rejected: Broadly weaken error-pattern matching | risked hiding real command and stack-trace failures
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Preserve stack-trace and tool-level failure detection when extending noise filters
Tested: npx vitest run src/__tests__/post-tool-verifier.test.mjs src/hooks/__tests__/bridge-routing.test.ts
Not-tested: Full project test suite
Related: issue #2558
Review-session steering prompts echo outcome labels (approve, request-changes,
merge-ready, BLOCKED) into the tmux pane before any real verdict is produced.
When that echo was re-submitted as a user prompt, hasActionableKeyword matched
the embedded "code review" / "security review" phrase and fired the mode — the
same false-positive path the formatter.ts trimReviewSeedPrefix fix addressed on
the notification side (commit 40e53bd).
Add isReviewSeedContext() to keyword-detector.mjs (both scripts/ and
templates/hooks/) mirroring the trimReviewSeedPrefix logic: when ≥2 distinct
outcome labels (approve, request-changes, merge-ready, blocked) appear in the
first 20 lines, the prompt is treated as echoed instruction text and the
code-review / security-review patterns are skipped.
Real "code review this diff" requests still activate the mode (positive
control test added).
Constraint: Must not disable code-review/security-review for genuine requests
Rejected: Broaden sanitizeForKeywordDetection to strip review labels globally | would hide real review verdicts
Rejected: Patch only formatter output | keyword-detector fires before formatter sees the text
Confidence: high
Scope-risk: narrow
Not-tested: Live clawhip tmux alert session with injected review-outcome menus
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
The CJS stop hook was still hard-coding {worktree}/.omc/state while the MCP state tools resolve through the centralized OMC_STATE_DIR project path. That split let stale local session files keep blocking stop/cancel even after centralized state had been cleared.\n\nThis patch ports the project-id based state-dir resolution into scripts/persistent-mode.cjs and adds focused regression coverage proving the script now reads centralized session state and ignores stale legacy local state when OMC_STATE_DIR is configured.\n\nConstraint: Keep the fix narrow to the runtime mismatch in scripts/persistent-mode.cjs\nRejected: Shared helper extraction across scripts and templates | broader than needed for issue #2518\nConfidence: high\nScope-risk: narrow\nReversibility: clean\nDirective: Keep persistent-mode.cjs state-dir resolution behavior aligned with MCP getOmcRoot/project-id semantics whenever centralized state behavior changes\nTested: npx vitest run src/hooks/persistent-mode/stop-hook-blocking.test.ts\nTested: npx tsc --noEmit\nTested: npx eslint src/hooks/persistent-mode/stop-hook-blocking.test.ts\nTested: node --check scripts/persistent-mode.cjs\nNot-tested: Full repository test suite
Issue #2497 traced the repeated backlog-empty follow-up spam to the
shipped stop-hook path still using a plain time-based idle cooldown.
This ports repo-snapshot-aware zero-backlog suppression into
scripts/persistent-mode.cjs and keeps the source helper aligned by
mirroring zero-backlog cooldown records across follow-up sessions.
Targeted regressions cover the two failure modes that mattered here:
new session ids should not re-arm identical zero-backlog nudges, and a
changed repo snapshot should immediately allow alerts again.
Constraint: Keep the fix minimal and preserve existing non-zero-backlog idle cooldown behavior
Rejected: Build a new hardening-lead generator | higher scope than needed because the issue explicitly allows backing off
Rejected: Increase the time-based cooldown only | still repeats identical zero-backlog alerts after expiry
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep script/runtime idle-notification behavior in parity with src/hooks/persistent-mode when backlog-aware suppression changes
Tested: npx vitest run src/hooks/persistent-mode/__tests__/idle-cooldown.test.ts src/hooks/persistent-mode/idle-cooldown.test.ts
Tested: npx tsc --noEmit
Tested: npx eslint src/hooks/persistent-mode/index.ts src/hooks/persistent-mode/__tests__/idle-cooldown.test.ts src/hooks/persistent-mode/idle-cooldown.test.ts
Tested: node --check scripts/persistent-mode.cjs
Not-tested: End-to-end OpenClaw/Discord follow-up delivery against a live channel
The TypeScript detector already treated single-mode definition-style prose plus a follow-up question as reference content, but the shipped script and installer template still used the older heuristic. That mismatch meant real hook installations could activate ultrawork for prompts like "OMC Ultrawork = 'special ops'. how much would it cost?" even though the source-level tests were green.
This syncs the packaged hook artifacts with the narrower reference-content heuristic and adds regression coverage at the script and installer-template layers so runtime behavior stays aligned with the source detector.
Constraint: Keep the keyword surface and explicit activation phrases unchanged unless a concrete reproduction requires otherwise
Rejected: Broaden keyword suppression across all follow-up questions | would risk weakening intentional mode activation beyond the issue scope
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: When keyword-detector heuristics change in src, keep scripts/ and templates/ behavior covered by runtime-facing regression tests
Tested: npx vitest run src/hooks/keyword-detector/__tests__/index.test.ts src/__tests__/keyword-detector-script.test.ts src/installer/__tests__/hook-templates.test.ts src/hooks/__tests__/bridge-routing.test.ts -t "2474|reference|quoted|explanatory|budget question|informational keyword questions|ultrawork state"; npm run lint -- src/__tests__/keyword-detector-script.test.ts src/installer/__tests__/hook-templates.test.ts; npx tsc --noEmit; direct node reproduction against scripts/keyword-detector.mjs for explanatory vs explicit activation prompts
Not-tested: Full repository test suite
The keyword detector still treated explanatory comparison text as an activation
request when it mentioned ultrawork or related mode names. This change adds
reference-shape heuristics for quoted follow-ups, comparison/article prose,
and structural markdown reference regions while preserving explicit local
activation verbs near the matched keyword.
The fix is applied in the shared TypeScript detector and mirrored into the
packaged hook template/script path so installed hooks behave the same as the
source tests. Focused regressions cover the issue #2474 reproduction, quoted
follow-up re-trigger suppression, and an explicit comparison-plus-command
control case.
Constraint: The detector logic is duplicated across src, hook templates, and packaged scripts
Constraint: Explicit activation phrases like "use ultrawork" must still trigger inside mixed prompts
Rejected: Blanket suppression for any prompt containing mode names in quotes or comparisons | would weaken intentional activation too much
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep keyword-detector heuristics aligned across src/hooks, templates/hooks, and scripts artifacts
Tested: npm test -- --run src/hooks/keyword-detector/__tests__/index.test.ts src/__tests__/keyword-detector-script.test.ts src/installer/__tests__/hook-templates.test.ts
Tested: npm run build
Tested: npm run lint
Tested: npx tsc --noEmit via MCP diagnostics (0 errors, 0 warnings)
Not-tested: End-to-end Claude Code hook loop with a live persistent-mode session
Related: #2474
Coarse team-state files can disappear or flip inactive while the canonical team
config/phase files still describe a live run. Read canonical team state as a
fallback so session-start, persistent-mode, and Task routing keep tracking the
active team instead of dropping overlap context.
Constraint: Keep the fix scoped to team-state tracking and regression tests
Rejected: Broaden into a shared runtime refactor | too much blast radius for a backport
Confidence: high
Scope-risk: moderate
Tested: npx vitest run src/__tests__/pre-tool-enforcer.test.ts -t "canonical team state"; npx vitest run src/hooks/__tests__/bridge-routing.test.ts -t "canonical team context"; npx vitest run src/hooks/persistent-mode/__tests__/team-ralplan-stop.test.ts -t "canonical team state remains live"; npm run build; node --check scripts/pre-tool-enforcer.mjs
Not-tested: Full bridge-routing suite still has unrelated pre-existing failures outside this slice
Stale mode-state files were still being treated as live in the stop hook when only newer timestamps were present. Teach the stale check to honor updated_at alongside the existing timestamp fields and add regressions for stale legacy ralph/ultrawork and blocker-skill paths.
Constraint: Must preserve real active-session blocking
Rejected: Narrow fix to skill-active only | would leave stale mode-state false positives
Confidence: high
Scope-risk: moderate
Directive: Keep the script/template hook and TS hook stale heuristics in sync
Tested: node --check scripts/persistent-mode.mjs; node --check templates/hooks/persistent-mode.mjs; npx eslint src/hooks/persistent-mode/index.ts src/hooks/persistent-mode/stop-hook-blocking.test.ts src/hooks/persistent-mode/__tests__/skill-state-stop.test.ts src/hooks/persistent-mode/__tests__/team-ralplan-stop.test.ts; npx vitest run src/hooks/persistent-mode/stop-hook-blocking.test.ts src/hooks/persistent-mode/__tests__/skill-state-stop.test.ts src/hooks/persistent-mode/__tests__/team-ralplan-stop.test.ts; npx tsc --noEmit
Not-tested: Live Claude Code session behavior
Standalone ralplan stop handling only recognized a narrow current_phase set,
so aborted or handoff-shaped persisted states could still emit
<ralplan-continuation> even after planning had effectively ended.
This normalizes ralplan phase lookup across current_phase/phase/status,
adds explicit terminal and handoff aliases in the TypeScript hook and
shipped CJS runtime, and locks the behavior with focused source and CJS
regression tests.
Constraint: Keep stop-hook behavior aligned between src/hooks/persistent-mode and shipped scripts/persistent-mode.cjs
Constraint: Preserve legitimate active ralplan reinforcement while allowing terminal/handoff exits
Rejected: Fix only skill cleanup paths | persisted stale state still reproduces the bug after cleanup is missed
Rejected: Treat every non-ralplan phase as terminal | could suppress future active planning phases
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: When adding new ralplan lifecycle phases, update both normalized terminal handling and the paired TS/CJS stop-hook tests together
Tested: npm exec -- vitest run src/hooks/persistent-mode/__tests__/team-ralplan-stop.test.ts
Tested: npm exec -- vitest run src/hooks/persistent-mode/stop-hook-blocking.test.ts
Tested: npm exec -- eslint src/hooks/persistent-mode/index.ts src/hooks/persistent-mode/__tests__/team-ralplan-stop.test.ts
Tested: npm exec -- tsc --noEmit
Not-tested: Full repository test suite
Ultrawork stop-hook reinforcement treated any active state as blocking, even
when tracked work had already reached zero. This clean redo auto-deactivates
ultrawork on terminal task completion in the TypeScript hook and the shipped
hook scripts, then updates the focused stop-hook/session-isolation tests to
separate incomplete-work blocking from completed-work exit behavior.
Constraint: Keep generated hook scripts aligned with the TypeScript stop-hook logic
Constraint: Preserve session-scoped ultrawork isolation while changing terminal-state behavior
Rejected: Require manual /oh-my-claudecode:cancel after completion | keeps the confusing repeated stop-hook loop from issue #2419
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Ultrawork stop-hook enforcement should only persist while incomplete tracked work remains
Tested: npx vitest run src/hooks/persistent-mode/stop-hook-blocking.test.ts src/hooks/persistent-mode/session-isolation.test.ts src/hooks/ultrawork/session-isolation.test.ts
Not-tested: Full repo vitest suite
Not-tested: Lint/typecheck/build