7 Commits
Author SHA1 Message Date
Spicy SharkandBellman d893c59e92 feat(installer): add claudeMdEnabled opt-out for the global CLAUDE.md (#4383)
* feat(installer): add claudeMdEnabled opt-out for the global CLAUDE.md

Some users maintain their own global CLAUDE.md and do not want every
install or update to merge the OMC block into it. Add a claudeMdEnabled
key to .omc-config.json, following the hudEnabled pattern: it defaults to
true and only an explicit false disables it.

When it is false, the global CLAUDE.md and CLAUDE-omc.md are neither
written nor merged by the installer (install, update reconcile) or by the
global modes of the CLAUDE.md coordinator used by omc-setup. Each skip is
logged. Project-scoped (local) CLAUDE.md handling is unchanged.

Doctor and SessionStart respect the flag as well: doctor reports the
missing markers as informational instead of asking for omc-setup, and
SessionStart no longer reports CLAUDE.md version drift. The key is added
to the doctor known-keys list and the auto-update config type, and is
documented in docs/REFERENCE.md.

* fix(skills): respect claudeMdEnabled in omc-doctor and omc-setup; test config passthrough

* fix(doctor): report the claudeMdEnabled opt-out when no CLAUDE.md exists

* fix(omc-doctor): expand ~ in CLAUDE_CONFIG_DIR before reading the opt-out

The Step 4 opt-out one-liner passed CLAUDE_CONFIG_DIR to path.join as given, so a value like ~/.claude-alt was read relative to the current directory. Expand it the way getClaudeConfigDir() does: trim, empty falls back to ~/.claude, a bare ~ is the home directory, and a leading ~/ or ~\ is joined onto it.

* docs(omc-setup): clarify claudeMdEnabled skip

* chore(inventory): refresh graph for PR #4383

* fix(omc-doctor): fail closed when the claudeMdEnabled probe cannot run

Step 4 treated any outcome other than the opt-out line as "enabled". When
node is not on PATH (the case Step 5 diagnoses) the probe prints nothing, so
the Missing/Outdated CLAUDE.md fix could overwrite a file the user opted out
of managing.

Continue with the CLAUDE.md checks only when the probe printed exactly
"CLAUDE.md management enabled". If it printed neither line or failed, report a
WARN, skip the rest of Step 4 and the CLAUDE.md fix, and never create or
overwrite CLAUDE.md or CLAUDE-omc.md.

Add tests pinning the rule and running the extracted probe with and without
node on PATH. Refresh the inventory graph.

* fix(doctor): report disabled global CLAUDE.md management

* fix(doctor): do not recommend omc-setup cleanup while CLAUDE.md management is off

With claudeMdEnabled set to false, omc-setup skips every global CLAUDE.md
write, so the exact-legacy-guide cleanup hint it printed could not work. The
hint was still shown for a markerless file that matches a known legacy guide.

When management is disabled, say that setup cleanup is skipped and that the
user can re-enable claudeMdEnabled or remove the legacy content manually. The
enabled path keeps its existing output.

Add a test for the disabled case and refresh the inventory graph.

* fix: honor CLAUDE.md opt-out on committed setup path

* chore(inventory): refresh graph after latest dev merge

* test(installer): cover coordinator CLAUDE opt-out

* chore(inventory): refresh graph after latest dev merge

* fix(omc-setup): skip install style prompt when disabled

* chore(inventory): refresh graph after latest dev merge

---------

Co-authored-by: Bellman <54757707+Yeachan-Heo@users.noreply.github.com>
2026-10-15 17:19:23 +01:00
Spicy SharkandBellman edb331a451 fix(session-start): bound the stabilization helper lifetime and batch the darwin identity probe (#4384)
* fix(session-start): bound the stabilization helper lifetime and batch the darwin identity probe

The detached --stabilize-legacy-tool-failure-state helper only left its loop
when the quiet window had passed and no other plugin version was live. Sessions
on an older version can stay open for days, so the helper (one per .omc root)
never exited and kept rescanning the occupancy registry every 5 s.

- Give the helper a hard lifetime: max(10 x stabilization window, 60 s), at
  most 5 min with the 30 s production window. At the deadline the helper stops
  waiting and returns. The loop still purges legacy artifacts on every
  iteration whenever any exist, as before; the deadline adds no extra purge.
  Trade-off: an older-version session that keeps writing legacy artifacts after
  the deadline leaves them on disk until the next SessionStart in that root
  starts a new helper, whereas before the helper scrubbed them within about
  250 ms for as long as that session lived. An optional fifth argument can
  shorten the cap; the cap also defaults when the function is called without it.
- On darwin, resolve process start identities with one
  `ps -p pid1,pid2,... -o pid=,lstart=` per scan instead of one spawn per
  registry record, like the win32 batch. ps exits 1 when none of the pids exist
  (some BSD builds also when only some are gone), so status 0 and 1 are both
  parsed; any other failure keeps the records. The Linux /proc path and the
  rescan intervals are unchanged.

Applied to both the plugin scripts and the standalone hook templates.
Inventory regenerated for the changed sources.

Fixes #4382

* fix(session-start): stop the stabilization helper from purging after its lifetime ends

The helper checked its hard lifetime only at the top of the loop. The cleanup
it ran inside an iteration waited up to 2.5 s for the PostToolUseFailure state
lock on its own, so a lock that freed up after the deadline but before that
wait ran out still let the helper delete legacy artifacts past its lifetime.

- Bound the cleanup lock wait by the time left in the lifetime.
- Check the deadline again once the lock is held: past it, release the lock
  and leave the artifacts for the next SessionStart in that root.
- Callers without a lifetime (SessionStart and the deferred purge child) keep
  the 2.5 s wait and purge whenever they hold the lock.

Applied to both the plugin script and the standalone hook template, with a
regression test for each that holds the lock past a shortened lifetime.
Inventory regenerated for the changed sources.

* fix(session-start): bound the stabilization helper's worker lock wait by its lifetime

The helper took its cleanup-worker lock with the lock library's default wait
of about 3 s and no overall timeout. When the state directory already existed,
or appeared at the deadline, the loop left the state-directory wait without
checking the lifetime, so a held worker lock kept the helper alive past it.

- Check the deadline at the top of the state-directory wait, before leaving it.
- Pass the time left in the lifetime as the acquisition's overall timeout; a
  spent lifetime fails the acquisition at once and the helper exits.
- Check the deadline again once the worker lock is held and release it without
  doing any work when it has passed.
- Other parameters of the lock acquisition keep their defaults.

Applied to both the plugin script and the standalone hook template, with a
regression test for each that holds the worker lock past a 200 ms lifetime.
Inventory regenerated for the changed sources.

* fix(session-start): bound the stabilization helper's plugin occupancy probes by its lifetime

The helper scanned for older plugin versions without a probe timeout, so a slow
Darwin batched ps (2 s) or PowerShell host (3 s) could run past the helper's
hard lifetime, before the wait loop and again while the cleanup-worker lock was
held.

- Pass the time left in the lifetime as the probe timeout of every older-plugin
  scan on the helper's path. The probe never exceeds its platform default, and a
  spent lifetime skips it.
- A probe that times out or has no time left identifies nothing, so every
  registry record is kept and its root counts as occupied: nothing is purged,
  the worker lock is released and the helper exits.
- Callers without a lifetime keep the default probe timeout. The darwin scan
  stays a single batched ps.

Applied to both the plugin script and the standalone hook template, with a
regression test for each that delays the post-lock darwin ps under a shortened
lifetime and asserts a timely exit with the worker lock released. Inventory
regenerated for the changed sources.

* chore(inventory): refresh graph for current dev

* chore(inventory): refresh graph for current dev

---------

Co-authored-by: Bellman <54757707+Yeachan-Heo@users.noreply.github.com>
2026-10-15 13:11:50 +01:00
Spicy Shark 0b07c4d71c test(atomic-write): give SQLite contention timing bounds a runner margin (#4385)
The two SQLite contention tests asserted that a contended acquire with a
75 ms busy timeout returns in under 500 ms. On macOS CI runners the
"PreCompact restore" job failed on that bound intermittently (575 and 533 ms).

The lock code issues a single busy wait for this call: attempts is 1, only
BEGIN IMMEDIATE blocks, and the open, pragma and CREATE TABLE IF NOT EXISTS
statements take well under 10 ms. SQLite's busy handler sums the sleeps it
requests rather than watching a clock, so the real wait is a little over the
nominal timeout locally (about 85 to 120 ms for 75 ms, roughly 1.1 times the
timeout plus a small constant at 300 and 1000 ms) and grows with scheduler
and VM stalls on a shared runner. The wait is bounded as intended; the
wall-clock ceiling was too tight for a CI runner.

Measure with the monotonic clock, allow the nominal timeout plus a 1000 ms
margin (still under the 2000 ms default, so a call that ignores the caller's
timeout fails), and add a 50 ms floor so a call that does not wait at all
fails too. Both bounds were checked against a lock helper that ignores the
caller's timeout and one that does not wait.
2026-10-15 07:13:30 +01:00
51bec397e7 fix(installer): log CLAUDE.md as already up to date when install leaves it unchanged (#4377)
executeClaudeMdTransaction drops write operations whose planned bytes equal
the existing file, so a repeated install over an up-to-date CLAUDE.md returns
no CLAUDE.md write. install() chose 'Created CLAUDE.md' whenever no write with
existedBefore was present, so the second run logged 'Created CLAUDE.md' for a
file that already existed and was not modified.

Look up the CLAUDE.md write operation first. No operation logs
'CLAUDE.md already up to date'; an operation with existedBefore keeps
'Updated CLAUDE.md (merged with existing content)'; otherwise
'Created CLAUDE.md' is unchanged.

Adds a test that installs twice into one temp config dir and checks the
second run logs the new message, not 'Created CLAUDE.md', and leaves the file
byte-identical.

Co-authored-by: gaebal-gajae <clawdbot@users.noreply.github.com>
Co-authored-by: gaebal-gajae <gaebal-gajae@users.noreply.github.com>
2026-10-14 06:29:21 +01:00
Spicy Sharkandgaebal-gajae 410f424c7e fix(plugin-cache): link stale plugin roots only to a real cache directory (#4375)
* fix(session-start): link stale plugin roots only to a real cache directory

The keep-latest-2 pass and the stale CLAUDE_PLUGIN_ROOT guard both used
versions[0] as the link target, and versions keeps symlink entries. When the
newest entry was a symlink (dangling after its directory was removed, or
created for a downgrade) the guard linked the stale path to that symlink,
e.g. 4.14.5 -> 4.14.2 next to 4.14.2 -> 4.14.5, so every later hook failed
with ELOOP. The keep-2 pass could also turn the only real directory into a
symlink to a newer-named symlink.

Pick the target from the newest entry that is a real directory, keep symlink
entries in the list so they can still be re-pointed, and skip both passes
when no real directory exists.

* fix(repair-plugin-cache): link stale cache versions only to a real directory

latestValidCacheRoot kept symlink entries in its candidate list, so a
valid-looking symlink such as 5.0.0 -> 4.4.3 was chosen as the latest root.
The fallback pass then replaced the real 4.4.3 directory with 4.4.3 -> 5.0.0,
a two-link cycle that destroys the only real copy and fails every later
lookup with ELOOP.

Choose the latest root only from real directories. Symlink entries are still
scanned by the fallback pass, so they are re-pointed at the real latest
directory as before, but they never serve as the link target.

---------

Co-authored-by: gaebal-gajae <clawdbot@users.noreply.github.com>
2026-10-14 05:48:26 +01:00
Spicy Shark 1c5a42d37f test(run-cjs): blank user-level OMC env in the worker skip-guard hook spawns (#4376)
test(run-cjs): blank user-level OMC env in the worker skip-guard hook spawns

run-cjs-worker-skip-guard.test.ts spawned hooks with the developer's full
environment. With OMC_QUIET=1 exported, pre-tool-enforcer emits no
hookSpecificOutput and "still delivers the payload to a hook that is not
skipped" fails; an exported DISABLE_OMC=1 or OMC_SKIP_HOOKS=pre-tool-use fails
it the same way.

Blank the same variables pre-tool-enforcer.test.ts blanks, plus the other
variables pre-tool-enforcer and its libs read (read-budget bytes, advisory
cooldown/clock, OMC_JEV). Values a test passes to runHook still win, so the
OMC_SKIP_HOOKS and DISABLE_OMC cases keep working.
2026-10-14 13:12:19 +09:00
Spicy Shark e087c3562f perf(hooks): launch the file-context injectors only for the tools they act on (#4373)
post-tool-rules-injector and post-tool-directory-context-injector were
registered under the PostToolUse "*" matcher, so Claude Code started two
Node launches after every tool call. Both hooks return '' from
processToolExecution() for any tool outside TRACKED_TOOLS (read, write, edit,
multiedit) before they touch storage, so for Bash, Grep, Glob, Agent, Skill,
TodoWrite, ToolSearch, WebFetch, MCP tools, etc. the launch only ever printed
{"continue":true}.

- hooks.json: move both hooks to a "Read|Write|Edit|MultiEdit" PostToolUse
  group. post-tool-verifier and project-memory-posttool stay on "*".
- run.cjs: add post-tool-directory-context-injector.mjs to
  TRUSTED_WORKER_HOOKS. It was registered after the #3928 Worker conversion
  and still took the generic path, which starts a second Node process (the
  same shape as #4221).
- registry.ts: selectApplicableEntries() now treats a matcher as a
  "|"-separated list of tool names, so the shadow registry keeps selecting
  these two entries for Read/Write/Edit/MultiEdit.
- Tests pin the matcher to the TRACKED_TOOLS of both hooks, extend the Worker
  matrices, and cover the matcher list in the registry.
2026-10-14 04:16:49 +01:00