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:
devin-ai-integration[bot] 2026-07-03 21:10:45 -07:00 committed by GitHub
parent 3167852753
commit 2d40baacbd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 143 additions and 41 deletions

View file

@ -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')
);

View file

@ -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

View file

@ -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);

View file

@ -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')
);

View file

@ -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-');