* fix(hooks,lib): fix hook detection and parsing edge cases
- auto-tmux-dev: dev\b -> dev(?![\w-]) so one-shot dev-build/dev-docs scripts
are not detached into tmux; align command shapes (yarn run dev, bun dev) with
pre-bash-dev-server-block.js DEV_PATTERN.
- pre-bash-commit-quality: skip obvious non-secret placeholders (env refs,
${...}, <...>, whitelisted tokens) in the api-key rule without suppressing
real high-entropy secrets; make -m message extraction quote- and
escaped-quote-aware so `-m "fix: \"x\""` / apostrophes are not truncated.
- pre-compact: annotate the CURRENT worktree's session (match **Worktree:** /
legacy **Project:**) instead of the newest *-session.tmp across all projects,
layered onto the LLM-summary flow from #2388; a present-but-blank Worktree
header is treated as non-legacy (no foreign project fallback).
- shell-substitution: stop double-appending a trailing backslash in an
unterminated backtick span.
- utils readStdinJson: on overflow, settle and resolve {} immediately (clear
timer + listeners) instead of waiting for end/timeout and parsing a partial
prefix; surface the overflow on stderr.
Regression tests added/extended (new tests/hooks/pre-compact.test.js).
Addresses review feedback on #2405. The earlier block-no-verify change was
dropped: its message-value skip on merge/cherry-pick/am/rebase would let
`git rebase -m --no-verify` bypass the hook (rebase's -m is the boolean
--merge), a false-negative worse than the contrived false-positive it fixed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(ci): align hook fixtures and drain oversized stdin
---------
Co-authored-by: djpjronline-netizen <276112803+djpjronline-netizen@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: haelyra <49814733+haelyra@users.noreply.github.com>
* fix: harden local data boundaries
Bind the capabilities dashboard exclusively to loopback and reject untrusted Host and Origin values. Constrain project-configured agent data paths to the Cursor data root, and harden lifecycle repair/uninstall operations against state-file traversal, symlink swaps, unsafe sources, and forged install-state destinations.\n\nCloses #2506
* fix: eliminate repair source read race
Read source bytes and mode from one no-follow file descriptor so a path replacement cannot mix metadata from one inode with content from another. Add a regression that rejects separate path-based source metadata lookup.
* fix: close dashboard hardening review gaps
Add a local-first, cross-harness memory vault with CLI and MCP surfaces, bounded search and storage, harness-scoped visibility, setup guidance, and comprehensive tests.
* fix(resolve-ecc-root): require ECC skills, not just scripts, before accepting a root (#2544)
resolveEccRoot() accepted a candidate root on script-only evidence
(scripts/lib/utils.js). A partial install that lands ECC's scripts into
~/.claude but not ECC's skills short-circuited at the standard-install
branch, so skill-resolving callers built skills/... paths against a root
where they do not exist and every command failed three layers away.
For the default probe (skill consumers, reached via INLINE_RESOLVE) a
candidate now qualifies only if it contains both the script tree and a
sentinel ECC skill; the same stricter check guards the plugin-root and
plugin-cache branches. An explicit caller probe is still honored exactly,
so script consumers (e.g. session-start-bootstrap, which probes for the
hook runner) are unaffected. Merely checking that skills/ exists is
insufficient — a user's own ~/.claude/skills/ can be present with none of
ECC's skills.
Adds a regression test for the exact partial-install scenario and updates
the resolver test fixtures to build complete roots.
* test(resolve-ecc-root): cover partial exact-plugin and cache roots; DRY skill sentinel (#2544)
Address CodeRabbit review on PR #2577:
- Extend #2544 regression coverage to the exact-plugin and versioned
plugin-cache branches, asserting the stricter both-sentinels predicate
rejects a scripts-only root there too (not only for ~/.claude).
- Extract the ECC_SKILL_SENTINEL constant in command-plugin-root.test.js
and reuse it at both fixture setup sites instead of duplicating the literal.
Expose the canonical Itō CLI's pinned sixtytwo node-qualification path through ECC with double opt-in, explicit node/config gates, credential isolation, and no new MCP or execution authority.
Validated across the full Linux, macOS, and Windows Node/package-manager matrix, hosted coverage, CodeQL, security, lint, and focused bridge tests.
* fix(suggest-compact): recognize large-window model families without a [1m] marker
resolveContextWindowTokens() only detected a 1M window via the env
override, the [1m] model-id marker, or observed tokens already above
200k. Large-window models whose ids carry none of these (e.g.
claude-fable-5) were misclassified as 200k windows, overstating
context usage ~5x in the compact suggestion.
Add a known-model-family substring table (claude-fable-5,
claude-mythos-5) checked after the env override and [1m] marker and
before the token-count heuristic. Env overrides still win, and unknown
model ids still fall back to the 200k default.
Closes#2461
* fix(suggest-compact): anchor known-model-family match at a token boundary
Unanchored substring matching would misclassify a hypothetical smaller
tier sharing a known family prefix (e.g. claude-fable-5-mini) as a 1M
window. Require the family id to end at a token boundary: end of id, a
delimiter, or a dated/versioned suffix (-20260115). Alphanumeric
continuations and letter suffixes no longer match.
Addresses CodeRabbit/Greptile review on #2468
Bun switched its default lockfile from the binary bun.lockb to the
text-based bun.lock, but detectFromLockFile() only ever checked for
bun.lockb — a project using modern Bun would never be detected as
using Bun at all.
Added bun.lock as the primary lockfile with bun.lockb kept as a
recognized legacy alias, so either format is detected correctly.
Also ignore stray bun.lock/bun.lockb at the repo root: yarn is this
repo's canonical package manager (package.json "packageManager"), so
a lockfile from someone running `bun install` locally shouldn't get
picked up by git status.
* fix: make the installer runtime pass strict supply-chain vetting
Remediate the four enterprise supply-chain vetting blockers from
affaan-m/ECC#2502 so the installer runtime (package.json + manifests +
scripts/lib/**) passes strict exact-pin evidence policy:
1. Remove the package.json `postinstall` lifecycle script (it only echoed a
post-install banner) and move that banner to an explicit opt-in
`npm run welcome` command. No install-time lifecycle script remains.
2. Exact-pin every dependency in package.json (dependencies + devDependencies)
to the versions already resolved in package-lock.json; no ^/~ ranges.
3. Replace non-ASCII characters on the installer runtime script/config surface:
em-dashes (U+2014) in scripts/lib/{path-safety,install-executor,
install/link-rewrite}.js comments and the two "Itô" (U+00F4) occurrences in
manifests/{install-components,install-modules}.json descriptions become
ASCII, so strict-surface Unicode scanners are clean.
4. Drop the bare `require("ajv")` from scripts/lib/install-state.js; the file
already carries a complete hand-rolled validator enforcing the same
schemas/install-state.schema.json (ecc.install.v1) constraints, so the
installer closure is dependency-free (zero non-builtin bare requires).
Refs affaan-m/ECC#2502
* fix: avoid unpinned welcome invocations
Signed-off-by: Samar Tomar <samar_tomar@hotmail.com>
* fix: validate translated skill frontmatter
Signed-off-by: Samar Tomar <samar_tomar@hotmail.com>
* fix: repair skill frontmatter YAML
Signed-off-by: Samar Tomar <samar_tomar@hotmail.com>
* fix: add MIT license to core skill manifests; pin verification-loop tsc invocation
* fix: preserve tsc/pyright exit status in verification-loop type-check (set -o pipefail)
* chore(deps): sync lockfiles with exact-pinned package.json
Regenerate package-lock.json and yarn.lock so the pinned dependency
specs are reflected in both lockfiles. npm ci and Yarn's --immutable
install now pass the sync check. The resolution tree is unchanged
(231 yarn resolutions, byte-identical set; zero npm transitive drift);
only the root descriptor strings move from ranges to the versions
already resolved in the committed lockfiles.
Addresses the Codex P1 on #2503.
---------
Signed-off-by: Samar Tomar <samar_tomar@hotmail.com>
Co-authored-by: Samarjeet Singh Tomar <samartomar@gmail.com>
* fix(project-detect): parse Python deps pinned with ~= and @ direct references
* test(project-detect): cover ~= compatible-release and @ direct-reference parsing
* test(project-detect): cover direct references
* fix(project-detect): skip bare VCS/URL requirement lines in getPythonDeps
A requirements.txt line like git+https://host/repo.git#egg=pkg carries
no leading package name, so the delimiter split recorded the whole URL
fragment as a dependency. Skip names that start with git+ or contain a
URL scheme, and tighten the test assertions so any leaked URL, scheme,
or @ delimiter fails loudly.
* fix(plan-canvas): stop dropping list items when a block's first item is over-indented
* fix: address greptile findings for PR #2501 - list-type detection and outdent nesting
- plan-canvas markdown: fix nested list rendering where outdented runs (indent
6→4) create duplicate sibling UL blocks instead of sharing parent (#2501)
- transcript-context: add LARGE_WINDOW_NATIVE_MODEL_IDS array for models whose
default context window is 1M but do NOT carry the [1m] marker (fixes#2497)
- Add test coverage for transcript-context and shell-substitution modules
- Add test coverage for project-detect module (#2498)
* fix(plan-canvas): handle outdented list runs
* fix(plan-canvas): start a new list when marker type changes at the same indent
CommonMark treats a marker-type change (bullet to ordered or back) at
the same indentation as the start of a new list. buildList previously
absorbed the run into the current list, so mixed runs rendered under a
single wrong tag. Stop the run on a tag change and let buildListBlock
render the next run as a sibling list with its own tag.
2026-07-17 17:10:58 -04:00
devin-ai-integration[bot]GitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>affaanDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* feat: add Plan Canvas - browser annotate-and-approve review for plan artifacts
- scripts/plan-canvas.js CLI (open/await/end/stop/server; bin ecc-plan-canvas)
- loopback server + ECC-styled chrome + annotation SDK + zero-dep markdown renderer
- Approve/Request-changes verdicts wired to the /plan confirmation gate
- plan-canvas skill, /plan-canvas command, SessionStart hook surfacing open reviews
- shared scripts/lib/loopback-guard.js extracted from control-pane (API re-exported)
- 121 new tests incl. full-workflow E2E; registered in manifests, catalog, registry
Inspired by lavish-axi (https://github.com/kunchenguid/lavish-axi) by @kunchenguid;
original ECC-native implementation, not a port.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor(plan-canvas): invoke via ecc-plan-canvas bin so the skill works from any project
Skill/command referenced a cwd-relative `node scripts/plan-canvas.js`, unusable
outside the ECC root. Switch to the ecc-plan-canvas bin (and $CLAUDE_PLUGIN_ROOT
fallback) and align CLI next_step hints so an agent can run it as a skill in any repo.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(plan-canvas): render Mermaid diagrams + ship Codex cross-harness surface
- markdown renderer emits <pre class="mermaid"> for ```mermaid blocks (source
entity-escaped so the browser decodes it for the renderer while blocking injection)
- artifact template loads a pinned Mermaid build only when a diagram is present,
themed to ECC dark, securityLevel strict, graceful offline fallback to source
(ECC_PLAN_CANVAS_MERMAID_URL overrides for a local mirror)
- skill teaches Mermaid-for-diagrams and states the CLI+JSON loop is harness-agnostic
- add .agents/skills/plan-canvas (Codex) with agents/openai.yaml interface manifest
- register in install-modules workflow-quality paths; docs updated
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(plan-canvas): add demo screenshot
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ci): sync yarn.lock with new bin; add contributor checklist
- yarn.lock records the ecc-plan-canvas bin so Yarn hardened-mode install no
longer wants to modify the lockfile on public PRs
- PR template + CONTRIBUTING gain a pre-push checklist covering the lockfile
trap and the full skill/command/CLI registration surfaces
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Haley Chen <2022hachen@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Rebuild of #2154 on current main, following the hermes/openclaw (#2433)
adapter recipe: 10-line kimi-project adapter (project kind, ./.kimi root),
registry + helpers ownership, SUPPORTED_INSTALL_TARGETS + legacy-compat
modules, kimi target on the 5 shared modules + .kimi in platform-configs
paths, both schema enums, npm files allowlist, help text, README stub.
Credit to @MoYiC6 for the original adapter design in #2154.
* feat: add Hermes and OpenClaw harness install targets
- Add scripts/lib/install-targets/hermes-home.js and openclaw-home.js
- Register new adapters in registry.js and helpers.js
- Add hermes/openclaw to SUPPORTED_INSTALL_TARGETS, schema enum, and install help text
- Add .hermes/ and .openclaw/ platform source directories with READMEs
- Update install-modules.json so rules/agents/commands/platform-configs cover the new targets
Verification:
- npx ecc doctor reports all 13 targets OK (including hermes-home and openclaw-home)
- install-targets regression guard passes for new targets
- Catalog check passes
* fix(install): register hermes/openclaw in install-modules schema enum + npm files allowlist
---------
Co-authored-by: Lxcardoza993 <265670745+Lxcardoza993@users.noreply.github.com>
Co-authored-by: Affaan Mustafa <me@affaanmustafa.com>
2026-07-03 22:20:17 -07:00
devin-ai-integration[bot]GitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>affaanDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>Affaan Mustafa
* fix(install): rewrite relative skill links for injected ecc namespace
Skill and rule markdown is byte-copied during a claude install, but the
home/project adapters inject an `ecc/` namespace segment
(skills/<id> -> skills/ecc/<id>, rules/<x> -> rules/ecc/<x>). Source-relative
links such as `../../rules/react/hooks.md` therefore broke after install: the
extra level changed what `../..` resolved to, and the link target itself moved
under rules/ecc/.
Rewrite relative links in namespaced markdown so they resolve to the file's
actual installed location, derived from the plan's own file placements (no
hardcoded namespace literal, so the links cannot drift from where files land).
Non-namespacing adapters and links to non-installed targets are left untouched;
URLs, anchors, absolute paths, and fenced code blocks are never rewritten.
Fixes#2340
* fix(install): keep non-namespaced markdown on the byte-for-byte copy path
Address review feedback: the markdown branch in applyInstallPlan diverted every
copy-file markdown operation through read+rewrite+write, so identity-mapped
markdown (source path == install path, no namespace injected) lost byte-for-byte
content and source mode bits even though no link rewrite was needed.
Gate the rewrite on isNamespacedSource() so only files whose install path
actually changed (e.g. skills/x -> skills/ecc/x) leave the copyFileSync path;
everything else is copied verbatim as before.
* test(install): emit failure stack in the link-rewrite test runner
Address review feedback: the local test() harness logged only error.message,
so a failing assertion lost its source line and diff. Print error.stack on
stderr on failure so broken rewrite cases stay diagnosable.
OpenCode ships override command files under .opencode/commands/ that
shadow the generic commands/*.md sources. The manifest install plan
recorded both writes against the same destination, so `ecc doctor`
reported perpetual drift for the 29 shadowed command files and `ecc
repair` "fixed" that phantom drift by copying the generic source over
the correct override, corrupting the installed command while never
clearing the warning.
Dedupe copy-file operations by destination in createManifestInstallPlan,
keeping the last writer to match the sequential apply order. install,
repair, and doctor all consume this one builder, so a fresh install is
clean and a single repair rewrites drifted state green.
Fixes#2414
2026-07-03 20:37:32 -07:00
devin-ai-integration[bot]GitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>affaanDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* fix(ci): resync lockfiles with package.json (eslint 10) + migrate yarn.lock to Yarn 4 format
package.json requires eslint@^10.6.0 but the committed locks pinned 9.39.2, so
npm ci aborted and Yarn 4 hardened mode rejected the stale v1-classic yarn.lock
(YN0028). Regenerate package-lock.json and rewrite yarn.lock in Yarn 4 (berry)
format so npm ci and immutable yarn installs both pass.
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* fix(ci): require clean probe exit for Windows shell/bash detection; add pyyaml dev dep
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* refactor: consolidate duplicated hook-root resolver into shared resolveEccRoot() (#2368)
The inline node -e resolver blob was duplicated ~60x across hooks.json,
command docs, and translations. Each copy inlined the full ~700-char
plugin-root search using a spread over nested array literals
(p.join(d,'plugins',...s) over [['ecc'],...]), which breaks Windows hook
execution due to shell quoting (#2368).
Collapse every copy to a 250-char locator that loads the committed
resolve-ecc-root module and delegates to resolveEccRoot() — no spread, no
nested array literals, no escaped double quotes. The real search logic now
lives in one tested module. Also route session-start-bootstrap.js through
resolveEccRoot() instead of its own duplicated reimplementation, and fix
the auto-update.md 'marketplace' (singular) typo along the way.
Guard tests updated: discovery behavior is asserted against resolveEccRoot();
the inline is asserted to delegate and to contain no Windows-fragile
constructs.
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* fix(resolve-ecc-root): restore full env-unset discovery in inline resolver
Address Greptile review on #2410: when CLAUDE_PLUGIN_ROOT is unset the
delegating inline could only load the resolver module from ~/.claude,
returning ~/.claude without ever reaching the plugin/cache search. Restore
the old inline's discovery breadth (exact plugin roots + versioned cache)
Windows-safely (no spread, nested arrays, or escaped quotes), then delegate
the authoritative decision to resolveEccRoot(). Add regression tests for
plugin-subdir and versioned-cache bootstrap with env unset.
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
---------
Co-authored-by: affaan <affaan@itomarkets.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Replace mechanical text extraction in session-end.js and pre-compact.js
with LLM-generated summaries using `claude -p`. Summaries now capture
design decisions, resolved bugs, changed files, and carry-over context
rather than just truncated user message snippets.
- Add scripts/lib/llm-summary.js: generateSessionSummary, extractConversationText,
getContextRemainingPct, getContextThreshold, getLLMModel
- Update scripts/hooks/session-end.js: trigger LLM when context < 20% or
every 50 messages (env-configurable via ECC_LLM_SUMMARY_*)
- Update scripts/hooks/pre-compact.js: generate LLM summary right before
compaction and write it to the active session .tmp file
- Add tests/lib/llm-summary.test.js: 18 unit tests
- Update tests/hooks/hooks.test.js: 3 integration tests for new behaviour
Recursion guard: sets ECC_SKIP_LLM_SUMMARY=1 in subprocess env so Stop
hooks fired by the claude -p subprocess do not re-enter summarisation.
Requires no ANTHROPIC_API_KEY — reuses Claude Code's own authentication.
Co-authored-by: Hiroshi Tanaka <hiroshi_tanaka@MBAM3.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- resolve-formatter: stop findProjectRoot walk before os.homedir() to
avoid mistaking global dotfiles (e.g. ~/.prettierrc) for a project root
- instinct-cli-projects: detect python3/python binary at runtime; skip
gracefully when Python 3 is unavailable instead of crashing with null status
- command-registry: regenerate COMMAND-REGISTRY.json (was stale)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds the Layer 4 observability view to the control pane: a self-contained,
dependency-free 3D point-cloud of the agent airspace (positions from the
proximity embedding, sized by working set, colored by collision risk, links
for converging pairs) plus an XSS-safe advisory panel that polls every 5s.
- proximity-viz.js: renderProximityVizHtml() (canvas projection, no external JS)
- server.js: GET /proximity (page) + GET /api/proximity (snapshot.proximity feed)
- test: asserts both routes serve and the feed carries positions/links/advisories
Finishes the steer/transmit loop — advisories now reach the agents' sessions.
- message-sink.js: createEccMessageSink() delivers via the canonical writer
'ecc-tui messages send' (maps steer/hold -> conflict kind, transmit -> query),
resolving the binary from override/env/built target/PATH. Injectable runner;
best-effort (a missing binary/failed send is counted skipped, never blocks).
- proximity.js: createProximityDispatcher() adds per-trigger cooldown so a
persistent collision fires once then stays quiet (agents get steered, not
spammed); runProximityTick() builds the snapshot and dispatches.
- scripts/proximity-tick.js: thin CLI — one-shot, --dry-run, --watch <sec>.
Messages are internal ECC agent-to-agent coordination, not any external channel.
- 14 new tests (sink argv/kind mapping, cooldown dedup, tick dispatch/dry-run,
CLI parse). Full suite 2891/2891; lint green.
- Line precision: parse git diff --unified=0 into per-file changed line ranges
(defaultWorkingSetFor), so two agents in the SAME file but DIFFERENT functions
no longer false-collide. Overlap channel now uses the overlap coefficient
(|A∩B|/min(|A|,|B|)) — high when one edit sits inside the other's region, low
for disjoint ranges; whole-file edit = 1. Docstring + design doc updated.
- Trigger firing: buildProximityTriggers() turns advisories into the concrete
messages — transmit-intent to both on a Traffic Advisory, steer-away to the
yielding agent + a hold notice on a Resolution Advisory. buildProximitySnapshot
now returns triggers; dispatchProximityTriggers(triggers, {sendMessage}) delivers
them through an injectable sink (the ECC messages table), best-effort.
- 12 new tests (line-range disjoint vs overlapping, parseDiffRanges, triggers,
dispatch). Full suite 2881/2881; lint green.
Turns live sessions into the airspace scan: each worktree session's git diff
becomes its working set, the dependency graph is built over the touched files,
and scanAirspace() produces the TCAS advisories + 3D positions.
- scripts/lib/control-pane/proximity.js: sessionsToAgents() + buildProximitySnapshot();
default working-set source shells `git diff --name-only <base>...HEAD` per
worktree (injectable for tests, fails closed to []).
- state.js: opt-in `proximity` field on the snapshot (includeProximity flag) so
the default hot path stays fast (git diffs only run when requested).
- 4 integration tests (same-file editors -> resolution, later agent steers,
<2 participants -> no advisories, labels). Full suite 2873/2873; lint green.
The moat layer: spatial deconfliction for multiple agents (and humans) on one
codebase, modeled on aircraft TCAS — measure how close two agents are in
code-space, then transmit-intent (Traffic Advisory) and steer-away (Resolution
Advisory) before they collide at the git layer.
scripts/lib/agent-proximity/:
- distance.js — the math: per-channel collision probabilities combined via
noisy-OR R = 1 - Π(1 - ω·r). Channels: edit overlap (file + line-range
Jaccard), dependency coupling (γ^(d-1) over the import graph, direction-
agnostic — catches 'edit there breaks here' even when tree-distant), and tree
proximity (LCA-based, soft prior). TCAS advise(): clear / advisory(transmit) /
resolution(steer), with deterministic right-of-way priority so the maneuver is
coordinated. closureRate() for approach-speed escalation.
- graph.js — lightweight require/import dependency-graph builder (fs or in-memory).
- index.js — scanAirspace(): pairwise advisories + 3D vector embedding (space-
filling path embedding pulled toward dependency neighbours) so a 'where are
the agents' visualization can render the file-cloud and watch agents crawl /
steer.
docs/design/agent-proximity.md — full mathematical formulation + protocol + viz
+ roadmap (v1 call-graph/symbol channels + live session-diff wiring; v2 cross-
machine airspace over Tailscale, the zero-conflict-swarm demo).
17 tests; full suite 2869/2869; lint green.
Critical: project-local install-state (e.g. a cloned repo's .cursor/ecc-install-state.json)
is attacker-controllable, and repair/uninstall/auto-update replayed its operations with
destinationPath validated only for non-emptiness — confirmed arbitrary file write/delete
and chained RCE (write ~/.bashrc, .git/hooks, or run a planted install-apply.js).
- New scripts/lib/path-safety.js: assertWithinTrustedRoot() canonicalizes (incl. symlink
escape via nearest-existing-ancestor realpath) and fails closed unless the destination is
within the adapter-derived trusted root.
- install-lifecycle.js: gate executeRepairOperation + executeUninstallOperation + the
install-state removal against record.targetRoot (the adapter-resolved root, NOT the
attacker-supplied state.target.root).
- auto-update.js: validateRepoRoot now requires package.json name to be an official ECC
package, so a planted nested repo can't drive auto-update into executing attacker code.
- 7 containment regression tests. Existing install-lifecycle/repair/uninstall/auto-update
suites still green (legit destinations are within the root).
The interactive claim/move buttons concatenated work-item ids into inline
onclick JS with only single-quote escaping — a crafted id (ids/titles come from
GitHub sync and manual upserts, not a strict allowlist) could break out and
inject script, even on the localhost-only server.
Fix: emit the id/lane in HTML-escaped data-* attributes (escapeHtml encodes
&<>"'), attach delegated click listeners that read them via getAttribute, and
pass the raw value as a JS string arg — never concatenated into code. Adds a
regression assertion that no inline onclick handlers with interpolated ids
remain. Flagged by automated security review.
Full suite 2845/2845; lint green.
The board was read-only; you can now drive the agent+human JIT workflow from the
local control pane.
- New shared scripts/lib/control-pane/work-item-mutations.js (claimWorkItem,
moveWorkItem) so the CLI and server never diverge; work-items.js claim now
delegates to it.
- server.js: gated POST /api/work-items/:id/claim and /:id/move (localhost-only,
honors --read-only with 403). Claim sets owner + assigneeKind and moves to
running; move retargets the kanban lane.
- ui.js: per-card Claim (on unassigned cards) + lane buttons that POST and
refresh; 15s live auto-refresh (paused when the tab is hidden).
- Tests: interactive claim/move endpoints, read-only 403, invalid-lane 400, and
snapshot reflects mutations.
Full suite 2845/2845; lint green.
The kanban board tracked lanes (ready/running/blocked/done) but not WHO owns
each card, which is the missing piece for agent+human just-in-time team workflows.
- state.js: classifyAssignee() labels each work item agent | human | unassigned
(session-linked or agent-pattern owners = agent; named owners = human; ownerless
= unassigned), with an explicit metadata.assigneeKind override.
- summarizeWorkItems(): adds an assignment summary {agent,human,unassigned} over
OPEN cards plus a priority-sorted needsAssignment queue — the JIT pickup list.
- ui.js: cards show an [agent]/[human]/[unassigned] badge; the board header shows
agent/human split and 'N need owner'.
- Tests: assignment classification + JIT queue coverage in control-pane-state.
Full suite 2839/2839; lint green.
- #2290 suggest-compact: honor ECC_CONTEXT_WINDOW_TOKENS / CLAUDE_CODE_AUTO_COMPACT_WINDOW
so 400k-window models (Opus 4.x) no longer report ~double context usage; add
override + isolation tests in transcript-context.test.js.
- #2282 install: bare-language syntax is legacy-only by design, but the error
now distinguishes a supported-but-wrong-mode target (gemini/codex/…) from a
genuinely unknown one and points to --profile/--modules/--skills.
- #2276 cost-report: the command + cost-tracking skill targeted a SQLite DB no
tracker writes. Repoint both at the real ~/.claude/metrics/costs.jsonl (JSONL,
estimated_cost_usd), reduce cumulative-per-session snapshots to latest-per-session,
and use node instead of sqlite3 for cross-platform support.
- #2272 gateguard: make the 'confirm no existing file' checklist item
tool-agnostic (Glob/Grep or find/grep via Bash) so hosts without a Glob tool
don't get a dead tool call.
Full suite 2839/2839; lint green.
- 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.
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>
- 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#2155Closes#2128
The store write was unconditional, persisting work items even during dry
runs. Move it inside the !dryRun block alongside editIssue and initialize
snapshot to null beforehand so results.push still receives snapshot: null
for dry runs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
applyPublish was forcing review='approved' for any state that wasn't
'changes-requested', bypassing policy.review.required entirely. Add a
guard that throws before buildIssueStateFromAction when review approval
is required but not yet granted.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove circular validation-status check in applyValidate that prevented
fresh claims (validation='pending') from ever reaching 'passed'
- Add staleCoordinationLabels helper to compute coordination:* labels to
remove on state transitions; replaces hardcoded removeLabels:[] across
all six editIssue call sites
- Fix duplicate label writes in applySync: syncIssueLabels already calls
editIssue for labels, so the follow-up editIssue now only updates body
- Skip acquireLock finding: store.acquireLock does not exist; comment
updated to explain why the fix was not applied
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
* 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>