709 Commits

  • docs+chore: add README Security section; fix lint regressions on main
    - README: add a visible ## Security section (official sources, vuln reporting via SECURITY.md, GateGuard/IOC/AgentShield guardrails, security guide); make stats line a plain paragraph to clear MD028
    - eslint: empty catch comment in run-with-flags.js; drop unneeded escape in github-coordination/parsing.js; remove unused execFileSync import in its test (#2236 follow-ups)
    - markdownlint: wrap bare URLs in rules/vue/*.md (#2250 follow-up)
    
    npm run lint green; full suite 2836/2836.
  • fix: detect destructive find -exec commands in gateguard (#2267)
    * fix: detect destructive find exec commands in gateguard
    
    * chore: ignore aider local files
  • chore: reconcile publish/agent surfaces after PR batch
    - agent.yaml: register epic-* commands (#2236) and vue-review (#2241)
    - package.json files: drop stray skills/ml-adoption-playbook entry (follows orphan-skill publish pattern; not in install-modules.json)
    - unicode-safety: strip decorative emoji from dashboard-web.js (#2100) and brand-discovery refs (#2221) to pass the CI gate
    - agent-compress: raise catalog token canary 5000 -> 6000 for the 67-agent catalog
    
    Full suite green (2836/2836).
  • Merge pull request #2236 from Victor-Casado/feat/github-native-coordination
    feat: add github-native coordination (epic-* commands + scripts + tests). Command registry + catalog reconciled.
  • Merge pull request #2241 from itkdm/feat/add-vue-ecosystem
    feat: add Vue ecosystem review support (vue-reviewer agent, /vue-review command, vue-patterns skill). Duplicate rules/vue/* kept from #2250; catalog counts reconciled.
  • feat(opencode): 全面升级OpenCode集成 (#2251)
    - 修复ecc-hooks.ts中的硬编码ECC_VERSION(从package.json读取)
    - 改进错误处理机制(统一模式、详细错误信息)
    - 增强类型安全(添加ToolArgs、ToolInput等类型定义)
    - 改进跨平台兼容性(支持macOS、Windows、Linux)
    - 添加dependency-analyzer工具(依赖分析)
    - 改进format-code工具(错误处理、跨平台支持)
    - 改进lint-check工具(错误处理、跨平台支持)
    - 更新文档(代理26个、工具8个、命令26个)
    - 添加工具测试(6个测试用例)
    - 改进现有测试(7个测试用例)
    
    所有测试通过(16/16)
    
    Co-authored-by: Pual-LI-6 <dj2112236494@outlook.com>
  • Finalize and enhance SLSA generic generator workflow (#2197)
    * Add SLSA generic generator workflow
    
    * ci: finalize SLSA generator and fix bun test timeout
    
    - Harden SLSA workflow with persist-credentials: false and pinned actions
    - Update SLSA workflow to build real npm artifacts and fix digest outputs
    - Increase trae-install test timeout to prevent ETIMEDOUT under Bun
    - Fix Validate Components security violation in SLSA workflow
    
    * ci: finalize SLSA generator and fix bun test timeout
    
    - Harden SLSA workflow with persist-credentials: false and pinned actions
    - Update SLSA workflow to build real npm artifacts and fix digest outputs
    - Rename workflow to "SLSA generic generator workflow #1"
    - Increase trae-install test timeout to prevent ETIMEDOUT under Bun
    - Fix Validate Components security violation in SLSA workflow
    
    * Update generator-generic-ossf-slsa3-publish.yml
    
    Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
    
    * generator-generic-ossf-slsa3-publish.yml
    
    * .github/workflows/generator-generic-ossf-slsa3-publish.yml
    
    Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
    
    * Update .github/workflows/generator-generic-ossf-slsa3-publish.yml
    
    Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
    
    ---------
    
    Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
    Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
  • fix: add plugin cache health check (#2249)
    * fix: add plugin cache health check
    
    * fix: harden plugin cache diagnostics
    
    * fix: reject escaping plugin cache refs
    
    * test: remove unused plugin cache fixture
  • feat: add dry-run mode for hook execution (#2116) (#2188)
    - Global --dry-run flag and ECC_DRY_RUN=1 env var
    - Enriched preview: shows target file path, tool name, and command
    - --dry-run stripped from argv so command routing works correctly
    - Handles non-JSON and empty stdin gracefully (session/stop hooks)
    - 10 tests covering isDryRun(), hook gating, enriched output, CLI routing
  • feat: add web capabilities dashboard (#2100)
    * feat: add web capabilities dashboard with agents, skills, commands, MCPs, rules, and hooks
    
    * fix: address code review - XSS, env exposure, port validation, error handling, packaging
    
    * add tests for dashboard
  • fix: sanitize subprocess call in runner.py (#2149)
    * fix: V-001 security vulnerability
    
    Automated security fix generated by OrbisAI Security
    
    * fix: sanitize subprocess call in runner.py
    
    The runner
    
    * fix: address PR review comments on V-001 allowlist and test coverage
    
    Remove dangerous interpreters (python, python3, node, curl, wget) from
    ALLOWED_SETUP_EXECUTABLES — they can execute arbitrary code via argument
    flags and are not needed for sandbox setup. Rewrite test_invariant_runner
    to call _setup_sandbox directly instead of spawning runner.py as a
    subprocess (which had no __main__ entrypoint and never exercised the fix).
    
    Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
    
    ---------
    
    Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
  • fix(security): add host/origin allowlist + validate git refs + quote workflow input (#2185)
    Three defense-in-depth fixes around untrusted input flowing to subprocess execution:
    
    1. **Control-pane HTTP server (scripts/lib/control-pane/server.js)**
       The local control-pane API binds to 127.0.0.1 but had no Host or Origin
       validation, so a DNS-rebinding attack from a malicious website could pivot
       into the loopback endpoints — including POST /api/actions/:id, which spawns
       'cargo run -- graph ...' with caller-supplied query strings. Add a hostname
       allowlist (loopback variants plus the explicitly configured --host) and
       reject mismatched Host (421) or non-loopback Origin (403) before any route
       handler runs.
    
    2. **OpenCode git-summary tool (.opencode/tools/git-summary.ts)**
       The tool was building 'git diff ${baseBranch}...HEAD --stat' with execSync
       and a raw model-supplied baseBranch string. Switch run() to execFileSync
       with an args array (no shell), validate baseBranch against a conservative
       git-ref allowlist (rejects shell metacharacters, leading -, embedded ..),
       and clamp the depth arg to a small positive integer before interpolating
       into 'git log --oneline -<N>'.
    
    3. **Reusable test workflow (.github/workflows/reusable-test.yml)**
       The 'Install dependencies' step interpolated ${{ inputs.package-manager }}
       directly into a bash 'case' and into an echo, so a downstream caller that
       forwarded attacker-controllable input could inject into the runner. Move
       the input into a PACKAGE_MANAGER env var and reference $PACKAGE_MANAGER
       inside the script per the GitHub script-injection guidance.
    
    Detected by Aeon + semgrep p/security-audit (host check via threat-model
    manual-review axis; git-summary via detect-child-process; workflow via
    run-shell-injection).
    
    Verification: node tests/run-all.js — 2686/2687 pre-existing tests pass; the
    one failure (observe.sh legacy output fallback) reproduces on main without
    this branch applied. Added 2 new control-pane tests covering the allowlist
    classifier and the DNS-rebinding-gate behavior end-to-end.
    
    ---
    Filed by [Aeon](https://github.com/aaronjmars/aeon-aaron).
    
    Co-authored-by: aeonframework <aeon@aaronjmars.com>
  • fix: prevent IOC scanner false positives on hook filenames and scan .cursor configs (#2245)
    * fix: prevent IOC scanner false positives on hook filenames and scan .cursor configs
    
    The supply-chain IOC scanner matched CRITICAL_TEXT_INDICATORS with plain
    substring search, so legitimate hook filenames that merely end with a known
    payload name (e.g. the stock Cursor hook before-shell-execution.js vs the
    payload execution.js) were flagged as CRITICAL. Indicator matching now
    requires a non-filename character before the match.
    
    Also add .cursor/ to the special config paths so Cursor hooks.json files
    (a known persistence vector already listed in PERSISTENCE_FILENAMES) are
    actually inspected in normal checkouts - previously they were only scanned
    by accident when the repo path happened to contain /.claude/.
    
    * test: cover underscore-prefixed filenames in IOC boundary suppression
    
    Make explicit that '_' is treated as a filename word character, so
    snake_case hook names like post_execution.js are intentionally not
    flagged by the execution.js indicator (real payload references appear
    after '/', quotes, or whitespace).
  • fix(hooks): stop pre/post Bash dispatcher from echoing the input event (#2240)
    runHooks() returned the unmodified raw stdin (the PreToolUse/PostToolUse
    input event) on stdout whenever no sub-hook produced additionalContext.
    Claude Code parses a hook's stdout as JSON and validates it against the
    hook-output schema, so echoing the input object
    ({session_id, hook_event_name, tool_name, tool_input, ...}) fails with
    "Hook JSON output validation failed — (root): Invalid input" on nearly
    every Bash command.
    
    Track whether a sub-hook deliberately set stdout (string / {stdout}, e.g.
    GateGuard) via a rawModified flag and emit '' in the pass-through case
    instead of the echoed input. Preserves GateGuard pass-through and
    block-no-verify's exit-2 blocking.
    
    Update the three dispatcher tests that codified the buggy echo behavior to
    expect empty stdout, and add a regression test for a plain pass-through
    command.
    
    Fixes #2239
    
    Co-authored-by: WOZCODE <contact@withwoz.com>
  • fix: context-size /compact trigger, Codex marketplace plugin path, live README badges (#2237)
    - suggest-compact hook now reads the latest usage record from the session
      transcript and suggests /compact at a window-scaled token threshold
      (160k/200k window, 250k/1M window; COMPACT_CONTEXT_THRESHOLD and
      COMPACT_CONTEXT_INTERVAL overridable), re-firing per 60k-token growth
      bucket; tool-call count stays as the secondary signal (#2155)
    - Codex repo marketplace now points at ./plugins/ecc instead of ./ — Codex
      never discovers plugins whose local marketplace source.path is the
      marketplace root (verified on Codex CLI 0.137.0); plugins/ecc is a thin
      folder referencing root skills/.mcp.json per maintainer direction on
      #2097; docs flag plugin mode as experimental with the upstream blocker
      openai/codex#26037 linked (#2128)
    - README badges for installs/stars/forks now use shields endpoint badges
      backed by api.ecc.tools (live install count 3,712 vs the stale static
      150), which also eliminates shields' 'Unable to select next GitHub token
      from pool' render in the stars badge
    
    Closes #2155
    Closes #2128
  • fix: address second round of code-review findings
    actions.js:
    - Add assertValidRepo/assertValidIssueNumber guards at the top of all
      action handlers (applyClaim, applySync, applyValidate, applyPublish,
      applyReview, applyDecompose, applyUnblock) for fast-fail validation
    - applyValidate: fix status transition — set 'validated' unconditionally
      when ok=true instead of preserving 'blocked' (was inconsistent with
      projectState becoming 'ready')
    
    gh-api.js:
    - runGh: preserve GITHUB_TOKEN by default; only delete when caller
      explicitly sets options.stripGithubToken=true (was deleting by
      default, breaking CI)
    
    parsing.js:
    - extractCoordinationState: throw SyntaxError on malformed JSON instead
      of silently returning null — lets callers distinguish bad JSON from
      absent marker
    - normalizeBodyForComparison: fix regex to match JSON-quoted form
      "lastSyncAt": ... instead of bare lastSyncAt: ...
    
    policy.js:
    - loadPolicy: validate that parsed JSON is a plain object before
      spreading; coerce nested fields (labels, review, validation,
      branchModel, project, fieldNames) to objects before merging
    
    state.js:
    - assertIssueClaimable: block re-claim on status alone (not status AND
      owner) to prevent {status:'claimed', owner:null} bypass; use
      state.owner || 'unknown' in error message
    - getCoordinationState: catch SyntaxError from extractCoordinationState,
      log warning to stderr, fall back to default state
    
    tests/lib:
    - Update malformed-JSON test to expect SyntaxError throw instead of null
    
    Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
  • refactor: apply code-review findings to github-native coordination
    scripts/github-coordination.js:
    - parseArgs: replace 13-entry if/else chain with BOOL_FLAGS/VALUE_FLAGS
      lookup maps; shrinks from 119 to ~45 lines
    - Extract dispatchCommand(options, ctx) and formatOutput(payload, options)
      from main(); main() shrinks to ~20 lines
    
    scripts/lib/github-coordination.js:
    - Split 1041-line monolith into 6 focused sub-modules under
      scripts/lib/github-coordination/ (policy, parsing, gh-api, state,
      actions, store); index becomes a thin re-export (~55 lines)
    - Document ECC_GH_SHIM trust boundary in runGh() (gh-api.js)
    - Document applyClaim() read→check→write race condition (actions.js)
    
    tests/lib/github-coordination.test.js:
    - Refactor runTests() to data-driven DESCRIPTORS array + runGroup()
      helper; runTests() shrinks to ~10 lines
    - Add 5 new edge-case tests: normalizeRepo('') and normalizeRepo('   ')
      throw, desiredLabelsForState for blocked/ready statuses, and
      buildIssueStateFromAction for validate action (15 → 20 tests)
    
    tests/scripts/github-coordination.test.js:
    - Replace console.log in test runner with process.stdout.write
    
    Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
  • feat: add github-native coordination (epic-* commands + scripts + tests)
    Adds a GitHub-native coordination layer on top of ECC:
    
    Commands (7 new slash commands):
    - epic-claim, epic-sync, epic-validate, epic-publish
    - epic-review, epic-unblock, epic-decompose
    
    Scripts:
    - scripts/github-coordination.js  — CLI entry point
    - scripts/lib/github-coordination.js  — core library (state machine, gh API wrappers)
    - scripts/status.js  — coordination status reporter
    
    Config:
    - config/github-native-coordination.json  — labels, review policy, validation gates
    
    Tests:
    - tests/lib/github-coordination.test.js  — 15 unit tests for pure functions
    - tests/scripts/github-coordination.test.js  — integration/CLI test suite
    
    Registry:
    - docs/COMMAND-REGISTRY.json  — adds 7 epic-* entries, totalCommands 84 → 91
    
    No encoding changes, no prp-* modifications, no Windows shims.
    
    Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
  • fix: stability batch — hook stdin truncation, Codex exa TOML, Stop hook JSON, GateGuard repetition (#2227)
    * fix(hooks): fail open on oversized stdin instead of echoing truncated JSON (#2222)
    
    run-with-flags.js capped stdin at 1MB but every fallthrough path still
    echoed the truncated string to stdout. The harness parses hook stdout as
    JSON, got a document cut mid-stream, and blocked the tool call — so any
    Edit/Write with a >1MB hook payload was permanently blocked by every
    registered pre-write hook, before ECC_HOOK_PROFILE / ECC_DISABLED_HOOKS
    gating could run.
    
    - Exit 0 with empty stdout (no opinion) when the stdin cap trips, before
      any echo or gating logic.
    - Flush stdout via write callback before process.exit: exiting right
      after stdout.write() dropped everything past the ~64KB pipe buffer,
      cutting even sub-cap pass-through payloads mid-JSON.
    
    Regression tests cover the enabled, disabled, and missing-arg paths for
    oversized payloads plus full echo of sub-cap >64KB payloads.
    
    * fix(codex): stop emitting invalid exa url entry, align merge with connector policy (#2224)
    
    The Codex MCP merge declared exa with a url key, but Codex's
    [mcp_servers.*] TOML schema is stdio-only — the url key makes the
    entire config.toml fail to load, bricking both the codex CLI and the
    desktop app. Every install/update re-injected the line because the
    urlEntry branch treated the broken entry as present.
    
    - ECC_SERVERS now emits only the current default set per
      docs/MCP-CONNECTOR-POLICY.md: chrome-devtools (stdio, command/args).
      Retired servers (supabase, playwright, context7, exa, github, memory,
      sequential-thinking) are never re-emitted; existing user-managed
      entries are untouched.
    - The merge now repairs the exact ECC-emitted broken form (url-only
      exa entry) on every run so re-running the installer fixes broken
      configs instead of preserving them. User stdio exa entries
      (command + mcp-remote) are left alone.
    - check-codex-global-state.sh requires chrome-devtools instead of the
      retired set, and flags url-only exa entries with a repair hint.
    
    Tests cover repair, re-run idempotence, stdio-entry preservation, and
    no-retired-server emission in add, update, dry-run, and disabled modes.
    
    * fix(hooks): never echo truncated stdin from Stop hooks (#2090)
    
    Stop hooks follow the ECC pass-through convention (echo stdin on
    stdout), but every echoing Stop hook capped stdin and echoed the capped
    string. The Stop payload carries last_assistant_message, so a long
    final assistant message produced a JSON document cut mid-stream on
    stdout, which the harness reports as 'Stop hook error: JSON validation
    failed' across the whole Stop chain.
    
    Reproduced: a Stop payload with a >64KB last_assistant_message run
    through run-with-flags + cost-tracker emitted exactly 65536 bytes of
    invalid JSON (cost-tracker capped stdin at 64KB — far below realistic
    Stop payloads).
    
    - cost-tracker: raise the cap to 1MB (matching all other hooks) and
      suppress the pass-through echo when stdin was truncated.
    - check-console-log, stop-format-typecheck, desktop-notify: suppress
      the echo when stdin was truncated; flush stdout before process.exit
      so sub-cap payloads are not cut at the ~64KB pipe buffer.
    - All hooks keep exiting 0 (fail-open); diagnostics go to stderr.
    
    New stop-hooks-stdout test asserts the contract for every registered
    Stop hook: stdout is empty or valid JSON, exit code 0 — for realistic
    100KB payloads and oversized >1MB payloads, via the production runner
    and via direct invocation. Updated the old hooks.test.js case that
    codified the truncated-echo behavior.
    
    * fix(hooks): dampen GateGuard fact-force repetition in long sessions (#2142)
    
    In long autonomous sessions the fact-force gate produced 10+
    near-identical 'state facts -> blocked -> restate -> retry' blocks in
    one context window, which measurably raises the odds of the model
    collapsing into a degenerate single-token repetition loop.
    
    - Track a per-session fact_force_denials counter in GateGuard state
      (merged max across concurrent writers, reset with the session, robust
      to malformed on-disk values).
    - The first GATEGUARD_FACT_FORCE_FULL_DENIALS denials (default 3) keep
      the full four-fact block; later denials emit a condensed single-line
      message that carries the denial ordinal, so consecutive denials are
      structurally different and never textually identical.
    - True retries of the same target remain allowed without re-prompting
      (unchanged). Destructive-Bash and routine-Bash gates are unchanged,
      as are the ECC_GATEGUARD=off / ECC_DISABLED_HOOKS escape hatches.
    
    Eight new tests cover budget counting, condensed format, ordinal
    advancement, retry pass-through, env tuning, malformed state, MultiEdit
    dampening, and destructive-gate exemption.
    
    * fix(hooks): keep security hooks able to block on oversized stdin (#2222)
    
    Refine the truncation fail-open: instead of skipping the hook entirely,
    the runner now suppresses only its own raw-echo when stdin was
    truncated. The hook still executes and receives the truncated flag
    (run() context / ECC_HOOK_INPUT_TRUNCATED), so config-protection keeps
    blocking truncated protected-config payloads (its test requires exit 2)
    while pass-through hooks fail open with empty stdout as before.
    
    * style: apply repo formatter to touched hook files
  • feat(mcp): single-connector default set + connector policy (#2219)
    Reduce the default .mcp.json to one connector (chrome-devtools) per the
    new policy in docs/MCP-CONNECTOR-POLICY.md: a default earns its slot only
    if it is universal AND MCP beats a CLI/API wrapped in a skill. June 2026
    audit verdicts: github -> gh via github-ops skill; context7 -> REST via
    documentation-lookup; exa -> harness-native search (+ exa-search skill);
    memory -> native harness memory + instincts; playwright -> playwright CLI
    skills (vendor moved agent flows off MCP); sequential-thinking -> native
    extended thinking. All six remain opt-in in mcp-configs/mcp-servers.json.
    Tests updated: plugin-manifest policy assertions + install-apply Cursor
    expectations.
    
    Co-authored-by: ECC Test <ecc@example.test>
  • release: 2.0.0 — the agent harness operating system
    Graduate 2.0.0-rc.1 to stable. Bump version across package, plugin,
    marketplace, OpenCode, agent metadata, VERSION, and all localized docs.
    Add 2.0.0 release notes + README sections (en/zh/pt-BR/tr), CHANGELOG
    entry, and the ECC community Discord bot (dependency-free gateway client
    + guild command registrar). Update copilot-support and release-surface
    tests for the sponsored-review migration and the 2.0.0 surface.
  • fix: make plugin hooks run on Node 21+ and green the suite under modern Node (#2184)
    ROOT CAUSE: hooks load plugin-hook-bootstrap.js via
    `node -e "...; process.argv.splice(1,0,s); require(s)"`. On Node 21+,
    require.main is `undefined` under --eval, so the `if (require.main === module)`
    guard was false and main() never ran — every plugin hook silently no-op'd
    (e.g. the MCP-health PreToolUse hook stopped blocking). CI (Node 18/20) hid
    this; it only surfaces on Node 21+. Fix: also run main() when require.main is
    undefined (the eval-bootstrap case), while staying dormant on real imports.
    
    Also clears pre-existing main debt the full local suite enforces:
    - catalog:sync — README/docs agent+skill counts drifted after recent merges
    - tests/ci/supply-chain-watch-workflow: update checkout SHA to the merged v6.0.3 (#2183)
    - markdownlint + check-unicode-safety --write across docs/skills
    
    Suite: 2683/2683 green under Node v25; lint + unicode clean.
    
    Co-authored-by: ECC Test <ecc@example.test>
  • fix(commands): resolve active plugin root in /instinct-status (#2037) (#2059)
    The `/instinct-status` slash command template expanded
    `${CLAUDE_PLUGIN_ROOT}` directly and documented a manual-install
    fallback to `~/.claude/skills/continuous-learning-v2/scripts/instinct-cli.py`.
    When users had both an active plugin install (under
    `~/.claude/plugins/cache/<slug>/<org>/<version>/`) and a legacy
    `~/.claude/skills/continuous-learning-v2/` directory left over from a
    previous manual install, an empty `CLAUDE_PLUGIN_ROOT` (which Claude
    Code does not always populate in slash-command shell contexts) silently
    made the command read the stale legacy install while the active plugin
    hooks and observer wrote to the new XDG path. The user saw "No
    instincts found" while the system was actively learning — exactly the
    divergence the bug reporter spent hours diagnosing.
    
    Replace the brittle two-block template with the same inline resolver
    pattern that `hooks/hooks.json` and `/sessions` / `/skill-health`
    already use: env var → standard install → known plugin roots → plugin
    cache walk → fallback. The resolver is the canonical `INLINE_RESOLVE`
    constant from `scripts/lib/resolve-ecc-root.js`, so no new code is
    introduced — just consistent adoption of the existing pattern.
    
    Apply the same fix to all five copies of the command:
      - commands/instinct-status.md (canonical)
      - .opencode/commands/instinct-status.md
      - docs/zh-CN/commands/instinct-status.md
      - docs/ja-JP/commands/instinct-status.md
      - docs/tr/commands/instinct-status.md
    
    Extend tests/lib/command-plugin-root.test.js with an assertion that the
    canonical instinct-status.md uses the inline resolver and no longer
    hard-codes the legacy `~/.claude/skills/...` fallback (regression
    guard).
    
    zh-CN copy: polish the Chinese phrasing per LanguageTool feedback
    (`使用与 ... 相同的解析器` → `以与 ... 相同的解析器`) so the verb is
    introduced by an explicit preposition instead of reading as an awkward
    verb-object construction.
  • 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(hooks): stop false loop warnings and repeated identical context warnings (#2121)
    * fix(hooks): stop false loop warnings and repeated identical context warnings
    
    Two PostToolUse monitor defects surfaced during a long single-turn session:
    
    1. ecc-metrics-bridge hashToolCall fingerprinted Edit/Write/MultiEdit on
       file_path ONLY, so several distinct edits to the same file produced the
       same hash and tripped the loop detector ("stuck loop") even though every
       edit was different. Now the hash includes the edit content
       (old_string/new_string/content/edits) so distinct edits to one file hash
       differently; identical edits still collide as intended.
    
    2. ecc-context-monitor re-emitted the SAME warning every DEBOUNCE_CALLS (5)
       tool calls even when nothing changed. Because the cost figure only refreshes
       at Stop (turn) boundaries, a single stale value printed the identical
       warning ~20 times within one turn. Dedupe on message content instead: a
       warning surfaces only when its text changes (cost moved, new file count, new
       loop) or on first escalation to critical, and is otherwise suppressed.
    
    Adds regression tests for the same-file/different-content hash case.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    
    * fix(hooks): address CodeRabbit review (#2121)
    
    - ecc-context-monitor: clear dedupe state when warnings resolve, so the same
      warning text recurring in a later turn (context dips/recovers/dips, a loop
      that stops then restarts) is surfaced again instead of suppressed as a
      duplicate. Guarded so the no-warning hot path stays write-free.
    - ecc-metrics-bridge: hash the FULL serialized edit payload and truncate the
      digest, not the input. Slicing the serialized string to HASH_INPUT_LIMIT
      first could collapse large edits sharing their first 2048 chars, reviving the
      false-loop collision for big Write/edit payloads.
    - Add regression test for >2048-char edit divergence.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    
    ---------
    
    Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
  • fix: surface legacy data warning in instinct-cli status (#2127)
    * fix: surface legacy data warning in instinct-cli status (#2036)
    
    When the data directory moved from ~/.claude/homunculus/ to the
    XDG-compliant ~/.local/share/ecc-homunculus/, legacy installs with data
    still in the old path saw "No instincts found" with no explanation.
    
    Add _warn_legacy_data() to cmd_status so users get a clear, actionable
    warning pointing them to the migration script or the CLV2_HOMUNCULUS_DIR
    override. Wrap the directory scan in try/except to handle permission
    errors gracefully.
    
    Closes #2036
    
    Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
    
    * fix: address review feedback — drop unused f-strings, resolve absolute migrate path
    
    Remove extraneous f-prefix from strings without interpolation (ruff F541).
    Resolve migrate-homunculus.sh path relative to instinct-cli.py instead of
    hard-coding a repo-relative path that only works from the repo root.
    
    Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
    
    * fix: quote migrate script path to handle spaces
    
    Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
    
    ---------
    
    Co-authored-by: kky <lingmu141592@gmail.com>
    Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
  • fix(gateguard): gate force/path git checkout as destructive (#2158)
    * fix(gateguard): gate force/path git checkout as destructive
    
    The destructive-command gate's `checkout` handler only flagged
    `git checkout -- <path>`. It missed `git checkout --force` / `-f <branch>`
    and `git checkout .`, all of which discard uncommitted working-tree changes,
    so they bypassed the gate (once the once-per-session routine-Bash gate is
    satisfied, they ran with no challenge). The sibling `switch` handler already
    covers these force forms; mirror it for `checkout`.
    
    * test(gateguard): document Test 7b force-checkout case
    
    ---------
    
    Co-authored-by: bymle <229636660+bymle@users.noreply.github.com>
  • docs(claude): install manual skills at top level (#2160)
    * docs(claude): install manual skills at top level
    
    * test(docs): guard Claude manual skill install path
    
    * test(docs): detect PowerShell/$HOME nested skill-install paths
    
    Address CodeRabbit on #2160: the nested-path regression guard only matched
    Unix `mkdir`/`cp` with `~`, so a reintroduced PowerShell `Copy-Item ...
    $HOME/.claude/skills/ecc` (or backslash-separated) form would have slipped
    through. Extend the pattern to also cover `Copy-Item`/`New-Item` (and the
    `md`/`copy`/`cpi` aliases), accept `$HOME` as an alternative to `~`, allow both
    `/` and `\` separators, and match case-insensitively.
    
    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
    
    ---------
    
    Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
  • fix: retire rules/zh from the always-loaded default rules install (#2170)
    rules/zh shipped ~17KB of Chinese rule text into the auto-loaded rules tree
    of every default install (rules-core installs the bare 'rules' path with
    defaultInstall: true), with no paths: frontmatter gating. The content had
    also drifted behind both rules/common and the maintained translations in
    docs/zh-CN/rules/common (e.g. zh/coding-style.md 48 lines vs the 52-line
    docs/zh-CN copy), and 'zh' was already dropped from the installer's language
    help in favor of the gated docs-zh-cn locale module (--locale zh-CN).
    
    - move rules/zh/code-review.md to docs/zh-CN/rules/common/code-review.md:
      the only file with no counterpart in the maintained locale tree (fills a
      zh-CN parity gap with rules/common/code-review.md)
    - delete the remaining 10 rules/zh files, all older duplicates of
      docs/zh-CN/rules/common content
    - update trae-install test to assert the rules tree via rules/web instead
    
    Not addressed here: rules/README.md (~5.5KB of installer docs) still ships
    into the auto-loaded tree via the bare 'rules' module path; filtering README
    files from rule-tree expansion is a separate decision
  • test: skip chmod-based permission tests when running as root (#2171)
    Two tests provoke EACCES via chmod (saveAliases backup double failure,
    appendSessionContent on a read-only file) and already skip on win32, but
    root ignores file modes so both fail when the suite runs as root (for
    example in a default Docker container). Every other chmod-based test in
    the repo already guards with process.getuid?.() === 0; these two were the
    only ones missing the guard. Apply the same skip condition and message.
  • fix: close install manifest packaging gaps (#2172)
    - commands-core now ships scripts/harness-audit.js and scripts/skills-health.js:
      the module installs the whole commands/ dir, so /harness-audit and
      /skill-health were installed without their backing engines on
      manifest-driven installs (the original 1.10.0 failure mode)
    - agentic-patterns now ships scripts/claw.js: the module installs the
      nanoclaw-repl skill, whose workflow operates scripts/claw.js
    - package.json files array gains scripts/skills-health.js so the npm publish
      surface stays aligned with the module graph (claw.js and harness-audit.js
      were already listed)
    - orchestration drops commands/multi-workflow.md and commands/sessions.md
      from its explicit paths: both are already shipped by commands-core, which
      is a declared dependency of the module, so the duplicate ownership produced
      two copy operations per destination in install-state. The two scripts/lib
      entries are kept because hooks-runtime is NOT a declared dependency and a
      standalone orchestration install still needs them
  • fix(skills): keep curl credentials out of argv (#2175)
    * fix(skills): avoid curl credential argv leaks
    
    * test(ci): guard secret curl examples
  • test: guard broken-symlink tests so the suite passes on Windows (#2176)
    * test: guard broken-symlink tests so the suite passes on Windows
    
    Four test cases create a dangling symlink with fs.symlinkSync() to exercise
    statSync catch branches, but did not guard for platforms where symlink
    creation is not permitted. On Windows without Developer Mode / admin rights,
    fs.symlinkSync throws EPERM, so these tests fail and `npm test` is red:
    
      - tests/ci/validators.test.js (Round 73, validate-commands skill entry)
      - tests/lib/session-manager.test.js (Round 83, getAllSessions)
      - tests/lib/session-manager.test.js (Round 84, getSessionById)
      - tests/lib/utils.test.js (Round 84, findFiles)
    
    Wrap each symlinkSync in try/catch and skip cleanly on failure, mirroring the
    existing convention already used in this repo (validators.test.js Round 57 and
    hooks/config-protection.test.js). On Linux/macOS and admin Windows the symlink
    still succeeds and the tests run unchanged; only the unsupported-symlink path
    now skips instead of failing.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    
    * test: only skip symlink tests on EPERM/EACCES, rethrow other errors
    
    Address CodeRabbit review: the catch blocks swallowed every error, which could
    mask a real test/setup failure as a false skip. Inspect err.code and only take
    the skip path for EPERM/EACCES (symlink creation blocked, e.g. Windows without
    Developer Mode); rethrow anything else so genuine failures still surface.
    
    Per the repo coding guideline: never silently swallow errors.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    
    ---------
    
    Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
  • fix(dev-server-block): stop blocking dev-<suffix> scripts (#2179)
    `DEV_PATTERN`'s trailing `\b` treats a hyphen as a word boundary, so
    `dev\b` matched the `dev` prefix of distinct npm scripts like
    `dev-setup` / `dev-docs` / `dev-build` and blocked them with exit 2.
    Replace the trailing `\b` with `(?![\w-])` so the dev server still
    matches (`dev`, `dev;`, `dev:ssr`) but `dev-<suffix>` scripts pass.
    
    Adds regression tests for dev-setup/dev-docs/dev-build (allowed) and
    dev:ssr (still blocked).
    
    Co-authored-by: bymle <229636660+bymle@users.noreply.github.com>
  • fix(session-end): preserve $-sequences in user messages when rewriting summary (#2180)
    The regenerated summary block embeds raw user-message text and was passed
    as the *replacement* argument to String.prototype.replace, where $-sequences
    ($&, $$, $`, $') are special. A user message containing $& re-injected the
    entire matched block (duplicating the summary markers) and $$ collapsed to $,
    silently corrupting the persisted session summary. buildSummarySection only
    escapes newlines and backticks, not $.
    
    Fix: use function replacers (() => summaryBlock) at both rewrite sites so the
    replacement text is treated literally. Adds an end-to-end regression test.
    
    Co-authored-by: bymle <229636660+bymle@users.noreply.github.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>
  • fix(observer): auto-scale max_turns by analysis batch size (#2062)
    * fix(observer): auto-scale max_turns by analysis batch size (#2035)
    
    The hardcoded default of MAX_TURNS=20 is insufficient when
    MAX_ANALYSIS_LINES=500 (also the default). Claude exhausts its turn
    budget before it can write all discovered instinct files, producing:
    
      Error: Reached max turns (20)
    
    Fix: when ECC_OBSERVER_MAX_TURNS is not explicitly set, compute
    max_turns proportionally to the actual analysis batch size:
      max_turns = clamp(analysis_count / 10, 20, 100)
    
    This gives:
      - 20–199 lines → 20 turns  (existing floor, unchanged)
      - 500 lines    → 50 turns  (resolves the reported failure)
      - 1000 lines   → 100 turns (cap)
    
    Explicitly setting ECC_OBSERVER_MAX_TURNS still overrides the
    auto-scaled value, preserving the existing escape hatch.
    
    * test(observer): update max_turns test for auto-scaling; document validation
    
    The max-turns budget test in tests/hooks/hooks.test.js still asserted the removed literal max_turns="${ECC_OBSERVER_MAX_TURNS:-20}", which would fail against the new auto-scaling logic. Assert the auto-scale formula and the 20/100 clamp bounds instead.
    
    Also add the explanatory comment CodeRabbit requested above the max_turns sanitization block, clarifying it guards the explicit ECC_OBSERVER_MAX_TURNS override path.
    
    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
    
    ---------
    
    Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
  • fix(.cursor/hooks): route block-no-verify through local hook to fix message-body false positives (#2107) (#2177)
    Cursor hooks still called `npx block-no-verify@1.1.2`, the broken external
    package whose matcher over-matches: it blocks legitimate `git commit`
    whenever `--no-verify` (or `no-verify`) appears anywhere in the command
    string, including inside the commit message body. The Claude Code surface
    already routes through the in-repo `scripts/hooks/block-no-verify.js`,
    which performs flag-position-aware tokenisation and passes 25 regression
    tests covering every false-positive case from #2107.
    
    Add a thin Cursor wrapper (`before-shell-execution-block-no-verify.js`)
    that reads Cursor stdin, transforms to the Claude Code `tool_input.command`
    shape, delegates to the local hook's exported `run()`, and forwards exit
    code and stderr. Update `.cursor/hooks.json` to call the wrapper instead
    of the npx package. New 14-case test file pins the false-positive cases
    from the issue plus the still-blocked real bypass attempts.
    
    Fixes #2107
  • fix(session-start): support ECC_SESSION_RETENTION_DAYS opt-out + document env var (#2151) (#2163)
    * fix(session-start): support ECC_SESSION_RETENTION_DAYS opt-out + document env var
    
    The retention pass for *-session.tmp files (issue #2151) landed previously,
    but the env var that controls it was undocumented in the README and rejected
    falsy values (0, off, disabled), silently falling back to the 30-day default.
    Users who want to keep all sessions for forensic or research workflows had no
    way to opt out.
    
    This patch:
    
    - Extends getSessionRetentionDays() so 0|off|false|disabled|never|none disables
      pruning entirely (returns null sentinel; default behavior unchanged).
    - Updates the call site in main() to skip pruneExpiredSessions when retention
      is null and emits a clear "[SessionStart] Pruning disabled via
      ECC_SESSION_RETENTION_DAYS" log line so the operator can tell pruning is off.
    - Documents ECC_SESSION_RETENTION_DAYS in the README "Hook Runtime Controls"
      section alongside the other ECC_SESSION_* knobs.
    - Adds three regression tests in tests/hooks/hooks.test.js covering opt-out
      via 0, opt-out via off, and garbage-value fallback to default 30.
    
    Verification:
    - node tests/hooks/hooks.test.js  — 240/240 green (incl. 3 new retention tests)
    - node tests/run-all.js           — 2622/2622 green
    - npx eslint scripts/hooks/session-start.js tests/hooks/hooks.test.js — clean
    - node scripts/ci/validate-no-personal-paths.js — clean
    - node scripts/ci/check-unicode-safety.js       — clean
    - node scripts/ci/validate-hooks.js — 28 matchers validated
    - node scripts/ci/validate-rules.js — 115 files validated
    
    Fixes #2151
    
    * docs(readme): list all ECC_SESSION_RETENTION_DAYS opt-out values + add Windows example
    
    Address reviewer feedback on PR #2163:
    - CodeRabbit and cubic both flagged that the README docs only listed 3 of 6
      opt-out values accepted by getSessionRetentionDays() (0, off, disabled),
      while the implementation also accepts false, never, none.
    - cubic also flagged the missing Windows PowerShell example for the new
      variable, breaking the parallel structure of the existing
      ECC_CONTEXT_MONITOR_COST_WARNINGS example block.
    
    Updated the README to:
    - Spell out all six opt-out values (0, off, false, disabled, never, none)
      and clarify they "keep all sessions (disable pruning)".
    - Add an ECC_SESSION_RETENTION_DAYS line to the Windows PowerShell example.
    
    No behavior change. README only.
    
    Verification:
    - npx markdownlint README.md — clean
    - npx eslint scripts/hooks/session-start.js tests/hooks/hooks.test.js — clean
  • feat(gateguard): add env knobs for routine bash gate + extra destructive patterns (#2161)
    * feat(gateguard): add env knobs for routine bash gate + extra destructive patterns
    
    The JS port of gateguard-fact-force has two bash gates: a destructive
    gate (rm -rf, drop table, git push --force, etc.) that operators want
    to keep, and a once-per-session routine gate that fires on the very
    first bash invocation regardless of intent. Operators on hosts where
    the routine gate is friction without signal (Cursor, OpenCode, etc.)
    have been maintaining local patches that get clobbered on every plugin
    update; the Python upstream gateguard-ai already exposes equivalent
    config via .gateguard.yml.
    
    Adds two env vars, both off-by-default so existing behavior is
    preserved:
    
    - GATEGUARD_BASH_ROUTINE_DISABLED — truthy values (1, true, on, yes,
      enabled) skip the routine bash gate. Destructive gate is unaffected.
    - GATEGUARD_BASH_EXTRA_DESTRUCTIVE — regex source string for additional
      destructive patterns. Matches against the same quote-stripped,
      subshell-flattened command the built-in DESTRUCTIVE_SQL_DD regex sees,
      so a custom phrase inside $(...) or backticks is also caught. A
      malformed regex is logged once to stderr and treated as not configured
      rather than crashing the hook (hooks must never block tool execution
      unexpectedly).
    
    Twelve new tests pin both env vars (truthy aliases, falsy values, unset
    baseline, destructive-gate-still-fires, alternation members, malformed
    regex degrades safely, custom phrase inside command substitution).
    Existing 2619/2619 tests still pass; eslint clean.
    
    Fixes #2078
    
    * fix(gateguard): reset extra-destructive warn-once gate when env value changes
    
    Both reviewers (CodeRabbit + cubic) flagged that
    extraDestructiveWarnLogged was never reset when GATEGUARD_BASH_EXTRA_DESTRUCTIVE
    flipped from one invalid regex to a different invalid regex. The
    sticky boolean meant a long-running process saw bad-pattern-a's
    warning then silently swallowed bad-pattern-b's parse failure.
    
    Fix: clear extraDestructiveWarnLogged whenever the cache key changes
    (i.e. before the regex compile attempt). The warn-once-per-distinct-
    pattern invariant now matches the per-key cache invariant.
    
    Adds a same-process regression test via loadDirectHook() that spies on
    process.stderr.write and asserts: same bad pattern warns once across
    multiple invocations; switching to a different bad pattern emits a
    second warning; switching to a valid regex emits zero warnings.
  • fix(suggest-compact): clean up old counter temp files (#2159)
    * fix(suggest-compact): clean up old counter temp files
    
    claude-tool-count-<sessionId> files were written into the OS temp dir
    on every hook run and never removed, accumulating one orphan per
    session indefinitely.
    
    Sweep stale counter files at the top of main() before opening the
    active counter. Retention is env-tunable via COMPACT_STATE_TTL_DAYS
    (default 14 days); invalid values fall back to the default. The
    active session's counter file is preserved unconditionally even if
    its mtime is past the cutoff. Failures during the sweep are swallowed
    to preserve the always-exit-0 hook contract.
    
    Adds 7 regression tests covering the sweep, env-var validation, and
    the always-exit-0 invariant under a populated temp dir.
    
    Fixes #2156
    
    * fix(suggest-compact): preserve counter files at the TTL cutoff boundary
    
    The cleanup sweep used `mtimeMs > cutoffMs` to short-circuit, which
    matched files whose mtime sits exactly on the cutoff boundary and
    deleted them. The cleanupOldCounters docstring promises only files
    *older than* retentionDays are removed; a file at age == retentionDays
    is not older than retentionDays, so it must survive.
    
    Switch the comparison to `>=` so only strictly older files fall
    through to deletion. Add a regression test that pins boundary-aged
    files (mtimeMs sitting just past the projected cutoff) are preserved.
    
    Refs #2156
  • fix(continuous-learning-v2): accept claude-vscode as valid entrypoint (#2134)
    The observe.sh Layer 1 entrypoint guard short-circuits with exit 0 when
    CLAUDE_CODE_ENTRYPOINT is not in {cli, sdk-ts, claude-desktop}. Claude
    Code's VS Code extension sets CLAUDE_CODE_ENTRYPOINT=claude-vscode, so
    VS Code users see no observations recorded — observations.jsonl never
    gets created and the instinct pipeline stays empty.
    
    Add claude-vscode to the allowlist, mirroring the precedent in #1522
    which added claude-desktop the same way.
    
    Add a regression test that spawns observe.sh under bash -x for each
    allowed entrypoint (cli, sdk-ts, claude-desktop, claude-vscode) and
    each denied entrypoint (unknown-host, claude-cody, mcp), asserting
    that allowed entrypoints reach Layer 2's ECC_HOOK_PROFILE check while
    denied entrypoints stop at Layer 1.
    
    Fixes #2102
  • 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