Merge pull request #3654 from affaan-m/stewardship/verified-pr-batch-3569-3578-owned

Integrate 34 reviewed ECC fixes with verified follow-up patches
This commit is contained in:
haelyra authored and GitHub committed 2026-10-25 15:39:22 -04:00
commit e494799377
108 files changed
+2664 -1025

No files matched your search

+19 -4
View File
@@ -21,8 +21,15 @@ function normalizeFailureReason(reason) {
.replace(/\d{4}-\d{2}-\d{2}[t ]\d{2}:\d{2}:\d{2}[.\dz]*/g, '<timestamp>')
// Strip UUIDs (already lowercased)
.replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/g, '<uuid>')
// Strip file paths
.replace(/\/[\w./-]+/g, '<path>')
// Quoted Windows paths preserve spaces and apostrophes inside path components.
// A closing quote must be followed by a delimiter, not another name character.
.replace(/(["'])(?:[a-z]:[\\/]|\\\\)[^\r\n]*?\1(?=[:\s),;.!?]|$)/gu, '$1<path>$1')
// Directories may contain spaces, but trailing whitespace separates diagnostic prose.
// Unquoted spaced leaves allow one final dotted token; longer prose stays diagnostic.
// Quote paths with multiple spaces in the final filename to remove ambiguity.
.replace(/(?:\b[a-z]:|\\)[\\/](?:[^\\/:*?"<>|\r\n]*[^\s\\/:*?"<>|][\\/])*[^\s\\/:*?"<>|(),;]+(?: [^\s\\/:*?"<>|]+\.[^\s\\/:*?"<>|]+(?=:))?[\\/]*/gu, '<path>')
// Strip remaining file paths
.replace(/(?:\b[a-z]:)?[\\/][\w./\\-]+/g, '<path>')
// Collapse whitespace
.replace(/\s+/g, ' ')
.trim();
@@ -60,6 +67,14 @@ function groupFailures(skillRuns) {
return groups;
}
function compareTimestampsDescending(a, b) {
const timeA = Date.parse(a);
const timeB = Date.parse(b);
if (Number.isNaN(timeA)) return Number.isNaN(timeB) ? 0 : 1;
if (Number.isNaN(timeB)) return -1;
return timeB - timeA;
}
/**
* Detect recurring failure patterns from skill runs.
*
@@ -79,7 +94,7 @@ function detectPatterns(skillRuns, options = {}) {
}
const sortedRuns = [...group.runs].sort(
(a, b) => (b.createdAt || '').localeCompare(a.createdAt || '')
(a, b) => compareTimestampsDescending(a.createdAt, b.createdAt)
);
const firstSeen = sortedRuns[sortedRuns.length - 1].createdAt || null;
@@ -106,7 +121,7 @@ function detectPatterns(skillRuns, options = {}) {
// Sort by count descending, then by lastSeen descending
return patterns.sort((a, b) => {
if (b.count !== a.count) return b.count - a.count;
return (b.lastSeen || '').localeCompare(a.lastSeen || '');
return compareTimestampsDescending(a.lastSeen, b.lastSeen);
});
}
+2 -1
View File
@@ -269,7 +269,7 @@ function planClaudeStyleLegacyInstall(context, { adapterId, adapterRootInput, ru
function planClaudeLegacyInstall(context) {
return planClaudeStyleLegacyInstall(context, {
adapterId: 'claude',
adapterRootInput: { homeDir: context.homeDir },
adapterRootInput: { homeDir: context.homeDir, env: context.env },
rulesDir: context.claudeRulesDir || null
});
}
@@ -461,6 +461,7 @@ function createLegacyInstallPlan(options = {}) {
sourceRoot,
projectRoot,
homeDir,
env: options.env || {},
languages: Array.isArray(options.languages) ? options.languages : [],
claudeRulesDir: options.claudeRulesDir || process.env.CLAUDE_RULES_DIR || null
};
@@ -1,4 +1,6 @@
const os = require('os');
const path = require('path');
const { resolveInvocationEnvironment } = require('../invocation-environment');
const {
HOME_INSTALL_EXCLUDED_SOURCE_PATHS,
@@ -7,6 +9,7 @@ const {
isForeignPlatformPath,
normalizeRelativePath,
planClaudeHooksOperations,
convertWindowsPathToWSL,
} = require('./helpers');
const CLAUDE_ECC_NAMESPACE = 'ecc';
@@ -47,11 +50,52 @@ function getClaudeManagedDestinationPath(adapter, sourceRelativePath, input) {
return null;
}
/**
* Expand leading ~, ~/, ~\ against the home directory.
* Mirrors the expansion in resolveEccRoot to keep install and runtime roots in sync.
*/
function expandTilde(inputPath, homeDir) {
if (typeof inputPath !== 'string') return inputPath;
const trimmed = inputPath.trim();
if (trimmed === '~') return homeDir;
if (trimmed.startsWith('~/') || trimmed.startsWith('~\\')) {
return path.join(homeDir, trimmed.slice(2));
}
return trimmed;
}
// Claude Code loads its user-level config from CLAUDE_CONFIG_DIR when set,
// otherwise ~/.claude. Resolve the install root the same way the runtime does
// (see resolveClaudePaths/inventory and getDefaultClaudeAgentDataHome) so an
// install lands where Claude Code will actually read it; without this, ECC
// writes to ~/.claude and "installs, but nothing loads" whenever CLAUDE_CONFIG_DIR
// redirects the config root (common on WSL / dev-container / dotfile setups).
// On WSL, convert Windows paths (e.g., C:\Users\name\.claude) to WSL paths
// (/mnt/c/Users/name/.claude) so the installer writes to the correct location.
function resolveClaudeConfigRoot(options = {}) {
const environment = resolveInvocationEnvironment(options);
const configured = environment && environment.CLAUDE_CONFIG_DIR;
const homeDir = path.resolve(options.homeDir || os.homedir());
if (typeof configured === 'string' && configured.trim() !== '') {
const normalized = configured.trim();
const converted = convertWindowsPathToWSL(normalized);
// After WSL conversion, a blank result means the config dir was invalid.
// Fall back to the default Claude directory just like the runtime adapter does.
const expanded = expandTilde(converted, homeDir);
if (typeof expanded === 'string' && expanded.trim() === '') {
return path.join(homeDir, '.claude');
}
return path.resolve(expanded);
}
return path.join(homeDir, '.claude');
}
module.exports = createInstallTargetAdapter({
id: 'claude-home',
target: 'claude',
kind: 'home',
rootSegments: ['.claude'],
resolveRoot: resolveClaudeConfigRoot,
installStatePathSegments: ['ecc', 'install-state.json'],
excludedSourcePaths: HOME_INSTALL_EXCLUDED_SOURCE_PATHS,
nativeRootRelativePath: '.claude-plugin',
@@ -63,6 +107,7 @@ module.exports = createInstallTargetAdapter({
repoRoot: input.repoRoot,
projectRoot: input.projectRoot,
homeDir: input.homeDir,
env: input.env,
};
const operations = modules.flatMap(module => {
+54
View File
@@ -7,6 +7,55 @@ const {
} = require('../install/claude-settings');
const { METADATA_FILENAME } = require('../hooks-config');
// Memoized WSL detection (avoids repeated /proc/version reads)
let _isWSL = null;
function isWSL() {
if (_isWSL !== null) return _isWSL;
if (process.platform !== 'linux') {
_isWSL = false;
return false;
}
try {
_isWSL = fs.readFileSync('/proc/version', 'utf8').toLowerCase().includes('microsoft');
} catch {
_isWSL = false;
}
return _isWSL;
}
// Reset WSL detection memoization (for testing only)
function resetWSLDetection() {
_isWSL = null;
}
// Set WSL detection for testing (for testing only)
function setWSLForTesting(value) {
_isWSL = value;
}
// Windows path pattern: drive letter followed by :\ or :/
const WINDOWS_PATH_PATTERN = /^[A-Za-z]:[\\/]/;
// Compact WSL conversion snippet for inline resolvers embedded in commands and hooks.
// Uses 8 backslashes in regex source so it preserves shell escaping when inlined into `node -e "..."`.
const INLINE_WSL_CONVERSION = `try{if(o.platform()==='linux'){var v=f.readFileSync('/proc/version','utf8').toLowerCase();if(v.includes('microsoft')){var m=d.match(/^([A-Za-z]):[\\\\\\\\/]/);if(m){var dl=m[1].toLowerCase();d='/mnt/'+dl+d.slice(2).replace(/\\\\\\\\/g,'/')}}}}catch(_){}`;
/**
* Convert a Windows path to a WSL path.
* Example: "C:\Users\name" -> "/mnt/c/Users/name"
* Example: "D:/Projects" -> "/mnt/d/Projects"
* Returns the original path if it's not a Windows path or not on WSL.
*/
function convertWindowsPathToWSL(windowsPath) {
if (!isWSL()) return windowsPath;
if (typeof windowsPath !== 'string') return windowsPath;
const trimmed = windowsPath.trim();
if (!WINDOWS_PATH_PATTERN.test(trimmed)) return trimmed;
const driveLetter = trimmed[0].toLowerCase();
const rest = trimmed.slice(2).replace(/\\/g, '/');
return `/mnt/${driveLetter}${rest}`;
}
const PLATFORM_SOURCE_PATH_OWNERS = Object.freeze({
'.claude-plugin': 'claude',
'.codex': 'codex',
@@ -441,4 +490,9 @@ module.exports = {
isForeignPlatformPath,
normalizeRelativePath,
planClaudeHooksOperations,
isWSL,
resetWSLDetection,
setWSLForTesting,
convertWindowsPathToWSL,
INLINE_WSL_CONVERSION,
};
+35 -9
View File
@@ -4,6 +4,7 @@ const fs = require('fs');
const os = require('os');
const path = require('path');
const { isWithinRoot, realpathNearestExisting } = require('../path-safety');
const { convertWindowsPathToWSL } = require('../install-targets/helpers');
const CURRENT_PLUGIN_ID = 'ecc@ecc';
// Legacy plugin ids, kept so existing installs are still recognized.
@@ -12,19 +13,44 @@ const LEGACY_PLUGIN_IDS = new Set([
'everything-claude-code@ecc',
]);
function expandTilde(inputPath, homeDir) {
if (typeof inputPath !== 'string') return inputPath;
const trimmed = inputPath.trim();
if (trimmed === '~') return homeDir;
if (trimmed.startsWith('~/') || trimmed.startsWith('~\\')) {
return path.join(homeDir, trimmed.slice(2));
}
return trimmed;
}
function resolveClaudePaths(options = {}) {
const homeDir = options.homeDir
|| process.env.HOME
|| process.env.USERPROFILE
|| os.homedir();
const configDir = options.configDir
|| process.env.CLAUDE_CONFIG_DIR
|| path.join(homeDir, '.claude');
const homeDir = path.resolve(
options.homeDir
|| process.env.HOME
|| process.env.USERPROFILE
|| os.homedir()
);
const configured = options.configDir !== undefined
? options.configDir
: process.env.CLAUDE_CONFIG_DIR;
let resolvedConfigDir;
if (typeof configured === 'string' && configured.trim() !== '') {
const normalized = configured.trim();
const converted = convertWindowsPathToWSL(normalized);
const expanded = expandTilde(converted, homeDir);
if (typeof expanded === 'string' && expanded.trim() !== '') {
resolvedConfigDir = path.resolve(expanded.trim());
} else {
resolvedConfigDir = path.join(homeDir, '.claude');
}
} else {
resolvedConfigDir = path.join(homeDir, '.claude');
}
const projectRoot = options.projectRoot || process.cwd();
return {
homeDir: path.resolve(homeDir),
configDir: path.resolve(configDir),
homeDir,
configDir: resolvedConfigDir,
projectRoot: path.resolve(projectRoot),
};
}
+1
View File
@@ -53,6 +53,7 @@ function createRawInstallPlan(request, options = {}) {
languages: request.languages,
projectRoot: options.projectRoot,
homeDir: options.homeDir,
env: resolveInvocationEnvironment(options),
claudeRulesDir: options.claudeRulesDir,
sourceRoot: options.sourceRoot,
});
+53 -2
View File
@@ -4,6 +4,23 @@ const fs = require('fs');
const path = require('path');
const os = require('os');
// Lazily loaded so this module can be required from stripped-down directories
// (e.g., temp dirs in tests that copy only resolve-ecc-root.js + utils.js).
// Falls back to identity on failure, which matches non-WSL behavior.
let _convertWindowsPathToWSL = null;
function convertWindowsPathToWSL(windowsPath) {
if (_convertWindowsPathToWSL === null) {
try {
_convertWindowsPathToWSL = require('./install-targets/helpers').convertWindowsPathToWSL;
} catch (_) {
// Module unavailable from this location; non-WSL behavior returns
// the input unchanged, so the fallback is safe.
_convertWindowsPathToWSL = (p) => p;
}
}
return _convertWindowsPathToWSL(windowsPath);
}
const CURRENT_PLUGIN_SLUG = 'ecc';
// Legacy install id, kept for existing installs.
const LEGACY_PLUGIN_SLUG = 'everything-claude-code';
@@ -66,7 +83,10 @@ function resolveEccRoot(options = {}) {
const configuredDir = options.configDir !== undefined
? options.configDir
: process.env.CLAUDE_CONFIG_DIR;
const rawDir = typeof configuredDir === 'string' ? configuredDir.trim() : '';
// On WSL, convert Windows paths (e.g., C:\Users\name\.claude) to WSL paths
// (/mnt/c/Users/name/.claude) so the resolver finds the correct location.
const wslConfiguredDir = convertWindowsPathToWSL(configuredDir);
const rawDir = typeof wslConfiguredDir === 'string' ? wslConfiguredDir.trim() : '';
const expandedDir = rawDir === '~'
? homeDir
: (rawDir.startsWith('~/') || rawDir.startsWith('~\\'))
@@ -154,10 +174,41 @@ function normalizePluginRootForPlatform(rootDir, platform = process.platform) {
* const _r = <paste INLINE_RESOLVE>;
* const sm = require(_r + '/scripts/lib/session-manager');
*/
const INLINE_RESOLVE = `(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=(process.env.CLAUDE_CONFIG_DIR||'').trim()||p.join(o.homedir(),'.claude');if(d==='~')d=o.homedir();else if(d.charAt(0)==='~'&&(d.charAt(1)==='/'||d.charCodeAt(1)===92))d=p.join(o.homedir(),d.slice(2));d=p.resolve(d);function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}function D(x){try{return f.readdirSync(x,{withFileTypes:true}).filter(function(z){return z.isDirectory()}).map(function(z){return z.name})}catch(e){if(e.code==='ENOENT')return [];throw e}}var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=D(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=D(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}return d})()`;
// Lazily loaded so this module can be required from stripped-down directories
// (e.g., temp dirs in tests that copy only resolve-ecc-root.js + utils.js).
// Falls back to canonical inline string with identical escaping when unavailable.
let _inlineWslConversion = null;
function getInlineWslConversion() {
if (_inlineWslConversion === null) {
try {
_inlineWslConversion = require('./install-targets/helpers').INLINE_WSL_CONVERSION;
} catch (_) {
_inlineWslConversion = `try{if(o.platform()==='linux'){var v=f.readFileSync('/proc/version','utf8').toLowerCase();if(v.includes('microsoft')){var m=d.match(/^([A-Za-z]):[\\\\\\\\/]/);if(m){var dl=m[1].toLowerCase();d='/mnt/'+dl+d.slice(2).replace(/\\\\\\\\/g,'/')}}}}catch(_){}`;
}
}
return _inlineWslConversion;
}
function buildInlineResolve(options = {}) {
const wslConversion = getInlineWslConversion();
let probeArg = '';
if (options && options.probe) {
const segments = String(options.probe).split(/[\\/]/).filter(Boolean);
for (const seg of segments) {
if (!/^[\w.-]+$/.test(seg)) {
throw new Error(`Invalid probe segment: ${seg}`);
}
}
probeArg = `{probe:p.join(${segments.map((s) => `'${s}'`).join(',')})}`;
}
return `(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=(process.env.CLAUDE_CONFIG_DIR||'').trim()||p.join(o.homedir(),'.claude');if(d==='~')d=o.homedir();else if(d.charAt(0)==='~'&&(d.charAt(1)==='/'||d.charCodeAt(1)===92))d=p.join(o.homedir(),d.slice(2));${wslConversion}d=p.resolve(d);function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot(${probeArg})}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}function D(x){try{return f.readdirSync(x,{withFileTypes:true}).filter(function(z){return z.isDirectory()}).map(function(z){return z.name})}catch(e){if(e.code==='ENOENT')return [];throw e}}var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=D(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=D(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}return d})()`;
}
const INLINE_RESOLVE = buildInlineResolve();
module.exports = {
resolveEccRoot,
normalizePluginRootForPlatform,
INLINE_RESOLVE,
buildInlineResolve,
};
+5 -1
View File
@@ -58,7 +58,7 @@ function loadAliases() {
const data = JSON.parse(content);
// Validate structure
if (!data.aliases || typeof data.aliases !== 'object') {
if (!data.aliases || typeof data.aliases !== 'object' || Array.isArray(data.aliases)) {
log('[Aliases] Invalid aliases file structure, resetting');
return getDefaultAliases();
}
@@ -215,6 +215,10 @@ function setAliasUnlocked(alias, sessionPath, title = null) {
return { success: false, error: 'Session path cannot be empty' };
}
if (title !== null && typeof title !== 'string') {
return { success: false, error: 'Title must be a string or null' };
}
if (alias.length > 128) {
return { success: false, error: 'Alias name cannot exceed 128 characters' };
}
+1 -1
View File
@@ -69,7 +69,7 @@ function bucketByDay(records, nowMs, days) {
}
for (const bucket of buckets) {
if (recordMs > bucket.start && recordMs <= bucket.end) {
if (recordMs >= bucket.start && recordMs <= bucket.end) {
bucket.records.push(record);
break;
}
+2 -2
View File
@@ -35,8 +35,8 @@ function toNullableNumber(value, fieldName) {
}
const numericValue = Number(value);
if (!Number.isFinite(numericValue)) {
throw new Error(`${fieldName} must be a number`);
if (!Number.isFinite(numericValue) || numericValue < 0) {
throw new Error(`${fieldName} must be a number greater than or equal to zero`);
}
return numericValue;
+7 -1
View File
@@ -39,7 +39,13 @@ function usage() {
function parseArgs(argv) {
const args = argv.slice(2);
const target = args.find(argument => !argument.startsWith('--'));
const valueOptions = ['--adapter', '--target-type', '--skill', '--amendment-id', '--observations', '--write'];
for (const [index, argument] of args.entries()) {
if (valueOptions.includes(argument) && (!args[index + 1] || args[index + 1].startsWith('--'))) {
throw new Error(`Missing value for ${argument}`);
}
}
const target = args.find((argument, index) => !argument.startsWith('--') && !valueOptions.includes(args[index - 1]));
const listAdapters = args.includes('--list-adapters');
const adapterIndex = args.indexOf('--adapter');
+6
View File
@@ -28,11 +28,17 @@ function parseArgs(argv) {
const arg = args[index];
if (arg === '--db') {
if (!args[index + 1] || args[index + 1].startsWith('--') || args[index + 1] === '-h') {
throw new Error(`Missing value for ${arg}`);
}
parsed.dbPath = args[index + 1] || null;
index += 1;
} else if (arg === '--json') {
parsed.json = true;
} else if (arg === '--limit') {
if (!args[index + 1] || args[index + 1].startsWith('--') || args[index + 1] === '-h') {
throw new Error(`Missing value for ${arg}`);
}
parsed.limit = args[index + 1] || null;
index += 1;
} else if (arg === '--help' || arg === '-h') {