From 2d40baacbda17780ca71fca24171d797a7d53caa Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 3 Jul 2026 21:10:45 -0700 Subject: [PATCH] =?UTF-8?q?fix:=20resolve=20open-issue=20cluster=20(#2295,?= =?UTF-8?q?=20#2298,=20#2303=E2=80=93#2306,=20#2340)=20+=20createdTime=20f?= =?UTF-8?q?allback=20bug=20(#2408)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: resolve issue cluster (#2295,#2298,#2303,#2304,#2305,#2306,#2340) + createdTime fallback bug - session-manager: fix createdTime birthtime||ctime fallback that never fired (a Date is always truthy); use birthtimeMs>0 check via resolveCreatedTime() - installer: rewrite source-relative rules/skills links for the injected ecc/ namespace so installed skills resolve correctly (#2340) - continuous-learning-v2: drop unused mock import (#2305); standardize bash shebangs (#2303); poll for PID file instead of fixed sleep (#2295); rename _ecc_* -> _clv2_* (#2304); align promotion confidence docs (#2298); de-brittle Scope Decision Guide cross-reference (#2306) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(ci): resync lockfiles with package.json (eslint 10) + migrate yarn.lock to Yarn 4 format package.json requires eslint@^10.6.0 but the committed locks pinned 9.39.2, so npm ci aborted and Yarn 4 hardened mode rejected the stale v1-classic yarn.lock (YN0028). Regenerate package-lock.json and rewrite yarn.lock in Yarn 4 (berry) format so npm ci and immutable yarn installs both pass. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(ci): require clean probe exit for Windows shell/bash detection; add pyyaml dev dep Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(observer): portable mktemp template on BSD/macOS (#2417); correct false attribution-disabled claim in git-workflow docs (#2426) (#2430) Co-authored-by: affaan Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix: remove duplicate resolveCreatedTime introduced by merge (no-redeclare) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix: restore heading-based Scope Decision Guide ref (line numbers drift) + keep behavioral #2340 install test --------- Co-authored-by: affaan Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Affaan Mustafa --- .cursor/rules/common-git-workflow.md | 2 +- .kiro/steering/git-workflow.md | 2 +- docs/es/rules/common/git-workflow.md | 2 +- docs/ja-JP/rules/common/git-workflow.md | 2 +- docs/ko-KR/rules/git-workflow.md | 2 +- docs/pt-BR/rules/git-workflow.md | 2 +- docs/tr/rules/common/git-workflow.md | 2 +- docs/zh-CN/rules/common/git-workflow.md | 2 +- docs/zh-TW/rules/git-workflow.md | 2 +- rules/common/git-workflow.md | 2 +- scripts/lib/session-manager.js | 16 +++-- .../agents/observer-loop.sh | 5 +- .../continuous-learning-v2/agents/observer.md | 2 +- .../continuous-learning-v2/hooks/observe.sh | 16 ++--- .../scripts/detect-project.sh | 2 +- .../scripts/lib/homunculus-dir.sh | 2 +- .../scripts/migrate-homunculus.sh | 2 +- .../scripts/test_parse_instinct.py | 1 - .../hooks/observe-signal-counter-race.test.js | 2 +- tests/hooks/observe-signal-timeout.test.js | 22 +++---- tests/hooks/observer-loop-mktemp.test.js | 58 +++++++++++++++++++ tests/hooks/observer-memory.test.js | 2 +- tests/scripts/install-apply.test.js | 34 +++++++++++ 23 files changed, 143 insertions(+), 41 deletions(-) create mode 100644 tests/hooks/observer-loop-mktemp.test.js diff --git a/.cursor/rules/common-git-workflow.md b/.cursor/rules/common-git-workflow.md index b275023e0..591d45ddf 100644 --- a/.cursor/rules/common-git-workflow.md +++ b/.cursor/rules/common-git-workflow.md @@ -13,7 +13,7 @@ alwaysApply: true Types: feat, fix, refactor, docs, test, chore, perf, ci -Note: Attribution disabled globally via ~/.claude/settings.json. +Note: To disable co-author attribution on commits, set `"includeCoAuthoredBy": false` in `~/.claude/settings.json` (Claude Code appends `Co-Authored-By` by default; ECC does not ship this setting). ## Pull Request Workflow diff --git a/.kiro/steering/git-workflow.md b/.kiro/steering/git-workflow.md index 56683a17b..9fee1ab20 100644 --- a/.kiro/steering/git-workflow.md +++ b/.kiro/steering/git-workflow.md @@ -15,7 +15,7 @@ description: Git workflow guidelines for conventional commits and pull request p Types: feat, fix, refactor, docs, test, chore, perf, ci -Note: Attribution disabled globally via ~/.claude/settings.json. +Note: To disable co-author attribution on commits, set `"includeCoAuthoredBy": false` in `~/.claude/settings.json` (Claude Code appends `Co-Authored-By` by default; ECC does not ship this setting). ## Pull Request Workflow diff --git a/docs/es/rules/common/git-workflow.md b/docs/es/rules/common/git-workflow.md index 927fb9a91..3b48b772e 100644 --- a/docs/es/rules/common/git-workflow.md +++ b/docs/es/rules/common/git-workflow.md @@ -9,7 +9,7 @@ Tipos: feat, fix, refactor, docs, test, chore, perf, ci -Nota: Atribución deshabilitada globalmente mediante ~/.claude/settings.json. +Nota: Para desactivar la atribución de coautoría, configure `"includeCoAuthoredBy": false` en `~/.claude/settings.json`; Claude Code agrega `Co-Authored-By` de forma predeterminada y ECC no incluye esta configuración. ## Flujo de Trabajo de Pull Request diff --git a/docs/ja-JP/rules/common/git-workflow.md b/docs/ja-JP/rules/common/git-workflow.md index 94c6ef5ec..7fb2e7f1e 100644 --- a/docs/ja-JP/rules/common/git-workflow.md +++ b/docs/ja-JP/rules/common/git-workflow.md @@ -10,7 +10,7 @@ タイプ: feat, fix, refactor, docs, test, chore, perf, ci -注記: Attribution は ~/.claude/settings.json でグローバルに無効化されています。 +注記: コミットの共同作成者の属性を無効にするには、`~/.claude/settings.json` に `"includeCoAuthoredBy": false` を設定します。Claude Code は既定で `Co-Authored-By` を付与し、ECC はこの設定を同梱しません。 ## Pull Request ワークフロー diff --git a/docs/ko-KR/rules/git-workflow.md b/docs/ko-KR/rules/git-workflow.md index 56cd58654..9ad47756c 100644 --- a/docs/ko-KR/rules/git-workflow.md +++ b/docs/ko-KR/rules/git-workflow.md @@ -9,7 +9,7 @@ 타입: feat, fix, refactor, docs, test, chore, perf, ci -참고: 어트리뷰션 비활성화 여부는 각자의 `~/.claude/settings.json` 로컬 설정에 따라 달라질 수 있습니다. +참고: 공동 작성자 표기를 비활성화하려면 `~/.claude/settings.json`에 `"includeCoAuthoredBy": false`를 설정하세요. Claude Code는 기본적으로 `Co-Authored-By`를 추가하며 ECC는 이 설정을 포함하지 않습니다. ## Pull Request 워크플로우 diff --git a/docs/pt-BR/rules/git-workflow.md b/docs/pt-BR/rules/git-workflow.md index 17af0cb4b..761ce3e2c 100644 --- a/docs/pt-BR/rules/git-workflow.md +++ b/docs/pt-BR/rules/git-workflow.md @@ -9,7 +9,7 @@ Tipos: feat, fix, refactor, docs, test, chore, perf, ci -Nota: Atribuição desabilitada globalmente via ~/.claude/settings.json. +Nota: Para desativar a atribuição de coautoria, defina `"includeCoAuthoredBy": false` em `~/.claude/settings.json`; o Claude Code adiciona `Co-Authored-By` por padrão e o ECC não inclui essa configuração. ## Fluxo de Trabalho de Pull Request diff --git a/docs/tr/rules/common/git-workflow.md b/docs/tr/rules/common/git-workflow.md index 31e62a3d6..25b71cab9 100644 --- a/docs/tr/rules/common/git-workflow.md +++ b/docs/tr/rules/common/git-workflow.md @@ -9,7 +9,7 @@ Types: feat, fix, refactor, docs, test, chore, perf, ci -Not: Attribution ~/.claude/settings.json aracılığıyla global olarak devre dışı bırakıldı. +Not: Ortak yazar atfını devre dışı bırakmak için `~/.claude/settings.json` içinde `"includeCoAuthoredBy": false` ayarlayın; Claude Code varsayılan olarak `Co-Authored-By` ekler ve ECC bu ayarı içermez. ## Pull Request İş Akışı diff --git a/docs/zh-CN/rules/common/git-workflow.md b/docs/zh-CN/rules/common/git-workflow.md index 77fa9c20a..a575ee137 100644 --- a/docs/zh-CN/rules/common/git-workflow.md +++ b/docs/zh-CN/rules/common/git-workflow.md @@ -10,7 +10,7 @@ 类型:feat, fix, refactor, docs, test, chore, perf, ci -注意:通过 ~/.claude/settings.json 全局禁用了归因。 +注意:若要禁用共同作者归因,请在 `~/.claude/settings.json` 中设置 `"includeCoAuthoredBy": false`;Claude Code 默认会附加 `Co-Authored-By`,而 ECC 不会附带此设置。 ## 拉取请求工作流程 diff --git a/docs/zh-TW/rules/git-workflow.md b/docs/zh-TW/rules/git-workflow.md index 73f078621..415a6b491 100644 --- a/docs/zh-TW/rules/git-workflow.md +++ b/docs/zh-TW/rules/git-workflow.md @@ -10,7 +10,7 @@ 類型:feat、fix、refactor、docs、test、chore、perf、ci -注意:歸屬透過 ~/.claude/settings.json 全域停用。 +注意:若要停用共同作者歸屬,請在 `~/.claude/settings.json` 中設定 `"includeCoAuthoredBy": false`;Claude Code 預設會附加 `Co-Authored-By`,而 ECC 不會隨附這個設定。 ## Pull Request 工作流程 diff --git a/rules/common/git-workflow.md b/rules/common/git-workflow.md index d57d9e281..304fba798 100644 --- a/rules/common/git-workflow.md +++ b/rules/common/git-workflow.md @@ -9,7 +9,7 @@ Types: feat, fix, refactor, docs, test, chore, perf, ci -Note: Attribution disabled globally via ~/.claude/settings.json. +Note: To disable co-author attribution on commits, set `"includeCoAuthoredBy": false` in `~/.claude/settings.json` (Claude Code appends `Co-Authored-By` by default; ECC does not ship this setting). ## Pull Request Workflow diff --git a/scripts/lib/session-manager.js b/scripts/lib/session-manager.js index 6d2f63b1f..a4b900058 100644 --- a/scripts/lib/session-manager.js +++ b/scripts/lib/session-manager.js @@ -18,10 +18,6 @@ const { log } = require('./utils'); -function resolveCreatedTime(stats) { - return stats.birthtimeMs > 0 ? stats.birthtime : stats.ctime; -} - // Session filename pattern: YYYY-MM-DD-[session-id]-session.tmp // The session-id is optional (old format) and can include letters, digits, // underscores, and hyphens, but must not start with a hyphen. @@ -30,6 +26,18 @@ function resolveCreatedTime(stats) { // "2026-02-01-ChezMoi_2-session.tmp" const SESSION_FILENAME_REGEX = /^(\d{4}-\d{2}-\d{2})(?:-([a-zA-Z0-9_][a-zA-Z0-9_-]*))?-session\.tmp$/; +/** + * Resolve a file's creation time, preferring birthtime but falling back to + * ctime when birthtime is unavailable. Some filesystems (e.g. overlayfs in + * containers) report birthtime as epoch 0; a Date object is always truthy, so + * `birthtime || ctime` would never fall back. Compare on milliseconds instead. + * @param {import('fs').Stats} stats + * @returns {Date} + */ +function resolveCreatedTime(stats) { + return stats.birthtimeMs > 0 ? stats.birthtime : stats.ctime; +} + /** * Parse session filename to extract metadata * @param {string} filename - Session filename (e.g., "2026-01-17-abc123-session.tmp" or "2026-01-17-session.tmp") diff --git a/skills/continuous-learning-v2/agents/observer-loop.sh b/skills/continuous-learning-v2/agents/observer-loop.sh index fa50c1f61..698cd8b68 100755 --- a/skills/continuous-learning-v2/agents/observer-loop.sh +++ b/skills/continuous-learning-v2/agents/observer-loop.sh @@ -145,7 +145,10 @@ analyze_observations() { MAX_ANALYSIS_LINES="${ECC_OBSERVER_MAX_ANALYSIS_LINES:-500}" observer_tmp_dir="${PROJECT_DIR}/.observer-tmp" mkdir -p "$observer_tmp_dir" - analysis_file="$(mktemp "${observer_tmp_dir}/ecc-observer-analysis.XXXXXX.jsonl")" + # Keep the XXXXXX run at the very end of the template: BSD/macOS mktemp only + # substitutes a trailing X run, so a suffix after it (e.g. `.jsonl`) produces a + # literal, non-random name that wedges every later cycle with "File exists" (#2417). + analysis_file="$(mktemp "${observer_tmp_dir}/ecc-observer-analysis.jsonl.XXXXXX")" tail -n "$MAX_ANALYSIS_LINES" "$OBSERVATIONS_FILE" > "$analysis_file" analysis_count=$(wc -l < "$analysis_file" 2>/dev/null || echo 0) echo "[$(date)] Using last $analysis_count of $obs_count observations for analysis" >> "$LOG_FILE" diff --git a/skills/continuous-learning-v2/agents/observer.md b/skills/continuous-learning-v2/agents/observer.md index 6e355b7b4..57c09b734 100644 --- a/skills/continuous-learning-v2/agents/observer.md +++ b/skills/continuous-learning-v2/agents/observer.md @@ -121,7 +121,7 @@ Validate and sanitize all user input before processing. When creating instincts, determine scope based on these heuristics: -> **Scope Decision Guide** – See the canonical table in `skills/continuous-learning-v2/SKILL.md` (lines 271‑282). +> **Scope Decision Guide** – See the canonical table under the "Scope Decision Guide" heading in `skills/continuous-learning-v2/SKILL.md`. **When in doubt, default to `scope: project`** — it's safer to be project-specific and promote later than to contaminate the global space. diff --git a/skills/continuous-learning-v2/hooks/observe.sh b/skills/continuous-learning-v2/hooks/observe.sh index 48172e981..49713957b 100755 --- a/skills/continuous-learning-v2/hooks/observe.sh +++ b/skills/continuous-learning-v2/hooks/observe.sh @@ -135,7 +135,7 @@ fi # shellcheck disable=SC1091 . "$(dirname "$0")/../scripts/lib/homunculus-dir.sh" -CONFIG_DIR="$(_ecc_resolve_homunculus_dir)" +CONFIG_DIR="$(_clv2_resolve_homunculus_dir)" # Skip if disabled (check both default and CLV2_CONFIG-derived locations) if [ -f "$CONFIG_DIR/disabled" ]; then @@ -279,11 +279,11 @@ _SECRET_RE = re.compile( ) import signal -def _ecc_bail(*_): +def _clv2_bail(*_): print("[observe] SIGALRM timeout: parse-error fallback observation dropped before write (#2300)", file=sys.stderr) sys.exit(0) try: - signal.signal(signal.SIGALRM, _ecc_bail) + signal.signal(signal.SIGALRM, _clv2_bail) signal.alarm(8) # self-terminate before the async hook 10s timeout can orphan us (#2278) except Exception: pass @@ -317,11 +317,11 @@ echo "$PARSED" | "$PYTHON_CMD" -c ' import json, sys, os, re import signal -def _ecc_bail(*_): +def _clv2_bail(*_): print("[observe] SIGALRM timeout: in-flight observation dropped before write (#2300)", file=sys.stderr) sys.exit(0) try: - signal.signal(signal.SIGALRM, _ecc_bail) + signal.signal(signal.SIGALRM, _clv2_bail) signal.alarm(8) # self-terminate before the async hook 10s timeout can orphan us (#2278) except Exception: pass @@ -493,7 +493,7 @@ touch "$ACTIVITY_FILE" 2>/dev/null || true # the lazy-start path above. Both wrap the same read-modify-write below. should_signal=0 -_ecc_bump_signal_counter() { +_clv2_bump_signal_counter() { if [ -f "$SIGNAL_COUNTER_FILE" ]; then counter=$(cat "$SIGNAL_COUNTER_FILE" 2>/dev/null || echo 0) # Guard against a corrupt counter file: a non-integer value would abort the @@ -518,7 +518,7 @@ if command -v flock >/dev/null 2>&1 && exec 8>"$SIGNAL_COUNTER_LOCK" 2>/dev/null # blocks indefinitely, and only bump the counter while the lock is held -- on # a timeout we skip the tick rather than doing an unlocked read-modify-write. if flock -w 2 8 2>/dev/null; then - _ecc_bump_signal_counter + _clv2_bump_signal_counter flock -u 8 2>/dev/null || true fi exec 8>&- 2>/dev/null || true @@ -547,7 +547,7 @@ else done if [ "$_signal_lock_held" -eq 1 ]; then # Bump only under the held lock -- never an unlocked read-modify-write. - _ecc_bump_signal_counter + _clv2_bump_signal_counter rmdir "$SIGNAL_COUNTER_LOCK" 2>/dev/null || true trap - EXIT INT TERM fi diff --git a/skills/continuous-learning-v2/scripts/detect-project.sh b/skills/continuous-learning-v2/scripts/detect-project.sh index 2d129fe8c..05bc20852 100755 --- a/skills/continuous-learning-v2/scripts/detect-project.sh +++ b/skills/continuous-learning-v2/scripts/detect-project.sh @@ -21,7 +21,7 @@ # shellcheck disable=SC1091 . "$(dirname "${BASH_SOURCE[0]}")/lib/homunculus-dir.sh" -_CLV2_HOMUNCULUS_DIR="$(_ecc_resolve_homunculus_dir)" +_CLV2_HOMUNCULUS_DIR="$(_clv2_resolve_homunculus_dir)" _CLV2_PROJECTS_DIR="${_CLV2_HOMUNCULUS_DIR}/projects" _CLV2_REGISTRY_FILE="${_CLV2_HOMUNCULUS_DIR}/projects.json" diff --git a/skills/continuous-learning-v2/scripts/lib/homunculus-dir.sh b/skills/continuous-learning-v2/scripts/lib/homunculus-dir.sh index 9f1e926a7..27f9adb84 100644 --- a/skills/continuous-learning-v2/scripts/lib/homunculus-dir.sh +++ b/skills/continuous-learning-v2/scripts/lib/homunculus-dir.sh @@ -6,7 +6,7 @@ # 2. XDG_DATA_HOME/ecc-homunculus, when XDG_DATA_HOME is absolute # 3. HOME/.local/share/ecc-homunculus -_ecc_resolve_homunculus_dir() { +_clv2_resolve_homunculus_dir() { if [ -n "${CLV2_HOMUNCULUS_DIR:-}" ]; then case "$CLV2_HOMUNCULUS_DIR" in /*) printf '%s\n' "$CLV2_HOMUNCULUS_DIR"; return 0 ;; diff --git a/skills/continuous-learning-v2/scripts/migrate-homunculus.sh b/skills/continuous-learning-v2/scripts/migrate-homunculus.sh index b6c19cacd..3453b9294 100755 --- a/skills/continuous-learning-v2/scripts/migrate-homunculus.sh +++ b/skills/continuous-learning-v2/scripts/migrate-homunculus.sh @@ -7,7 +7,7 @@ OLD="${HOME}/.claude/homunculus" # shellcheck disable=SC1091 . "$(dirname "$0")/lib/homunculus-dir.sh" -NEW="$(_ecc_resolve_homunculus_dir)" +NEW="$(_clv2_resolve_homunculus_dir)" if [ "$NEW" = "$OLD" ]; then echo "Resolved destination equals source ($OLD); nothing to migrate." diff --git a/skills/continuous-learning-v2/scripts/test_parse_instinct.py b/skills/continuous-learning-v2/scripts/test_parse_instinct.py index 290ff9114..f58c58853 100644 --- a/skills/continuous-learning-v2/scripts/test_parse_instinct.py +++ b/skills/continuous-learning-v2/scripts/test_parse_instinct.py @@ -19,7 +19,6 @@ import os import sys from pathlib import Path from types import SimpleNamespace -from unittest import mock import pytest diff --git a/tests/hooks/observe-signal-counter-race.test.js b/tests/hooks/observe-signal-counter-race.test.js index 2fb978aac..55c9aea77 100644 --- a/tests/hooks/observe-signal-counter-race.test.js +++ b/tests/hooks/observe-signal-counter-race.test.js @@ -104,7 +104,7 @@ function buildSandbox() { path.join(scriptsLibDir, 'homunculus-dir.sh'), [ '#!/bin/bash', - '_ecc_resolve_homunculus_dir() { printf "%s\\n" "$HOME/.local/share/ecc-homunculus"; }', + '_clv2_resolve_homunculus_dir() { printf "%s\\n" "$HOME/.local/share/ecc-homunculus"; }', '' ].join('\n') ); diff --git a/tests/hooks/observe-signal-timeout.test.js b/tests/hooks/observe-signal-timeout.test.js index 2f7e48047..00ff23b86 100644 --- a/tests/hooks/observe-signal-timeout.test.js +++ b/tests/hooks/observe-signal-timeout.test.js @@ -3,14 +3,14 @@ * * observe.sh arms a signal.SIGALRM alarm (8s) inside its inline-Python blocks so * the observation writer self-terminates before the async hook's 10s timeout can - * orphan it (#2278). Before #2300 the handler `_ecc_bail` called sys.exit(0) with + * orphan it (#2278). Before #2300 the handler `_clv2_bail` called sys.exit(0) with * no logging, so a timeout silently dropped the in-flight observation: nothing was * logged and the shell saw a clean exit. The fix adds a stderr visibility line to * each handler while keeping exit 0 (changing to a non-zero exit would make the * Claude hook report a block, per the repo's "always exit 0; log to stderr" rule). * * Two checks: - * 1. Static regression guard — every `_ecc_bail` handler in observe.sh writes to + * 1. Static regression guard — every `_clv2_bail` handler in observe.sh writes to * sys.stderr before sys.exit(0). * 2. Behavioral check — the REAL handler text extracted from observe.sh, when its * alarm fires, exits 0 and emits the `[observe]` visibility token on stderr @@ -73,14 +73,14 @@ const observeShPath = path.join( const observeSrc = fs.readFileSync(observeShPath, 'utf8'); -// Extract each `_ecc_bail` handler body: the `def` line plus the indented lines +// Extract each `_clv2_bail` handler body: the `def` line plus the indented lines // that follow it, up to (and including) the first dedented `sys.exit(0)` line at // the same indentation as the def's body. function extractHandlers(src) { const lines = src.split('\n'); const handlers = []; for (let i = 0; i < lines.length; i += 1) { - if (/^def _ecc_bail\(\*_\):\s*$/.test(lines[i])) { + if (/^def _clv2_bail\(\*_\):\s*$/.test(lines[i])) { const body = [lines[i]]; for (let j = i + 1; j < lines.length; j += 1) { // Stop when we hit a line that is not indented (next top-level stmt). @@ -103,15 +103,15 @@ const handlers = extractHandlers(observeSrc); // The #2300 timeout handlers are the ones that log the `[observe] SIGALRM // timeout` marker. Selecting by marker (rather than by array index) keeps the // behavioral check pinned to the timeout handlers even if an unrelated -// `_ecc_bail` is ever added elsewhere in observe.sh. +// `_clv2_bail` is ever added elsewhere in observe.sh. const timeoutHandlers = handlers.filter(body => body.includes('[observe] SIGALRM timeout') ); -test('observe.sh defines at least two _ecc_bail timeout handlers', () => { +test('observe.sh defines at least two _clv2_bail timeout handlers', () => { assert.ok( handlers.length >= 2, - `expected >= 2 _ecc_bail handlers, found ${handlers.length}` + `expected >= 2 _clv2_bail handlers, found ${handlers.length}` ); assert.ok( timeoutHandlers.length >= 2, @@ -119,7 +119,7 @@ test('observe.sh defines at least two _ecc_bail timeout handlers', () => { ); }); -test('every _ecc_bail handler logs to stderr before exiting (regression guard)', () => { +test('every _clv2_bail handler logs to stderr before exiting (regression guard)', () => { handlers.forEach((body, idx) => { const stderrIdx = body.indexOf('file=sys.stderr'); const exitIdx = body.indexOf('sys.exit(0)'); @@ -142,7 +142,7 @@ test('every _ecc_bail handler logs to stderr before exiting (regression guard)', }); }); -test('_ecc_bail handlers keep exit code 0 (no exit 2 / block regression)', () => { +test('_clv2_bail handlers keep exit code 0 (no exit 2 / block regression)', () => { handlers.forEach((body, idx) => { assert.ok( /sys\.exit\(0\)/.test(body), @@ -160,7 +160,7 @@ function runHandlerTimeout(python, handler) { const program = [ 'import sys, signal, time', handler, - 'signal.signal(signal.SIGALRM, _ecc_bail)', + 'signal.signal(signal.SIGALRM, _clv2_bail)', 'signal.alarm(1)', 'time.sleep(3)', 'print("REACHED_END_SHOULD_NOT_HAPPEN")', @@ -178,7 +178,7 @@ function runHandlerTimeout(python, handler) { // the worst case. A behavioral check on only one handler would not catch a // regression that silenced another. timeoutHandlers.forEach((handler, idx) => { - test(`real _ecc_bail timeout handler #${idx + 1}: SIGALRM fire emits stderr token and exits 0`, () => { + test(`real _clv2_bail timeout handler #${idx + 1}: SIGALRM fire emits stderr token and exits 0`, () => { const python = findPython(); if (!python) { // Fail fast rather than returning (which the harness would record as a diff --git a/tests/hooks/observer-loop-mktemp.test.js b/tests/hooks/observer-loop-mktemp.test.js new file mode 100644 index 000000000..554dbf8a1 --- /dev/null +++ b/tests/hooks/observer-loop-mktemp.test.js @@ -0,0 +1,58 @@ +/** + * Regression test for #2417 mktemp template portability in observer-loop.sh + * + * BSD/macOS mktemp only substitutes a trailing run of X characters. The + * observer-loop analysis template must therefore keep the randomized X run at + * the end of the quoted template string. + */ + +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); + +let passed = 0; +let failed = 0; + +function test(name, fn) { + try { + fn(); + console.log(` ✓ ${name}`); + passed++; + } catch (err) { + console.log(` ✗ ${name}`); + console.log(` Error: ${err.message}`); + failed++; + } +} + +const repoRoot = path.resolve(__dirname, '..', '..'); +const observerLoopPath = path.join( + repoRoot, + 'skills', + 'continuous-learning-v2', + 'agents', + 'observer-loop.sh' +); + +console.log('\n=== Observer-loop mktemp portability regression (#2417) ===\n'); + +test('every mktemp template ends with the randomized X run', () => { + const content = fs.readFileSync(observerLoopPath, 'utf8'); + const mktempTemplates = [...content.matchAll(/mktemp\s+"([^"]+)"/g)].map(match => match[1]); + + assert.ok(mktempTemplates.length > 0, 'expected at least one mktemp template'); + + for (const template of mktempTemplates) { + assert.ok( + /X+$/.test(template), + `mktemp template must end with Xs for BSD/macOS portability: ${template}` + ); + } +}); + +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); diff --git a/tests/hooks/observer-memory.test.js b/tests/hooks/observer-memory.test.js index 9bfbdde09..86c324c46 100644 --- a/tests/hooks/observer-memory.test.js +++ b/tests/hooks/observer-memory.test.js @@ -375,7 +375,7 @@ test('observe.sh creates counter file and increments on each call', () => { path.join(scriptsLibDir, 'homunculus-dir.sh'), [ '#!/bin/bash', - '_ecc_resolve_homunculus_dir() { printf "%s\\n" "$HOME/.local/share/ecc-homunculus"; }', + '_clv2_resolve_homunculus_dir() { printf "%s\\n" "$HOME/.local/share/ecc-homunculus"; }', '' ].join('\n') ); diff --git a/tests/scripts/install-apply.test.js b/tests/scripts/install-apply.test.js index cfe8816b2..da1a258b4 100644 --- a/tests/scripts/install-apply.test.js +++ b/tests/scripts/install-apply.test.js @@ -123,6 +123,40 @@ function runTests() { } })) passed++; else failed++; + if (test('rewrites namespaced skill links to the ecc/ rules path (#2340)', () => { + const homeDir = createTempDir('install-apply-home-'); + const projectDir = createTempDir('install-apply-project-'); + + try { + const result = run(['typescript'], { cwd: projectDir, homeDir }); + assert.strictEqual(result.code, 0, result.stderr); + + const claudeRoot = path.join(homeDir, '.claude'); + const skillPath = path.join(claudeRoot, 'skills', 'ecc', 'react-patterns', 'SKILL.md'); + assert.ok(fs.existsSync(skillPath), 'react-patterns SKILL.md should be installed'); + + const content = fs.readFileSync(skillPath, 'utf8'); + assert.ok( + content.includes('../../../rules/ecc/react/'), + 'source-relative rules link should be rewritten for the ecc/ namespace' + ); + assert.ok( + !content.includes('](../../rules/'), + 'no un-namespaced ](../../rules/ links should remain' + ); + + // The rewritten link must resolve to a file that actually exists on disk. + const linkTarget = path.join( + path.dirname(skillPath), + '../../../rules/ecc/react/hooks.md' + ); + assert.ok(fs.existsSync(linkTarget), 'rewritten link target should exist'); + } finally { + cleanup(homeDir); + cleanup(projectDir); + } + })) passed++; else failed++; + if (test('installs Cursor configs and writes install-state', () => { const homeDir = createTempDir('install-apply-home-'); const projectDir = createTempDir('install-apply-project-');