diff --git a/manifests/context-packs/skill-triggers@1.json b/manifests/context-packs/skill-triggers@1.json index b8d929c4c..e77beeacb 100644 --- a/manifests/context-packs/skill-triggers@1.json +++ b/manifests/context-packs/skill-triggers@1.json @@ -1 +1 @@ -{"coverage":{"skills":292,"withTriggers":32},"generatedAt":"2026-09-24T23:51:22.784Z","id":"skill-triggers@1","model":{"effort":null,"id":"hand-seeded","source":"manual-curation-pending-regeneration"},"registryDigest":"3ad5c7662f61241f97b2dbafc4e87a90ad1fcb143f01ff737513dfa075564196","schemaVersion":1,"triggers":{"skill:api-connector-builder":["add api integration","new provider connector","match existing integration pattern"],"skill:api-design":["rest endpoint design","pagination api","status codes","api versioning","rate limiting api","resource naming","filtering api","api error responses","offset pagination","limit query parameter","pagination defaults"],"skill:backend-patterns":["express api","node backend architecture","nextjs api routes","server side patterns","data access layer","static file server","url path handling","file server"],"skill:browser-qa":["deployed feature test","visual regression screenshots","core web vitals check","axe accessibility audit","ship do not ship","staging verification"],"skill:canary-watch":["post deploy monitoring","smoke test url","production url check","console errors production","sse stream check","after deploy verification"],"skill:code-tour":["onboarding walkthrough","explain subsystem","architecture tour","pr walkthrough","rca tour"],"skill:coding-standards":["code review standards","naming conventions","readability review","immutability conventions","fix naming typo","export naming","consistent exports"],"skill:content-hash-cache-pattern":["cache file processing","content addressed cache","sha256 hash cache"],"skill:database-migrations":["zero downtime migration","schema change production","add column large table","backfill data","expand contract","concurrent index","migration rollback","prisma migration","django migration"],"skill:deployment-patterns":["ci cd setup","dockerize app","health checks","rollback strategy","production readiness","deploy pipeline","containerize application"],"skill:design-system":["design tokens","visual consistency audit","css custom properties","ui audit","design system bootstrap"],"skill:django-patterns":["django orm","drf api","django rest framework","django caching","django signals","django middleware"],"skill:django-security":["django authentication","csrf protection","sql injection prevention","xss prevention","django deployment security","role based access control","authorization middleware","permissions checks"],"skill:docker-patterns":["dockerfile review","docker compose setup","container security","multi service orchestration"],"skill:error-handling":["error types","retry logic","circuit breaker","user facing errors","exception handling patterns","typed errors","error boundaries","go error handling","custom error class","error codes","config validation"],"skill:evm-token-decimals":["token decimals","wei conversion","erc20 balance off","bridge token precision"],"skill:frontend-a11y":["aria attributes","screen reader support","focus management","semantic html","form labeling","keyboard navigation react","a11y lint errors"],"skill:git-workflow":["merge vs rebase","commit conventions","resolve merge conflict","branching strategy","clean up commits","pull request cleanup","git history tidy"],"skill:hexagonal-architecture":["ports and adapters","dependency injection boundaries","decouple domain from io"],"skill:kubernetes-patterns":["kubernetes manifests","kubectl debugging","pod probes","k8s rbac","autoscaling config","configmap secrets"],"skill:orch-fix-defect":["fix a bug","broken behavior","regression fix","reproduce bug","defect repair"],"skill:postgres-patterns":["slow postgres query","query optimization","index design","rls policies","supabase schema","postgres indexing","database performance","schema design postgres","postgres driver","node postgres","query planner"],"skill:python-patterns":["pythonic code","pep 8","type hints python","python code review","idiomatic python"],"skill:python-testing":["pytest fixtures","mocking python","parametrized tests","coverage python","tdd python"],"skill:redis-patterns":["cache aside pattern","distributed lock","redis rate limiting","cache invalidation"],"skill:regex-vs-llm-structured-text":["parse invoice","extract receipt data","text extraction pipeline","parse form fields","cheap document parser","extract table data","parse log lines","parse access logs","common log format","log line parsing"],"skill:rust-patterns":["rust ownership","borrow checker","rust error handling","traits rust","rust concurrency","idiomatic rust"],"skill:search-first":["find existing library","npm package research","before writing custom code","evaluate existing tools","add dependency research"],"skill:security-review":["security audit","authentication review","sanitize user input","secrets handling","payment security checklist","prevent injection attacks","secure api endpoints","authn authz review","vulnerability checklist","input validation security","parameterized queries","sql injection"],"skill:security-scan":["audit claude config","claudemd security","mcp server audit","agentshield scan","hook configuration audit","settings json security"],"skill:tdd-workflow":["write test first","failing test","red green refactor","test driven development","regression test first","write a regression test"],"skill:verification-loop":["pre pr checks","verification report","quality gates","build lint test coverage","before creating a pr"]},"triggersDigest":"25b97a9e06fc336c7cf95ab854ed1a41033a54bcd6e1fb1cf69dc906332462aa"} +{"coverage":{"skills":292,"withTriggers":32},"generatedAt":"2026-09-24T23:51:22.784Z","id":"skill-triggers@1","model":{"effort":null,"id":"hand-seeded","source":"manual-curation-pending-regeneration"},"registryDigest":"564dd9225a252bbeed2cf4c474500779681353cbaccfb8df4e0838840e04da5a","schemaVersion":1,"triggers":{"skill:api-connector-builder":["add api integration","new provider connector","match existing integration pattern"],"skill:api-design":["rest endpoint design","pagination api","status codes","api versioning","rate limiting api","resource naming","filtering api","api error responses","offset pagination","limit query parameter","pagination defaults"],"skill:backend-patterns":["express api","node backend architecture","nextjs api routes","server side patterns","data access layer","static file server","url path handling","file server"],"skill:browser-qa":["deployed feature test","visual regression screenshots","core web vitals check","axe accessibility audit","ship do not ship","staging verification"],"skill:canary-watch":["post deploy monitoring","smoke test url","production url check","console errors production","sse stream check","after deploy verification"],"skill:code-tour":["onboarding walkthrough","explain subsystem","architecture tour","pr walkthrough","rca tour"],"skill:coding-standards":["code review standards","naming conventions","readability review","immutability conventions","fix naming typo","export naming","consistent exports"],"skill:content-hash-cache-pattern":["cache file processing","content addressed cache","sha256 hash cache"],"skill:database-migrations":["zero downtime migration","schema change production","add column large table","backfill data","expand contract","concurrent index","migration rollback","prisma migration","django migration"],"skill:deployment-patterns":["ci cd setup","dockerize app","health checks","rollback strategy","production readiness","deploy pipeline","containerize application"],"skill:design-system":["design tokens","visual consistency audit","css custom properties","ui audit","design system bootstrap"],"skill:django-patterns":["django orm","drf api","django rest framework","django caching","django signals","django middleware"],"skill:django-security":["django authentication","csrf protection","sql injection prevention","xss prevention","django deployment security","role based access control","authorization middleware","permissions checks"],"skill:docker-patterns":["dockerfile review","docker compose setup","container security","multi service orchestration"],"skill:error-handling":["error types","retry logic","circuit breaker","user facing errors","exception handling patterns","typed errors","error boundaries","go error handling","custom error class","error codes","config validation"],"skill:evm-token-decimals":["token decimals","wei conversion","erc20 balance off","bridge token precision"],"skill:frontend-a11y":["aria attributes","screen reader support","focus management","semantic html","form labeling","keyboard navigation react","a11y lint errors"],"skill:git-workflow":["merge vs rebase","commit conventions","resolve merge conflict","branching strategy","clean up commits","pull request cleanup","git history tidy"],"skill:hexagonal-architecture":["ports and adapters","dependency injection boundaries","decouple domain from io"],"skill:kubernetes-patterns":["kubernetes manifests","kubectl debugging","pod probes","k8s rbac","autoscaling config","configmap secrets"],"skill:orch-fix-defect":["fix a bug","broken behavior","regression fix","reproduce bug","defect repair"],"skill:postgres-patterns":["slow postgres query","query optimization","index design","rls policies","supabase schema","postgres indexing","database performance","schema design postgres","postgres driver","node postgres","query planner"],"skill:python-patterns":["pythonic code","pep 8","type hints python","python code review","idiomatic python"],"skill:python-testing":["pytest fixtures","mocking python","parametrized tests","coverage python","tdd python"],"skill:redis-patterns":["cache aside pattern","distributed lock","redis rate limiting","cache invalidation"],"skill:regex-vs-llm-structured-text":["parse invoice","extract receipt data","text extraction pipeline","parse form fields","cheap document parser","extract table data","parse log lines","parse access logs","common log format","log line parsing"],"skill:rust-patterns":["rust ownership","borrow checker","rust error handling","traits rust","rust concurrency","idiomatic rust"],"skill:search-first":["find existing library","npm package research","before writing custom code","evaluate existing tools","add dependency research"],"skill:security-review":["security audit","authentication review","sanitize user input","secrets handling","payment security checklist","prevent injection attacks","secure api endpoints","authn authz review","vulnerability checklist","input validation security","parameterized queries","sql injection"],"skill:security-scan":["audit claude config","claudemd security","mcp server audit","agentshield scan","hook configuration audit","settings json security"],"skill:tdd-workflow":["write test first","failing test","red green refactor","test driven development","regression test first","write a regression test"],"skill:verification-loop":["pre pr checks","verification report","quality gates","build lint test coverage","before creating a pr"]},"triggersDigest":"25b97a9e06fc336c7cf95ab854ed1a41033a54bcd6e1fb1cf69dc906332462aa"} diff --git a/skills/continuous-learning-v2/agents/start-observer.sh b/skills/continuous-learning-v2/agents/start-observer.sh index 5485a79e3..d00ffcf06 100755 --- a/skills/continuous-learning-v2/agents/start-observer.sh +++ b/skills/continuous-learning-v2/agents/start-observer.sh @@ -156,8 +156,14 @@ case "$ACTION" in echo "Observer is running (PID: $pid)" echo "Log: $LOG_FILE" echo "Observations: $(wc -l < "$OBSERVATIONS_FILE" 2>/dev/null || echo 0) lines" - # Also show instinct count - instinct_count=$(find "$INSTINCTS_DIR" -name "*.yaml" 2>/dev/null | wc -l) + # Count eligible files, not parsed records: the loader accepts these + # suffixes case-insensitively and follows links to regular files. + # Stay at the top level, excluding dot-only names with no Path.suffix. + # Count NUL records so newlines in filenames cannot inflate the result. + instinct_find_expr=( \( -iname "*.yaml" -o -iname "*.yml" -o -iname "*.md" \) ) + instinct_count=$(find -L "$INSTINCTS_DIR" -mindepth 1 -maxdepth 1 -type f \ + "${instinct_find_expr[@]}" ! -iname ".yaml" ! -iname ".yml" ! -iname ".md" \ + -print0 2>/dev/null | tr -cd '\000' | wc -c | tr -d '[:space:]') echo "Instincts: $instinct_count" exit 0 else diff --git a/tests/skills/observer-status-instinct-count.test.js b/tests/skills/observer-status-instinct-count.test.js new file mode 100644 index 000000000..4b59cd188 --- /dev/null +++ b/tests/skills/observer-status-instinct-count.test.js @@ -0,0 +1,272 @@ +/** + * Regression tests for #2859: status counts eligible top-level instinct files. + * The loader accepts .yaml/.yml/.md; files can contain zero or many instincts, + * so this count does not establish parsed-record counts or observer health. + */ + +'use strict'; + +const assert = require('assert'); +const { spawnSync } = require('child_process'); +const fs = require('fs'); +const os = require('os'); +const path = require('path'); + +const repoRoot = path.resolve(__dirname, '..', '..'); +const skillRoot = path.join(repoRoot, 'skills', 'continuous-learning-v2'); +const observerScript = path.join(skillRoot, 'agents', 'start-observer.sh'); +const instinctCli = path.join(skillRoot, 'scripts', 'instinct-cli.py'); +const bashBinary = process.env.ECC_TEST_BASH || (process.platform === 'win32' ? null : '/bin/bash'); +const childTimeoutMs = 3000; +const childMaxBuffer = 64 * 1024; + +class SkipTest extends Error {} + +function toShellPath(filePath) { + const normalized = filePath.split(path.sep).join('/'); + return normalized.replace(/^([A-Za-z]):\//, (_, drive) => `/${drive.toLowerCase()}/`); +} + +function readAllowedExtensions() { + const cliSource = fs.readFileSync(instinctCli, 'utf8'); + const match = cliSource.match(/ALLOWED_INSTINCT_EXTENSIONS\s*=\s*\(([^)]*)\)/); + assert.ok(match, 'ALLOWED_INSTINCT_EXTENSIONS not found in instinct-cli.py'); + return match[1].split(',') + .map(part => part.trim().replace(/^["']|["']$/g, '')) + .filter(Boolean); +} + +function fixtureAt(root) { + const home = path.join(root, 'home'); + const temp = path.join(root, 'tmp'); + const bin = path.join(root, 'bin'); + const homunculus = path.join(root, 'homunculus'); + const instincts = path.join(homunculus, 'instincts', 'personal'); + const unexpected = path.join(root, 'unexpected-command'); + for (const directory of [home, temp, bin, instincts]) fs.mkdirSync(directory, { recursive: true }); + for (const command of ['python', 'python3', 'git', 'claude']) { + fs.writeFileSync(path.join(bin, command), + '#!/bin/sh\nprintf unexpected > "$FIXTURE_UNEXPECTED_COMMAND"\nexit 97\n', { mode: 0o700 }); + } + fs.writeFileSync(path.join(homunculus, 'observations.jsonl'), ''); + return { + root, instincts, unexpected, + env: { + PATH: [toShellPath(bin), '/usr/bin', '/bin'].join(':'), + HOME: toShellPath(home), USERPROFILE: home, TMPDIR: toShellPath(temp), + TMP: temp, TEMP: temp, LC_ALL: 'C', + ...(process.platform === 'win32' ? { SystemRoot: process.env.SystemRoot } : {}), + CLV2_NO_PROJECT: '1', CLV2_HOMUNCULUS_DIR: toShellPath(homunculus), + CLV2_CONFIG: toShellPath(path.join(root, 'absent-config.json')), + CLV2_PYTHON_CMD: toShellPath(path.join(bin, 'python3')), + FIXTURE_UNEXPECTED_COMMAND: toShellPath(unexpected), + }, + }; +} + +function withFixture(callback, remove = fs.rmSync) { + const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-observer-'))); + let failed = false; let primary; let result; + try { + result = callback(fixtureAt(root)); + } catch (error) { + failed = true; + primary = error; + } + try { remove(root, { recursive: true, force: true }); } + catch (error) { if (!failed) throw error; } + if (failed) throw primary; + return result; +} + +function checkChild(result) { + if (result.error) throw result.error; + assert.strictEqual(result.status, 0, result.stderr || result.stdout || `child signal: ${result.signal}`); +} + +function runStatus(files, { run = spawnSync, setup = () => {}, remove = fs.rmSync } = {}) { + return withFixture(fixture => { + for (const name of files) { + const target = path.join(fixture.instincts, name); + fs.mkdirSync(path.dirname(target), { recursive: true }); + fs.writeFileSync(target, 'id: fixture\n'); + } + setup(fixture); + // Only this foreground shell's own PID is advertised. No observer, sleep, + // provider or other background child is started or signalled. + const program = 'printf "%s\\n" "$$" > "$CLV2_HOMUNCULUS_DIR/.observer.pid"\nexec "$BASH" "$1" status'; + const result = run(bashBinary, ['--noprofile', '--norc', '-c', program, + 'observer-status-test', toShellPath(observerScript)], { + cwd: fixture.root, encoding: 'utf8', env: fixture.env, + timeout: childTimeoutMs, maxBuffer: childMaxBuffer, killSignal: 'SIGKILL', + }); + checkChild(result); + assert.ok(!fs.existsSync(fixture.unexpected), 'status must not invoke Git, Python or a provider'); + const lines = (result.stdout || '').split('\n').filter(line => line.startsWith('Instincts:')); + assert.strictEqual(lines.length, 1, `expected one count in status output:\n${result.stdout}`); + assert.match(lines[0], /^Instincts:\s+\d+\s*$/); + return Number(lines[0].split(':')[1].trim()); + }, remove); +} + +function fileLink(target, link, type = 'file') { + try { fs.symlinkSync(target, link, type); } + catch (error) { + const unsupported = ['ENOSYS', 'ENOTSUP'].includes(error.code) + || (process.platform === 'win32' && ['EPERM', 'EACCES'].includes(error.code)); + if (unsupported) throw new SkipTest(`symlink capability unavailable: ${error.code}`); + throw error; + } +} + +function shellTest(fn) { + return () => { + if (!bashBinary) throw new SkipTest('requires bash; set ECC_TEST_BASH on Windows'); + return fn(); + }; +} + +function cleanupTests() { + return [ + ['status child has a private environment and bounded execution', () => { + let root; + assert.strictEqual(runStatus([], { run: (_command, args, options) => { + root = options.cwd; + assert.strictEqual(options.timeout, childTimeoutMs); + assert.strictEqual(options.maxBuffer, childMaxBuffer); + assert.strictEqual(options.killSignal, 'SIGKILL'); + assert.strictEqual(options.env.CLV2_NO_PROJECT, '1'); + for (const key of ['BASH_ENV', 'ENV', 'NODE_OPTIONS', 'ANTHROPIC_API_KEY', 'CLAUDE_PROJECT_DIR']) { + assert.ok(!Object.hasOwn(options.env, key), `unexpected inherited ${key}`); + } + assert.ok(options.env.CLV2_CONFIG.endsWith('/absent-config.json')); + assert.ok(!fs.existsSync(options.env.CLV2_CONFIG)); + assert.strictEqual(args.at(-1), toShellPath(observerScript)); + assert.match(args[3], /\nexec "\$BASH" "\$1" status$/); + assert.doesNotMatch(args[3], /sleep|kill|&/); + return { status: 0, stdout: 'Instincts: 0\n', stderr: '' }; + } }), 0); + assert.ok(!fs.existsSync(root)); + }], + ...['timeout', 'nonzero', 'spawn error'].map(kind => [`${kind} cleans its private fixture`, () => { + let root; + const failure = Object.assign(new Error(kind), { code: kind === 'timeout' ? 'ETIMEDOUT' : 'EIO' }); + assert.throws(() => runStatus([], { run: (_command, _args, options) => { + root = options.cwd; + return kind === 'nonzero' ? { status: 7, stdout: '', stderr: 'fixture rejected' } : { error: failure }; + } }), error => kind === 'nonzero' ? /fixture rejected/.test(error.message) : error === failure); + assert.ok(!fs.existsSync(root)); + }]), + ['cleanup preserves frozen and falsy primary failures', () => { + for (const primary of [Object.freeze(new Error('primary')), null, false, 0, undefined]) { + let root; let caught = false; + try { + withFixture(value => { root = value.root; throw primary; }, (value, options) => { + fs.rmSync(value, options); throw new Error('cleanup'); + }); + } catch (error) { caught = true; assert.strictEqual(error, primary); } + assert.ok(caught); + assert.ok(!fs.existsSync(root)); + } + }], + ['cleanup failure is reported when the fixture otherwise succeeds', () => { + let root; + const failure = new Error('cleanup'); + assert.throws(() => withFixture(value => { root = value.root; }, (value, options) => { + fs.rmSync(value, options); throw failure; + }), error => error === failure); + assert.ok(!fs.existsSync(root)); + }], + ]; +} + +function buildTests() { + const allowed = readAllowedExtensions(); + return [ + ['the loader still declares several instinct extensions', () => { + assert.ok(allowed.length >= 3, `expected several extensions, got ${allowed}`); + }], + ...allowed.map(ext => [`status counts ${ext} - the loader accepts it`, shellTest(() => { + assert.strictEqual(runStatus([`one${ext}`]), 1); + })]), + ['status does not recurse - the loader does not', shellTest(() => { + assert.strictEqual(runStatus(['a.md', 'nested/deep.yaml']), 1); + })], + ['status skips directories', shellTest(() => { + assert.strictEqual(runStatus([], { setup: value => fs.mkdirSync(path.join(value.instincts, 'directory.md')) }), 0); + })], + ['status matches case-insensitively', shellTest(() => { + assert.strictEqual(runStatus(['a.YAML', 'b.YmL', 'c.MD']), 3); + })], + ['start-observer.sh parses', shellTest(() => withFixture(fixture => { + checkChild(spawnSync(bashBinary, ['--noprofile', '--norc', '-n', toShellPath(observerScript)], { + cwd: fixture.root, env: fixture.env, encoding: 'utf8', + timeout: childTimeoutMs, maxBuffer: childMaxBuffer, killSignal: 'SIGKILL', + })); + }))], + ['markdown instincts are counted', shellTest(() => { + assert.strictEqual(runStatus(['a.md', 'b.md', 'c.md']), 3); + })], + ['the count matches the loader eligible-file rules', shellTest(() => { + assert.strictEqual(runStatus(['a.md', 'b.yaml', 'c.yml', 'd.YAML', 'notes.txt', 'nested/deep.md']), 4); + })], + ['an empty instincts directory reports 0', shellTest(() => { + assert.strictEqual(runStatus([]), 0); + })], + ['hidden stems count but dot-only extension names have no suffix', shellTest(() => { + assert.strictEqual(runStatus(['.note.MD', '.yaml', '.YML', '.md']), 1); + })], + ['newlines and spaces in one eligible filename count once', shellTest(() => { + assert.strictEqual(runStatus(['two\nlines with spaces.MD']), 1); + })], + ['shell metacharacters in a filename remain inert data', shellTest(() => { + assert.strictEqual(runStatus(['$(touch unwanted).md'], { setup: value => { + assert.ok(!fs.existsSync(path.join(value.root, 'unwanted'))); + }, run: (command, args, options) => { + const result = spawnSync(command, args, options); + assert.ok(!fs.existsSync(path.join(options.cwd, 'unwanted'))); + return result; + } }), 1); + })], + ['linked regular files count without traversing directory links', shellTest(() => { + assert.strictEqual(runStatus(['a.md', 'b.yaml', 'c.yml', 'd.YAML', '.note.MD', + '.md', '.yaml', '.YML', 'notes.txt', 'nested/deep.md'], { setup: value => { + const outside = path.join(value.root, 'outside'); + fs.mkdirSync(outside); + const target = path.join(outside, 'regular.txt'); + fs.writeFileSync(target, 'private fixture\n'); + fs.writeFileSync(path.join(outside, 'deep.md'), 'not top-level\n'); + fs.mkdirSync(path.join(value.instincts, 'directory.md')); + fileLink(target, path.join(value.instincts, 'linked.MD')); + fileLink(outside, path.join(value.instincts, 'directory-link.yaml'), 'dir'); + fileLink(path.join(outside, 'missing'), path.join(value.instincts, 'dangling.yml')); + } }), 6); + })], + ...cleanupTests(), + ]; +} + +function main() { + console.log('\n=== Testing observer status instinct count (#2859) ===\n'); + let passed = 0; let failed = 0; let skipped = 0; let tests; + try { tests = buildTests(); } + catch (error) { + console.error(` FAIL could not build the test list: ${error.message}`); + tests = []; failed += 1; + } + for (const [name, fn] of tests) { + try { fn(); console.log(` PASS ${name}`); passed += 1; } + catch (error) { + if (error instanceof SkipTest) { + console.log(` SKIP ${name}: ${error.message}`); skipped += 1; + } else { + console.error(` FAIL ${name}: ${error.message}`); failed += 1; + } + } + } + console.log(`\n Passed: ${passed}\n Failed: ${failed}\n Skipped: ${skipped}`); + // Let piped summary output drain before the aggregate runner reads it. + if (failed > 0) process.exitCode = 1; +} + +main();