From e22ab5e5cb7cd0c052b545bd890be9893bd8217f Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Sun, 29 Mar 2026 09:06:44 -0400 Subject: [PATCH] fix: narrow unicode cleanup scope --- .kiro/install.sh | 10 +- .kiro/scripts/format.sh | 4 +- .kiro/scripts/quality-gate.sh | 2 +- .trae/install.sh | 10 +- .trae/uninstall.sh | 34 +++--- scripts/ci/catalog.js | 6 +- scripts/ci/check-unicode-safety.js | 30 ++++- scripts/codex/install-global-git-hooks.sh | 4 +- scripts/hooks/insaits-security-monitor.py | 2 +- scripts/hooks/pre-bash-commit-quality.js | 104 +++++++++--------- scripts/skill-create-output.js | 20 ++-- skills/ck/commands/resume.mjs | 2 +- skills/ck/hooks/session-start.mjs | 6 +- .../agents/session-guardian.sh | 16 +-- .../agents/start-observer.sh | 10 +- .../continuous-learning-v2/hooks/observe.sh | 8 +- .../scripts/detect-project.sh | 18 +-- .../continuous-learning/evaluate-session.sh | 18 +-- skills/rules-distill/scripts/scan-rules.sh | 2 +- skills/rules-distill/scripts/scan-skills.sh | 6 +- skills/skill-stocktake/scripts/quick-diff.sh | 6 +- .../skill-stocktake/scripts/save-results.sh | 2 +- skills/skill-stocktake/scripts/scan.sh | 6 +- skills/strategic-compact/suggest-compact.sh | 18 +-- skills/videodb/scripts/ws_listener.py | 24 ++-- tests/lib/session-aliases.test.js | 3 +- tests/lib/utils.test.js | 25 +++-- tests/run-all.js | 2 +- tests/scripts/check-unicode-safety.test.js | 31 ++++++ 29 files changed, 249 insertions(+), 180 deletions(-) diff --git a/.kiro/install.sh b/.kiro/install.sh index ebefdd6e..1cb25e95 100755 --- a/.kiro/install.sh +++ b/.kiro/install.sh @@ -4,10 +4,12 @@ # Installs Everything Claude Code workflows into a Kiro project. # # Usage: -# ./install.sh # Install to current directory -# ./install.sh /path/to/dir # Install to specific directory -# ./install.sh ~ # Install globally to ~/.kiro/ -# set -euo pipefail +# ./install.sh # Install to current directory +# ./install.sh /path/to/dir # Install to specific directory +# ./install.sh ~ # Install globally to ~/.kiro/ +# + +set -euo pipefail # When globs match nothing, expand to empty list instead of the literal pattern shopt -s nullglob diff --git a/.kiro/scripts/format.sh b/.kiro/scripts/format.sh index eaacba07..664efac8 100755 --- a/.kiro/scripts/format.sh +++ b/.kiro/scripts/format.sh @@ -50,7 +50,7 @@ case "$FORMATTER" in exit 1 fi ;; - + prettier) if command -v npx &>/dev/null; then echo "Formatting $FILE with Prettier..." @@ -61,7 +61,7 @@ case "$FORMATTER" in exit 1 fi ;; - + none) echo "No formatter detected (biome.json, .prettierrc, or installed formatter)" echo "Skipping format for: $FILE" diff --git a/.kiro/scripts/quality-gate.sh b/.kiro/scripts/quality-gate.sh index 05279c04..4d93a026 100755 --- a/.kiro/scripts/quality-gate.sh +++ b/.kiro/scripts/quality-gate.sh @@ -36,7 +36,7 @@ detect_pm() { } PM=$(detect_pm) -echo " Package manager: $PM" +echo "Package manager: $PM" echo "" # ── Helper: run a check ───────────────────────────────────── diff --git a/.trae/install.sh b/.trae/install.sh index 7b855601..da0d860c 100755 --- a/.trae/install.sh +++ b/.trae/install.sh @@ -4,12 +4,14 @@ # Installs Everything Claude Code workflows into a Trae project. # # Usage: -# ./install.sh # Install to current directory -# ./install.sh ~ # Install globally to ~/.trae/ or ~/.trae-cn/ +# ./install.sh # Install to current directory +# ./install.sh ~ # Install globally to ~/.trae/ or ~/.trae-cn/ # # Environment: -# TRAE_ENV=cn # Force use .trae-cn directory -# set -euo pipefail +# TRAE_ENV=cn # Force use .trae-cn directory +# + +set -euo pipefail # When globs match nothing, expand to empty list instead of the literal pattern shopt -s nullglob diff --git a/.trae/uninstall.sh b/.trae/uninstall.sh index 191bd869..005216d9 100755 --- a/.trae/uninstall.sh +++ b/.trae/uninstall.sh @@ -4,12 +4,14 @@ # Uninstalls Everything Claude Code workflows from a Trae project. # # Usage: -# ./uninstall.sh # Uninstall from current directory -# ./uninstall.sh ~ # Uninstall globally from ~/.trae/ +# ./uninstall.sh # Uninstall from current directory +# ./uninstall.sh ~ # Uninstall globally from ~/.trae/ # # Environment: -# TRAE_ENV=cn # Force use .trae-cn directory -# set -euo pipefail +# TRAE_ENV=cn # Force use .trae-cn directory +# + +set -euo pipefail # Resolve the directory where this script lives SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" @@ -44,18 +46,18 @@ is_valid_manifest_entry() { do_uninstall() { local target_dir="$PWD" local trae_dir="$(get_trae_dir)" - + # Check if ~ was specified (or expanded to $HOME) if [ "$#" -ge 1 ]; then if [ "$1" = "~" ] || [ "$1" = "$HOME" ]; then target_dir="$HOME" fi fi - + # Check if we're already inside a .trae or .trae-cn directory local current_dir_name="$(basename "$target_dir")" local trae_full_path - + if [ "$current_dir_name" = ".trae" ] || [ "$current_dir_name" = ".trae-cn" ]; then # Already inside the trae directory, use it directly trae_full_path="$target_dir" @@ -63,23 +65,23 @@ do_uninstall() { # Normal case: append trae_dir to target_dir trae_full_path="$target_dir/$trae_dir" fi - + echo "ECC Trae Uninstaller" echo "====================" echo "" echo "Target: $trae_full_path/" echo "" - + if [ ! -d "$trae_full_path" ]; then echo "Error: $trae_dir directory not found at $target_dir" exit 1 fi - + trae_root_resolved="$(resolve_path "$trae_full_path")" # Manifest file path MANIFEST="$trae_full_path/.ecc-manifest" - + if [ ! -f "$MANIFEST" ]; then echo "Warning: No manifest file found (.ecc-manifest)" echo "" @@ -99,7 +101,7 @@ do_uninstall() { echo "Removed: $trae_full_path/" exit 0 fi - + echo "Found manifest file - will only remove files installed by ECC" echo "" read -p "Are you sure you want to uninstall ECC from $trae_dir? (y/N) " -n 1 -r @@ -108,11 +110,11 @@ do_uninstall() { echo "Uninstall cancelled." exit 0 fi - + # Counters removed=0 skipped=0 - + # Read manifest and remove files while IFS= read -r file_path; do [ -z "$file_path" ] && continue @@ -166,7 +168,7 @@ do_uninstall() { removed=$((removed + 1)) fi done < <(find "$trae_full_path" -depth -type d -empty 2>/dev/null | sort -r) - + # Try to remove the main trae directory if it's empty if [ -d "$trae_full_path" ] && [ -z "$(ls -A "$trae_full_path" 2>/dev/null)" ]; then rmdir "$trae_full_path" 2>/dev/null || true @@ -175,7 +177,7 @@ do_uninstall() { removed=$((removed + 1)) fi fi - + echo "" echo "Uninstall complete!" echo "" diff --git a/scripts/ci/catalog.js b/scripts/ci/catalog.js index 54b11e07..8ef3dc92 100644 --- a/scripts/ci/catalog.js +++ b/scripts/ci/catalog.js @@ -76,9 +76,9 @@ function parseReadmeExpectations(readmeContent) { ); const tablePatterns = [ - { category: 'agents', regex: /\|\s*(?:\*\*)?Agents(?:\*\*)?\s*\|\s*PASS:\s*(\d+)\s+agents\s*\|/i, source: 'README.md comparison table' }, - { category: 'commands', regex: /\|\s*(?:\*\*)?Commands(?:\*\*)?\s*\|\s*PASS:\s*(\d+)\s+commands\s*\|/i, source: 'README.md comparison table' }, - { category: 'skills', regex: /\|\s*(?:\*\*)?Skills(?:\*\*)?\s*\|\s*PASS:\s*(\d+)\s+skills\s*\|/i, source: 'README.md comparison table' } + { category: 'agents', regex: /\|\s*(?:\*\*)?Agents(?:\*\*)?\s*\|\s*(?:(?:PASS:|\u2705)\s*)?(\d+)\s+agents\s*\|/i, source: 'README.md comparison table' }, + { category: 'commands', regex: /\|\s*(?:\*\*)?Commands(?:\*\*)?\s*\|\s*(?:(?:PASS:|\u2705)\s*)?(\d+)\s+commands\s*\|/i, source: 'README.md comparison table' }, + { category: 'skills', regex: /\|\s*(?:\*\*)?Skills(?:\*\*)?\s*\|\s*(?:(?:PASS:|\u2705)\s*)?(\d+)\s+skills\s*\|/i, source: 'README.md comparison table' } ]; for (const pattern of tablePatterns) { diff --git a/scripts/ci/check-unicode-safety.js b/scripts/ci/check-unicode-safety.js index 4a01ee5e..4c35980d 100644 --- a/scripts/ci/check-unicode-safety.js +++ b/scripts/ci/check-unicode-safety.js @@ -39,6 +39,12 @@ const textExtensions = new Set([ '.rs', ]); +const writableExtensions = new Set([ + '.md', + '.mdx', + '.txt', +]); + const writeModeSkip = new Set([ path.normalize('scripts/ci/check-unicode-safety.js'), path.normalize('tests/scripts/check-unicode-safety.test.js'), @@ -47,6 +53,11 @@ const writeModeSkip = new Set([ const dangerousInvisibleRe = /[\u200B-\u200D\u2060\uFEFF\u202A-\u202E\u2066-\u2069\uFE00-\uFE0F\u{E0100}-\u{E01EF}]/gu; const emojiRe = /[\p{Extended_Pictographic}\p{Regional_Indicator}]/gu; +const allowedSymbolCodePoints = new Set([ + 0x00A9, + 0x00AE, + 0x2122, +]); const targetedReplacements = [ [new RegExp(`${String.fromCodePoint(0x26A0)}(?:\\uFE0F)?`, 'gu'), 'WARNING:'], @@ -64,6 +75,10 @@ function isTextFile(filePath) { return textExtensions.has(path.extname(filePath).toLowerCase()); } +function canAutoWrite(relativePath) { + return writableExtensions.has(path.extname(relativePath).toLowerCase()); +} + function listFiles(dirPath) { const results = []; for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) { @@ -87,6 +102,10 @@ function lineAndColumn(text, index) { return { line, column }; } +function isAllowedEmojiLikeSymbol(char) { + return allowedSymbolCodePoints.has(char.codePointAt(0)); +} + function sanitizeText(text) { let next = text; next = next.replace(dangerousInvisibleRe, ''); @@ -95,7 +114,7 @@ function sanitizeText(text) { next = next.replace(pattern, replacement); } - next = next.replace(emojiRe, ''); + next = next.replace(emojiRe, match => (isAllowedEmojiLikeSymbol(match) ? match : '')); next = next.replace(/^ +(?=\*\*)/gm, ''); next = next.replace(/^(\*\*)\s+/gm, '$1'); next = next.replace(/^(#+)\s{2,}/gm, '$1 '); @@ -111,6 +130,9 @@ function collectMatches(text, regex, kind) { const matches = []; for (const match of text.matchAll(regex)) { const char = match[0]; + if (kind === 'emoji' && isAllowedEmojiLikeSymbol(char)) { + continue; + } const index = match.index ?? 0; const { line, column } = lineAndColumn(text, index); matches.push({ @@ -136,7 +158,11 @@ for (const filePath of listFiles(repoRoot)) { continue; } - if (writeMode && !writeModeSkip.has(path.normalize(relativePath))) { + if ( + writeMode && + !writeModeSkip.has(path.normalize(relativePath)) && + canAutoWrite(relativePath) + ) { const sanitized = sanitizeText(text); if (sanitized !== text) { fs.writeFileSync(filePath, sanitized, 'utf8'); diff --git a/scripts/codex/install-global-git-hooks.sh b/scripts/codex/install-global-git-hooks.sh index 312a3701..ea11d852 100755 --- a/scripts/codex/install-global-git-hooks.sh +++ b/scripts/codex/install-global-git-hooks.sh @@ -3,8 +3,8 @@ set -euo pipefail # Install ECC git safety hooks globally via core.hooksPath. # Usage: -# ./scripts/codex/install-global-git-hooks.sh -# ./scripts/codex/install-global-git-hooks.sh --dry-run +# ./scripts/codex/install-global-git-hooks.sh +# ./scripts/codex/install-global-git-hooks.sh --dry-run MODE="apply" if [[ "${1:-}" == "--dry-run" ]]; then diff --git a/scripts/hooks/insaits-security-monitor.py b/scripts/hooks/insaits-security-monitor.py index ffec52fc..da1bbf24 100644 --- a/scripts/hooks/insaits-security-monitor.py +++ b/scripts/hooks/insaits-security-monitor.py @@ -133,7 +133,7 @@ def write_audit(event: Dict[str, Any]) -> None: """ try: enriched: Dict[str, Any] = { -**event, + **event, "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), } enriched["hash"] = hashlib.sha256( diff --git a/scripts/hooks/pre-bash-commit-quality.js b/scripts/hooks/pre-bash-commit-quality.js index 7dee5b06..a601317d 100644 --- a/scripts/hooks/pre-bash-commit-quality.js +++ b/scripts/hooks/pre-bash-commit-quality.js @@ -49,7 +49,7 @@ function getStagedFileContent(filePath) { /** * Check if a file should be quality-checked - * @param {string} filePath + * @param {string} filePath * @returns {boolean} */ function shouldCheckFile(filePath) { @@ -59,22 +59,22 @@ function shouldCheckFile(filePath) { /** * Find issues in file content - * @param {string} filePath + * @param {string} filePath * @returns {object[]} Array of issues found */ function findFileIssues(filePath) { const issues = []; - + try { const content = getStagedFileContent(filePath); if (content == null) { return issues; } const lines = content.split('\n'); - + lines.forEach((line, index) => { const lineNum = index + 1; - + // Check for console.log if (line.includes('console.log') && !line.trim().startsWith('//') && !line.trim().startsWith('*')) { issues.push({ @@ -84,7 +84,7 @@ function findFileIssues(filePath) { severity: 'warning' }); } - + // Check for debugger statements if (/\bdebugger\b/.test(line) && !line.trim().startsWith('//')) { issues.push({ @@ -94,7 +94,7 @@ function findFileIssues(filePath) { severity: 'error' }); } - + // Check for TODO/FIXME without issue reference const todoMatch = line.match(/\/\/\s*(TODO|FIXME):?\s*(.+)/); if (todoMatch && !todoMatch[2].match(/#\d+|issue/i)) { @@ -105,7 +105,7 @@ function findFileIssues(filePath) { severity: 'info' }); } - + // Check for hardcoded secrets (basic patterns) const secretPatterns = [ { pattern: /sk-[a-zA-Z0-9]{20,}/, name: 'OpenAI API key' }, @@ -113,7 +113,7 @@ function findFileIssues(filePath) { { pattern: /AKIA[A-Z0-9]{16}/, name: 'AWS Access Key' }, { pattern: /api[_-]?key\s*[=:]\s*['"][^'"]+['"]/i, name: 'API key' } ]; - + for (const { pattern, name } of secretPatterns) { if (pattern.test(line)) { issues.push({ @@ -128,23 +128,23 @@ function findFileIssues(filePath) { } catch { // File not readable, skip } - + return issues; } /** * Validate commit message format - * @param {string} command + * @param {string} command * @returns {object|null} Validation result or null if no message to validate */ function validateCommitMessage(command) { // Extract commit message from command const messageMatch = command.match(/(?:-m|--message)[=\s]+["']?([^"']+)["']?/); if (!messageMatch) return null; - + const message = messageMatch[1]; const issues = []; - + // Check conventional commit format const conventionalCommit = /^(feat|fix|docs|style|refactor|test|chore|build|ci|perf|revert)(\(.+\))?:\s*.+/; if (!conventionalCommit.test(message)) { @@ -154,7 +154,7 @@ function validateCommitMessage(command) { suggestion: 'Use format: type(scope): description (e.g., "feat(auth): add login flow")' }); } - + // Check message length if (message.length > 72) { issues.push({ @@ -163,7 +163,7 @@ function validateCommitMessage(command) { suggestion: 'Keep the first line under 72 characters' }); } - + // Check for lowercase first letter (conventional) if (conventionalCommit.test(message)) { const afterColon = message.split(':')[1]; @@ -175,7 +175,7 @@ function validateCommitMessage(command) { }); } } - + // Check for trailing period if (message.endsWith('.')) { issues.push({ @@ -184,26 +184,26 @@ function validateCommitMessage(command) { suggestion: 'Remove the trailing period' }); } - + return { message, issues }; } /** * Run linter on staged files - * @param {string[]} files + * @param {string[]} files * @returns {object} Lint results */ function runLinter(files) { const jsFiles = files.filter(f => /\.(js|jsx|ts|tsx)$/.test(f)); const pyFiles = files.filter(f => f.endsWith('.py')); const goFiles = files.filter(f => f.endsWith('.go')); - + const results = { eslint: null, pylint: null, golint: null }; - + // Run ESLint if available if (jsFiles.length > 0) { const eslintBin = process.platform === 'win32' ? 'eslint.cmd' : 'eslint'; @@ -220,7 +220,7 @@ function runLinter(files) { }; } } - + // Run Pylint if available if (pyFiles.length > 0) { try { @@ -241,7 +241,7 @@ function runLinter(files) { // Pylint not available } } - + // Run golint if available if (goFiles.length > 0) { try { @@ -262,7 +262,7 @@ function runLinter(files) { // golint not available } } - + return results; } @@ -275,41 +275,41 @@ function evaluate(rawInput) { try { const input = JSON.parse(rawInput); const command = input.tool_input?.command || ''; - + // Only run for git commit commands if (!command.includes('git commit')) { return { output: rawInput, exitCode: 0 }; } - + // Check if this is an amend (skip checks for amends to avoid blocking) if (command.includes('--amend')) { return { output: rawInput, exitCode: 0 }; } - + // Get staged files const stagedFiles = getStagedFiles(); - + if (stagedFiles.length === 0) { console.error('[Hook] No staged files found. Use "git add" to stage files first.'); return { output: rawInput, exitCode: 0 }; } - + console.error(`[Hook] Checking ${stagedFiles.length} staged file(s)...`); - + // Check each staged file const filesToCheck = stagedFiles.filter(shouldCheckFile); let totalIssues = 0; let errorCount = 0; let warningCount = 0; let infoCount = 0; - + for (const file of filesToCheck) { const fileIssues = findFileIssues(file); if (fileIssues.length > 0) { - console.error(`\n ${file}`); + console.error(`\n[FILE] ${file}`); for (const issue of fileIssues) { - const icon = issue.severity === 'error' ? 'FAIL:' : issue.severity === 'warning' ? 'WARNING:' : ''; - console.error(` ${icon} Line ${issue.line}: ${issue.message}`); + const label = issue.severity === 'error' ? 'ERROR' : issue.severity === 'warning' ? 'WARNING' : 'INFO'; + console.error(` ${label} Line ${issue.line}: ${issue.message}`); totalIssues++; if (issue.severity === 'error') errorCount++; if (issue.severity === 'warning') warningCount++; @@ -317,51 +317,51 @@ function evaluate(rawInput) { } } } - + // Validate commit message if provided const messageValidation = validateCommitMessage(command); if (messageValidation && messageValidation.issues.length > 0) { - console.error('\n Commit Message Issues:'); + console.error('\nCommit Message Issues:'); for (const issue of messageValidation.issues) { - console.error(` WARNING: ${issue.message}`); + console.error(` WARNING ${issue.message}`); if (issue.suggestion) { - console.error(` ${issue.suggestion}`); + console.error(` TIP ${issue.suggestion}`); } totalIssues++; warningCount++; } } - + // Run linter const lintResults = runLinter(filesToCheck); - + if (lintResults.eslint && !lintResults.eslint.success) { - console.error('\n ESLint Issues:'); + console.error('\nESLint Issues:'); console.error(lintResults.eslint.output); totalIssues++; errorCount++; } - + if (lintResults.pylint && !lintResults.pylint.success) { - console.error('\n Pylint Issues:'); + console.error('\nPylint Issues:'); console.error(lintResults.pylint.output); totalIssues++; errorCount++; } - + if (lintResults.golint && !lintResults.golint.success) { - console.error('\n golint Issues:'); + console.error('\ngolint Issues:'); console.error(lintResults.golint.output); totalIssues++; errorCount++; } - + // Summary if (totalIssues > 0) { - console.error(`\n Summary: ${totalIssues} issue(s) found (${errorCount} error(s), ${warningCount} warning(s), ${infoCount} info)`); - + console.error(`\nSummary: ${totalIssues} issue(s) found (${errorCount} error(s), ${warningCount} warning(s), ${infoCount} info)`); + if (errorCount > 0) { - console.error('\n[Hook] FAIL: Commit blocked due to critical issues. Fix them before committing.'); + console.error('\n[Hook] ERROR: Commit blocked due to critical issues. Fix them before committing.'); return { output: rawInput, exitCode: 2 }; } else { console.error('\n[Hook] WARNING: Warnings found. Consider fixing them, but commit is allowed.'); @@ -370,12 +370,12 @@ function evaluate(rawInput) { } else { console.error('\n[Hook] PASS: All checks passed!'); } - + } catch (error) { console.error(`[Hook] Error: ${error.message}`); // Non-blocking on error } - + return { output: rawInput, exitCode: 0 }; } @@ -387,14 +387,14 @@ function run(rawInput) { if (require.main === module) { let data = ''; process.stdin.setEncoding('utf8'); - + process.stdin.on('data', chunk => { if (data.length < MAX_STDIN) { const remaining = MAX_STDIN - data.length; data += chunk.substring(0, remaining); } }); - + process.stdin.on('end', () => { const result = evaluate(data); process.stdout.write(result.output); diff --git a/scripts/skill-create-output.js b/scripts/skill-create-output.js index f5bf360f..66c900f0 100644 --- a/scripts/skill-create-output.js +++ b/scripts/skill-create-output.js @@ -64,7 +64,7 @@ function sleep(ms) { } async function animateProgress(label, steps, callback) { - process.stdout.write(`\n${chalk.cyan('')} ${label}...\n`); + process.stdout.write(`\n${chalk.cyan('[RUN]')} ${label}...\n`); for (let i = 0; i < steps.length; i++) { const step = steps[i]; @@ -72,7 +72,7 @@ async function animateProgress(label, steps, callback) { await sleep(step.duration || 500); process.stdout.clearLine?.(0) || process.stdout.write('\r'); process.stdout.cursorTo?.(0) || process.stdout.write('\r'); - process.stdout.write(` ${chalk.green('✓')} ${step.name}\n`); + process.stdout.write(` ${chalk.green('[DONE]')} ${step.name}\n`); if (callback) callback(step, i); } } @@ -90,7 +90,7 @@ class SkillCreateOutput { console.log('\n'); console.log(chalk.bold(chalk.magenta('╔════════════════════════════════════════════════════════════════╗'))); - console.log(chalk.bold(chalk.magenta('║')) + chalk.bold(' ECC Skill Creator ') + chalk.bold(chalk.magenta('║'))); + console.log(chalk.bold(chalk.magenta('║')) + chalk.bold(' ECC Skill Creator ') + chalk.bold(chalk.magenta('║'))); console.log(chalk.bold(chalk.magenta('║')) + ` ${subtitle}${' '.repeat(Math.max(0, 59 - stripAnsi(subtitle).length))}` + chalk.bold(chalk.magenta('║'))); console.log(chalk.bold(chalk.magenta('╚════════════════════════════════════════════════════════════════╝'))); console.log(''); @@ -111,7 +111,7 @@ class SkillCreateOutput { analysisResults(data) { console.log('\n'); - console.log(box(' Analysis Results', ` + console.log(box('Analysis Results', ` ${chalk.bold('Commits Analyzed:')} ${chalk.yellow(data.commits)} ${chalk.bold('Time Range:')} ${chalk.gray(data.timeRange)} ${chalk.bold('Contributors:')} ${chalk.cyan(data.contributors)} @@ -121,7 +121,7 @@ ${chalk.bold('Files Tracked:')} ${chalk.green(data.files)} patterns(patterns) { console.log('\n'); - console.log(chalk.bold(chalk.cyan(' Key Patterns Discovered:'))); + console.log(chalk.bold(chalk.cyan('Key Patterns Discovered:'))); console.log(chalk.gray('─'.repeat(50))); patterns.forEach((pattern, i) => { @@ -137,26 +137,26 @@ ${chalk.bold('Files Tracked:')} ${chalk.green(data.files)} instincts(instincts) { console.log('\n'); - console.log(box(' Instincts Generated', instincts.map((inst, i) => + console.log(box('Instincts Generated', instincts.map((inst, i) => `${chalk.yellow(`${i + 1}.`)} ${chalk.bold(inst.name)} ${chalk.gray(`(${Math.round(inst.confidence * 100)}%)`)}` ).join('\n'))); } output(skillPath, instinctsPath) { console.log('\n'); - console.log(chalk.bold(chalk.green(' Generation Complete!'))); + console.log(chalk.bold(chalk.green('Generation Complete!'))); console.log(chalk.gray('─'.repeat(50))); console.log(` - ${chalk.green('')} ${chalk.bold('Skill File:')} + ${chalk.green('-')} ${chalk.bold('Skill File:')} ${chalk.cyan(skillPath)} - ${chalk.green('')} ${chalk.bold('Instincts File:')} + ${chalk.green('-')} ${chalk.bold('Instincts File:')} ${chalk.cyan(instinctsPath)} `); } nextSteps() { - console.log(box(' Next Steps', ` + console.log(box('Next Steps', ` ${chalk.yellow('1.')} Review the generated SKILL.md ${chalk.yellow('2.')} Import instincts: ${chalk.cyan('/instinct-import ')} ${chalk.yellow('3.')} View learned patterns: ${chalk.cyan('/instinct-status')} diff --git a/skills/ck/commands/resume.mjs b/skills/ck/commands/resume.mjs index 3a939361..605f1049 100644 --- a/skills/ck/commands/resume.mjs +++ b/skills/ck/commands/resume.mjs @@ -28,7 +28,7 @@ if (projectPath && projectPath !== cwd) { if (existsSync(projectPath)) { console.log(`→ cd ${projectPath}`); } else { - console.log(`WARNING: Path not found: ${projectPath}`); + console.log(`WARNING Path not found: ${projectPath}`); } } diff --git a/skills/ck/hooks/session-start.mjs b/skills/ck/hooks/session-start.mjs index 9c5a61e9..e109149a 100644 --- a/skills/ck/hooks/session-start.mjs +++ b/skills/ck/hooks/session-start.mjs @@ -130,7 +130,7 @@ function main() { // Check if previous session ID exists in sessions array const alreadySaved = context.sessions?.some(s => s.id === prevSession.sessionId); if (!alreadySaved) { - summaryLines.push(`WARNING: Last session wasn't saved — run /ck:save to capture it`); + summaryLines.push(`WARNING Last session wasn't saved — run /ck:save to capture it`); } } @@ -142,7 +142,7 @@ function main() { const claudeMdGoal = extractClaudeMdGoal(cwd); if (claudeMdGoal && context.goal && claudeMdGoal.toLowerCase().trim() !== context.goal.toLowerCase().trim()) { - summaryLines.push(`WARNING: Goal mismatch — ck: "${context.goal.slice(0, 40)}" · CLAUDE.md: "${claudeMdGoal.slice(0, 40)}"`); + summaryLines.push(`WARNING Goal mismatch — ck: "${context.goal.slice(0, 40)}" · CLAUDE.md: "${claudeMdGoal.slice(0, 40)}"`); summaryLines.push(` Run /ck:save with updated goal to sync`); } @@ -165,7 +165,7 @@ function main() { '```', ``, `After the block, add one line: "Ready — what are we working on?"`, - `If you see WARNING: warnings above, mention them briefly after the block.`, + `If you see WARNING lines above, mention them briefly after the block.`, ].join('\n')); return parts; diff --git a/skills/continuous-learning-v2/agents/session-guardian.sh b/skills/continuous-learning-v2/agents/session-guardian.sh index 686aa04b..39fd7485 100755 --- a/skills/continuous-learning-v2/agents/session-guardian.sh +++ b/skills/continuous-learning-v2/agents/session-guardian.sh @@ -4,16 +4,16 @@ # Called by observer-loop.sh before spawning any Claude session. # # Config (env vars, all optional): -# OBSERVER_INTERVAL_SECONDS default: 300 (per-project cooldown) -# OBSERVER_LAST_RUN_LOG default: ~/.claude/observer-last-run.log -# OBSERVER_ACTIVE_HOURS_START default: 800 (8:00 AM local, set to 0 to disable) -# OBSERVER_ACTIVE_HOURS_END default: 2300 (11:00 PM local, set to 0 to disable) -# OBSERVER_MAX_IDLE_SECONDS default: 1800 (30 min; set to 0 to disable) +# OBSERVER_INTERVAL_SECONDS default: 300 (per-project cooldown) +# OBSERVER_LAST_RUN_LOG default: ~/.claude/observer-last-run.log +# OBSERVER_ACTIVE_HOURS_START default: 800 (8:00 AM local, set to 0 to disable) +# OBSERVER_ACTIVE_HOURS_END default: 2300 (11:00 PM local, set to 0 to disable) +# OBSERVER_MAX_IDLE_SECONDS default: 1800 (30 min; set to 0 to disable) # # Gate execution order (cheapest first): -# Gate 1: Time window check (~0ms, string comparison) -# Gate 2: Project cooldown log (~1ms, file read + mkdir lock) -# Gate 3: Idle detection (~5-50ms, OS syscall; fail open) +# Gate 1: Time window check (~0ms, string comparison) +# Gate 2: Project cooldown log (~1ms, file read + mkdir lock) +# Gate 3: Idle detection (~5-50ms, OS syscall; fail open) set -euo pipefail diff --git a/skills/continuous-learning-v2/agents/start-observer.sh b/skills/continuous-learning-v2/agents/start-observer.sh index ef3bc128..e9418a5c 100755 --- a/skills/continuous-learning-v2/agents/start-observer.sh +++ b/skills/continuous-learning-v2/agents/start-observer.sh @@ -5,13 +5,13 @@ # and creates instincts. Uses Haiku model for cost efficiency. # # v2.1: Project-scoped — detects current project and analyzes -# project-specific observations into project-scoped instincts. +# project-specific observations into project-scoped instincts. # # Usage: -# start-observer.sh # Start observer for current project (or global) -# start-observer.sh --reset # Clear lock and restart observer for current project -# start-observer.sh stop # Stop running observer -# start-observer.sh status # Check if observer is running +# start-observer.sh # Start observer for current project (or global) +# start-observer.sh --reset # Clear lock and restart observer for current project +# start-observer.sh stop # Stop running observer +# start-observer.sh status # Check if observer is running set -e diff --git a/skills/continuous-learning-v2/hooks/observe.sh b/skills/continuous-learning-v2/hooks/observe.sh index 2a3edff4..727eb47c 100755 --- a/skills/continuous-learning-v2/hooks/observe.sh +++ b/skills/continuous-learning-v2/hooks/observe.sh @@ -5,7 +5,7 @@ # Claude Code passes hook data via stdin as JSON. # # v2.1: Project-scoped observations — detects current project context -# and writes observations to project-specific directory. +# and writes observations to project-specific directory. # # Registered via plugin hooks/hooks.json (auto-loaded when plugin is enabled). # Can also be registered manually in ~/.claude/settings.json. @@ -92,9 +92,9 @@ if [ -n "${CLV2_CONFIG:-}" ] && [ -f "$(dirname "$CLV2_CONFIG")/disabled" ]; the fi # Prevent observe.sh from firing on non-human sessions to avoid: -# - ECC observing its own Haiku observer sessions (self-loop) -# - ECC observing other tools' automated sessions -# - automated sessions creating project-scoped homunculus metadata +# - ECC observing its own Haiku observer sessions (self-loop) +# - ECC observing other tools' automated sessions +# - automated sessions creating project-scoped homunculus metadata # Layer 1: entrypoint. Only interactive terminal sessions should continue. # sdk-ts: Agent SDK sessions can be human-interactive (e.g. via Happy). diff --git a/skills/continuous-learning-v2/scripts/detect-project.sh b/skills/continuous-learning-v2/scripts/detect-project.sh index 6d046ffb..47b1e363 100755 --- a/skills/continuous-learning-v2/scripts/detect-project.sh +++ b/skills/continuous-learning-v2/scripts/detect-project.sh @@ -5,19 +5,19 @@ # Sourced by observe.sh and start-observer.sh. # # Exports: -# _CLV2_PROJECT_ID - Short hash identifying the project (or "global") -# _CLV2_PROJECT_NAME - Human-readable project name -# _CLV2_PROJECT_ROOT - Absolute path to project root -# _CLV2_PROJECT_DIR - Project-scoped storage directory under homunculus +# _CLV2_PROJECT_ID - Short hash identifying the project (or "global") +# _CLV2_PROJECT_NAME - Human-readable project name +# _CLV2_PROJECT_ROOT - Absolute path to project root +# _CLV2_PROJECT_DIR - Project-scoped storage directory under homunculus # # Also sets unprefixed convenience aliases: -# PROJECT_ID, PROJECT_NAME, PROJECT_ROOT, PROJECT_DIR +# PROJECT_ID, PROJECT_NAME, PROJECT_ROOT, PROJECT_DIR # # Detection priority: -# 1. CLAUDE_PROJECT_DIR env var (if set) -# 2. git remote URL (hashed for uniqueness across machines) -# 3. git repo root path (fallback, machine-specific) -# 4. "global" (no project context detected) +# 1. CLAUDE_PROJECT_DIR env var (if set) +# 2. git remote URL (hashed for uniqueness across machines) +# 3. git repo root path (fallback, machine-specific) +# 4. "global" (no project context detected) _CLV2_HOMUNCULUS_DIR="${HOME}/.claude/homunculus" _CLV2_PROJECTS_DIR="${_CLV2_HOMUNCULUS_DIR}/projects" diff --git a/skills/continuous-learning/evaluate-session.sh b/skills/continuous-learning/evaluate-session.sh index 6c86af9e..a5946fc8 100755 --- a/skills/continuous-learning/evaluate-session.sh +++ b/skills/continuous-learning/evaluate-session.sh @@ -8,15 +8,15 @@ # # Hook config (in ~/.claude/settings.json): # { -# "hooks": { -# "Stop": [{ -# "matcher": "*", -# "hooks": [{ -# "type": "command", -# "command": "~/.claude/skills/continuous-learning/evaluate-session.sh" -# }] -# }] -# } +# "hooks": { +# "Stop": [{ +# "matcher": "*", +# "hooks": [{ +# "type": "command", +# "command": "~/.claude/skills/continuous-learning/evaluate-session.sh" +# }] +# }] +# } # } # # Patterns to detect: error_resolution, debugging_techniques, workarounds, project_specific diff --git a/skills/rules-distill/scripts/scan-rules.sh b/skills/rules-distill/scripts/scan-rules.sh index 6d47841e..ff011bcb 100755 --- a/skills/rules-distill/scripts/scan-rules.sh +++ b/skills/rules-distill/scripts/scan-rules.sh @@ -4,7 +4,7 @@ # Output: JSON to stdout # # Environment: -# RULES_DISTILL_DIR Override ~/.claude/rules (for testing only) +# RULES_DISTILL_DIR Override ~/.claude/rules (for testing only) set -euo pipefail diff --git a/skills/rules-distill/scripts/scan-skills.sh b/skills/rules-distill/scripts/scan-skills.sh index 84ef3d9d..1c49cd9d 100755 --- a/skills/rules-distill/scripts/scan-skills.sh +++ b/skills/rules-distill/scripts/scan-skills.sh @@ -7,9 +7,9 @@ # script always picks up project-level skills without relying on the caller. # # Environment: -# RULES_DISTILL_GLOBAL_DIR Override ~/.claude/skills (for testing only; -# do not set in production — intended for bats tests) -# RULES_DISTILL_PROJECT_DIR Override project dir detection (for testing only) +# RULES_DISTILL_GLOBAL_DIR Override ~/.claude/skills (for testing only; +# do not set in production — intended for bats tests) +# RULES_DISTILL_PROJECT_DIR Override project dir detection (for testing only) set -euo pipefail diff --git a/skills/skill-stocktake/scripts/quick-diff.sh b/skills/skill-stocktake/scripts/quick-diff.sh index 6bd33ca1..c145100a 100755 --- a/skills/skill-stocktake/scripts/quick-diff.sh +++ b/skills/skill-stocktake/scripts/quick-diff.sh @@ -7,9 +7,9 @@ # script always picks up project-level skills without relying on the caller. # # Environment: -# SKILL_STOCKTAKE_GLOBAL_DIR Override ~/.claude/skills (for testing only; -# do not set in production — intended for bats tests) -# SKILL_STOCKTAKE_PROJECT_DIR Override project dir detection (for testing only) +# SKILL_STOCKTAKE_GLOBAL_DIR Override ~/.claude/skills (for testing only; +# do not set in production — intended for bats tests) +# SKILL_STOCKTAKE_PROJECT_DIR Override project dir detection (for testing only) set -euo pipefail diff --git a/skills/skill-stocktake/scripts/save-results.sh b/skills/skill-stocktake/scripts/save-results.sh index 46a8c37f..32952007 100755 --- a/skills/skill-stocktake/scripts/save-results.sh +++ b/skills/skill-stocktake/scripts/save-results.sh @@ -3,7 +3,7 @@ # Usage: save-results.sh RESULTS_JSON <<< "$EVAL_JSON" # # stdin format: -# { "skills": {...}, "mode"?: "full"|"quick", "batch_progress"?: {...} } +# { "skills": {...}, "mode"?: "full"|"quick", "batch_progress"?: {...} } # # Always sets evaluated_at to current UTC time via `date -u`. # Merges stdin .skills into existing results.json (new entries override old). diff --git a/skills/skill-stocktake/scripts/scan.sh b/skills/skill-stocktake/scripts/scan.sh index 8328f565..5f1d12db 100755 --- a/skills/skill-stocktake/scripts/scan.sh +++ b/skills/skill-stocktake/scripts/scan.sh @@ -7,9 +7,9 @@ # script always picks up project-level skills without relying on the caller. # # Environment: -# SKILL_STOCKTAKE_GLOBAL_DIR Override ~/.claude/skills (for testing only; -# do not set in production — intended for bats tests) -# SKILL_STOCKTAKE_PROJECT_DIR Override project dir detection (for testing only) +# SKILL_STOCKTAKE_GLOBAL_DIR Override ~/.claude/skills (for testing only; +# do not set in production — intended for bats tests) +# SKILL_STOCKTAKE_PROJECT_DIR Override project dir detection (for testing only) set -euo pipefail diff --git a/skills/strategic-compact/suggest-compact.sh b/skills/strategic-compact/suggest-compact.sh index f866bde3..38f5aa91 100755 --- a/skills/strategic-compact/suggest-compact.sh +++ b/skills/strategic-compact/suggest-compact.sh @@ -10,15 +10,15 @@ # # Hook config (in ~/.claude/settings.json): # { -# "hooks": { -# "PreToolUse": [{ -# "matcher": "Edit|Write", -# "hooks": [{ -# "type": "command", -# "command": "~/.claude/skills/strategic-compact/suggest-compact.sh" -# }] -# }] -# } +# "hooks": { +# "PreToolUse": [{ +# "matcher": "Edit|Write", +# "hooks": [{ +# "type": "command", +# "command": "~/.claude/skills/strategic-compact/suggest-compact.sh" +# }] +# }] +# } # } # # Criteria for suggesting compact: diff --git a/skills/videodb/scripts/ws_listener.py b/skills/videodb/scripts/ws_listener.py index d9579b81..0ec20105 100644 --- a/skills/videodb/scripts/ws_listener.py +++ b/skills/videodb/scripts/ws_listener.py @@ -78,7 +78,7 @@ def ensure_private_dir(path: Path) -> Path: def parse_args() -> tuple[bool, Path]: clear = False output_dir: str | None = None - + args = sys.argv[1:] for arg in args: if arg == "--clear": @@ -87,7 +87,7 @@ def parse_args() -> tuple[bool, Path]: raise SystemExit(f"Unknown flag: {arg}") elif not arg.startswith("-"): output_dir = arg - + if output_dir is None: events_dir = os.environ.get("VIDEODB_EVENTS_DIR") if events_dir: @@ -147,10 +147,10 @@ def is_fatal_error(exc: Exception) -> bool: async def listen_with_retry(): """Main listen loop with auto-reconnect and exponential backoff.""" global _first_connection - + retry_count = 0 backoff = INITIAL_BACKOFF - + while retry_count < MAX_RETRIES: try: conn = videodb.connect() @@ -168,11 +168,11 @@ async def listen_with_retry(): raise retry_count += 1 log(f"Connection error: {e}") - + if retry_count >= MAX_RETRIES: log(f"Max retries ({MAX_RETRIES}) exceeded, exiting") break - + log(f"Reconnecting in {backoff}s (attempt {retry_count}/{MAX_RETRIES})...") await asyncio.sleep(backoff) backoff = min(backoff * 2, MAX_BACKOFF) @@ -233,20 +233,20 @@ async def main_async(): """Async main with signal handling.""" loop = asyncio.get_running_loop() shutdown_event = asyncio.Event() - + def handle_signal(): log("Received shutdown signal") shutdown_event.set() - + # Register signal handlers for sig in (signal.SIGINT, signal.SIGTERM): with contextlib.suppress(NotImplementedError): loop.add_signal_handler(sig, handle_signal) - + # Run listener with cancellation support listen_task = asyncio.create_task(listen_with_retry()) shutdown_task = asyncio.create_task(shutdown_event.wait()) - + _done, pending = await asyncio.wait( [listen_task, shutdown_task], return_when=asyncio.FIRST_COMPLETED, @@ -254,7 +254,7 @@ async def main_async(): if listen_task.done(): await listen_task - + # Cancel remaining tasks for task in pending: task.cancel() @@ -266,7 +266,7 @@ async def main_async(): for sig in (signal.SIGINT, signal.SIGTERM): with contextlib.suppress(NotImplementedError): loop.remove_signal_handler(sig) - + log("Shutdown complete") diff --git a/tests/lib/session-aliases.test.js b/tests/lib/session-aliases.test.js index 5e40679d..8e4b7226 100644 --- a/tests/lib/session-aliases.test.js +++ b/tests/lib/session-aliases.test.js @@ -48,6 +48,7 @@ function resetAliases() { } function runTests() { + const rocketEmoji = String.fromCodePoint(0x1F680); console.log('\n=== Testing session-aliases.js ===\n'); let passed = 0; @@ -1441,7 +1442,7 @@ function runTests() { 'CJK characters should be rejected'); // Emoji - const emojiResult = aliases.resolveAlias('rocket-'); + const emojiResult = aliases.resolveAlias(`rocket-${rocketEmoji}`); assert.strictEqual(emojiResult, null, 'Emoji should be rejected by the ASCII-only regex'); diff --git a/tests/lib/utils.test.js b/tests/lib/utils.test.js index f050e954..2f7a0300 100644 --- a/tests/lib/utils.test.js +++ b/tests/lib/utils.test.js @@ -27,6 +27,8 @@ function test(name, fn) { // Test suite function runTests() { + const rocketParty = String.fromCodePoint(0x1F680, 0x1F389); + const partyEmoji = String.fromCodePoint(0x1F389); console.log('\n=== Testing utils.js ===\n'); let passed = 0; @@ -166,9 +168,12 @@ function runTests() { if (test('sanitizeSessionId returns stable hashes for non-ASCII values', () => { const chinese = utils.sanitizeSessionId('我的项目'); const cyrillic = utils.sanitizeSessionId('проект'); + const emoji = utils.sanitizeSessionId(rocketParty); assert.ok(/^[a-f0-9]{8}$/.test(chinese), `Expected 8-char hash, got: ${chinese}`); assert.ok(/^[a-f0-9]{8}$/.test(cyrillic), `Expected 8-char hash, got: ${cyrillic}`); + assert.ok(/^[a-f0-9]{8}$/.test(emoji), `Expected 8-char hash, got: ${emoji}`); assert.notStrictEqual(chinese, cyrillic); + assert.notStrictEqual(chinese, emoji); assert.strictEqual(utils.sanitizeSessionId('日本語プロジェクト'), utils.sanitizeSessionId('日本語プロジェクト')); })) passed++; else failed++; @@ -704,7 +709,7 @@ function runTests() { if (test('writeFile handles unicode content', () => { const testFile = path.join(utils.getTempDir(), `utils-test-${Date.now()}.txt`); try { - const unicode = '日本語テスト 中文 émojis'; + const unicode = `日本語テスト ${String.fromCodePoint(0x1F680)} émojis`; utils.writeFile(testFile, unicode); const content = utils.readFile(testFile); assert.strictEqual(content, unicode); @@ -1868,18 +1873,18 @@ function runTests() { } })) passed++; else failed++; - // ── Round 108: grepFile with Unicode content — UTF-16 string matching on split lines ── - console.log('\nRound 108: grepFile (Unicode — regex matching on UTF-16 split lines):'); - if (test('grepFile finds Unicode patterns across lines', () => { + // ── Round 108: grepFile with Unicode/emoji content — UTF-16 string matching on split lines ── + console.log('\nRound 108: grepFile (Unicode/emoji — regex matching on UTF-16 split lines):'); + if (test('grepFile finds Unicode emoji patterns across lines', () => { const tmpDir = fs.mkdtempSync(path.join(utils.getTempDir(), 'r108-grep-unicode-')); const testFile = path.join(tmpDir, 'test.txt'); try { - fs.writeFileSync(testFile, '猫 celebration\nnormal line\n猫 party\n日本語テスト'); - const unicodeResults = utils.grepFile(testFile, /猫/); - assert.strictEqual(unicodeResults.length, 2, - 'Should find Unicode matches on 2 lines (lines 1 and 3)'); - assert.strictEqual(unicodeResults[0].lineNumber, 1); - assert.strictEqual(unicodeResults[1].lineNumber, 3); + fs.writeFileSync(testFile, `${partyEmoji} celebration\nnormal line\n${partyEmoji} party\n日本語テスト`); + const emojiResults = utils.grepFile(testFile, new RegExp(partyEmoji, 'u')); + assert.strictEqual(emojiResults.length, 2, + 'Should find emoji on 2 lines (lines 1 and 3)'); + assert.strictEqual(emojiResults[0].lineNumber, 1); + assert.strictEqual(emojiResults[1].lineNumber, 3); const cjkResults = utils.grepFile(testFile, /日本語/); assert.strictEqual(cjkResults.length, 1, 'Should find CJK characters on line 4'); diff --git a/tests/run-all.js b/tests/run-all.js index a417b453..46a3eb19 100644 --- a/tests/run-all.js +++ b/tests/run-all.js @@ -67,7 +67,7 @@ for (const testFile of testFiles) { const displayPath = testFile.split(path.sep).join('/'); if (!fs.existsSync(testPath)) { - console.log(`WARNING: Skipping ${displayPath} (file not found)`); + console.log(`WARNING Skipping ${displayPath} (file not found)`); continue; } diff --git a/tests/scripts/check-unicode-safety.test.js b/tests/scripts/check-unicode-safety.test.js index fafc1c81..d52b7c15 100644 --- a/tests/scripts/check-unicode-safety.test.js +++ b/tests/scripts/check-unicode-safety.test.js @@ -35,6 +35,7 @@ function makeTempRoot(prefix) { const warningEmoji = String.fromCodePoint(0x26A0, 0xFE0F); const toolsEmoji = String.fromCodePoint(0x1F6E0, 0xFE0F); const zeroWidthSpace = String.fromCodePoint(0x200B); +const rocketEmoji = String.fromCodePoint(0x1F680); let passed = 0; let failed = 0; @@ -78,6 +79,36 @@ if ( passed++; else failed++; +if ( + test('write mode does not rewrite executable files', () => { + const root = makeTempRoot('ecc-unicode-code-'); + fs.mkdirSync(path.join(root, 'scripts'), { recursive: true }); + const scriptFile = path.join(root, 'scripts', 'sample.js'); + const original = `const label = "Launch ${rocketEmoji}";\n`; + fs.writeFileSync(scriptFile, original); + + const result = runCheck(root, ['--write']); + assert.notStrictEqual(result.status, 0, result.stdout + result.stderr); + assert.match(result.stderr, /scripts\/sample\.js:1:23 emoji U\+1F680/); + assert.strictEqual(fs.readFileSync(scriptFile, 'utf8'), original); + }) +) + passed++; +else failed++; + +if ( + test('plain symbols like copyright remain allowed', () => { + const root = makeTempRoot('ecc-unicode-symbols-'); + fs.mkdirSync(path.join(root, 'docs'), { recursive: true }); + fs.writeFileSync(path.join(root, 'docs', 'legal.md'), 'Copyright © ECC\nTrademark ® ECC\n'); + + const result = runCheck(root); + assert.strictEqual(result.status, 0, result.stdout + result.stderr); + }) +) + passed++; +else failed++; + console.log(`\nPassed: ${passed}`); console.log(`Failed: ${failed}`); process.exit(failed > 0 ? 1 : 0);