The uninstall cases run two full CLI passes (install, then uninstall)
over several hundred files under a flat 30s timeout, which is tight
enough on Windows CI to fail intermittently with spawnSync ETIMEDOUT.
install-apply.test.js already scales its timeout by platform for the
same reason; match that precedent rather than re-running past the flake.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The guided installer asks how ECC hooks should run, but that consent
lived only in the wizard path. Running install-apply directly with a
profile that includes hooks-runtime still materialized the hook runtime
with no disclosure and no decision.
Gate the apply layer instead, so every entry point is covered:
- disclose the six hook capability groups when a plan would materialize
the hook runtime, and refuse to apply until the caller decides
- --enable-hooks confirms the hook runtime; --no-hooks installs the rest
of the selection without it and records the reduced module closure in
install-state
- surface the pending decision as a dry-run warning
- show the same capability disclosure in the guided installer's plan
preview, so the wizard's hook question states what it is asking about
Plans that never materialize hooks (Kimi, --profile minimal,
--without baseline:hooks) are unaffected and need no flag. Repair and
uninstall operate on already-recorded state and stay unchanged.
The capability taxonomy and the held-materialization behavior come from
Samarjeet Singh Tomar's PR #2634, reworked to fit the single-decision
consent model that shipped with the guided installer in #2649.
Co-Authored-By: Samarjeet Singh Tomar <samar_tomar@hotmail.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ecc memory writes and --body-file reads fail on Windows. sameFileIdentity()
compares the dev field of a path-based stat against a handle-based fstat, and
libuv 1.49.0 through 1.50.x resolve path-based stat() and lstat() on Windows
through GetFileInformationByName, which leaves the volume serial unset while
fstat() reports it. The comparison never matches, so the TOCTOU guard rejects
every operation.
Keep the inode strict and compare dev only when both sides report one. POSIX
always reports a non-zero dev, so the original strict behaviour is preserved
there.
Request the guard's stats as BigInt. On the affected libuv versions dev is 0,
which leaves the inode as the only identity signal, and Windows file IDs run
past Number.MAX_SAFE_INTEGER where two distinct files can collapse to the same
number-valued inode.
Fixes#2626
main has been red since the Itô skill series landed. Two independent
problems, both in test files rather than shipped behavior:
- tests/ci/ito-inference-skill.test.js asserted a stale copy of the
capability:ito-compute description. #2706 added device revocation to
the lifecycle and updated manifests/install-components.json, but this
expectation was not updated with it. The manifest is the shipped
artifact, so the test expectation is what was wrong.
- three ito test files matched YAML frontmatter indentation with two
literal spaces inside a regex literal, which trips no-regex-spaces.
Replaced with an explicit ` {2}` quantifier, which matches identically.
The basket-compare occurrence was not visible in CI: npm run lint is
`eslint . && markdownlint ...`, so ESLint reported only the first file
and stopped. Fixing only what CI printed would have left main red on the
next run. The markdownlint half of that chain had therefore never
executed; it passes.
Verified on this branch: full suite 3707/3707, repo-wide ESLint clean,
and markdownlint clean under the exact CI glob.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix: ship new Ito skills through install manifests
* ci: audit shipped dependencies separately from tooling
* test(release): pass previous version to heading helper
release.test.js only greps release.sh for one of the five
update_latest_release_heading call sites, and plugin-manifest.test.js
only checks the headings committed today. Neither executes the rewrite,
so a helper that silently no-ops on a missing heading would ship green.
Extract the embedded node program from release.sh and run it against
fixtures to pin the fail-closed contract: bump stable and prerelease
headings, leave the rest of the file untouched, and exit non-zero
without writing when no heading matches. Also pin all five call sites so
the docs/zh-CN/README.md regression cannot recur.
Runs standalone via node tests/scripts/release-heading.test.js.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The Stop hook formats every JS/TS file edited during a response, grouped by the project root each file happens to sit in. That includes trees under .claude/plugins, which are third-party checkouts we only read.
Formatting them writes to code the user does not own. It also does real damage when a repo's committed code has drifted from its own formatter config: the rewrite is not a no-op but a wholesale reformat, so an unrelated bugfix ends up carrying hundreds of untouched lines. I hit this contributing to this repo — a 162-line fix arrived as a 478-line diff, most of it reformatted code the change never went near.
Skips both the user-level install root and a project-local one, mirroring the lookup in scripts/harness-audit.js. Paths are resolved before the prefix comparison, and a sibling such as .claude/plugins-backup does not match. The user own .claude config outside plugins is still formatted.
Adds 7 tests for the predicate, plus an end-to-end check that a clone file listed in the accumulator is left byte-identical. Suite 16 to 23.
Co-authored-by: haelyra <49814733+haelyra@users.noreply.github.com>
isCommitNoVerifyShortFlag anchored on the first character, so it only recognised the flag when it led the cluster. Git clusters short options, which means git commit -an is -a plus the bypass flag and skips the hooks. -sn and -vn slip through the same way, while -na and -nm are caught — the difference is position, not intent.
Scanning now walks the cluster and stops at a value-taking option, since that option swallows the rest as its inline value. The n in -mn stays message text, and the existing -tn case keeps working.
Adds 4 tests: the three clustered forms that were escaping, plus -mn to pin the inline-value boundary. Verified the three fail against current main. Suite 25 to 29.
Co-authored-by: haelyra <49814733+haelyra@users.noreply.github.com>
* fix: exclude ECC skills from antigravity install target
* test(install): cover antigravity skills exclusion
Two tests encoded the collision the parent commit fixes.
install-manifests used skills/example as its example of a supported
antigravity path; it now asserts skills are filtered and uses
commands/example for the positive case, so the test still proves
supported paths survive filtering.
install-apply asserted .agent/skills/tdd-workflow/SKILL.md exists. That
directory is antigravity's agent directory and already receives ECC
agents/, so the assertion was pinning ECC skills and ECC agents to the
same destination. Inverted, with the reason recorded inline.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Calum Reeves <reevesc88@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix(continuous-learning): cluster instincts by keyword overlap in /evolve
`cmd_evolve` grouped instincts by exact string equality of the whole
normalized trigger sentence. Triggers are free-form sentences, so every
instinct landed in its own bucket and `skill_candidates` was always empty.
`agent_candidates` is derived from `skill_candidates`, so agents never
generated either — `/evolve --generate` could only ever emit commands.
Measured on a 42-instinct project: 42 instincts produced 42 unique cluster
keys, largest cluster size 1.
Group on keyword overlap instead. Jaccard is the wrong metric here — trigger
keyword sets average ~7 words, so even clearly related pairs top out around
0.33 — so this uses the overlap coefficient (shared / smaller set) at 0.5,
plus a floor of 2 shared keywords so one incidental word cannot pull
unrelated instincts together. The same 42 instincts now yield 4 clusters.
Also unify the command/agent slug used by the preview and the writer. The
preview called `.replace('a ', '')`, which strips "a " anywhere in the
string, mangling "extracting data from Reddit" into
`/extracting-datfrom-R` while `--generate` wrote `extracting-data-from.md`.
Both paths now share `_evolved_command_name()` / `_evolved_agent_name()`.
Adds tests/scripts/instinct-cli-evolve.test.js, which fails on the previous
implementation (0 clusters instead of 1; preview name `extracting-datfrom-R`)
and covers the negative cases so unrelated triggers still stay apart.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs(continuous-learning): correct clustering metric name in docstring
The docstring said "Jaccard" while the implementation uses the overlap
coefficient, which is the point of the change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(continuous-learning): generate every evolve candidate and cut slugs on word boundaries
_generate_evolved() wrote only skill_candidates[:5], workflow_instincts[:5]
and agent_candidates[:3]. On a project with 36 command candidates that meant
5 files and no warning, so the output read as complete while 86% of the
candidates were dropped.
Generation is now unbounded by default and takes a --limit N flag for callers
that want a cap. A cap that truncates says so:
Note: writing 3 of 36 command candidates (--limit 3); 33 skipped.
The analysis preview keeps showing five per kind but now names the remainder
("... and 31 more command candidates not shown") instead of presenting a
sample as the whole set.
Slugs were also cut with a hard slice, which split words mid-token and
produced /investigating-comple, /learning-about-compl and
/researching-mechanis. _truncate_slug() retreats to the last separator that
fits, and keeps the full head when the cut already lands on one, so
"analyzing large text files" stays /analyzing-large-text rather than losing
a word. A first word longer than the limit still falls back to a hard cut
because no boundary is available.
Shorter slugs collide more easily, and a collision used to mean one file
silently overwriting another. _assign_unique_slugs() suffixes duplicates
(-2, -3) and is called by both the preview and the writer over the same
ordered list, so advertised names and written names cannot drift apart.
Skill directory naming moved to _evolved_skill_name(); it previously used its
own inline slug expression, so it was the one truncation the shared helper
did not cover.
Adds tests/scripts/instinct-cli-evolve-generate.test.js: 7 cases covering
word-boundary cuts, the separator-aligned cut, unbounded generation, --limit
reporting, collision dedup, preview remainder and preview/writer agreement.
Six of the seven fail against the previous implementation.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: haelyra <49814733+haelyra@users.noreply.github.com>
Address CodeRabbit review on #2311: dedupe the local test(name, fn)
harness and route all reporter output through a shared helper
(tests/lib/helpers/mini-test-runner.js) instead of direct console.log.
The auto-update command's inline ECC_ROOT resolver delegates to
resolveEccRoot() with the default probe (scripts/lib/utils.js). A
hooks-runtime-only install copies scripts/lib/ into ~/.claude, so the
partial install satisfies the probe and shadows the full plugin root
under ~/.claude/plugins/marketplaces/. The command then fails with
MODULE_NOT_FOUND because ~/.claude/scripts/auto-update.js does not
exist.
Pass {probe: scripts/auto-update.js} so the resolver only accepts a
root that actually contains the script the command runs. Applied to
the command doc and its ja-JP/zh-CN translations, with regression
tests for both the resolver behavior and the embedded snippets.
* fix(mcp): accept reserved _meta field in tools/call params
The memory MCP server rejected any tools/call whose params contained a key
other than name/arguments, returning -32602 "Unknown or missing memory tool."
MCP clients (e.g. Claude Code) attach the spec-reserved `_meta` field
(such as progressToken) to request params, so every tool call from a
compliant client failed and the entire memory MCP surface was unreachable —
even though initialize/tools-list and the `ecc memory` CLI kept working.
Per the MCP base protocol, `_meta` is reserved for request metadata and
must be accepted. Add it to the params key allowlist.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(mcp): validate _meta shape and cover tools/call param allowlist
Address CodeRabbit review on #2670:
- Validate params._meta when present: accept metadata objects, reject null,
arrays, and scalar values (reuses isRecord). Keeps _meta optional and
preserves existing name/arguments/unexpected-key rejection.
- Add regression tests: accept _meta with progressToken, reject malformed
_meta values, and continue rejecting unrelated top-level params.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix: add claude-opus-5 to KNOWN_MODEL_WINDOW_TOKENS
claude-opus-5 has a 1M context window (verified: 250k tokens at 25%
usage = ~1M), but was missing from the model table. This caused
resolveContextWindowTokens() to fall back to the 200k default when
tokens < 200k, incorrectly triggering compact warnings in the first
20% of a 1M session.
Same failure class as #2290 (Opus 4.x) and #2461 (fable-5/mythos-5).
The env override (ECC_CONTEXT_WINDOW_TOKENS) remains the escape hatch
for unlisted models.
Refs: #2290, #2461, #2468
* test: add regression test for claude-opus-5 context window
Verifies resolveContextWindowTokens returns LARGE_CONTEXT_WINDOW_TOKENS
for claude-opus-5 at 50k tokens, matching the behavior of fable-5 and
mythos-5 in the known-model table.
* 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>
Claude Code writes one transcript JSONL line per content block, so a
single API response (one message.id) spans multiple assistant lines that
each repeat the same message.usage. sumUsageFromTranscript summed every
line, inflating token totals and estimated_cost_usd roughly 2.5-3x.
Verified on a real session: 704 assistant lines but only 286 unique
message.ids (2.46 lines/response on average); line-summing reported
$866.52 while the deduped total is $332.62. Usage payloads are identical
across lines of the same id (0/286 varied), so counting once per id is
equivalent to taking the last line per id.
Fix: collect usage into a Map keyed by message.id (last line wins) and
sum unique entries. Lines without a message.id (older transcript shapes)
keep the previous per-line behavior via a synthetic key, so existing
tests and old transcripts are unaffected.
Adds a regression test: a response split into 3 content-block lines with
the same message.id is counted exactly once.
Note: rows already written to ~/.claude/metrics/costs.jsonl by the old
code carry inflated token counts and estimates (except rows whose cost
came from the harness-cost cache, where cost is authoritative but token
counts are still inflated). Downstream consumers may want to annotate
history; this change intentionally does not rewrite the raw log.
Co-authored-by: Claude Fable 5 <noreply@anthropic.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.
Honor an explicit non-git CLAUDE_PROJECT_DIR as an isolated project scope, canonicalize and hash it consistently in the shell observer and Python CLI, and preserve the global fallback for arbitrary non-git working directories.
Preserve complete Stop-hook stdout through lifecycle wrappers, wait for queued output to flush before exiting, bound child output with a larger explicit buffer, and add end-to-end regressions for large, multibyte, dry-run, and failure cases.
* 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.
Replace the generated Itō SVG wordmark with the supplied transparent monogram assets, keep the exact white-and-gold mark for dark mode, add a same-geometry light-mode variant, and refresh the dependency lock entry flagged by CI.
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.