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>
hasMarkerBlock previously swallowed every read/open error and returned
false, so an unreadable AGENTS.md (EACCES, EMFILE, EISDIR, ...) made
detectLegacyCodexSync report a clean Codex home instead of an
indeterminate inspection result. The fallback path could then skip
legacy cleanup and exit 0 with legacy artifacts still in place.
Restrict the catch to ENOENT (a missing file legitimately means no
marker block) and rethrow everything else. detectLegacyCodexSync
already propagates from hasMarkerBlock, so callers now see the actual
inspection error instead of a misleading 'no marker'.
Regression test in tests/lib/codex-legacy-sync.test.js makes a
detectLegacyCodexSync call against an unreadable AGENTS.md and asserts
that it throws something other than ENOENT, plus a sanity check that
a missing AGENTS.md still reads as no-marker.
Restrict the automatic `uninstall` legacy Codex sync fallback to homes
that have a legacy ownership manifest (`~/.codex/ecc/legacy-sync-state.json`).
Marker-only AGENTS.md files are no longer auto-detected as legacy installs,
so a normal `uninstall` will not silently modify user-owned instructions.
The explicit `--legacy-codex-sync` flag still handles marker-only and
manifest-backed cleanup.
Also:
- Track the AGENTS.md path in removedPaths when a marker block is removed.
- Refactor codex home resolution into a helper.
- Add regression tests for marker-only auto vs. explicit behavior.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Tighten uninstall auto-detection so it only falls back to the legacy
sync-ecc-to-codex.sh path when there is an ownership manifest
(~/.codex/ecc/legacy-sync-state.json) or an ECC marker block in
~/.codex/AGENTS.md. Previously a clean Codex home with unrelated prompt
files could be misclassified as a legacy install, causing uninstall to
skip normal install-state reconciliation and exit with a partial warning.
Also make the no-state fallback return 'not-found' when there is no
marker to remove and no candidate files to clean, and make explicit
--legacy-codex-sync report the same on a clean home.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
When no install-state is found for the current context, `ecc uninstall`
now checks for the legacy `sync-ecc-to-codex.sh` ownership manifest under
`~/.codex/ecc/legacy-sync-state.json` and, if present, rolls back the
managed Codex artifacts it recorded. It restores previous `config.toml`
and `AGENTS.md` content instead of deleting them, removes generated
prompts/docs/copies, and leaves unrelated Codex conversation history and
user config keys untouched. A fallback `--legacy-codex-sync` flag still
forces the legacy path explicitly, and `--dry-run` previews the cleanup.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
OpenCode natively uses ~/.config/opencode per XDG conventions. The
install target was writing to ~/.opencode, which only worked on systems
where that path happened to be symlinked to ~/.config/opencode. The MCP
inventory reader already looked in ~/.config/opencode, so the installer
and reader were inconsistent.
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(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.