From 8b6543929e8e7ec80c851b8a0d4e04eebcbf1026 Mon Sep 17 00:00:00 2001 From: Gaurav Dubey Date: Sat, 4 Jul 2026 09:10:44 +0530 Subject: [PATCH] chore(continuous-learning-v2): standardize shell shebangs to env bash (#2401) * chore(continuous-learning-v2): standardize shell shebangs to env bash Three scripts under skills/continuous-learning-v2/ used the hardcoded `#!/bin/bash` shebang while the other four already used the portable `#!/usr/bin/env bash`: - hooks/observe.sh (runs on every hook invocation) - scripts/detect-project.sh - agents/start-observer.sh The hardcoded interpreter path fails to execute on systems where bash is not installed at /bin/bash (NixOS, some Homebrew layouts, FreeBSD). Standardize all three to `#!/usr/bin/env bash`, matching the repo-wide majority convention, and add a regression test that asserts shebang uniformity for every shell script in this skill so the inconsistency cannot reappear. Fixes #2303 * test(continuous-learning-v2): harden shebang test runner Address review feedback on the shebang-consistency regression test: - firstLine() now splits on /\r?\n/ so a script checked out with CRLF line endings does not leave a trailing carriage return that would break the shebang comparison on Windows. - The test() helper now surfaces the full error (stack trace, not just the message) on failure and writes pass/fail lines via process.stdout/stderr so diagnostics are preserved. * test(continuous-learning-v2): skip hidden dirs in shebang scan The recursive shell-script scan now skips hidden directories (e.g. the observer's runtime `.observer-tmp`). This keeps the shebang-consistency check deterministic: only committed skill scripts are examined, and an untracked local artifact left over from an observer run can no longer cause a false failure. --- .../agents/start-observer.sh | 2 +- .../continuous-learning-v2/hooks/observe.sh | 2 +- .../scripts/detect-project.sh | 2 +- ...nuous-learning-shebang-consistency.test.js | 101 ++++++++++++++++++ 4 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 tests/hooks/continuous-learning-shebang-consistency.test.js diff --git a/skills/continuous-learning-v2/agents/start-observer.sh b/skills/continuous-learning-v2/agents/start-observer.sh index 096a5d7bb..e31209f9a 100755 --- a/skills/continuous-learning-v2/agents/start-observer.sh +++ b/skills/continuous-learning-v2/agents/start-observer.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # Continuous Learning v2 - Observer Agent Launcher # # Starts the background observer agent that analyzes observations diff --git a/skills/continuous-learning-v2/hooks/observe.sh b/skills/continuous-learning-v2/hooks/observe.sh index 45d962971..48172e981 100755 --- a/skills/continuous-learning-v2/hooks/observe.sh +++ b/skills/continuous-learning-v2/hooks/observe.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # Continuous Learning v2 - Observation Hook # # Captures tool use events for pattern analysis. diff --git a/skills/continuous-learning-v2/scripts/detect-project.sh b/skills/continuous-learning-v2/scripts/detect-project.sh index dbe9c5edc..2d129fe8c 100755 --- a/skills/continuous-learning-v2/scripts/detect-project.sh +++ b/skills/continuous-learning-v2/scripts/detect-project.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # Continuous Learning v2 - Project Detection Helper # # Shared logic for detecting current project context. diff --git a/tests/hooks/continuous-learning-shebang-consistency.test.js b/tests/hooks/continuous-learning-shebang-consistency.test.js new file mode 100644 index 000000000..630124205 --- /dev/null +++ b/tests/hooks/continuous-learning-shebang-consistency.test.js @@ -0,0 +1,101 @@ +/** + * Tests for shebang consistency across continuous-learning-v2 shell scripts + * + * Every `*.sh` script under skills/continuous-learning-v2/ must use the + * portable `#!/usr/bin/env bash` shebang rather than the hardcoded + * `#!/bin/bash`. The hardcoded interpreter path fails on systems where bash + * is not installed at /bin/bash (NixOS, some Homebrew layouts, FreeBSD), and + * observe.sh runs on every hook invocation. This guards against the + * inconsistency (#2303) reappearing. + * + * Run with: node tests/hooks/continuous-learning-shebang-consistency.test.js + */ + +'use strict'; + +const assert = require('assert'); +const path = require('path'); +const fs = require('fs'); + +let passed = 0; +let failed = 0; + +function test(name, fn) { + try { + fn(); + process.stdout.write(` ✓ ${name}\n`); + passed++; + } catch (err) { + process.stderr.write(` ✗ ${name}\n`); + process.stderr.write(` ${err && err.stack ? err.stack : String(err)}\n`); + failed++; + } +} + +const repoRoot = path.resolve(__dirname, '..', '..'); +const skillDir = path.join(repoRoot, 'skills', 'continuous-learning-v2'); +const PORTABLE_SHEBANG = '#!/usr/bin/env bash'; +const HARDCODED_SHEBANG = '#!/bin/bash'; + +function collectShellScripts(dir, acc = []) { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + // Skip hidden/runtime directories (e.g. the observer's `.observer-tmp`) + // so an untracked local artifact cannot trigger a false failure; only + // committed skill scripts are checked. + if (entry.name.startsWith('.')) { + continue; + } + collectShellScripts(fullPath, acc); + } else if (entry.isFile() && entry.name.endsWith('.sh')) { + acc.push(fullPath); + } + } + return acc; +} + +function firstLine(filePath) { + return fs.readFileSync(filePath, 'utf8').split(/\r?\n/, 1)[0]; +} + +console.log('\n=== continuous-learning-v2 shebang consistency ===\n'); + +const scripts = collectShellScripts(skillDir); + +test('skill directory contains shell scripts to check', () => { + assert.ok(scripts.length > 0, `expected at least one .sh under ${skillDir}`); +}); + +for (const scriptPath of scripts) { + const rel = path.relative(repoRoot, scriptPath).split(path.sep).join('/'); + test(`${rel} uses portable '#!/usr/bin/env bash'`, () => { + assert.strictEqual( + firstLine(scriptPath), + PORTABLE_SHEBANG, + `${rel} should start with '${PORTABLE_SHEBANG}'` + ); + }); +} + +test('no continuous-learning-v2 script uses hardcoded #!/bin/bash', () => { + const offenders = scripts + .filter(scriptPath => firstLine(scriptPath) === HARDCODED_SHEBANG) + .map(scriptPath => path.relative(repoRoot, scriptPath).split(path.sep).join('/')); + assert.strictEqual( + offenders.length, + 0, + `hardcoded #!/bin/bash found in: ${offenders.join(', ')}` + ); +}); + +// ────────────────────────────────────────────────────── +// Summary +// ────────────────────────────────────────────────────── + +console.log('\n=== Test Results ==='); +console.log(`Passed: ${passed}`); +console.log(`Failed: ${failed}`); +console.log(`Total: ${passed + failed}\n`); + +process.exit(failed > 0 ? 1 : 0);