2166 Commits

  • test(install-targets): add positive rules assertion to claude-project foreign-path test
    Addresses CodeRabbit review: the negative-only assertions could have
    passed on an empty plan. Add a positive assertion that the non-foreign
    'rules' path is still planned under .claude/rules/ecc so regression to
    zero ops would fail loudly.
  • 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/)
  • feat: extend harness audit integration scoring (#1990)
    Salvages the useful harness-audit scoring work from #1989 while preserving the current hook registry and newer plugin install detection. Adds GitHub integration checks, conditional deploy-provider categories, dynamic applicable category metadata, and CODEOWNERS coverage.
  • docs: define ECC 2.0 hypergrowth release lane
    Refresh the active 2.0 release surface for the affaan-m/ECC repo identity, update package/plugin/workflow launch metadata, and add an operator command center for release video, partner, sponsor, consulting, and social launch execution.
  • docs: keep renamed README install paths usable
    Adjust README manual-install snippets after the affaan-m/ECC repo rename so cloned paths use the new ECC checkout or relative paths.
  • Update README links to new repository name 'ECC'
    Changed `everything-claude-code` to `ECC`
  • 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(hooks): use shared renameWithRetry in writeWarnState (ecc-context-monitor)
    Mirror the previous commit's Windows-EPERM retry on the companion
    `writeWarnState` in `scripts/hooks/ecc-context-monitor.js`. Same
    race: two PostToolUse subprocesses writing concurrent debounce
    state racing on `MoveFileExW`, target-in-use throwing EPERM on
    Windows even though each writer's tmp path is now unique.
    
    Implementation: import `renameWithRetry` from `scripts/lib/session-bridge.js`
    (exported in the previous commit) instead of duplicating the helper.
    The retry policy, backoff schedule, and main-thread `Atomics.wait`
    strategy stay identical to `writeBridgeAtomic`.
    
    Three writers in the repo now share the same atomic-write contract:
    - `writeBridgeAtomic` (scripts/lib/session-bridge.js) — round 1 +
      this round's retry
    - `writeWarnState` (this file) — round 1 + this round's retry via shared helper
    - `writeCostWarningIfChanged` (scripts/hooks/ecc-metrics-bridge.js) —
      out of scope for this PR (already uses unique tmp suffix; a future
      consolidation could move it to the shared helper too).
    
    Local: `yarn test` green, `yarn lint` clean. The companion test
    suite for `ecc-context-monitor.js` does not currently exercise
    concurrent `writeWarnState` writes, but the helper it now uses is
    covered by the `tests/lib/session-bridge.test.js` concurrent-write
    regression added in round 1's last commit.
  • 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.
  • test(lib): make concurrent-write test actually concurrent + use regex matcher for assert.throws
    Two round-1 review findings in `tests/lib/session-bridge.test.js`,
    both about test correctness rather than the underlying fix:
    
    1. **greptile P1 + coderabbitai Major + cubic P2 (all three): concurrent-write test ran sequentially.**
    
       The test spawned two child processes with two consecutive
       `spawnSync` calls. Because `spawnSync` blocks until the child
       exits, the second writer started *after* the first finished —
       the two writers never overlapped, so the rename race the fix
       targets was never actually exercised. The test would have passed
       with the old broken `${target}.tmp` suffix.
    
       Fix: introduce a one-off "race runner" helper that runs inside
       its own subprocess and uses async `spawn` to start both writers
       simultaneously. The runner waits for both to exit (the event
       loop is local to the runner subprocess, so this stays compatible
       with the synchronous test harness used elsewhere in this file)
       and reports both exit codes plus stderrs on stdout. The test
       then calls the runner via `spawnSync` and parses the result.
       Both writer children now overlap for the duration of their 200
       `writeBridgeAtomic` calls each, which is enough wall time to
       reliably trigger the rename race against the pre-fix code.
    
       Verified: with the fixed `${target}.${pid}.${nonce}.tmp` suffix,
       the test passes; with the old fixed `${target}.tmp` suffix
       reintroduced, it fails as expected (one writer hits ENOENT on
       roughly half its rename calls).
    
    2. **greptile P2 + cubic P3: `assert.throws` used a string as the second argument.**
    
       Node deprecated passing a string as the second argument to
       `assert.throws` years ago: the string is silently treated as
       the assertion failure message (what to print when the function
       does *not* throw) rather than as an error matcher. The check
       passed for any thrown error, not just the rename failure.
    
       Fix: pass a regex matcher as the second arg and keep the
       explanatory text as the third. The regex matches `EISDIR`,
       `EPERM`, `ENOTDIR`, or `ENOENT` because `renameSync` of a
       regular tmp file onto an existing directory raises different
       codes on Linux / macOS / BSD — making the matcher portable
       across CI runners.
    
    Test count unchanged at 14; `npm test` green; `npm run lint` clean.
    
    The two helper files (`tests/__tmp_bridge_writer.js`,
    `tests/__tmp_bridge_race_runner.js`) are written and unlinked
    inside the test's try/finally so they never persist beyond the
    test run.
  • test(lib): concurrent writeBridgeAtomic + tmp-cleanup regression
    Two regression tests pin down the previous two commits' atomic-rename
    fixes:
    
    1. **concurrent writes don't throw ENOENT or corrupt the file** —
       spawns two child Node processes (`tests/__tmp_bridge_writer.js`
       created in-test, cleaned up in finally) that each call
       `writeBridgeAtomic(sid, …)` 200 times against the same session
       ID with independent payloads. Asserts both subprocesses exit 0
       (the previous implementation produced ENOENT on roughly 50% of
       rename calls, all swallowed by the in-test catch) and the final
       bridge file is parseable JSON belonging to one of the two writers
       (last-writer-wins is fine; the contract is *no corruption* and
       *no rename ENOENT*, not data preservation).
    
    2. **tmp file cleanup on rename failure** — pre-creates a directory
       at the target bridge path so `renameSync(tmp, target)` fails,
       calls `writeBridgeAtomic`, asserts the call throws AND that no
       tmp file with the writer's `pid.<nonce>.tmp` prefix is left
       behind in `os.tmpdir()`. The previous code had no cleanup; the
       fix's `try/catch + unlinkSync` keeps tmpdir from accumulating
       orphan files across repeated rename failures.
    
    The first test deliberately writes independent payloads from each
    subprocess so this regression doesn't try to claim a property the
    fix doesn't actually deliver (read-modify-write race in the caller
    is a separate issue and out of scope per PR body).
    
    Test count: 12 → 14 in `tests/lib/session-bridge.test.js`;
    `npm test` green; `npm run lint` clean.
  • fix(hooks): use unique tmp suffix in writeWarnState (ecc-context-monitor)
    Mirror the previous commit's `writeBridgeAtomic` fix on the
    companion `writeWarnState` in `ecc-context-monitor.js`. Same shape:
    fixed `${target}.tmp` → `${target}.${process.pid}.${randomNonce}.tmp`,
    plus best-effort cleanup of the tmp file on `renameSync` failure
    (throws original error after cleanup).
    
    `writeWarnState` debounces the context-monitor's threshold alarms
    (`COST_NOTICE_USD`, `COST_WARNING_USD`, `COST_CRITICAL_USD`, plus the
    context-remaining and loop-detection ones). Without unique suffixes,
    two PostToolUse subprocesses racing on the warn-state file produce
    either a corrupted JSON debounce-state on disk or an ENOENT throw
    that the hook catches and swallows — either way the next warn-state
    read returns the default `{callsSinceWarn: 0, lastSeverity: null}`
    and the threshold alarms re-fire or stop firing erratically. Users
    see warning messages flicker or vanish; debounce no longer works.
    
    Three call sites in this repo now share the same atomic-write
    contract:
    - `writeBridgeAtomic` (scripts/lib/session-bridge.js) — primary
    - `writeCostWarningIfChanged` (scripts/hooks/ecc-metrics-bridge.js) — cost cache
    - `writeWarnState` (this file) — debounce state
    
    `yarn lint` clean. Regression test covering both `writeBridgeAtomic`
    and `writeWarnState` under concurrent load lands in the next commit.
  • 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.
  • test(ci): regression coverage for newly-covered invisible code points
    9 new test cases pin down the two previous commits' denylist
    extensions. Each verifies both detection (validator exit non-zero +
    the expected `dangerous-invisible U+<HEX>` line on stderr) and,
    where applicable, `--write` sanitization.
    
    Coverage:
    
    Tag block (commit 1):
    - U+E0041 TAG LATIN CAPITAL LETTER A — the range's printable ASCII
      shadow; this is the byte sequence demonstrated in published ASCII
      smuggling proofs of concept.
    - U+E007F CANCEL TAG — the range end.
    
    Other invisibles (commit 2):
    - U+180E MONGOLIAN VOWEL SEPARATOR
    - U+115F HANGUL CHOSEONG FILLER
    - U+1160 HANGUL JUNGSEONG FILLER
    - U+2061 FUNCTION APPLICATION (range start)
    - U+2064 INVISIBLE PLUS (range end)
    - U+3164 HANGUL FILLER
    
    Detection table is data-driven (one loop, one assertion per row) so
    adding the next invisible to the denylist also gets a paired
    regression test by simply appending to NEWLY_COVERED_RANGES.
    
    Plus a `--write` integration test:
    - writes a markdown file containing both Tag block (5 chars) and
      U+180E, runs `--write`, asserts both removed and surrounding text
      preserved character-for-character ('# Title\n\nBenigntext.\n').
    - re-runs the validator without `--write` and asserts exit 0,
      confirming the sanitizer's output is idempotent under the
      extended denylist.
    
    Test count: 5 → 14 in this file; full `yarn test` green; `yarn lint`
    clean.
  • fix(ci): cover other widely-cited invisible code points in check-unicode-safety
    Extend `isDangerousInvisibleCodePoint` with five additional code
    points / ranges that are routinely cited in invisible-character
    smuggling references but were not in the previous denylist:
    
    - **U+180E** MONGOLIAN VOWEL SEPARATOR. Formerly classified as a
      space separator (Zs) until Unicode 6.3 reclassified it as Cf
      (Format control). Renders as zero-width; widely abused for
      homograph attacks and prompt smuggling.
    
    - **U+115F** HANGUL CHOSEONG FILLER and **U+1160** HANGUL JUNGSEONG
      FILLER. Zero-width fillers used in Korean text shaping. Both are
      cited as common LLM-injection vectors in Korean / multilingual
      threat models.
    
    - **U+2061–U+2064** invisible math operators (FUNCTION APPLICATION,
      INVISIBLE TIMES, INVISIBLE SEPARATOR, INVISIBLE PLUS). Zero-width
      and only meaningful inside math typesetting. No legitimate
      Markdown or source code uses them.
    
    - **U+3164** HANGUL FILLER. Reported in real-world Discord and
      Twitter smuggling incidents; not used in legitimate Korean text.
    
    Reproduced before this commit: a file containing any one of these
    code points passed `check-unicode-safety.js` silently.
    
    After this commit each one is reported as
    `dangerous-invisible U+<HEX>` and `--write` mode strips it.
    
    Verified by writing 8 single-character probe files
    (`probe-0x180E.md`, `probe-0x115F.md`, …) and confirming exit=1 with
    each violation listed.
    
    ECC repo self-scan reports only the pre-existing `U+2605` BLACK
    STAR warnings (unchanged) and exits with the same status (no new
    in-repo violations introduced). Existing 5 unicode-safety tests
    still pass; `yarn lint` clean.
    
    Regression coverage for both the previous commit's Tag block fix
    and this commit's additions lands in the next commit.
  • fix(ci): cover Unicode Tag block (U+E0000–U+E007F) in check-unicode-safety
    `isDangerousInvisibleCodePoint` enumerated seven ranges of invisible/
    bidi/variation-selector code points but omitted the Unicode Tag block
    (U+E0000–U+E007F). Tag characters were proposed for language tagging
    in Unicode 3.1 and have been deprecated since Unicode 5.1, so no
    legitimate text uses them. They are the canonical vector for
    "ASCII Smuggling" / "Tag Smuggling" LLM prompt injection: an attacker
    hides instructions inside an ASCII-looking string, the model reads
    the tag bytes, the human reviewer sees nothing. Demonstrated against
    multiple LLM assistants during 2024–2025.
    
    `check-unicode-safety.js` is the repo's last line of defence before
    contributor content reaches agent context; the same script also runs
    in `--write` auto-sanitize mode on `.md` / `.mdx` / `.txt`. Today it
    silently passes tag-block characters through unchanged in both
    detection mode and `--write` mode.
    
    Reproduced before this commit:
    
      $ mkdir -p /tmp/uni-test && node -e "
          const fs = require('fs');
          const hidden = [...Array(5)].map((_,i) =>
            String.fromCodePoint(0xE0041 + i)).join('');
          fs.writeFileSync('/tmp/uni-test/innocent.md',
            '# Title\\n\\nBenign text' + hidden + ' more.\\n');"
    
      $ ECC_UNICODE_SCAN_ROOT=/tmp/uni-test \
          node scripts/ci/check-unicode-safety.js
      Unicode safety check passed.
      $ echo $?
      0
    
    Expected: tag-block characters reported as `dangerous-invisible`
    violations (exit 1) and stripped under `--write`.
    Actual: validator passes, `--write` leaves the bytes intact.
    
    Fix: extend the denylist with one new range
    `(codePoint >= 0xE0000 && codePoint <= 0xE007F)`. The change is
    purely additive; the existing seven ranges are untouched.
    
    After this commit the same reproduction returns:
    
      $ ECC_UNICODE_SCAN_ROOT=/tmp/uni-test \
          node scripts/ci/check-unicode-safety.js
      Unicode safety violations detected:
      innocent.md:3:12 dangerous-invisible U+E0041
      innocent.md:3:14 dangerous-invisible U+E0042
      innocent.md:3:16 dangerous-invisible U+E0043
      innocent.md:3:18 dangerous-invisible U+E0044
      innocent.md:3:20 dangerous-invisible U+E0045
      exit=1
    
    `--write` mode also strips the bytes (verified: file length 47 → 42
    after sanitize, regex `/[\u{E0000}-\u{E007F}]/u` no longer matches).
    
    Existing 5 unicode-safety tests still pass; `yarn lint` clean. The
    ECC repo's own self-scan (`node scripts/ci/check-unicode-safety.js`
    with no `ECC_UNICODE_SCAN_ROOT`) reports the same warnings as before
    this commit and exits with the same status (no regressions on
    in-repo content).
    
    A handful of other widely-cited invisible code points are missing
    from the denylist (`U+180E`, `U+115F`, `U+1160`, `U+2061–U+2064`,
    `U+3164`); those are addressed in the next commit so each fix
    remains independently reviewable. Regression coverage for both
    fixes lands two commits later.