From 57ea2b786aebca2bcce4eb0fca0b0e468da524df Mon Sep 17 00:00:00 2001 From: b1rdmania <102524336+b1rdmania@users.noreply.github.com> Date: Thu, 19 Mar 2026 10:02:49 +0000 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20v0.6.0=20=E2=80=94=20reliability=20?= =?UTF-8?q?+=20remote=20control?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix infinite retry loop: scheduleRetry no longer resets retryCount after MAX_RETRIES - Orphan PID cleanup on startup: track agent PIDs in data/agent-pids.json, kill survivors on boot - /status command: active agents, queue depth, uptime via Telegram - /skills command: lists installed skills with descriptions from .claude/skills/ - setMyCommands() at startup: Telegram command menu auto-populated - GroupQueue.getStatus(): exposes live queue state for external consumers Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 12 +++++ package.json | 2 +- src/channels/telegram.ts | 67 ++++++++++++++++++++++++++++ src/group-queue.ts | 30 ++++++++++++- src/index.ts | 96 +++++++++++++++++++++++++++++++++++++++- 5 files changed, 203 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd3cf3e..7e0f1c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## v0.6.0 (2026-03-19) — Reliability + remote control + +### Fixes +- Infinite retry loop eliminated: `scheduleRetry` no longer resets `retryCount` after `MAX_RETRIES`. Previously, a group that hit max retries would silently reset the counter and retry forever. +- Orphaned agent processes from previous runs are now killed on startup. PIDs are tracked in `data/agent-pids.json` and cleaned up on boot, preventing slot starvation and timeout cascades after a crash or forced restart. + +### New +- `/status` command: shows active agents per group, queue depth (pending tasks + messages), and uptime. Available via Telegram. +- `/skills` command: lists all installed skills with descriptions, read live from `.claude/skills/`. Available via Telegram. +- Telegram command menu: `setMyCommands()` called at startup so all commands appear with descriptions when the user types `/`. +- `GroupQueue.getStatus()`: exposes live queue state (active count, waiting groups, per-group task/message queues) for external consumers. + ## v0.5.5 (2026-03-18) — Remote control hotfix ### Fixes diff --git a/package.json b/package.json index 4d2fe00..f2f2da8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostclaw", - "version": "0.5.5", + "version": "0.6.0", "description": "Personal AI assistant. Bare metal, Telegram-first, no containers.", "type": "module", "main": "dist/index.js", diff --git a/src/channels/telegram.ts b/src/channels/telegram.ts index f6dc2a3..730040b 100644 --- a/src/channels/telegram.ts +++ b/src/channels/telegram.ts @@ -20,6 +20,7 @@ export interface TelegramChannelOpts { onChatMetadata: OnChatMetadata; registeredGroups: () => Record; onReset?: (chatJid: string) => boolean; + onGetStatus?: () => string; } export class TelegramChannel implements Channel { @@ -97,6 +98,62 @@ export class TelegramChannel implements Channel { } }); + // Command to show active agents, queue depth, and uptime + this.bot.command('status', (ctx) => { + const chatJid = `tg:${ctx.chat.id}`; + const group = this.opts.registeredGroups()[chatJid]; + if (!group) { + ctx.reply('Not a registered chat.'); + return; + } + const text = this.opts.onGetStatus?.() ?? 'Status unavailable.'; + ctx.reply(text, { parse_mode: 'HTML' }); + }); + + // Command to list installed skills + this.bot.command('skills', (ctx) => { + const chatJid = `tg:${ctx.chat.id}`; + const group = this.opts.registeredGroups()[chatJid]; + if (!group) { + ctx.reply('Not a registered chat.'); + return; + } + const skillsDir = path.join(process.cwd(), '.claude', 'skills'); + if (!fs.existsSync(skillsDir)) { + ctx.reply('No skills directory found.'); + return; + } + const lines: string[] = ['Installed skills:']; + const dirs = fs.readdirSync(skillsDir).sort(); + for (const dir of dirs) { + const stat = fs.statSync(path.join(skillsDir, dir)); + if (!stat.isDirectory()) continue; + const skillMd = path.join(skillsDir, dir, 'SKILL.md'); + if (!fs.existsSync(skillMd)) continue; + const content = fs.readFileSync(skillMd, 'utf-8'); + const descMatch = content.match(/^description:\s*(.+)$/m); + const desc = descMatch ? descMatch[1].trim() : ''; + lines.push(`• /${dir}${desc ? ` — ${desc.slice(0, 80)}` : ''}`); + } + const text = lines.length > 1 ? lines.join('\n') : 'No skills installed.'; + // Chunk if needed — Telegram 4096 char limit + const MAX = 4096; + if (text.length <= MAX) { + ctx.reply(text, { parse_mode: 'HTML' }); + } else { + let chunk = ''; + for (const line of lines) { + if (chunk.length + line.length + 1 > MAX) { + ctx.reply(chunk, { parse_mode: 'HTML' }); + chunk = line; + } else { + chunk = chunk ? `${chunk}\n${line}` : line; + } + } + if (chunk) ctx.reply(chunk, { parse_mode: 'HTML' }); + } + }); + this.bot.on('message:text', async (ctx) => { // Skip commands if (ctx.message.text.startsWith('/')) return; @@ -296,6 +353,16 @@ export class TelegramChannel implements Channel { logger.error({ err: err.message }, 'Telegram bot error'); }); + // Register commands in Telegram's menu (shows when user types /) + await this.bot.api.setMyCommands([ + { command: 'ping', description: 'Check the bot is online' }, + { command: 'status', description: 'Active agents, queue depth, uptime' }, + { command: 'skills', description: 'List installed skills' }, + { command: 'reset', description: 'Kill stalled agent and clear queue' }, + { command: 'update', description: 'Pull latest code and restart' }, + { command: 'chatid', description: 'Get this chat\'s registration ID' }, + ]).catch((err) => logger.warn({ err }, 'setMyCommands failed (non-fatal)')); + return new Promise((resolve) => { this.bot!.start({ onStart: (botInfo) => { diff --git a/src/group-queue.ts b/src/group-queue.ts index 949304d..0a0559c 100644 --- a/src/group-queue.ts +++ b/src/group-queue.ts @@ -283,6 +283,35 @@ export class GroupQueue { } } + getStatus(): { + active: number; + waiting: number; + groups: { + jid: string; + active: boolean; + queuedTasks: number; + queuedMessages: boolean; + }[]; + } { + const groups: { + jid: string; + active: boolean; + queuedTasks: number; + queuedMessages: boolean; + }[] = []; + for (const [jid, state] of this.groups) { + if (state.active || state.pendingTasks.length > 0 || state.pendingMessages) { + groups.push({ + jid, + active: state.active, + queuedTasks: state.pendingTasks.length, + queuedMessages: state.pendingMessages, + }); + } + } + return { active: this.activeCount, waiting: this.waitingGroups.length, groups }; + } + private scheduleRetry(groupJid: string, state: GroupState): void { state.retryCount++; if (state.retryCount > MAX_RETRIES) { @@ -290,7 +319,6 @@ export class GroupQueue { { groupJid, retryCount: state.retryCount }, 'Max retries exceeded, dropping messages (will retry on next incoming message)', ); - state.retryCount = 0; return; } diff --git a/src/index.ts b/src/index.ts index bb00c62..e2d89c3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -54,6 +54,7 @@ let sessions: Record = {}; let registeredGroups: Record = {}; let lastAgentTimestamp: Record = {}; let messageLoopRunning = false; +const startTime = Date.now(); let whatsapp: WhatsAppChannel; const channels: Channel[] = []; @@ -307,8 +308,13 @@ async function runAgent( isMain, assistantName: ASSISTANT_NAME, }, - (proc, containerName) => - queue.registerProcess(chatJid, proc, containerName, group.folder), + (proc, containerName) => { + queue.registerProcess(chatJid, proc, containerName, group.folder); + if (proc.pid) { + trackAgentPid(proc.pid); + proc.once('exit', () => untrackAgentPid(proc.pid!)); + } + }, wrappedOnOutput, ); @@ -481,8 +487,62 @@ function releasePidLock(): void { } } +const agentPidsFile = path.join(DATA_DIR, 'agent-pids.json'); + +function readAgentPids(): number[] { + try { + return JSON.parse(fs.readFileSync(agentPidsFile, 'utf-8')); + } catch { + return []; + } +} + +function writeAgentPids(pids: number[]): void { + try { + fs.mkdirSync(DATA_DIR, { recursive: true }); + fs.writeFileSync(agentPidsFile, JSON.stringify(pids)); + } catch { + /* ignore */ + } +} + +function trackAgentPid(pid: number): void { + const pids = readAgentPids(); + if (!pids.includes(pid)) { + pids.push(pid); + writeAgentPids(pids); + } +} + +function untrackAgentPid(pid: number): void { + const pids = readAgentPids().filter((p) => p !== pid); + writeAgentPids(pids); +} + +function cleanupOrphanedAgents(): void { + const pids = readAgentPids(); + if (pids.length === 0) return; + + let killed = 0; + for (const pid of pids) { + try { + process.kill(pid, 0); // throws if dead + process.kill(pid, 'SIGKILL'); + killed++; + logger.warn({ pid }, 'Killed orphaned agent process from previous run'); + } catch { + /* already dead */ + } + } + writeAgentPids([]); + if (killed > 0) { + logger.info({ killed }, 'Orphan agent cleanup complete'); + } +} + async function main(): Promise { acquirePidLock(); + cleanupOrphanedAgents(); const errorsLog = path.join(process.cwd(), 'logs', 'errors.log'); try { @@ -531,6 +591,38 @@ async function main(): Promise { queue.clearQueue(chatJid); return queue.killAgent(chatJid); }, + onGetStatus: () => { + const status = queue.getStatus(); + const uptimeMs = Date.now() - startTime; + const uptimeMin = Math.floor(uptimeMs / 60000); + const uptimeHr = Math.floor(uptimeMin / 60); + const uptime = + uptimeHr > 0 + ? `${uptimeHr}h ${uptimeMin % 60}m` + : `${uptimeMin}m`; + + const lines = [ + `GhostClaw status`, + `Uptime: ${uptime}`, + `Active agents: ${status.active}`, + `Waiting groups: ${status.waiting}`, + ]; + + if (status.groups.length > 0) { + lines.push(''); + for (const g of status.groups) { + const group = registeredGroups[g.jid]; + const name = group?.name || g.jid; + const parts: string[] = []; + if (g.active) parts.push('running'); + if (g.queuedTasks > 0) parts.push(`${g.queuedTasks} task(s) queued`); + if (g.queuedMessages) parts.push('messages queued'); + lines.push(`• ${name}: ${parts.join(', ')}`); + } + } + + return lines.join('\n'); + }, }; if (TELEGRAM_BOT_TOKEN) { From 67e38b933a4aef746360391ee4d5dc849bb6e53e Mon Sep 17 00:00:00 2001 From: b1rdmania <102524336+b1rdmania@users.noreply.github.com> Date: Thu, 19 Mar 2026 10:15:14 +0000 Subject: [PATCH 2/6] style: apply prettier formatting Co-Authored-By: Claude Sonnet 4.6 --- src/channels/telegram.ts | 29 +++++++++++++++++++---------- src/group-queue.ts | 12 ++++++++++-- src/index.ts | 4 +--- 3 files changed, 30 insertions(+), 15 deletions(-) diff --git a/src/channels/telegram.ts b/src/channels/telegram.ts index 730040b..b2e9900 100644 --- a/src/channels/telegram.ts +++ b/src/channels/telegram.ts @@ -67,7 +67,9 @@ export class TelegramChannel implements Channel { return; } this.opts.onReset?.(chatJid); - ctx.reply('Reset. Agent killed and queue cleared — send me something to start fresh.'); + ctx.reply( + 'Reset. Agent killed and queue cleared — send me something to start fresh.', + ); }); // Command to pull latest code and restart @@ -133,7 +135,9 @@ export class TelegramChannel implements Channel { const content = fs.readFileSync(skillMd, 'utf-8'); const descMatch = content.match(/^description:\s*(.+)$/m); const desc = descMatch ? descMatch[1].trim() : ''; - lines.push(`• /${dir}${desc ? ` — ${desc.slice(0, 80)}` : ''}`); + lines.push( + `• /${dir}${desc ? ` — ${desc.slice(0, 80)}` : ''}`, + ); } const text = lines.length > 1 ? lines.join('\n') : 'No skills installed.'; // Chunk if needed — Telegram 4096 char limit @@ -354,14 +358,19 @@ export class TelegramChannel implements Channel { }); // Register commands in Telegram's menu (shows when user types /) - await this.bot.api.setMyCommands([ - { command: 'ping', description: 'Check the bot is online' }, - { command: 'status', description: 'Active agents, queue depth, uptime' }, - { command: 'skills', description: 'List installed skills' }, - { command: 'reset', description: 'Kill stalled agent and clear queue' }, - { command: 'update', description: 'Pull latest code and restart' }, - { command: 'chatid', description: 'Get this chat\'s registration ID' }, - ]).catch((err) => logger.warn({ err }, 'setMyCommands failed (non-fatal)')); + await this.bot.api + .setMyCommands([ + { command: 'ping', description: 'Check the bot is online' }, + { + command: 'status', + description: 'Active agents, queue depth, uptime', + }, + { command: 'skills', description: 'List installed skills' }, + { command: 'reset', description: 'Kill stalled agent and clear queue' }, + { command: 'update', description: 'Pull latest code and restart' }, + { command: 'chatid', description: "Get this chat's registration ID" }, + ]) + .catch((err) => logger.warn({ err }, 'setMyCommands failed (non-fatal)')); return new Promise((resolve) => { this.bot!.start({ diff --git a/src/group-queue.ts b/src/group-queue.ts index 0a0559c..46cb7dd 100644 --- a/src/group-queue.ts +++ b/src/group-queue.ts @@ -300,7 +300,11 @@ export class GroupQueue { queuedMessages: boolean; }[] = []; for (const [jid, state] of this.groups) { - if (state.active || state.pendingTasks.length > 0 || state.pendingMessages) { + if ( + state.active || + state.pendingTasks.length > 0 || + state.pendingMessages + ) { groups.push({ jid, active: state.active, @@ -309,7 +313,11 @@ export class GroupQueue { }); } } - return { active: this.activeCount, waiting: this.waitingGroups.length, groups }; + return { + active: this.activeCount, + waiting: this.waitingGroups.length, + groups, + }; } private scheduleRetry(groupJid: string, state: GroupState): void { diff --git a/src/index.ts b/src/index.ts index e2d89c3..f3a9dde 100644 --- a/src/index.ts +++ b/src/index.ts @@ -597,9 +597,7 @@ async function main(): Promise { const uptimeMin = Math.floor(uptimeMs / 60000); const uptimeHr = Math.floor(uptimeMin / 60); const uptime = - uptimeHr > 0 - ? `${uptimeHr}h ${uptimeMin % 60}m` - : `${uptimeMin}m`; + uptimeHr > 0 ? `${uptimeHr}h ${uptimeMin % 60}m` : `${uptimeMin}m`; const lines = [ `GhostClaw status`, From 2ec578042212888c51f3dba26d4c7e6c93b5d009 Mon Sep 17 00:00:00 2001 From: b1rdmania <102524336+b1rdmania@users.noreply.github.com> Date: Thu, 19 Mar 2026 10:20:05 +0000 Subject: [PATCH 3/6] fix: address CodeRabbit review comments - Make /skills handler async; await all ctx.reply() calls to preserve chunk order - Escape skill dir name and description with escapeXml() before HTML interpolation - Escape group name/JID in /status output with escapeXml() - Mirror PID tracking on scheduler onProcess hook (scheduled tasks were not tracked) - Validate agent-pids.json before killing: accept only positive integers, ignore malformed entries Co-Authored-By: Claude Sonnet 4.6 --- src/channels/telegram.ts | 20 ++++++++++---------- src/index.ts | 18 +++++++++++++----- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/src/channels/telegram.ts b/src/channels/telegram.ts index b2e9900..9bd51ad 100644 --- a/src/channels/telegram.ts +++ b/src/channels/telegram.ts @@ -7,7 +7,7 @@ import { ASSISTANT_NAME, TRIGGER_PATTERN } from '../config.js'; import { logger } from '../logger.js'; import { signalNewMessage } from '../message-signal.js'; import { transcribeBuffer, textToSpeech } from '../transcription.js'; -import { markdownToTelegramHtml } from '../router.js'; +import { markdownToTelegramHtml, escapeXml } from '../router.js'; import { Channel, OnChatMetadata, @@ -113,16 +113,16 @@ export class TelegramChannel implements Channel { }); // Command to list installed skills - this.bot.command('skills', (ctx) => { + this.bot.command('skills', async (ctx) => { const chatJid = `tg:${ctx.chat.id}`; const group = this.opts.registeredGroups()[chatJid]; if (!group) { - ctx.reply('Not a registered chat.'); + await ctx.reply('Not a registered chat.'); return; } const skillsDir = path.join(process.cwd(), '.claude', 'skills'); if (!fs.existsSync(skillsDir)) { - ctx.reply('No skills directory found.'); + await ctx.reply('No skills directory found.'); return; } const lines: string[] = ['Installed skills:']; @@ -135,26 +135,26 @@ export class TelegramChannel implements Channel { const content = fs.readFileSync(skillMd, 'utf-8'); const descMatch = content.match(/^description:\s*(.+)$/m); const desc = descMatch ? descMatch[1].trim() : ''; - lines.push( - `• /${dir}${desc ? ` — ${desc.slice(0, 80)}` : ''}`, - ); + const safeName = escapeXml(dir); + const safeDesc = desc ? escapeXml(desc.slice(0, 80)) : ''; + lines.push(`• /${safeName}${safeDesc ? ` — ${safeDesc}` : ''}`); } const text = lines.length > 1 ? lines.join('\n') : 'No skills installed.'; // Chunk if needed — Telegram 4096 char limit const MAX = 4096; if (text.length <= MAX) { - ctx.reply(text, { parse_mode: 'HTML' }); + await ctx.reply(text, { parse_mode: 'HTML' }); } else { let chunk = ''; for (const line of lines) { if (chunk.length + line.length + 1 > MAX) { - ctx.reply(chunk, { parse_mode: 'HTML' }); + await ctx.reply(chunk, { parse_mode: 'HTML' }); chunk = line; } else { chunk = chunk ? `${chunk}\n${line}` : line; } } - if (chunk) ctx.reply(chunk, { parse_mode: 'HTML' }); + if (chunk) await ctx.reply(chunk, { parse_mode: 'HTML' }); } }); diff --git a/src/index.ts b/src/index.ts index f3a9dde..cd50484 100644 --- a/src/index.ts +++ b/src/index.ts @@ -39,7 +39,7 @@ import { import { GroupQueue } from './group-queue.js'; import { resolveGroupFolderPath } from './group-folder.js'; import { startIpcWatcher } from './ipc.js'; -import { findChannel, formatMessages, formatOutbound } from './router.js'; +import { findChannel, formatMessages, formatOutbound, escapeXml } from './router.js'; import { startSchedulerLoop } from './task-scheduler.js'; import { Channel, NewMessage, RegisteredGroup } from './types.js'; import { logger } from './logger.js'; @@ -491,7 +491,10 @@ const agentPidsFile = path.join(DATA_DIR, 'agent-pids.json'); function readAgentPids(): number[] { try { - return JSON.parse(fs.readFileSync(agentPidsFile, 'utf-8')); + const raw: unknown = JSON.parse(fs.readFileSync(agentPidsFile, 'utf-8')); + if (!Array.isArray(raw)) return []; + // Accept only positive integers — 0/negative have process-group semantics on POSIX + return raw.filter((v): v is number => typeof v === 'number' && Number.isInteger(v) && v > 0); } catch { return []; } @@ -610,7 +613,7 @@ async function main(): Promise { lines.push(''); for (const g of status.groups) { const group = registeredGroups[g.jid]; - const name = group?.name || g.jid; + const name = escapeXml(group?.name || g.jid); const parts: string[] = []; if (g.active) parts.push('running'); if (g.queuedTasks > 0) parts.push(`${g.queuedTasks} task(s) queued`); @@ -654,8 +657,13 @@ async function main(): Promise { registeredGroups: () => registeredGroups, getSessions: () => sessions, queue, - onProcess: (groupJid, proc, containerName, groupFolder) => - queue.registerProcess(groupJid, proc, containerName, groupFolder), + onProcess: (groupJid, proc, containerName, groupFolder) => { + queue.registerProcess(groupJid, proc, containerName, groupFolder); + if (proc.pid) { + trackAgentPid(proc.pid); + proc.once('exit', () => untrackAgentPid(proc.pid!)); + } + }, sendMessage: async (jid, rawText) => { const channel = findChannel(channels, jid); if (!channel) { From 1d55a21a8649c7b57d54b0d09b7a744fb953d279 Mon Sep 17 00:00:00 2001 From: b1rdmania <102524336+b1rdmania@users.noreply.github.com> Date: Thu, 19 Mar 2026 10:26:41 +0000 Subject: [PATCH 4/6] style: restage prettier-formatted files Pre-commit hook was reformatting but not re-staging, causing committed versions to diverge from what prettier --check expects. Co-Authored-By: Claude Sonnet 4.6 --- src/channels/telegram.ts | 4 +++- src/index.ts | 11 +++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/channels/telegram.ts b/src/channels/telegram.ts index 9bd51ad..2ed5e37 100644 --- a/src/channels/telegram.ts +++ b/src/channels/telegram.ts @@ -137,7 +137,9 @@ export class TelegramChannel implements Channel { const desc = descMatch ? descMatch[1].trim() : ''; const safeName = escapeXml(dir); const safeDesc = desc ? escapeXml(desc.slice(0, 80)) : ''; - lines.push(`• /${safeName}${safeDesc ? ` — ${safeDesc}` : ''}`); + lines.push( + `• /${safeName}${safeDesc ? ` — ${safeDesc}` : ''}`, + ); } const text = lines.length > 1 ? lines.join('\n') : 'No skills installed.'; // Chunk if needed — Telegram 4096 char limit diff --git a/src/index.ts b/src/index.ts index cd50484..ee32202 100644 --- a/src/index.ts +++ b/src/index.ts @@ -39,7 +39,12 @@ import { import { GroupQueue } from './group-queue.js'; import { resolveGroupFolderPath } from './group-folder.js'; import { startIpcWatcher } from './ipc.js'; -import { findChannel, formatMessages, formatOutbound, escapeXml } from './router.js'; +import { + findChannel, + formatMessages, + formatOutbound, + escapeXml, +} from './router.js'; import { startSchedulerLoop } from './task-scheduler.js'; import { Channel, NewMessage, RegisteredGroup } from './types.js'; import { logger } from './logger.js'; @@ -494,7 +499,9 @@ function readAgentPids(): number[] { const raw: unknown = JSON.parse(fs.readFileSync(agentPidsFile, 'utf-8')); if (!Array.isArray(raw)) return []; // Accept only positive integers — 0/negative have process-group semantics on POSIX - return raw.filter((v): v is number => typeof v === 'number' && Number.isInteger(v) && v > 0); + return raw.filter( + (v): v is number => typeof v === 'number' && Number.isInteger(v) && v > 0, + ); } catch { return []; } From d1ae7a21e4cfde57352d721983bf429bbbd944cf Mon Sep 17 00:00:00 2001 From: b1rdmania <102524336+b1rdmania@users.noreply.github.com> Date: Thu, 19 Mar 2026 10:28:22 +0000 Subject: [PATCH 5/6] fix: add setMyCommands mock to telegram tests; fix husky hook to restage formatted files - telegram.test.ts: add setMyCommands vi.fn() to the mock bot api so connect() doesn't throw - .husky/pre-commit: add 'git add -u' after format:fix so prettier-reformatted files are included in the commit rather than left as unstaged changes (was causing CI format checks to fail) Co-Authored-By: Claude Sonnet 4.6 --- .husky/pre-commit | 1 + src/channels/telegram.test.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/.husky/pre-commit b/.husky/pre-commit index 73c726d..209d079 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1 +1,2 @@ npm run format:fix +git add -u diff --git a/src/channels/telegram.test.ts b/src/channels/telegram.test.ts index d97f3c5..1aaae04 100644 --- a/src/channels/telegram.test.ts +++ b/src/channels/telegram.test.ts @@ -34,6 +34,7 @@ vi.mock('grammy', () => ({ api = { sendMessage: vi.fn().mockResolvedValue(undefined), sendChatAction: vi.fn().mockResolvedValue(undefined), + setMyCommands: vi.fn().mockResolvedValue(undefined), }; constructor(token: string) { From 77f08fdda763a2322239fae5467abc24f54fe08d Mon Sep 17 00:00:00 2001 From: b1rdmania <102524336+b1rdmania@users.noreply.github.com> Date: Thu, 19 Mar 2026 10:57:54 +0000 Subject: [PATCH 6/6] fix: resolve all pre-existing test failures - container-runner.ts: replace dynamic require('child_process') with static import so vi.mock() intercepts execSync in tests - container-runner.test.ts: add execSync mock to child_process stub - telegram.test.ts: add setMyCommands to mock bot api (already in previous commit) All 32 test files, 448 tests now pass locally. Co-Authored-By: Claude Sonnet 4.6 --- src/container-runner.test.ts | 1 + src/container-runner.ts | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/container-runner.test.ts b/src/container-runner.test.ts index f17e159..bbec031 100644 --- a/src/container-runner.test.ts +++ b/src/container-runner.test.ts @@ -71,6 +71,7 @@ vi.mock('child_process', async () => { return { ...actual, spawn: vi.fn(() => fakeProc), + execSync: vi.fn(() => ''), exec: vi.fn( (_cmd: string, _opts: unknown, cb?: (err: Error | null) => void) => { if (cb) cb(null); diff --git a/src/container-runner.ts b/src/container-runner.ts index 99aab68..5b7e678 100644 --- a/src/container-runner.ts +++ b/src/container-runner.ts @@ -2,7 +2,7 @@ * Agent Runner for GhostClaw * Spawns agent execution as direct Node.js processes (no containers) */ -import { ChildProcess, spawn } from 'child_process'; +import { ChildProcess, spawn, execSync } from 'child_process'; import fs from 'fs'; import path from 'path'; @@ -194,7 +194,6 @@ function getAgentRunnerEntrypoint(): string { if (!fs.existsSync(distEntry)) { logger.info('Agent runner not compiled, compiling now...'); - const { execSync } = require('child_process'); execSync('npx tsc', { cwd: agentRunnerRoot, stdio: 'pipe' }); }