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.
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.
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/)
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.
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.
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.
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.
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.