* docs: correct MODULE_NOT_FOUND plugin cache path
Point the install command at cache/ecc/ecc/<version>.
The cache/ecc directory itself has no package.json.
* docs: explain the MODULE_NOT_FOUND version placeholder
Tell readers to replace <version> with the version from claude plugin list
before running the install command.
* docs: quote plugin cache version placeholder
---------
Co-authored-by: haelyra <49814733+haelyra@users.noreply.github.com>
The platform-configs module lists `.pi`, but PLATFORM_SOURCE_PATH_OWNERS
had no entry for it, so isForeignPlatformPath() returned false and every
install-apply target (e.g. `--target opencode`) copied the Pi harness
adapter (.pi/README.md, .pi/extensions/*) into its config root and
recorded it as ECC-managed.
Pi consumes ECC through `pi install`, not install-apply, so map `.pi` to
the `pi` owner. No install target has that id, so the directory is now
filtered out everywhere, like `.codex`, `.cursor`, and `.zed`.
Add regression tests for the opencode plan and for every registered
target.
Fixes#3558
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: haelyra <49814733+haelyra@users.noreply.github.com>
The test job ran the entire suite on 3 OSes x 3 Node lines x 4 package
managers, 33 full runs per push. Windows lanes took 28 to 39 minutes and
macOS 14 to 19, so one push queued about 1.5 hours of wall clock behind
runner concurrency, and timing-sensitive tests got 33 chances to flake.
The package-manager axis only ever changed one environment variable that
nine suites read.
Now there are 13 lanes. Five run the full suite: Linux on Node 18, 20
and 22, plus Windows and macOS on Node 22. Eight are package-manager
lanes on Node 22 that run only the suites which read
CLAUDE_CODE_PACKAGE_MANAGER or CLAUDE_PACKAGE_MANAGER; the file list is
grepped at run time so it cannot drift as tests are added. Full lanes
use the runner's 4-process pool on Linux and macOS; Windows stays serial
because its process spawn and temp-dir cleanup are the flaky part.
The runner executed 422 files one process at a time, which is most of
the 12 to 40 minutes a CI lane spends in "Run tests". ECC_TEST_JOBS
above 1 now runs files through a pool of that many processes; output is
still buffered and printed per file, so logs read the same. The default
stays 1 and that path is the original synchronous spawnSync loop, so the
VM-driven runner test and the piped-output tests are unchanged.
ECC_TEST_ONLY is the mirror of ECC_TEST_SKIP: a comma-separated list of
path substrings, and only matching files run. CI uses it to give the
package-manager lanes just the suites that read the package-manager
variable instead of the whole suite.
Two real-process tests cover the pool (grouped output, counted failure,
summary last) and the ONLY filter.
extractQualifiedTarGzip walked the archive, validated the terminator, read each header and classified the entry in one loop; installNasiko and uninstallNasiko each resolved the release and paths, then ran the whole download or staging sequence inside one try with rollback flags. The extractor now uses gunzipArchive, assertTarTerminator, readTarEntry, isBinaryEntry and isSidecarEntry. The two lifecycle calls share qualifiedReleaseFor and installPathsFor; install fetches through fetchVerifiedBinary and publishes through publishBinary, and uninstall stages and deletes through removeInstalledFiles with removeOrRetain.
The rollback rules are the same: a failed publish removes only the files this call created, a failed rename puts the staged file back, and a failed delete keeps the tombstone and reports it in cleanupPending. The plan still takes its version from the requested release, not from releaseOverride, and every dependency hook is called in the same order. Exports are unchanged, and a run of the old and new extractor over 20k generated archives covering sidecars, bad padding, bad size fields, truncation and trailing data produced identical results and messages.
searchEvidence built the Glob, Grep and LS evidence inline before falling through to the shell path, and findCreditingSearch and findClosestMiss each nested the per-search and per-item loops with the stem and directory checks in one body. The tool builders are now globEvidence, grepEvidence and lsEvidence behind a small Map, the item loop of findCreditingSearch is creditingItem, and findClosestMiss hands the miss reasons to missReason and the Read scan to considerReads. searchHasMessage replaces the repeated messageId check.
Results are unchanged: the same first crediting search wins, the same lowest-ranked miss is reported, and errors still resolve to null inside the two public functions. Checked by running the old and new factory side by side over 40k generated scans and targets with identical output.
lexCodeLines ran every state (code, line comment, block comment, string) through one loop body with three closures over its locals, and shCodeLines and psCodeLines each inlined their escape, quote and comment readers. The C-family lexer now keeps its partial line on a small lx record and dispatches by state to lexNewline, lexLineComment, lexBlockComment, lexString and lexCode, with lexCommentStart, startsJsx and lexStringStart under lexCode. The shell lexers share pushShellLine, readEscapedPair, readQuotedSpan and readLineComment; shCodeLines looks its readers up in a table and psCodeLines splits into psBlockComment and psToken.
Every reader returns the index of the last character it consumed, so the loops advance exactly as before, and the sentinel for an unlexable edit is still null from the C-family path and a null result from the shell paths. Exports are unchanged. A differential run against the previous version over the unit test strings plus 40k generated inputs, across all nine spec shapes change-profile builds, produced identical lines, normalized signatures and isTrivialEdit verdicts.
heredocDelimiter, executionRegion and scanShell each carried one long character loop with every quote, escape, heredoc, case and scope rule inline, which is where the three complexity warnings came from. The delimiter reader now has skipDelimiterBlanks, delimiterEscape and readDelimiterWord; the $(...) walker dispatches to regionQuotedEnd, regionSubstitution, openRegionQuote, regionHeredocOperator, regionHeredocBodies, casePhaseStep and closeRegionParen; and the top-level scanner keeps its position and pending word on a scan object that scanQuotedChar, scanEscape, scanSubstitution, openQuote, scanOperator and scanRedirect advance.
Every budget.spend call keeps its amount and order, so the work quota tests still land in the same window. The module still has no requires and exports createBudget, scanShell and scanExpansions unchanged; a differential run of the old and new scanner over the hook test corpus plus 58k generated inputs produced identical commands, nested regions and spend totals.
runEvaluation is now plan, execute, report: option validation and family
resolution, registration pinning, provider and state creation, install
preparation, the selection and trial loops, and the report object each sit in
their own function. outcomeTrial takes one trial object instead of ten
positionals and splits into single-step and stepped paths. The provider
adapter separates the guarded provider call from output parsing, and the
complex corpus validator checks one task at a time.
Report keys, error messages, and call ordering are unchanged. The file is in
the implementation digest list, so registration digests move as they do for
any edit here. Clears the four complexity warnings and the max-params warning
on this file.
parseArgs now reads flags from two lookup tables, one for flags that take a
value in either form and one for standalone switches, instead of thirty-odd
if blocks. The ECC Tools evidence and gap text live in ordered tier tables
keyed by roadmap markers, with the shared marker lists named once.
buildRequirements reads its docs and readiness checks up front, then
assembles the list from four requirement groups. The PR and issue budget
rows share one builder.
Flag errors, requirement order, and every message string are unchanged.
Verified by comparing parseArgs over a flag matrix and buildReport over the
repo, an empty root, and one synthetic roadmap per marker. Clears the four
complexity warnings on this file.
cFamilyStartsFresh ran one 130-line loop over template literals, comments,
regex-vs-division, C# and Rust raw strings, quotes, words, and punctuation.
It now keeps scanner state in one object and walks a fixed list of step
functions, each returning consumed, abort, or not-mine. The Python f-string
field scan, the shell heredoc and quote tokens, and the PowerShell
here-string, block comment, and hash tokens get the same treatment.
Verified byte-for-byte against the recorded test inputs, four hundred real
source files from the repo, random prefixes, and single-character mutations
across every flavor spec. Clears the five complexity warnings on this file.
executeRepairOperation, executeUninstallOperation, and inspectManagedOperation
now dispatch on operation kind to one small function per kind, with the
repeated uninstall result shapes and previous-content restore pulled into
helpers. executeRepairOperation takes an options object instead of four
trailing positionals. analyzeRecord is a sequence of collect* steps, and the
per-record repair closure inside repairInstalledStates hands off to
runRecordRepair with the legacy OpenCode, build-only dry run, and managed
repair paths as separate functions.
Result objects keep the same keys in the same order, and lock, build, and
require ordering is unchanged. Clears the max-params and complexity warnings
on this file.
Six summary-line replacers in catalog.js each took seven to nine positional
capture groups and rebuilt the line by hand. They now go through one
catalogCountReplacer factory that takes the regex group count and the groups
holding the agents, skills, and commands counts. Every other group is written
back verbatim.
Output is byte-identical across the real docs and several synthetic catalogs.
Clears the six max-params warnings on this file.
The here-string normalizer, comment stripper, and balanced-group reader each
carried their own copy of the same inert-region state machine. They now share
createLexState, consumeInertChar, and openInertRegion. The container
extractor, statement tokenizer, nested pwsh scanner, cmd scanner, Start-Process
scanner, and top-level statement walk are split into small named helpers that
each handle one region or one command family.
No classification change. Verified against the recorded test inputs plus
mutated variants with an order-sensitive comparison of findings. Clears the
eleven complexity and prefer-const warnings on this file.
validateInlineJs unescaped the shell body with four chained replaces,
so a backslash pair followed by n or t was unescaped twice: the pair
became one backslash, then the new backslash-n became a newline. A
valid hook command such as console.log('\\n') failed validation with
an unterminated string. CodeQL flags the chain as double unescaping.
One regex pass maps each escape exactly once. The regression test
feeds the escaped-backslash program through the validator CLI.
validateHookEntry handled command, http and prompt hooks in one function; it now dispatches to validateCommandHook, validateHttpHook and validatePromptHook with the inline node -e check in validateInlineJs. validateHooks reads the file, loads the sidecar and validates the object or legacy array shape through readHooksFile, loadMetadata, validateObjectFormat and validateArrayFormat, so the deepest nesting is gone.
Error messages, their order, exit codes and the --update-fingerprints flow are unchanged.
extractSessionSummary did user-message filtering and two kinds of tool_use collection in one loop body six blocks deep. The per-entry work now lives in userMessageText, recordToolUse and collectEntry. main() is down to the sequence of steps: resolveTranscriptPath, sessionShortId, maybeGenerateLlmSummary, then updateSessionFile or createSessionFile, with the shared footer and empty-session template as constants.
Call order, log lines, file names and the written markdown are unchanged, including the exact summary block replacement and the migration path for files without markers.
gate() handled Edit, MultiEdit and shell in one 220-line function. It now builds a small context and dispatches to gateSingleEdit, gateMultiEdit and gateShell, with the first-touch, sibling-collapse and routine/destructive shell steps as their own functions. isDestructiveGit and isReadOnlyGitIntrospection became subcommand tables; unwrapLeadWrappers got one handler per wrapper; ddLauncherCommandIndex, isDestructiveFindExec and sanitizePath lost their inline option and code point ladders. condensedGateMsg takes an options object instead of seven positional parameters.
Decision order, recorded metrics reasons, state keys and every denial message are unchanged, and the module exports are the same.
extractSubshellGroups and extractBraceGroups each carried their own copies of the quote-aware skipping for $(...), backtick and (...) spans, which is where the complexity and nesting warnings came from. The skipping now lives in findParenSpanEnd, findBacktickEnd and skipOuterSpan, with readSubshellBody and readBraceBody doing the body collection.
Behavior is unchanged: the same bodies come back in the same order, trailing backslashes and unterminated spans resolve the same way, and the three exports keep their signatures.
checkGitWords, executableWords, shellRole, updateShellState and checkCommand were each well over the complexity budget and checkCommand nested seven blocks deep. Split them into named helpers: git global option scanning and bypass flag search, per-wrapper handlers for command/exec/env/sudo, a shell option cluster classifier, declaration flag parsing, and a per-task/per-state/per-environment pipeline inside checkCommand.
Every budget.spend call keeps its amount and order so the work quota tests still land in the same window. The module still only requires ./lib/shell-scan and exports run unchanged. Wrapper option sets moved to module constants instead of being rebuilt on every call.
* test(lib): cover feedback-links routes and payload
Pins the three public issue routes to the ECC repository and checks each template name actually exists under .github/ISSUE_TEMPLATE. Also locks the ecc.feedback.v1 payload shape, the no-diagnostics-upload promise, and that every call returns fresh copies so a caller cannot alter the frozen route table.
* test(lib): cover cursor-agent-names prefixing and path handling
Pins the ecc- prefix rule the Cursor install applies to agent files: idempotent, lowercase-prefix only, empty names pass through. Covers the relative path variant across backslash, repeated, and trailing separators, and that a leading separator cannot re-root the destination under the target agents directory.
* test(lib): cover agent-tool-map Pi mapping and authority invariant
Pins the Claude to Pi tool table: six core tools, every target a real Pi built-in, and read-only tools never escalating to bash, edit, or write. Covers the unsupported path for MCP, web, unknown, empty, and non-string names, and that prototype member names like constructor are not treated as table hits.
* test(lib): cover compute-sponsor URL and passive copy
Pins the compute URL to the bare https route with no path, query, or credentials, and that the sponsor copy links it exactly once and nowhere else. Locks the wording that keeps the link passive, keeps the ecc ito find bridge opt-in and non-reserving, and does not claim managed inference is live. The existing surface test only reads docs and CLI output, so the module itself had no direct test.
* test(lib): cover invocation-environment precedence and isolation
Pins the three branches every install path relies on: an explicit env is copied and wins, a scoped homeDir yields an empty environment so ambient variables cannot leak into a contained install, and anything else snapshots process.env. Also checks the copy is shallow, own-keys only, and never writes back.
The multi-backend, multi-frontend, multi-plan, multi-execute and multi-workflow commands were copied from ccg-workflow's command templates, which carry {{LITE_MODE_FLAG}} and {{GEMINI_MODEL_FLAG}} for that project's installer to substitute. ECC has no substitution step, so bash passed the literal token to codeagent-wrapper as a positional argument, the backend name landed in the working-directory slot, and every call exited 1. Remove the tokens from the five commands and the ja-JP, tr and zh-CN docs copies so the wrapper receives a plain --backend invocation. A regression test scans every multi-* command and translated copy for codeagent-wrapper lines that still contain a {{TEMPLATE}} token.
Fixes#3595
Extend the PR burst guard with an open-PR cap. On every opened or reopened
PR by a non-collaborator, the guard lists the repository's open PRs, keeps
the author's oldest `maxOpen` (10) open and closes the newer ones with a
bot comment that names the count and the limit. No lock, no maintainer
alert: the cap is about review capacity, not spam. PRs the burst logic
already closed in the same run are excluded so nothing is mutated twice.
Collaborators with write access stay exempt, dry run reports without
mutating, and the comment is deduplicated on a bot-authored marker.
Haley asked for this on 2026-10-19: each contributor should only have 10
to 15 PRs open at a time. Existing open PRs are not bulk-closed; the cap
applies as each author opens or reopens their next PR.
Verification: 22 unit tests and the HTTP workflow test pass,
validate-workflow-security passes, eslint and markdownlint clean. Dry run
against affaan-m/ECC on #3621 reports openCount 19 and nine cap excess
PRs for that author, with no writes.
_parse_classification returned {} for garbage or non-dict output, so a failed classification looked like zero matched steps and graded as non-compliant. Raise ClassifierParseError (a RuntimeError, like the non-zero-rc path) instead.