140 Commits

  • feat: Cursor-independent ECC memory via ECC_AGENT_DATA_HOME (#2066)
    * feat: auto-isolate ECC memory data for Cursor via ECC_AGENT_DATA_HOME
    
    Add ECC_AGENT_DATA_HOME (defaults to ~/.claude) with Cursor-aware resolution,
    sessionStart env injection, install scaffolds, and hook bootstrap so memory
    hooks do not collide with Claude Code when both harnesses are used.
    
    Closes #2065
    
    Co-authored-by: Cursor <cursoragent@cursor.com>
    
    * fix: log agent-data config errors and ship cursor sessionStart deps
    
    Address CodeRabbit review: log invalid .cursor/ecc-agent-data.json parse
    failures, and copy cursor-session-env.js plus lib deps on legacy Cursor
    install so sessionStart hook path exists without hooks-runtime alone.
    
    Co-authored-by: Cursor <cursoragent@cursor.com>
    
    * fix: resolve relative agentDataHome paths from project root
    
    Project config values like ".ecc-data" now resolve against the
    repository root (parent of .cursor/), not process.cwd(), so Cursor
    hooks persist memory in the intended directory regardless of hook cwd.
    
    Addresses cubic review on PR #2066.
    
    Co-authored-by: Cursor <cursoragent@cursor.com>
    
    * docs: explain getHomeDir duplicate and docstring policy
    
    Document why agent-data-home keeps a local home-dir helper (circular
    require with utils.js) and list consolidation options for maintainers.
    Note that CodeRabbit JSDoc coverage warnings are informational relative
    to ECC's usual script documentation style.
    
    Addresses cubic P2 context on PR #2066.
    
    Co-authored-by: Cursor <cursoragent@cursor.com>
    
    * test: isolate agent-data-home tests from dogfooded .cursor config
    
    Use isolated temp cwd for default-resolution cases and assert
    resolveAgentDataHome({ projectDir }) reads ecc-agent-data.json.
    Document cwd/project caveats in the test file header.
    
    Co-authored-by: Cursor <cursoragent@cursor.com>
    
    ---------
    
    Co-authored-by: Cursor <cursoragent@cursor.com>
  • fix(project-detect): match packageKeys on boundaries, not substrings (#2181)
    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: worktree-lifecycle service (deterministic conflict prediction + safe GC) (#2164)
    * 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: MCP inventory (ecc.mcp.v1) — unified cross-harness MCP config view (#2146)
    * 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: extend session-adapter layer with codex-worktree + opencode adapters (#2145)
    * 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%.
  • feat: add dynamic workflow team orchestration surface
    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.
  • feat: add ECC2 local control pane (#2131)
    * feat: add ECC2 local control pane
    
    * fix: refresh control pane package locks
    
    * test: harden control pane coverage
    
    * test: allow portable control pane shutdown
    
    * test: retry local control pane fetches
    
    * fix: harden control pane error handling
    
    * fix: wrap control pane metadata
  • docs(i18n): add German localization scout (#2029)
    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.
  • fix(install-targets): validate compiled OpenCode plugin before install (#2041)
    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.
  • feat(install-targets): add claude-project (per-project Claude Code) adapter
    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/)
  • fix(hooks): avoid escaped quotes in plugin bootstrap
    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.
  • fix(lib): retry rename on Windows EPERM/EACCES/EBUSY in writeBridgeAtomic
    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.
  • fix(lib): use unique tmp suffix in writeBridgeAtomic to eliminate ENOENT race
    `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.
  • feat(installer): add --locale flag for translated docs installation
    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)
  • fix(hooks): close grouped command bypasses in gateguard (#1912)
    Inspect executable bodies inside plain subshells and brace groups before applying destructive command classifiers.\n\nCo-authored-by: Jamkris <82251632+Jamkris@users.noreply.github.com>
  • fix: integrate recent hook and docs PRs (#1905)
    Integrates useful changes from #1882, #1884, #1889, #1893, #1898, #1899, and #1903:
    - fix rule install docs to preserve language directories
    - correct Ruby security command examples
    - harden dev-server hook command-substitution parsing
    - add Prisma patterns skill and catalog/package surfaces
    - allow first-time protected config creation while blocking existing configs
    - read cost metrics from Stop hook transcripts
    - emit suggest-compact additionalContext on stdout
    
    Co-authored-by: Jamkris <dltmdgus1412@gmail.com>
    Co-authored-by: Levi-Evan <levishantz@gmail.com>
    Co-authored-by: gaurav0107 <gauravdubey0107@gmail.com>
    Co-authored-by: richm-spp <richard.millar@salarypackagingplus.com.au>
    Co-authored-by: zomia <zomians@outlook.jp>
    Co-authored-by: donghyeun02 <donghyeun02@gmail.com>
  • docs: add data-backed harness adapter scorecard (#1785)
    * docs: add data-backed harness adapter scorecard
    
    * fix: normalize adapter matrix line endings
    
    * test: avoid doubled CRLF simulation
  • feat: add ECC statusline observability hooks
    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.
  • fix: port continuous-learning observer fixes
    Ports continuous-learning observer signal, storage, remote normalization, and v1 deprecation fixes onto current main.
  • fix: port hook session and dashboard safety fixes
    Ports suggest-compact session_id isolation and dashboard terminal/document launch safety onto current main.
  • fix: install native Cursor hook and MCP config (#1543)
    * fix: install native cursor hook and MCP config
    
    * fix: avoid false healthy stdio mcp probes