Framework detection matched a dependency against a framework's packageKeys
with unbounded substring containment (dep.includes(key)), so any dependency
whose name merely contained a key was misclassified: `preact` and even
`reactive` were both detected as `react`.
Match only when the dependency equals the key, or the key is a prefix
immediately followed by a delimiter (/ . _ -). This still matches every real
case (react-dom, @remix-run/node, spring-boot-starter, org.springframework.boot,
github.com/labstack/echo/v4, phoenix_live_view) while excluding preact/reactive
(and incidentally nextra). Adds regression tests.
Co-authored-by: bymle <229636660+bymle@users.noreply.github.com>
* feat: add worktree-lifecycle service (ecc.worktree-lifecycle.v1)
The "unowned moat" from the orchestrator landscape research: no existing
tool ships deterministic merge-conflict prediction or a safe worktree GC.
- scripts/lib/worktree-lifecycle/git.js: injectable, hermetic git layer.
Predicts merge conflicts WITHOUT touching the working tree via
`git merge-tree`. Strips inherited GIT_* env so it is safe inside hooks.
- scripts/lib/worktree-lifecycle/lifecycle.js: deterministic state machine
(main/dirty/conflict/merge-ready/merged/stale/idle) + planCleanup that
buckets worktrees into remove / salvage / keep. Only fully-merged trees
are auto-removable; stale (unmerged+inactive) => salvage, never deleted.
- scripts/worktree-lifecycle.js: CLI (--json/--conflicts/--stale/
--cleanup-plan/--base/--stale-days/--repo).
- tests/lib/worktree-lifecycle.test.js: 11 tests (fake-git + real-git).
Safety model mirrors the reference-arch salvage rule, validated by the
2026-06-05 MacBook->Mac Mini consolidation. Tests: 11/0.
* fix: hermetic git env in session adapters + mcp-inventory lint
- session adapters (codex-worktree, opencode): resolveGitBranch stripped
no git env, so the "outside a repo" path returned the host branch when
run inside a git hook (GIT_DIR set). Strip GIT_* before rev-parse.
- mcp-inventory: fix eslint no-unused-vars (signatures) and a stale
eslint-disable directive in the merged code.
* test: run each test with inherited git env stripped (hermetic runner)
When the suite runs inside a git hook (pre-push), git sets GIT_DIR/
GIT_WORK_TREE, which hijack 'git -C <dir>' calls in tests that exercise
real git, making them operate on the host repo. Strip GIT_* before
spawning each test so the suite is isolated from ambient git state.
---------
Co-authored-by: ECC Test <ecc@example.test>
* feat: add MCP inventory (ecc.mcp.v1) across harnesses
Read-only MCP-gateway groundwork: discover MCP server configs across
every installed harness, normalize to a canonical ecc.mcp.v1 inventory,
redact secrets, and report which servers are configured in 2+ harnesses
(the configure-N-times pain). The read+dedup side of a unified gateway,
mirroring how the session-adapter layer started read-only.
Readers (per-harness config formats):
- claude-code: ~/.claude.json mcpServers + project .mcp.json
- codex: ~/.codex/config.toml [mcp_servers.*] TOML via @iarna/toml
- opencode: ~/.config/opencode/opencode.json mcp block (command ARRAY)
canonical-mcp.js:
- normalize transport labels (local=>stdio, remote=>http) to stdio/http/sse
- merge servers by name across harnesses; flag DRIFT when signatures differ
- fragmentation report + aggregates
- SECRET REDACTION: env values stripped to key names; secrets in args
(--modelApiKey sk-ant-...), inline --flag=secret, and URL userinfo/token
query params all redacted before storage AND before the dedup signature.
scripts/mcp-inventory.js: CLI (--json, --fragmented, --help).
tests/lib/mcp-inventory.test.js: 12 tests incl. a regression for the
real arg-carried-secret leak found while smoke-testing on live configs.
Tests: 12/0. Real-data smoke: 33 servers across 3 harnesses, 21
configured in 2+ harnesses (7 drift); secret-leak audit clean.
* test: cover reader error paths, collect skip-logic, and CLI main() for mcp-inventory
Lift global branch coverage past the 80% gate (was 79.86%). Adds 6
tests exercising: missing-file/malformed-JSON/missing-block reader
fallbacks, codex no-parser path, collect skipping non-function readers
and swallowing reader errors, CLI usage()/main() help+json+human paths,
and formatHumanReport no-fragmentation + fragmented-only branches.
Also scrub a real API-key fragment that had leaked into a test fixture;
all secret-like fixtures are now obviously-fake FAKE... tokens.
mcp-inventory.js branch 30%->93%, collect.js ->100%. Global branch 80.33%.
* feat: add codex-worktree session adapter
Adds the third session adapter (after dmux-tmux and claude-history),
normalizing Codex rollout sessions into the harness-neutral
ecc.session.v1 snapshot. Reads ~/.codex/sessions rollout JSONL,
derives objective (skipping the AGENTS.md preamble + leading message
UUID), model, originator, worktree cwd, and best-effort git branch.
This is step 1 of ECC-2.0-SESSION-ADAPTER-DISCOVERY (move the
abstraction beyond tmux + Claude-history) and supports the
wrap/adapt control-pane strategy: ECC reads sessions from any
harness rather than owning one UX.
- scripts/lib/session-adapters/codex-worktree.js: adapter + rollout parser
- canonical-session.js: normalizeCodexWorktreeSession
- registry.js: register adapter, codex/codex-worktree target types
- tests/lib/session-adapters-codex.test.js: 4 tests (unit + registry routing)
* feat: add opencode session adapter + allow empty intent objective
Adds the fourth session adapter (after dmux-tmux, claude-history,
codex-worktree), normalizing OpenCode sessions into ecc.session.v1.
Reads ~/.local/share/opencode/storage: session/<project>/ses_*.json
for metadata (id, directory, title, version, projectID, time) and
message/<session>/msg_*.json to extract the model (modelID/providerID
from the first assistant message). Derives objective from the session
title, treating the auto-generated "New session - <date>" title as no
objective. Recency-based active/recorded state.
Schema: relax intent.objective from non-empty to allow empty string
(ensureStringAllowEmpty). Sessions legitimately have no objective yet
(fresh/auto-titled), and claude-history already emitted "" via
metadata.title fallback. This fixes a latent over-strict validation.
- scripts/lib/session-adapters/opencode.js: adapter + storage parser
- canonical-session.js: normalizeOpencodeSession + ensureStringAllowEmpty
- registry.js: register adapter + opencode target type
- tests/lib/session-adapters-opencode.test.js: 5 tests
Tests: opencode 5/0, codex 4/0, session-adapters 14/0,
control-pane-state 10/0, session-inspect 8/0, control-pane 12/0.
Smoke-tested on a real OpenCode session (140 messages, gpt-5.3-codex).
* test: cover error/fallback branches for codex-worktree + opencode adapters
Lift global branch coverage past the 80% gate (was 79.53%). Adds error
and fallback path tests: missing-session/unknown-id throws, findRolloutById/
findSessionInfoById, direct file targets, objective truncation, model
fallbacks, corrupt-line skip, mtime activity fallback, and the real
resolveGitBranch path outside a repo.
codex-worktree.js branch 52.8%->78.3%; global branch 80.04%.
Adds dynamic workflow/team orchestration skills, the content pack, and control-pane work-item/Kanban state DB support. Includes reviewer hardening for state-db CLI validation, optional state DB failure handling, and mergeStateStatus projection.
Adds de-DE docs, installer wiring, and locale tests. Pre-validated on current main with install manifest checks, markdownlint, locale-install tests, and ECC 2.0 release-surface tests.
Fail fast when the OpenCode home install is attempted from a source checkout without the compiled .opencode/dist payload. PR had the full CI matrix green.
Completes the install-target matrix for Claude Code. Until now, ECC's
Claude support was home-scope only (~/.claude/) via the `claude` target.
This adds a project-scope counterpart (./.claude/) via a new
`claude-project` target so teams can install ECC per-repo without
contaminating ~/.claude/ — matching the existing project-scope adapters
for Cursor, Antigravity, Gemini, CodeBuddy, Joycode, and Zed.
Symmetric with `claude`:
- Same namespace under rules/ecc and skills/ecc
- Same docs/<locale> handling for --locale
- Same hooks placeholder substitution for hooks.json
- Reuses claude-home's destination-mapping logic 1:1
Use cases:
- Monorepos with multiple Flow-managed projects
- Teams that want ECC scoped per-project without touching ~/.claude/
- Per-project skill/rule isolation when global install isn't desirable
No breaking change: existing --target claude continues to route to
claude-home (user-scope) unchanged. New target is opt-in.
Tests
-----
- 4 new tests in tests/lib/install-targets.test.js
(root resolution, lookup-by-id, plan parity with claude, foreign-path filtering)
- All install-target regression guards (schema enum / SUPPORTED_INSTALL_TARGETS)
still pass
- End-to-end smoke: `--target claude-project --profile minimal --dry-run`
emits 359 ops with destinations rooted at <projectRoot>/.claude/ (parity
with --target claude which emits 359 ops rooted at ~/.claude/)
Generate the inline hook root resolver with single-quoted JavaScript literals so Windows Git Bash does not choke on nested escaped double quotes before Node starts. Refresh hooks.json and add regression coverage for parsed hook commands and installed hook manifests.
PR #1983 round 1 introduced unique-suffix tmp paths so two concurrent
writers no longer share a single `.tmp` file. That fix is correct
under POSIX semantics — `rename(2)` is atomic between source and
destination, so each writer renames onto the same target without
conflict.
Windows `MoveFileExW` is not the same. It fails with
EPERM / EACCES / EBUSY when the target is currently being renamed
by *another* process — a short race window that fires reliably under
this hook's PostToolUse + statusline concurrency. Round 1's CI run
made this visible:
Test (windows-latest, Node 18.x, npm) — FAILURE
Error: EPERM: operation not permitted, rename
'C:\…\ecc-metrics-test-bridge-race-….json.9504.4aef575a.tmp' ->
'C:\…\ecc-metrics-test-bridge-race-….json'
at writeBridgeAtomic (scripts/lib/session-bridge.js:79:8)
All nine Windows matrix cells (Node 18 / 20 / 22 × npm / pnpm / yarn)
hit the same path. POSIX matrices (Linux + macOS) passed unchanged.
Fix: extract a `renameWithRetry(tmp, target)` helper that retries
`fs.renameSync` up to 5 times on EPERM / EACCES / EBUSY with
exponential backoff (20 ms → 320 ms total). Other error codes
(ENOENT, ENOSPC, EROFS, …) re-throw on the first attempt — they are
not transient. POSIX runs hit the first try and exit immediately.
The backoff uses `Atomics.wait` on a throwaway `SharedArrayBuffer`
so the retry path does not busy-spin the CPU; verified on Node ≥ 17
that this works on the main thread. There is a `try/catch` fallback
to a brief busy-wait for older runtimes where `Atomics.wait` is
restricted to workers.
`writeBridgeAtomic` calls the helper instead of `fs.renameSync` and
keeps its existing best-effort tmp cleanup on terminal failure.
`renameWithRetry` is added to `module.exports` so the companion
`writeWarnState` in `scripts/hooks/ecc-context-monitor.js` can
adopt the same retry policy without duplicating the helper. That
adoption lands in the next commit.
Local: `node tests/lib/session-bridge.test.js` 14/14, `yarn test`
green, `yarn lint` clean. The round-1 test (two concurrent child
writers, 200 iterations each) now passes on macOS without retrying
at all (POSIX path) and is expected to pass on Windows via the new
retry loop.
`writeBridgeAtomic` wrote to a fixed `${target}.tmp` path before
calling `renameSync`. When two processes write to the same session
bridge concurrently (e.g. PostToolUse `ecc-metrics-bridge` + the
background `ecc-statusline`, both calling `writeBridgeAtomic(sessionId, ...)`),
the canonical atomic-rename race fires:
1. Process A: writeFileSync(target.tmp, JSON_A) — tmp file exists.
2. Process B: writeFileSync(target.tmp, JSON_B) — tmp file overwritten.
3. Process A: renameSync(target.tmp, target) — succeeds; target = JSON_B
(A's payload silently corrupted en-route).
4. Process B: renameSync(target.tmp, target) — throws ENOENT (the
rename consumed the file).
Every caller in the repo wraps `writeBridgeAtomic` in `try {} catch {}`,
so the ENOENT exception is swallowed and the user-visible symptom is
just "the bridge file occasionally contains the wrong process's
payload" with no diagnostic.
Reproduced before this commit:
$ # two concurrent writers, each calling writeBridgeAtomic 500 times
$ # against the same session ID
[A] errors=244 # 244 ENOENT exceptions swallowed
[B] errors=248 # ditto
After this commit the same workload reports 0 errors in both
subprocesses: tmp paths no longer collide.
Fix: change `${target}.tmp` to
`${target}.${process.pid}.${crypto.randomBytes(4).toString('hex')}.tmp`,
matching the pattern already used by `writeCostWarningIfChanged` in
`scripts/hooks/ecc-metrics-bridge.js` (commit 9b1d8918). The pid +
4-byte nonce gives each writer process a distinct tmp path, so step 2
above no longer overwrites step 1's payload and step 4 no longer
races step 3.
Also added: on `renameSync` failure, attempt `fs.unlinkSync(tmp)` so
a writer that fails (disk full, permission, parent dir gone) does
not leak its tmp file. The cleanup is best-effort and the original
error is still re-thrown.
**Scope clarification.** This commit closes the atomic-rename
primitive's race only. The *read-modify-write* race in callers —
two writers each read the same bridge state, increment, and write
back, the second clobbering the first — is a separate concern that
needs locking or per-writer logs, and is intentionally out of scope
for this PR. The cost-tracker / metrics-bridge callers tolerate
last-writer-wins on their cumulative aggregates today and this
commit does not change that contract.
The companion `writeWarnState` in `ecc-context-monitor.js` has the
same fixed-suffix pattern and the same race; that fix lands in the
next commit so each can be reviewed against its own diff.
Adds `--locale <code>` support to the ECC installer so users can install
localized reference docs (agents, commands, skills, rules) into
`~/.claude/docs/<locale>/` alongside the existing English installation.
Changes:
- manifests/install-modules.json: add 8 locale doc modules (docs-ja-JP,
docs-zh-CN, docs-ko-KR, docs-pt-BR, docs-ru, docs-tr, docs-vi-VN,
docs-zh-TW), each with kind="docs" and defaultInstall=false
- manifests/install-components.json: add 8 locale: components mapping to
the new modules
- scripts/lib/install-manifests.js: add locale: family prefix,
SUPPORTED_LOCALES, LOCALE_ALIAS_TO_COMPONENT_ID (with aliases like
ja=ja-JP, zh=zh-CN, ko=ko-KR), and listSupportedLocales()
- scripts/lib/install/request.js: add --locale flag to parseInstallArgs(),
resolve locale alias → component ID in normalizeInstallRequest(), throw
on unsupported locale codes
- scripts/lib/install-targets/claude-home.js: map docs/<locale>/ source
paths to ~/.claude/docs/<locale>/ destination (side-by-side, no overwrite
of English files)
- scripts/install-apply.js: import listSupportedLocales, add --locale
usage line and available locales list to --help output
Usage examples:
./install.sh --locale ja # Japanese docs only
./install.sh --profile core --locale zh-CN # core profile + zh-CN docs
./install.sh typescript --locale ja # legacy + locale (errors)
Salvages the useful statusline/context monitor work from stale PR #1504 while preserving the current continuous-learning hook runner wiring.
Adds the metrics bridge, context monitor, statusline script, shared cost/session bridge utilities, and tests. Fixes the reviewed false loop-detection hash collision for non-file tools, avoids default-session cost inflation, sanitizes statusline task lookup, and records hook payload session IDs in cost-tracker.
On Windows (native cmd/PowerShell), process.env.HOME is undefined.
Seven CLI entry points and two library files pass process.env.HOME
directly as homeDir without a cross-platform fallback, causing all
path resolutions to silently fail (resolving to "undefined/.claude/...").
Node.js os.homedir() correctly handles all platforms (HOME on Unix,
USERPROFILE on Windows, OS-level fallback). The project already uses
this pattern in scripts/lib/state-store/index.js and has a getHomeDir()
utility in scripts/lib/utils.js, but it was not applied consistently.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Lidang-Jiang <lidangjiang@gmail.com>
* fix(install): add rust, cpp, csharp to legacy language alias map
The legacy installer compatibility layer in install-manifests.js was
missing entries for rust, cpp, and csharp — languages that have
rules/ directories and (for rust/cpp) install-components.json entries.
Running `./install.sh rust` fails with "Unknown legacy language: rust"
because LEGACY_LANGUAGE_ALIAS_TO_CANONICAL and
LEGACY_LANGUAGE_EXTRA_MODULE_IDS didn't include these languages.
Fixes the issue reported in #694 by @mpiton.
Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
* fix(install): complete csharp legacy support and add resolution tests
- Add lang:csharp component to install-components.json with
framework-language module (matching cpp/rust pattern)
- Update csharp mapping in LEGACY_LANGUAGE_EXTRA_MODULE_IDS from
empty array to ['framework-language']
- Add end-to-end resolution tests for rust, cpp, and csharp verifying
framework-language module is included in resolved moduleIds
Addresses review feedback from Copilot, Greptile, CodeRabbit, and Cubic.
Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Happy <yesreply@happy.engineering>
* feat: add agent description compression with lazy loading (#491)
Agent descriptions consume ~26k tokens (121KB across 27 agents). This adds
a compression library with three modes:
- catalog: metadata only (~2-3k tokens) for agent selection
- summary: metadata + first paragraph (~4-5k tokens) for routing
- full: no compression, for when agent is invoked
Includes lazy-load function to fetch full agent body on demand.
21 tests covering parsing, compression, filtering, and real agents dir.
* fix: update JSDoc to include all stats fields in buildAgentCatalog
Add compressedBytes and mode to the documented return type, matching
the actual implementation.
When ECC is installed as a Claude Code plugin via the marketplace,
scripts live in the plugin cache (~/.claude/plugins/cache/...) but
commands fallback to ~/.claude/ which doesn't have the scripts.
Add resolve-ecc-root.js with a 3-step fallback chain:
1. CLAUDE_PLUGIN_ROOT env var (existing)
2. Standard install at ~/.claude/ (existing)
3. NEW: auto-scan the plugin cache directory
Update sessions.md and skill-health.md commands to use the new
inline resolver. Includes 15 tests covering all fallback paths
including env var priority, standard install, cache discovery,
and the compact INLINE_RESOLVE used in command .md files.
Windows terminals emit control sequences (cursor movement, screen
clearing) that leaked into session.tmp files and were injected
verbatim into Claude's context on the next session start.
Add a comprehensive stripAnsi() to utils.js that handles CSI, OSC,
charset selection, and bare ESC sequences. Apply it in session-end.js
(when extracting user messages from the transcript) and in
session-start.js (safety net before injecting session content).
- Add duplicate slug detection in buildOrchestrationPlan to reject
worker names that collapse to the same slug
- Use buildTemplateVariables() for launcher command interpolation
so _sh and _raw suffixes are available in templates
* Add install.ps1 PowerShell wrapper and tests
Add a Windows-native PowerShell wrapper (install.ps1) that resolves symlinks and delegates to the Node-based installer runtime. Update README with PowerShell usage examples and cross-platform npx entrypoint guidance. Point the ecc-install bin to the Node installer (scripts/install-apply.js) in package.json (and refresh package-lock), include install.ps1 in package files, and add tests: a new install-ps1.test.js and a tweak to install-sh.test.js to skip on Windows. These changes provide native Windows installer support while keeping npm-compatible cross-platform invocation.
* Improve tests for Windows HOME/USERPROFILE
Make tests more cross-platform by ensuring HOME and USERPROFILE are kept in sync and by normalizing test file paths for display.
- tests/lib/session-adapters.test.js: set USERPROFILE when temporarily setting HOME and restore previous USERPROFILE on teardown.
- tests/run-all.js: use a normalized displayPath (forward-slash separated) for logging and error messages so output is consistent across platforms.
- tests/scripts/ecc.test.js & tests/scripts/session-inspect.test.js: build envOverrides from options.env and add HOME <-> USERPROFILE fallbacks so spawned child processes receive both variables when only one is provided.
These changes prevent test failures and inconsistent logs on Windows where USERPROFILE is used instead of HOME.
* Fix Windows paths and test flakiness
Improve cross-platform behavior and test stability.
- Remove unused createLegacyInstallPlan import from install-lifecycle.js.
- Change resolveInstallConfigPath to use path.normalize(path.join(cwd, configPath)) to produce normalized relative paths.
- Tests: add toBashPath and normalizedRelativePath helpers to normalize Windows paths for bash and comparisons.
- Make cleanupTestDir retry rmSync on transient Windows errors (EPERM/EBUSY/ENOTEMPTY) with short backoff using sleepMs.
- Ensure spawned test processes receive USERPROFILE and convert repo/detect paths to bash format when invoking bash.
These changes reduce Windows-specific failures and flakiness in the test suite and tidy up a small unused import.
- canonical-session: fall back to JSON file recording when the loaded
state-store module has no writer methods (factory vs instance)
- install-executor: skip node_modules and .git dirs in listFilesRecursive
to prevent ETIMEDOUT copying thousands of .opencode dependency files
- ecc.js: increase maxBuffer to 10MB for spawned subcommands to prevent
ENOBUFS on large install plan JSON output
- install-apply.test: update Cursor and Antigravity path assertions to
match flattened rule layout and remapped dirs (workflows, skills)
- ecc.test: increase maxBuffer in test runner to handle large output
- orchestrate-codex-worker.sh: guard against unreadable task file before
cat, write failure status and handoff artifacts on early exit
* feat: add SQLite state store and ECC status CLI
* fix: replace better-sqlite3 with sql.js to eliminate native module CI failures
better-sqlite3 requires native C++ compilation (node-gyp, prebuild-install)
which fails in CI across npm/pnpm on all platforms:
- npm ci: lock file out of sync with native transitive deps
- pnpm: native bindings not found at runtime
- Windows: native compilation fails entirely
sql.js is a pure JavaScript/WASM SQLite implementation with zero native
dependencies. The adapter in index.js wraps the sql.js API to match the
better-sqlite3 interface used by migrations.js and queries.js.
Key implementation detail: sql.js db.export() implicitly ends active
transactions, so the adapter defers disk writes (saveToDisk) until
after transaction commit via an inTransaction guard flag.
createStateStore is now async (sql.js requires async WASM init).
Updated status.js, sessions-cli.js, and tests accordingly.
- Registry accepts { type, value } structured targets
- Add --list-adapters and --target-type CLI flags to session-inspect
- Export adapter type from claude-history and dmux-tmux adapters
- 71 new session adapter tests, 34 new session-inspect tests
- All 1142 tests passing
Extract BIOME_CONFIGS and PRETTIER_CONFIGS as shared constants to eliminate
duplication between PROJECT_ROOT_MARKERS and detectFormatter(). Unify the
biome/prettier branches in resolveFormatterBin() via a FORMATTER_PACKAGES
map. Remove redundant path.resolve() in quality-gate.js.
Handle Windows .cmd shim resolution via spawnSync with strict path
validation. Removes shell:true injection risk, uses strict equality,
and restores .cmd support with path injection guard.