mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-06 09:47:53 +02:00
fix: resolve open-issue cluster (#2295, #2298, #2303–#2306, #2340) + createdTime fallback bug (#2408)
* 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 <affaan@itomarkets.com> 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 <affaan@itomarkets.com> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Affaan Mustafa <me@affaanmustafa.com>
This commit is contained in:
co-authored by
Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
affaan
Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Affaan Mustafa
parent
3167852753
commit
2d40baacbd
@@ -13,7 +13,7 @@ alwaysApply: true
|
|||||||
|
|
||||||
Types: feat, fix, refactor, docs, test, chore, perf, ci
|
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
|
## Pull Request Workflow
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ description: Git workflow guidelines for conventional commits and pull request p
|
|||||||
|
|
||||||
Types: feat, fix, refactor, docs, test, chore, perf, ci
|
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
|
## Pull Request Workflow
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
Tipos: feat, fix, refactor, docs, test, chore, perf, ci
|
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
|
## Flujo de Trabajo de Pull Request
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
タイプ: feat, fix, refactor, docs, test, chore, perf, ci
|
タイプ: 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 ワークフロー
|
## Pull Request ワークフロー
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
타입: feat, fix, refactor, docs, test, chore, perf, ci
|
타입: feat, fix, refactor, docs, test, chore, perf, ci
|
||||||
|
|
||||||
참고: 어트리뷰션 비활성화 여부는 각자의 `~/.claude/settings.json` 로컬 설정에 따라 달라질 수 있습니다.
|
참고: 공동 작성자 표기를 비활성화하려면 `~/.claude/settings.json`에 `"includeCoAuthoredBy": false`를 설정하세요. Claude Code는 기본적으로 `Co-Authored-By`를 추가하며 ECC는 이 설정을 포함하지 않습니다.
|
||||||
|
|
||||||
## Pull Request 워크플로우
|
## Pull Request 워크플로우
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
Tipos: feat, fix, refactor, docs, test, chore, perf, ci
|
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
|
## Fluxo de Trabalho de Pull Request
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
Types: feat, fix, refactor, docs, test, chore, perf, ci
|
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ışı
|
## Pull Request İş Akışı
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
类型:feat, fix, refactor, docs, test, chore, perf, ci
|
类型:feat, fix, refactor, docs, test, chore, perf, ci
|
||||||
|
|
||||||
注意:通过 ~/.claude/settings.json 全局禁用了归因。
|
注意:若要禁用共同作者归因,请在 `~/.claude/settings.json` 中设置 `"includeCoAuthoredBy": false`;Claude Code 默认会附加 `Co-Authored-By`,而 ECC 不会附带此设置。
|
||||||
|
|
||||||
## 拉取请求工作流程
|
## 拉取请求工作流程
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
類型:feat、fix、refactor、docs、test、chore、perf、ci
|
類型:feat、fix、refactor、docs、test、chore、perf、ci
|
||||||
|
|
||||||
注意:歸屬透過 ~/.claude/settings.json 全域停用。
|
注意:若要停用共同作者歸屬,請在 `~/.claude/settings.json` 中設定 `"includeCoAuthoredBy": false`;Claude Code 預設會附加 `Co-Authored-By`,而 ECC 不會隨附這個設定。
|
||||||
|
|
||||||
## Pull Request 工作流程
|
## Pull Request 工作流程
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
Types: feat, fix, refactor, docs, test, chore, perf, ci
|
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
|
## Pull Request Workflow
|
||||||
|
|
||||||
|
|||||||
@@ -18,10 +18,6 @@ const {
|
|||||||
log
|
log
|
||||||
} = require('./utils');
|
} = require('./utils');
|
||||||
|
|
||||||
function resolveCreatedTime(stats) {
|
|
||||||
return stats.birthtimeMs > 0 ? stats.birthtime : stats.ctime;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Session filename pattern: YYYY-MM-DD-[session-id]-session.tmp
|
// Session filename pattern: YYYY-MM-DD-[session-id]-session.tmp
|
||||||
// The session-id is optional (old format) and can include letters, digits,
|
// The session-id is optional (old format) and can include letters, digits,
|
||||||
// underscores, and hyphens, but must not start with a hyphen.
|
// underscores, and hyphens, but must not start with a hyphen.
|
||||||
@@ -30,6 +26,18 @@ function resolveCreatedTime(stats) {
|
|||||||
// "2026-02-01-ChezMoi_2-session.tmp"
|
// "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$/;
|
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
|
* 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")
|
* @param {string} filename - Session filename (e.g., "2026-01-17-abc123-session.tmp" or "2026-01-17-session.tmp")
|
||||||
|
|||||||
@@ -145,7 +145,10 @@ analyze_observations() {
|
|||||||
MAX_ANALYSIS_LINES="${ECC_OBSERVER_MAX_ANALYSIS_LINES:-500}"
|
MAX_ANALYSIS_LINES="${ECC_OBSERVER_MAX_ANALYSIS_LINES:-500}"
|
||||||
observer_tmp_dir="${PROJECT_DIR}/.observer-tmp"
|
observer_tmp_dir="${PROJECT_DIR}/.observer-tmp"
|
||||||
mkdir -p "$observer_tmp_dir"
|
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"
|
tail -n "$MAX_ANALYSIS_LINES" "$OBSERVATIONS_FILE" > "$analysis_file"
|
||||||
analysis_count=$(wc -l < "$analysis_file" 2>/dev/null || echo 0)
|
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"
|
echo "[$(date)] Using last $analysis_count of $obs_count observations for analysis" >> "$LOG_FILE"
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ Validate and sanitize all user input before processing.
|
|||||||
|
|
||||||
When creating instincts, determine scope based on these heuristics:
|
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.
|
**When in doubt, default to `scope: project`** — it's safer to be project-specific and promote later than to contaminate the global space.
|
||||||
|
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ fi
|
|||||||
|
|
||||||
# shellcheck disable=SC1091
|
# shellcheck disable=SC1091
|
||||||
. "$(dirname "$0")/../scripts/lib/homunculus-dir.sh"
|
. "$(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)
|
# Skip if disabled (check both default and CLV2_CONFIG-derived locations)
|
||||||
if [ -f "$CONFIG_DIR/disabled" ]; then
|
if [ -f "$CONFIG_DIR/disabled" ]; then
|
||||||
@@ -279,11 +279,11 @@ _SECRET_RE = re.compile(
|
|||||||
)
|
)
|
||||||
|
|
||||||
import signal
|
import signal
|
||||||
def _ecc_bail(*_):
|
def _clv2_bail(*_):
|
||||||
print("[observe] SIGALRM timeout: parse-error fallback observation dropped before write (#2300)", file=sys.stderr)
|
print("[observe] SIGALRM timeout: parse-error fallback observation dropped before write (#2300)", file=sys.stderr)
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
try:
|
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)
|
signal.alarm(8) # self-terminate before the async hook 10s timeout can orphan us (#2278)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
@@ -317,11 +317,11 @@ echo "$PARSED" | "$PYTHON_CMD" -c '
|
|||||||
import json, sys, os, re
|
import json, sys, os, re
|
||||||
import signal
|
import signal
|
||||||
|
|
||||||
def _ecc_bail(*_):
|
def _clv2_bail(*_):
|
||||||
print("[observe] SIGALRM timeout: in-flight observation dropped before write (#2300)", file=sys.stderr)
|
print("[observe] SIGALRM timeout: in-flight observation dropped before write (#2300)", file=sys.stderr)
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
try:
|
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)
|
signal.alarm(8) # self-terminate before the async hook 10s timeout can orphan us (#2278)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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.
|
# the lazy-start path above. Both wrap the same read-modify-write below.
|
||||||
should_signal=0
|
should_signal=0
|
||||||
|
|
||||||
_ecc_bump_signal_counter() {
|
_clv2_bump_signal_counter() {
|
||||||
if [ -f "$SIGNAL_COUNTER_FILE" ]; then
|
if [ -f "$SIGNAL_COUNTER_FILE" ]; then
|
||||||
counter=$(cat "$SIGNAL_COUNTER_FILE" 2>/dev/null || echo 0)
|
counter=$(cat "$SIGNAL_COUNTER_FILE" 2>/dev/null || echo 0)
|
||||||
# Guard against a corrupt counter file: a non-integer value would abort the
|
# 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
|
# 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.
|
# a timeout we skip the tick rather than doing an unlocked read-modify-write.
|
||||||
if flock -w 2 8 2>/dev/null; then
|
if flock -w 2 8 2>/dev/null; then
|
||||||
_ecc_bump_signal_counter
|
_clv2_bump_signal_counter
|
||||||
flock -u 8 2>/dev/null || true
|
flock -u 8 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
exec 8>&- 2>/dev/null || true
|
exec 8>&- 2>/dev/null || true
|
||||||
@@ -547,7 +547,7 @@ else
|
|||||||
done
|
done
|
||||||
if [ "$_signal_lock_held" -eq 1 ]; then
|
if [ "$_signal_lock_held" -eq 1 ]; then
|
||||||
# Bump only under the held lock -- never an unlocked read-modify-write.
|
# 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
|
rmdir "$SIGNAL_COUNTER_LOCK" 2>/dev/null || true
|
||||||
trap - EXIT INT TERM
|
trap - EXIT INT TERM
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
# shellcheck disable=SC1091
|
# shellcheck disable=SC1091
|
||||||
. "$(dirname "${BASH_SOURCE[0]}")/lib/homunculus-dir.sh"
|
. "$(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_PROJECTS_DIR="${_CLV2_HOMUNCULUS_DIR}/projects"
|
||||||
_CLV2_REGISTRY_FILE="${_CLV2_HOMUNCULUS_DIR}/projects.json"
|
_CLV2_REGISTRY_FILE="${_CLV2_HOMUNCULUS_DIR}/projects.json"
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
# 2. XDG_DATA_HOME/ecc-homunculus, when XDG_DATA_HOME is absolute
|
# 2. XDG_DATA_HOME/ecc-homunculus, when XDG_DATA_HOME is absolute
|
||||||
# 3. HOME/.local/share/ecc-homunculus
|
# 3. HOME/.local/share/ecc-homunculus
|
||||||
|
|
||||||
_ecc_resolve_homunculus_dir() {
|
_clv2_resolve_homunculus_dir() {
|
||||||
if [ -n "${CLV2_HOMUNCULUS_DIR:-}" ]; then
|
if [ -n "${CLV2_HOMUNCULUS_DIR:-}" ]; then
|
||||||
case "$CLV2_HOMUNCULUS_DIR" in
|
case "$CLV2_HOMUNCULUS_DIR" in
|
||||||
/*) printf '%s\n' "$CLV2_HOMUNCULUS_DIR"; return 0 ;;
|
/*) printf '%s\n' "$CLV2_HOMUNCULUS_DIR"; return 0 ;;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ OLD="${HOME}/.claude/homunculus"
|
|||||||
|
|
||||||
# shellcheck disable=SC1091
|
# shellcheck disable=SC1091
|
||||||
. "$(dirname "$0")/lib/homunculus-dir.sh"
|
. "$(dirname "$0")/lib/homunculus-dir.sh"
|
||||||
NEW="$(_ecc_resolve_homunculus_dir)"
|
NEW="$(_clv2_resolve_homunculus_dir)"
|
||||||
|
|
||||||
if [ "$NEW" = "$OLD" ]; then
|
if [ "$NEW" = "$OLD" ]; then
|
||||||
echo "Resolved destination equals source ($OLD); nothing to migrate."
|
echo "Resolved destination equals source ($OLD); nothing to migrate."
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from unittest import mock
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ function buildSandbox() {
|
|||||||
path.join(scriptsLibDir, 'homunculus-dir.sh'),
|
path.join(scriptsLibDir, 'homunculus-dir.sh'),
|
||||||
[
|
[
|
||||||
'#!/bin/bash',
|
'#!/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')
|
].join('\n')
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,14 +3,14 @@
|
|||||||
*
|
*
|
||||||
* observe.sh arms a signal.SIGALRM alarm (8s) inside its inline-Python blocks so
|
* 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
|
* 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
|
* 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
|
* 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
|
* 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).
|
* Claude hook report a block, per the repo's "always exit 0; log to stderr" rule).
|
||||||
*
|
*
|
||||||
* Two checks:
|
* 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).
|
* sys.stderr before sys.exit(0).
|
||||||
* 2. Behavioral check — the REAL handler text extracted from observe.sh, when its
|
* 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
|
* 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');
|
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
|
// that follow it, up to (and including) the first dedented `sys.exit(0)` line at
|
||||||
// the same indentation as the def's body.
|
// the same indentation as the def's body.
|
||||||
function extractHandlers(src) {
|
function extractHandlers(src) {
|
||||||
const lines = src.split('\n');
|
const lines = src.split('\n');
|
||||||
const handlers = [];
|
const handlers = [];
|
||||||
for (let i = 0; i < lines.length; i += 1) {
|
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]];
|
const body = [lines[i]];
|
||||||
for (let j = i + 1; j < lines.length; j += 1) {
|
for (let j = i + 1; j < lines.length; j += 1) {
|
||||||
// Stop when we hit a line that is not indented (next top-level stmt).
|
// 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
|
// The #2300 timeout handlers are the ones that log the `[observe] SIGALRM
|
||||||
// timeout` marker. Selecting by marker (rather than by array index) keeps the
|
// timeout` marker. Selecting by marker (rather than by array index) keeps the
|
||||||
// behavioral check pinned to the timeout handlers even if an unrelated
|
// 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 =>
|
const timeoutHandlers = handlers.filter(body =>
|
||||||
body.includes('[observe] SIGALRM timeout')
|
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(
|
assert.ok(
|
||||||
handlers.length >= 2,
|
handlers.length >= 2,
|
||||||
`expected >= 2 _ecc_bail handlers, found ${handlers.length}`
|
`expected >= 2 _clv2_bail handlers, found ${handlers.length}`
|
||||||
);
|
);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
timeoutHandlers.length >= 2,
|
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) => {
|
handlers.forEach((body, idx) => {
|
||||||
const stderrIdx = body.indexOf('file=sys.stderr');
|
const stderrIdx = body.indexOf('file=sys.stderr');
|
||||||
const exitIdx = body.indexOf('sys.exit(0)');
|
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) => {
|
handlers.forEach((body, idx) => {
|
||||||
assert.ok(
|
assert.ok(
|
||||||
/sys\.exit\(0\)/.test(body),
|
/sys\.exit\(0\)/.test(body),
|
||||||
@@ -160,7 +160,7 @@ function runHandlerTimeout(python, handler) {
|
|||||||
const program = [
|
const program = [
|
||||||
'import sys, signal, time',
|
'import sys, signal, time',
|
||||||
handler,
|
handler,
|
||||||
'signal.signal(signal.SIGALRM, _ecc_bail)',
|
'signal.signal(signal.SIGALRM, _clv2_bail)',
|
||||||
'signal.alarm(1)',
|
'signal.alarm(1)',
|
||||||
'time.sleep(3)',
|
'time.sleep(3)',
|
||||||
'print("REACHED_END_SHOULD_NOT_HAPPEN")',
|
'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
|
// the worst case. A behavioral check on only one handler would not catch a
|
||||||
// regression that silenced another.
|
// regression that silenced another.
|
||||||
timeoutHandlers.forEach((handler, idx) => {
|
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();
|
const python = findPython();
|
||||||
if (!python) {
|
if (!python) {
|
||||||
// Fail fast rather than returning (which the harness would record as a
|
// Fail fast rather than returning (which the harness would record as a
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -375,7 +375,7 @@ test('observe.sh creates counter file and increments on each call', () => {
|
|||||||
path.join(scriptsLibDir, 'homunculus-dir.sh'),
|
path.join(scriptsLibDir, 'homunculus-dir.sh'),
|
||||||
[
|
[
|
||||||
'#!/bin/bash',
|
'#!/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')
|
].join('\n')
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -123,6 +123,40 @@ function runTests() {
|
|||||||
}
|
}
|
||||||
})) passed++; else failed++;
|
})) 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', () => {
|
if (test('installs Cursor configs and writes install-state', () => {
|
||||||
const homeDir = createTempDir('install-apply-home-');
|
const homeDir = createTempDir('install-apply-home-');
|
||||||
const projectDir = createTempDir('install-apply-project-');
|
const projectDir = createTempDir('install-apply-project-');
|
||||||
|
|||||||
Reference in New Issue
Block a user