Prevent stale /tmp/harness-cost cache files from affecting Sonnet 5, dated,
near-miss, and cache-rate pricing tests by using Date.now() in each session ID.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
cost-estimate.js carries its own copy of the stale Opus/Haiku/Sonnet rate
table already reported in #2574, but grepping every .js/.json/.md file
outside node_modules turns up zero callers besides its own test. It was
added in 940135e alongside the statusline observability hooks and never
wired into any of them.
The maintainer's comment on #2656 named two acceptable outcomes: remove
the unused duplicate, or share one rate source with the live tracker.
cost-tracker.js's own fix (#2574) has not landed yet, so sharing its
table now would import numbers that are still wrong. Removing the dead
file is the smaller, immediately-correct step.
Fixes#2656
cost-tracker.js: RATE_TABLE priced all Opus models at the legacy $15/$75
tier and routed Fable/Mythos 5 to Sonnet rates, overstating Opus 5
sessions ~3x and understating Fable ~3.3x in costs.jsonl. Adds fable
($10/$50) and current opus ($5/$25) tiers, keeps Opus 4.0/4.1/3 on the
legacy tier, updates haiku to 4.5 pricing ($1/$5).
session-end.js: extractSessionSummary included local-command echoes
(<local-command-caveat>, <command-name>, <local-command-stdout>),
system reminders, tool_result carrier turns and isMeta entries in the
Tasks list, so SessionStart reloaded noise instead of user asks. Adds
a noise filter.
Both test suites pass (10/10 cost-tracker, 1/1 session-end).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Curated skill delegating to the canonical tasteforge package in
Ito-Markets/ito-video: taste interviews, style-pack validation, offline
distillation with measured grounding, deterministic cadence application to
local footage, EDL/FCPXML export, and generated-media provenance audits.
Provider (Fal) generation requires explicit separately authorized execution
and fails closed in ECC; local references never mean a saved provider
workflow. Registered in the opt-in media-generation install module, npm
files, and catalog counts via scripts/ci/catalog.js. Contract tests cover
frontmatter/triggers, the fail-closed boundary, manifest and npm-packed
discoverability (real tarball check opt-in via ECC_TEST_NPM_PACK=1).
Promote the self-hosted models and custom endpoints content out of a
collapsed <details> in Advanced Install Options into its own top-level
section higher in the page.
Add the star-history trending and global rank badges above the language
selector.
Replace the star-history chart embed with a locally generated SVG. The
upstream api.star-history.com/svg endpoint returns a "GitHub restricted
access to star data" placeholder since GitHub limited the stargazers API
on 2026-06-30. Chart covers the first 40,000 stars and is labeled as such;
source data included for reproducibility.
Use no-follow file descriptors for legacy Codex snapshots, verification, restoration, and marker cleanup. Quarantine candidate removals and verify inode identity before deletion.
Carry the lifecycle runner as a verified artifact so privileged release workflows never dynamically check out and execute an output-selected revision.
* feat: add thin Pi adapter mounting ECC's canonical skills and commands
Adds first-class Pi (@earendil-works/pi-coding-agent) support as a thin
adapter layer, following the maintainer review on #2352. ECC's canonical
assets stay the single source of truth: nothing is copied or generated
under .pi/.
The `pi` manifest in package.json points Pi directly at `skills/` and
`commands/`. No transformation is needed — ECC's SKILL.md files already
follow the Agent Skills standard Pi implements, and ECC's command
frontmatter is already Pi's prompt-template format.
.pi/extensions/index.ts is the only adapter logic. It:
- uses Pi's documented `pi.on(...)` lifecycle, not an undocumented event bus
- resolves hook scripts from the installed package via `__dirname`, never
`process.cwd()`, so global installs work from any project directory
- runs hooks with `execFile(process.execPath, [...])` and no shell, so paths
containing spaces or shell metacharacters are safe
- invokes hooks through ECC's own `run-with-flags.js`, so `ECC_HOOK_PROFILE`
and `ECC_DISABLED_HOOKS` keep gating hooks under Pi
- runs hooks in the user's project directory so project detection stays
correct, while resolving the scripts themselves package-relative
- injects the SessionStart hook's `additionalContext` into the system prompt
on the next `before_agent_start`
- isolates hook failures behind a timeout and an output limit
- registers `/ecc-doctor` for install diagnostics
Registers `.pi` in the platform-configs install module and adds a Pi row to
the harness adapter compliance matrix.
Verified against Pi 0.84.1: a global `pi install` exposes 285 skills and 94
commands resolved from `skills/` and `commands/`, plus `/ecc-doctor`, with
no generated copies.
Scope deliberately excludes subagents, chains, approval gates, todos,
profiles, and MCP; ECC works in Pi without any companion package.
* fix: address review findings on the Pi adapter
Bot review on #2759 surfaced two real runtime defects and several
hardening gaps.
Runtime fixes:
- Attach an `error` listener to the hook child's stdin. `stdin.end()`
writes asynchronously, so a hook that exits, short-circuits, or is
killed by the timeout before reading the payload raises EPIPE as an
`error` event that the surrounding try/catch cannot see. Unhandled,
that event would terminate the Pi session and break the isolation
guarantee the adapter documents.
- Clear `pendingContext` at the top of the `session_start` handler. Pi
can start a new session (/new, /resume, /fork) before
`before_agent_start` consumes the previous value; if the newer hook
then failed, the next agent start received context describing a
different session's project state.
- Replace `require.resolve` companion detection with a read of Pi's own
`packages` list, honoring `PI_CODING_AGENT_DIR`. Pi installs packages
under its config directory, which is not on Node's module resolution
path from the extension, so the previous check reported every
companion as missing no matter what was installed.
Compliance matrix: remove internal semicolons and a trailing period from
the Pi record's list entries. The renderer joins entries with "; ", so
those characters split one entry into several in the rendered cell.
Tests: run profile gating against the temp skeleton instead of the real
checkout so it cannot leave marker artifacts behind; count files under
.pi/ by walking disk rather than git, so untracked copies cannot bypass
the regression guard; allow negated phrasing in the README heuristic;
pin the adapter's real parser guards with source assertions so the local
mirrors cannot silently diverge; add coverage for EPIPE isolation, stale
context clearing, and companion detection.
* docs: point users at existing companion Pi packages instead of bundling them
Every capability listed as out of scope is already provided by a maintained
community Pi package: pi-subagents, @juicesharp/rpiv-ask-user-question,
@juicesharp/rpiv-todo, and pi-mcp-adapter for MCP.
Pi supports pulling other pi packages in via dependencies plus
bundledDependencies, but this adapter deliberately does not. Bundling would
ship third-party code that executes with full user permissions in every ECC
install, turn optional capabilities into mandatory ones, and add four
fast-moving pins to maintain.
Instead /ecc-doctor now prints the exact `pi install npm:<name>` command for
each companion it does not find, so adopting one stays a deliberate user
choice.
Also corrects the MCP claim: Pi core has no MCP surface by design, but the
community pi-mcp-adapter package adds one. This adapter neither installs nor
verifies it, and ECC's MCP reference configs are not known to be compatible.
* docs: ECC's MCP configs work in Pi through pi-mcp-adapter, verbatim
Tested rather than assumed. The community pi-mcp-adapter package reads the
standard mcpServers format from .mcp.json and ~/.config/mcp/mcp.json, which
is exactly the format ECC already uses in .mcp.json and
mcp-configs/mcp-servers.json.
Verified against pi-mcp-adapter 2.21.2 in an isolated PI_CODING_AGENT_DIR:
copying mcp-configs/mcp-servers.json to a project's .mcp.json registers Pi's
`mcp` tool and `/mcp` command with all 35 ECC servers discovered, coexisting
with this adapter's /ecc-doctor. No translation layer and no ECC change are
needed, so this stops being a limitation and becomes documentation.
Recorded caveats: the adapter's first run against a new config performs
initialization that blocks in non-interactive mode, and only discovery was
verified, not live tool invocation.
ECC still neither installs nor depends on the package.
* feat: inject ECC's canonical engineering rules into Pi's system prompt
ECC's rules were the one durable asset the adapter did not deliver: skills
and commands reached Pi in full, but the 122 rule files that carry ECC's
coding style, testing, security, git workflow, and code-review standards
did not, so ECC in Pi was a library of skills rather than a set of
enforced standards.
Rules are read at runtime from the canonical rules/common/ directory of
the installed package and appended to the system prompt inside an
<ecc-engineering-rules> block. Nothing is copied or generated under .pi/,
which keeps the single-source-of-truth constraint this PR exists to
satisfy. Injection reuses the before_agent_start path already built for
session context, so no new lifecycle mapping is introduced.
Rules are re-applied every turn because they are standing policy, while
the session context stays one-shot and is consumed on first use.
agents.md, hooks.md, and performance.md are excluded: they describe Claude
Code primitives Pi does not have (Task/TodoWrite delegation, Claude hook
event types, thinking-budget toggles), so injecting them would point the
model at tools that are not there. A test asserts they stay excluded, and
a leakage test asserts none of those primitives appear in the injected
text. Language-specific rules under rules/<language>/ are out of scope for
this first adapter.
Injection is bounded by MAX_RULES_BYTES and can be disabled with
ECC_PI_RULES, following ECC's existing off-switch convention. /ecc-doctor
reports the state and injected size.
Measured on this repo: 7 files, 12,361 characters, roughly 3k tokens.
Also replaces a Function() call in the test helper with direct arithmetic,
and repins a stale assertion that pinned one spelling of the context
handoff rather than the guarantee (read before clear, clear before return).
* fix: /ecc-doctor misreported filtered packages and partial rule installs
Two reporting defects in /ecc-doctor, the command whose whole job is telling
a user what is actually installed.
Pi's settings accept a `packages` entry in two shapes: the bare source string
("npm:pi-subagents") and an object carrying that source alongside resource
filters ({ source: "npm:pi-subagents", skills: [] }). normalizePiPackageName
only recognized the string, so a user who narrowed which resources a companion
contributes was told the companion was not installed, along with an install
command for something already present. The source type still decides whether a
name is comparable, so an object wrapping a git source or a path stays
unrecognized exactly as before.
loadPortableRules drops rule files it cannot read, drops empty ones, and stops
at MAX_RULES_BYTES, but describeRulesStatus reported PORTABLE_RULE_FILES.length
regardless. A partial install that loaded 3 of 7 files reported "7 rule file(s)"
to the one command a user runs to find a partial install. The loaded count is
now tracked next to the cache and reported as a ratio, with the shortfall named.
Also reconciles the Notes bullet in .pi/README.md, which still called MCP out of
scope after the MCP section landed documenting that ECC's configs load in Pi
through pi-mcp-adapter.
Both defects were reported by CodeRabbit and verified against Pi's own
packages.md before fixing. Adapter tests go from 24 to 26; the two source
contracts that pinned the previous spellings now pin the new guards, so the
object-form unwrapping and the loaded-count reporting cannot be silently
reverted.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(continuous-learning-v2): warn when the observer never survives a hook invocation (#2489)
The observer is lazy-started from a hook process that exits immediately
afterwards. start-observer.sh's liveness check runs inside that still-living
process tree, so it always sees a healthy observer and prints "Observer
started (PID: N)". On native Windows (Git Bash/MSYS2) the reap happens later,
when the hook's Job Object closes, so no self-check placed in
start-observer.sh can ever observe the failure.
The next hook invocation is the only place the death is visible, and
_CHECK_OBSERVER_RUNNING already found it there -- then discarded it, deleting
the stale PID file and restarting silently, once per tool call, forever. Users
were left with an observer-start.log full of success lines and an observer
that never completed a single analysis cycle.
Record the "well-formed PID that is no longer alive" case, count consecutive
non-survivals in ${PROJECT_DIR}/.observer-nosurvive-count, and log one
explanatory warning when the streak reaches ECC_OBSERVER_NOSURVIVE_WARN_AFTER
(default 3). Warning fires on equality so a persistent failure logs once per
streak rather than once per tool call; finding the observer alive resets the
streak. The Windows-specific explanation is gated on uname so Linux/macOS
users are pointed at observer.log instead of a wrong diagnosis.
Counting happens in the caller, not inside _CHECK_OBSERVER_RUNNING, because
that function is invoked once per PID file and again under the start lock.
The PowerShell backgrounding rewrite is deliberately not included: it cannot
be exercised on a non-Windows machine, and untested process-spawning code is
a worse outcome than an accurate diagnostic.
* docs(continuous-learning-v2): state observer platform support and the new warn threshold
The observer's Windows limitation was only discoverable by hitting it. Record
it next to observer.enabled, where it is read before the flag is set, and
document ECC_OBSERVER_NOSURVIVE_WARN_AFTER so the knob added alongside the
warning does not repeat the undocumented-env-var problem tracked in #2573.
zh-TW is intentionally left alone: translation parity is not enforced here and
the repo rejects blind translation imports without translator review.
* fix(continuous-learning-v2): serialize the non-survival streak under the lazy-start lock
observe.sh runs on every tool call, so the streak read-modify-write could race
between concurrent invocations -- losing an increment or logging the warning
twice. That is the same class of bug the signal counter hit in #2296, and this
repo's rule is to never fall back to an unlocked read-modify-write.
Rather than add a second lock, move the increment into _START_OBSERVER_LOGGED.
All three of its call sites already run inside the lazy-start lock
(flock / lockfile / mkdir), so the update is serialized with no new machinery.
Counting at the restart instead of at detection also means N racing hooks
record one death rather than N.
The reset stays in the caller: it is an idempotent unlink, not a
read-modify-write, so it needs no lock.
Adds a regression case pinning the increment inside _START_OBSERVER_LOGGED and
asserting all three call sites remain locked.
* fix(continuous-learning-v2): harden the non-survival threshold and warning output
Three review findings on the #2489 diagnostic:
- An all-zero threshold silently disabled it. `00` passes a digits-only check
but compares as zero, and the streak only grows, so the warning could never
fire. Normalize with base-10 arithmetic and fall back to the default for
anything below 1. Base 10 is forced explicitly because a leading zero would
otherwise be read as octal, and `08` is an arithmetic error that would abort
the hook under `set -e`. The same normalization now guards the streak read.
- An unwritable log silently swallowed the diagnostic. Build the message once
and fall back to stderr when the append fails. This cannot spam: the block
runs once per streak, not once per tool call. The counter write keeps its
`|| true` -- observe.sh runs on every tool call and the repo rule is that
hooks exit 0 on non-critical errors, so a full disk must not break tool use.
- The live-PID test fixture used process.pid, which is 1 in a container and is
deliberately rejected by _CHECK_OBSERVER_RUNNING; the reset case would then
fail for the wrong reason. Use a spawned child and clean it up.
Adds a regression case for the all-zero threshold. Verified on bash 3.2 (the
macOS CI runner shell) as well as bash 5.
* fix(continuous-learning-v2): warn only on a persisted streak increment
If the counter write fails, the file stays below the threshold, so every later
hook invocation rereads it, re-increments in memory, hits the equality check
and warns again -- turning the once-per-streak diagnostic into once-per-tool-
call spam. That is worse in exactly the case the stderr fallback added in the
previous commit was meant to cover, since a disk that cannot take the log
usually cannot take the counter either.
Gate the warning on the write succeeding. The write stays non-fatal: it runs
as an `if` condition, so `set -e` is satisfied and an unwritable counter costs
a delayed diagnostic rather than a broken tool call.
Tests: an unwritable counter must stay silent across repeated invocations while
the hook still exits 0, and a leading-zero threshold ("08") must be read as
decimal -- "00" alone did not exercise the base-10 conversion, since it is zero
either way.
---------
Co-authored-by: haelyra <49814733+haelyra@users.noreply.github.com>
* fix(skill-evolution): wire Skill PostToolUse tracker so skill-health shows real runs (#2463)
recordSkillExecution() had no production callers, so
~/.claude/state/skill-runs.jsonl was never written and
`scripts/skills-health.js --dashboard` always reported 0 runs.
Adds scripts/hooks/skill-run-tracker.js and registers it as an async
PostToolUse hook (matcher: Skill) in posttooluse-dispatcher.js, which is now
the single PostToolUse entrypoint on main.
Addresses the privacy and bounds review on #2555's sibling PR:
- No prompt text is persisted. task_description is synthesized as
"Skill invocation: <skill_id>"; tool_input.task_description/description/
prompt are never read.
- Every persisted string is bounded and charset-restricted. A skill id is an
identifier, so free text, newlines, or an over-long value are dropped rather
than truncated and written through.
- The JSONL sink is created 0600 and re-tightened on each append, repairing
files written before this bound existed.
- The sink is capped at MAX_RUN_RECORDS (5000), trimmed oldest-first, so the
append-only file can no longer grow without limit.
Tests cover the privacy guarantee (no prompt text reaches a record), the
identifier bounds, the file mode on POSIX, and the retention cap.
* fix(skill-evolution): re-register the tracker for PostToolUseFailure
The rebase onto current main dropped the hooks.json entry, which silently
resurrected the P1 from the earlier review round: deriveOutcome() still
branches on hook_event_name === 'PostToolUseFailure', but the PostToolUse
dispatcher does not fan that event out, so the branch was unreachable in
production. Hard Skill failures were dropped from telemetry entirely, which
inflates the dashboard success rate — the opposite of what #2463 asks for.
Restores the dedicated PostToolUseFailure entry (matcher Skill, id
post:skill:track, same run-with-flags wrapper and standard,strict gating as
the dispatcher registration). Verified end-to-end: a PostToolUseFailure
payload piped through run-with-flags now records outcome "failure".
Adds a regression test asserting the registration so a future rebase cannot
quietly drop it again.
---------
Co-authored-by: haelyra <49814733+haelyra@users.noreply.github.com>
* fix: disable Claude co-author attribution by default
* fix: harden default co-author opt-out and correct the docs
Follow-up on the co-author default in this PR.
- Remove the existsSync/writeFileSync race in the installer settings write
(CodeQL js/file-system-race, high). A single guarded read now covers the
fresh-install case, and unreadable or non-object settings are left untouched.
- Respect `attribution` as an explicit user choice. It supersedes
`includeCoAuthoredBy` in Claude Code 2.1.x, so a user who configured it would
otherwise have had a dead key written into their settings.
- Share one opt-out rule via scripts/lib/claude-commit-attribution.js instead of
duplicating it across the installer and plugin setup.
- Update the git-workflow rule and its nine mirrors and translations, which
still told users ECC does not ship this setting.
We keep writing the deprecated `includeCoAuthoredBy` key rather than
`attribution`: unknown keys fail Claude Code settings validation, so writing
`attribution` would break users on older versions.