Preserve main OpenCode lease and activation gates and native Claude namespace receipts. Detect same-size in-place edits using bigint modification and change times during hashing and before deletion; verify both races and actual public CLI reinstall lifecycles.
Drop the duplicate ECC_PREPUSH_RUN_CHECKS key now that main sets it,
refresh the skill-triggers registry digest, classify skill-host-compat
for pi/core, and bump the Turkish AGENTS.md counts the catalog sync
does not cover.
Assert dry-run install writes nothing under projectRoot too (cursor target writes there, not home). Rewrite the concurrent writeInstallState test with genuinely overlapping writers via async spawn plus done-file rendezvous, including a mid-flight read; sanitize rendezvous filenames for Windows.
Honor ECC_DRY_RUN=1 in install and repair arg parsing (previously only --dry-run flag worked, so ecc --dry-run install/repair silently performed real writes). Make writeInstallState atomic via writeFileAtomic (O_EXCL staging, fsync, cleanup on failure, atomic rename preserving previous state). Add writeInstallState failure/concurrency/symlink regression tests and disposable-home CLI dry-run regression tests for both global and subcommand forms.
Resolve six conflicts:
- hooks/hooks.metadata.json: keep this branch's gateguard description and
fingerprint, add upstream's hookify-runtime entries.
- package.json: keep both gateguard-report.js and github-coordination.js.
- scripts/hooks/gateguard-fact-force.js, tests/hooks/gateguard-fact-force.test.js,
skills/gateguard/SKILL.md: upstream added a simpler session denial cap; this
branch already has the fuller one (cap threaded through
markCheckedAndCountDenial, malformed-value warning, miss and target notes),
so keep it. Upstream's taskset and su wrapper handling, fail-closed
recursion limit and soft-hyphen sanitising merged cleanly and their tests pass.
- manifests/context-packs/skill-triggers@1.json: take upstream's file and
recompute registryDigest for the merged registry.
Treat non-2xx activity responses as feed errors, preserve non-Markdown Copilot agent assets, and apply the reviewed reference escape parity correction with regenerated bindings. Allow the test runner to drain piped output on its existing success or failure exit status. Validate installed workflows and bounded real Node20/24 pipe regressions.
Two first-round review findings were never addressed, and both reproduce
on the current head:
- records.require('./label'), obj?.import('./x') and obj . require('./y')
are method calls, but the call pattern's word boundary is satisfied by
the dot, so a self-contained object method fails every projection for a
file the script never loads.
- The dynamic-argument pattern allows one level of nested parentheses, so
require(path.join(__dirname, getName())) produces no dynamic warning
while require(getName()) does.
Control: a genuine require beside the member calls is still a dependency.
Review on dd14493e found four more ways the closure gate misjudges a
carrier, each reproduced against that head:
- require('./helper') resolves when only helper.cjs or helper.mjs is
planned, but Node's CommonJS search tries only .js, .json and .node.
- In `if (ok) {} /"/.test(text); require(...)` the regex after the block
is read as division and its quote swallows the require.
- `obj.\nif(x) / 2` is a method call, but the condition check only saw
the character immediately before `if`, so the division read as a regex
and swallowed a later require. Introduced by the previous fix.
- A tagged template named `from` was recorded as an import; with exact
import resolution it now fails a carrier that never loads that path.
Introduced by the previous fix.
Controls: an extensionless require still resolves to a .json file, and a
template literal passed to import() is still a dependency.
Review on aec97150 found four ways the closure gate passes a carrier that
fails at run time. Each is reproduced here against the current head:
- import('./helper') with only helper.js planned is accepted, but Node's
ESM resolver does no extension search.
- import('./data') with only data/index.json (or index.js) planned is
accepted, but Node rejects directory imports even from CommonJS.
- require('./lib\helper.js') with lib/helper.js planned is accepted
because backslashes were normalized, but Linux and macOS read the
backslash as part of the filename.
- In `if (ok) /['"]/.test(text); require('./missing')` the regex after
the condition's closing paren is read as division, its quote opens a
fake string, and the require disappears.
Controls that must keep passing: an exact import() of a planned file, an
extensionless require through the CommonJS search, and division after a
method named `if` or a call nested inside a condition. extractReferences
gains an `imports` list so the resolver can tell the two forms apart.
Carry forward PR #3055 at 92f53c4c onto ef648e01, preserving contributor attribution and current main. Focused maintainer fixes reuse shared input ceilings, preserve late root context through outer bootstrap, bound frontmatter before immutable parsing, and assert real oversized input in the limits workflow.
Carry forward PR #3115ef475c79 and 780dfb78 onto current main. Preserve the newer hook-consent transforms during install and repair. Main already includes the source import fix from #3167.
On a Windows machine with Claude Code installed natively, Git for Windows,
and the ECC plugin installed, `node tests/run-all.js` on main fails 44 tests
in 7 files. Linux CI never sees them. Three of the four causes are test
isolation gaps, fixed here (43 tests):
- claude-plugin-setup, claude-scope-migration, setup (38): fixtures put a
fake claude.cmd first on PATH, but on Windows spawning a bare `claude`
resolves only .com/.exe, so a native ~/.local/bin/claude.exe ran instead
of the fake. Fixture PATHs now drop entries holding claude.exe/claude.com.
- ecc-universal-bin, eval-harness-package (4): tar received an absolute
C:\ archive path, which Git Bash's GNU tar reads as a host:path remote
spec ("Cannot connect to C: resolve failed"). Extract from inside the
destination with a relative, forward-slash archive name, which both GNU
tar and bsdtar accept.
- harness-audit (1): the cache-layout test overrode HOME only, while
findPluginInstall also reads USERPROFILE and os.homedir(), so the real
installed plugin won over the fixture. Override USERPROFILE too.
The two helpers live in tests/lib/helpers/windows-test-env.js with a
platform-independent test, so Linux CI holds both invariants too.
The 44th failure, plugin-hook-bootstrap's no-bash test, is a production
bug rather than a test leak: System32\bash.exe launches an installed WSL
distro, which passes the bash probe. It gets a separate fix.
Three review findings, all verified before fixing.
1. Publish surface (blocking). package.json `files` lists
scripts/gemini-adapt-agents.js but not this script, so the npm tarball
omitted it and ecc-universal users hit MODULE_NOT_FOUND. Added to `files`
and to the allowlist in tests/scripts/npm-publish-surface.test.js, which now
passes 2/2.
2. CRLF agents were silently skipped. The frontmatter regex matched LF only, so
a Windows-saved agent was reported as 'already compatible' while still
carrying tools: Read, Bash and color: teal -- a success message with no
adaptation. Normalize to LF, adapt, then restore the original ending style.
Reproduced before and after: the same CRLF fixture went from 'Updated 0' with
Read/teal intact to 'Updated 1' with read_file/cyan and CRLF preserved.
3. Unknown flags were discarded. Because the script rewrites files in place,
`--dry-run` rewrote every installed agent and exited 0. parseArgs now throws
on any unrecognized option before a directory is selected.
Tests: 12/12 (added CRLF preservation and no-mutation-on-unknown-flag),
gemini-adapt-agents 4/4 unaffected, npm-publish-surface 2/2. Re-run against all
68 canonical definitions: 68 updated, second run 0 updated, no Claude-shaped
frontmatter remaining, no spurious CRLF introduced.
Note: findings 2 and 3 apply equally to scripts/gemini-adapt-agents.js, which
has the same LF-only regex and the same flag filter. Left unchanged here to keep
this PR scoped to the Qwen adapter; worth a follow-up.