mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-03 14:09:42 +02:00
chore: bump version to 0.1491
This commit is contained in:
@@ -96,6 +96,11 @@ export class RalphLoop extends EventEmitter {
|
||||
// If we crashed while running, reset to stopped
|
||||
this._status = 'stopped';
|
||||
this.store.setRalphLoopState({ status: 'stopped' });
|
||||
// Reset orphaned in_progress tasks back to pending
|
||||
for (const task of this.taskQueue.getRunningTasks()) {
|
||||
task.reset();
|
||||
this.taskQueue.updateTask(task);
|
||||
}
|
||||
}
|
||||
|
||||
this.setupEventHandlers();
|
||||
|
||||
@@ -2883,6 +2883,8 @@ export class RalphTracker extends EventEmitter {
|
||||
this.clearDebounceTimers();
|
||||
// Stop fix plan file watcher to prevent memory leak
|
||||
this.stopWatchingFixPlan();
|
||||
// Stop iteration stall detection timer to prevent leak
|
||||
this.stopIterationStallDetection();
|
||||
this._loopState = createInitialRalphTrackerState(); // This sets enabled: false
|
||||
this._todos.clear();
|
||||
this._taskNumberToContent.clear();
|
||||
@@ -3833,6 +3835,7 @@ export class RalphTracker extends EventEmitter {
|
||||
destroy(): void {
|
||||
this.clearDebounceTimers();
|
||||
this.stopWatchingFixPlan();
|
||||
this.stopIterationStallDetection();
|
||||
this._todos.clear();
|
||||
this._taskNumberToContent.clear();
|
||||
this._completionPhraseCount.clear();
|
||||
|
||||
+2
-1
@@ -1990,7 +1990,8 @@ export class Session extends EventEmitter {
|
||||
|
||||
// Immediately cleanup Promise callbacks to prevent orphaned references
|
||||
// during the rest of stop() processing (e.g., if screen kill times out)
|
||||
if (this.rejectPromise) {
|
||||
if (this.rejectPromise && !this._promptResolved) {
|
||||
this._promptResolved = true;
|
||||
this.rejectPromise(new Error('Session stopped'));
|
||||
}
|
||||
this.resolvePromise = null;
|
||||
|
||||
+2
-5
@@ -146,7 +146,6 @@ export class StateStore {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dirty = false;
|
||||
this.ensureDir();
|
||||
|
||||
const tempPath = this.filePath + '.tmp';
|
||||
@@ -163,7 +162,6 @@ export class StateStore {
|
||||
console.error('[StateStore] Circuit breaker OPEN - serialization failing repeatedly');
|
||||
this.circuitBreakerOpen = true;
|
||||
}
|
||||
this.dirty = true;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -180,7 +178,8 @@ export class StateStore {
|
||||
await writeFile(tempPath, json, 'utf-8');
|
||||
await rename(tempPath, this.filePath);
|
||||
|
||||
// Success! Reset failure counter
|
||||
// Success! Clear dirty flag AFTER write completes
|
||||
this.dirty = false;
|
||||
this.consecutiveSaveFailures = 0;
|
||||
if (this.circuitBreakerOpen) {
|
||||
console.log('[StateStore] Circuit breaker CLOSED - save succeeded');
|
||||
@@ -202,8 +201,6 @@ export class StateStore {
|
||||
console.error('[StateStore] Circuit breaker OPEN - writes failing repeatedly');
|
||||
this.circuitBreakerOpen = true;
|
||||
}
|
||||
|
||||
this.dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -355,6 +355,55 @@ Final verification:
|
||||
|
||||
Instead: Fix the issue, verify, then complete. For time-aware loops: generate more tasks and keep improving until minimum time elapsed.
|
||||
|
||||
### RALPH_STATUS Block (Required During Ralph Loop)
|
||||
|
||||
At the **END of every response** during a Ralph Loop, output this structured status block:
|
||||
|
||||
```
|
||||
---RALPH_STATUS---
|
||||
STATUS: IN_PROGRESS | COMPLETE | BLOCKED
|
||||
TASKS_COMPLETED_THIS_LOOP: <number>
|
||||
FILES_MODIFIED: <number>
|
||||
TESTS_STATUS: PASSING | FAILING | NOT_RUN
|
||||
WORK_TYPE: IMPLEMENTATION | TESTING | DOCUMENTATION | REFACTORING
|
||||
EXIT_SIGNAL: false | true
|
||||
RECOMMENDATION: <one line summary of what to do next>
|
||||
---END_RALPH_STATUS---
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- Output this block at the end of **every** response, no exceptions
|
||||
- Set `EXIT_SIGNAL` to `true` ONLY when ALL tasks are verifiably done
|
||||
- Set `STATUS` to `BLOCKED` when you need human intervention
|
||||
- Do NOT continue with busy work when `EXIT_SIGNAL` should be `true`
|
||||
- Do NOT forget the status block — it is required for loop tracking
|
||||
|
||||
### Testing Limits
|
||||
|
||||
- **LIMIT testing to ~20% of total effort** per loop
|
||||
- PRIORITIZE: Implementation > Documentation > Tests
|
||||
- Only write tests for NEW functionality
|
||||
- Do NOT refactor existing tests unless broken
|
||||
- Do NOT run tests repeatedly without implementing new features
|
||||
|
||||
### Exit Scenarios (When to Set EXIT_SIGNAL)
|
||||
|
||||
| Scenario | STATUS | EXIT_SIGNAL | Action |
|
||||
|----------|--------|-------------|--------|
|
||||
| All tasks completed, tests pass | COMPLETE | true | Output completion phrase |
|
||||
| No work remaining, specs done | COMPLETE | true | Output completion phrase |
|
||||
| Making normal progress | IN_PROGRESS | false | Continue to next task |
|
||||
| Test-only loop (no implementation) | IN_PROGRESS | false | Warn and shift to implementation |
|
||||
| Stuck on same error repeatedly | BLOCKED | false | Describe blocker, request help |
|
||||
| Needs human decision/intervention | BLOCKED | false | Describe what's needed |
|
||||
|
||||
**Anti-patterns to avoid:**
|
||||
- Setting `EXIT_SIGNAL: true` when tests are failing
|
||||
- Continuing to work when all tasks are genuinely done (busy work)
|
||||
- Running the same failing test repeatedly without changing approach
|
||||
- Adding features not in the original specifications
|
||||
- Refactoring working code instead of completing assigned tasks
|
||||
|
||||
---
|
||||
|
||||
## Code Standards
|
||||
|
||||
+8
-5
@@ -26,7 +26,8 @@ import { spawn, execSync, exec } from 'node:child_process';
|
||||
import { promisify } from 'node:util';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
import { existsSync, readFileSync, mkdirSync, writeFile } from 'node:fs';
|
||||
import { existsSync, readFileSync, mkdirSync } from 'node:fs';
|
||||
import { writeFile, rename } from 'node:fs/promises';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { homedir } from 'node:os';
|
||||
import { ProcessStats, PersistedRespawnConfig, getErrorMessage, NiceConfig, DEFAULT_NICE_CONFIG, type PaneInfo } from './types.js';
|
||||
@@ -190,6 +191,7 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
|
||||
/**
|
||||
* Save sessions to disk asynchronously. (NEVER writes in test mode)
|
||||
* Uses atomic temp+rename to prevent corruption on crash.
|
||||
*/
|
||||
private saveSessions(): void {
|
||||
if (IS_TEST_MODE) return;
|
||||
@@ -202,11 +204,12 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
const data = Array.from(this.sessions.values());
|
||||
const json = JSON.stringify(data, null, 2);
|
||||
|
||||
writeFile(MUX_SESSIONS_FILE, json, (err) => {
|
||||
if (err) {
|
||||
const tempPath = MUX_SESSIONS_FILE + '.tmp';
|
||||
writeFile(tempPath, json, 'utf-8')
|
||||
.then(() => rename(tempPath, MUX_SESSIONS_FILE))
|
||||
.catch((err) => {
|
||||
console.error('[TmuxManager] Failed to save sessions:', err);
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('[TmuxManager] Failed to save sessions:', err);
|
||||
}
|
||||
|
||||
+1
-1
@@ -79,7 +79,7 @@ export const QuickStartSchema = z.object({
|
||||
export const HookEventSchema = z.object({
|
||||
event: z.enum(['permission_prompt', 'elicitation_dialog', 'idle_prompt', 'stop', 'teammate_idle', 'task_completed']),
|
||||
sessionId: z.string().min(1),
|
||||
data: z.record(z.string(), z.unknown()).optional(),
|
||||
data: z.record(z.string(), z.unknown()).nullable().optional(),
|
||||
});
|
||||
|
||||
// ========== Configuration ==========
|
||||
|
||||
+126
-119
@@ -152,7 +152,7 @@ function formatUptime(seconds: number): string {
|
||||
* Extracts only relevant fields and limits total size to prevent
|
||||
* oversized payloads from being broadcast to all connected clients.
|
||||
*/
|
||||
function sanitizeHookData(data: Record<string, unknown> | undefined): Record<string, unknown> {
|
||||
function sanitizeHookData(data: Record<string, unknown> | null | undefined): Record<string, unknown> {
|
||||
if (!data || typeof data !== 'object') return {};
|
||||
|
||||
// Only forward known safe fields from Claude Code hook stdin
|
||||
@@ -336,6 +336,8 @@ export class WebServer extends EventEmitter {
|
||||
});
|
||||
// Per-session adaptive batch intervals (sessions with rapid output get longer batches)
|
||||
private adaptiveBatchIntervals: Map<string, number> = new Map();
|
||||
// Tracked minimum across adaptiveBatchIntervals (avoids spreading into Math.min on every batch)
|
||||
private _minBatchInterval: number = TERMINAL_BATCH_INTERVAL;
|
||||
// Scheduled runs cleanup timer
|
||||
private scheduledCleanupTimer: NodeJS.Timeout | null = null;
|
||||
// SSE event batching
|
||||
@@ -667,7 +669,7 @@ export class WebServer extends EventEmitter {
|
||||
updateCaseEnvVars(workingDir, body.envOverrides);
|
||||
}
|
||||
|
||||
const globalNice = this.getGlobalNiceConfig();
|
||||
const globalNice = await this.getGlobalNiceConfig();
|
||||
const session = new Session({
|
||||
workingDir,
|
||||
mode: body.mode || 'claude',
|
||||
@@ -680,7 +682,7 @@ export class WebServer extends EventEmitter {
|
||||
this.sessions.set(session.id, session);
|
||||
this.store.incrementSessionsCreated();
|
||||
this.persistSessionState(session);
|
||||
this.setupSessionListeners(session);
|
||||
await this.setupSessionListeners(session);
|
||||
|
||||
this.broadcast('session:created', session.toDetailedState());
|
||||
return { success: true, session: session.toDetailedState() };
|
||||
@@ -2015,7 +2017,7 @@ export class WebServer extends EventEmitter {
|
||||
this.sessions.set(session.id, session);
|
||||
this.store.incrementSessionsCreated();
|
||||
this.persistSessionState(session);
|
||||
this.setupSessionListeners(session);
|
||||
await this.setupSessionListeners(session);
|
||||
|
||||
this.broadcast('session:created', session.toDetailedState());
|
||||
|
||||
@@ -2089,22 +2091,21 @@ export class WebServer extends EventEmitter {
|
||||
// Get linked cases
|
||||
const linkedCasesFile = join(homedir(), '.claudeman', 'linked-cases.json');
|
||||
try {
|
||||
if (existsSync(linkedCasesFile)) {
|
||||
const linkedCases: Record<string, string> = JSON.parse(readFileSync(linkedCasesFile, 'utf-8'));
|
||||
for (const [name, path] of Object.entries(linkedCases)) {
|
||||
// Only add if not already in cases (avoid duplicates) and path exists
|
||||
if (!cases.some(c => c.name === name) && existsSync(path)) {
|
||||
cases.push({
|
||||
name,
|
||||
path,
|
||||
hasClaudeMd: existsSync(join(path, 'CLAUDE.md')),
|
||||
});
|
||||
}
|
||||
const linkedCases: Record<string, string> = JSON.parse(await fs.readFile(linkedCasesFile, 'utf-8'));
|
||||
for (const [name, path] of Object.entries(linkedCases)) {
|
||||
// Only add if not already in cases (avoid duplicates) and path exists
|
||||
if (!cases.some(c => c.name === name) && existsSync(path)) {
|
||||
cases.push({
|
||||
name,
|
||||
path,
|
||||
hasClaudeMd: existsSync(join(path, 'CLAUDE.md')),
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// Log but don't fail - linked cases are optional
|
||||
console.warn('[Server] Failed to read linked cases:', err);
|
||||
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
console.warn('[Server] Failed to read linked cases:', err);
|
||||
}
|
||||
}
|
||||
|
||||
return cases;
|
||||
@@ -2136,7 +2137,7 @@ export class WebServer extends EventEmitter {
|
||||
mkdirSync(join(casePath, 'src'), { recursive: true });
|
||||
|
||||
// Read settings to get custom template path
|
||||
const templatePath = this.getDefaultClaudeMdPath();
|
||||
const templatePath = await this.getDefaultClaudeMdPath();
|
||||
const claudeMd = generateClaudeMd(name, description || '', templatePath);
|
||||
writeFileSync(join(casePath, 'CLAUDE.md'), claudeMd);
|
||||
|
||||
@@ -2183,11 +2184,11 @@ export class WebServer extends EventEmitter {
|
||||
const linkedCasesFile = join(homedir(), '.claudeman', 'linked-cases.json');
|
||||
let linkedCases: Record<string, string> = {};
|
||||
try {
|
||||
if (existsSync(linkedCasesFile)) {
|
||||
linkedCases = JSON.parse(readFileSync(linkedCasesFile, 'utf-8'));
|
||||
linkedCases = JSON.parse(await fs.readFile(linkedCasesFile, 'utf-8'));
|
||||
} catch (err) {
|
||||
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
console.warn('[Server] Failed to read linked cases:', err);
|
||||
}
|
||||
} catch {
|
||||
// Ignore parse errors, start fresh
|
||||
}
|
||||
|
||||
// Check if name is already linked
|
||||
@@ -2216,20 +2217,18 @@ export class WebServer extends EventEmitter {
|
||||
// First check linked cases
|
||||
const linkedCasesFile = join(homedir(), '.claudeman', 'linked-cases.json');
|
||||
try {
|
||||
if (existsSync(linkedCasesFile)) {
|
||||
const linkedCases: Record<string, string> = JSON.parse(readFileSync(linkedCasesFile, 'utf-8'));
|
||||
if (linkedCases[name]) {
|
||||
const linkedPath = linkedCases[name];
|
||||
return {
|
||||
name,
|
||||
path: linkedPath,
|
||||
hasClaudeMd: existsSync(join(linkedPath, 'CLAUDE.md')),
|
||||
linked: true,
|
||||
};
|
||||
}
|
||||
const linkedCases: Record<string, string> = JSON.parse(await fs.readFile(linkedCasesFile, 'utf-8'));
|
||||
if (linkedCases[name]) {
|
||||
const linkedPath = linkedCases[name];
|
||||
return {
|
||||
name,
|
||||
path: linkedPath,
|
||||
hasClaudeMd: existsSync(join(linkedPath, 'CLAUDE.md')),
|
||||
linked: true,
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors, fall through to casesDir check
|
||||
// ENOENT or parse errors - fall through to casesDir check
|
||||
}
|
||||
|
||||
// Then check casesDir
|
||||
@@ -2255,14 +2254,12 @@ export class WebServer extends EventEmitter {
|
||||
|
||||
const linkedCasesFile = join(homedir(), '.claudeman', 'linked-cases.json');
|
||||
try {
|
||||
if (existsSync(linkedCasesFile)) {
|
||||
const linkedCases: Record<string, string> = JSON.parse(readFileSync(linkedCasesFile, 'utf-8'));
|
||||
if (linkedCases[name]) {
|
||||
casePath = linkedCases[name];
|
||||
}
|
||||
const linkedCases: Record<string, string> = JSON.parse(await fs.readFile(linkedCasesFile, 'utf-8'));
|
||||
if (linkedCases[name]) {
|
||||
casePath = linkedCases[name];
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors
|
||||
// ENOENT or parse errors - fall through to casesDir
|
||||
}
|
||||
|
||||
if (!casePath) {
|
||||
@@ -2276,7 +2273,7 @@ export class WebServer extends EventEmitter {
|
||||
}
|
||||
|
||||
try {
|
||||
const content = readFileSync(fixPlanPath, 'utf-8');
|
||||
const content = await fs.readFile(fixPlanPath, 'utf-8');
|
||||
|
||||
// Parse todos from the content (similar to ralph-tracker's importFixPlanMarkdown)
|
||||
const todos: Array<{ content: string; status: 'pending' | 'in_progress' | 'completed'; priority: string | null }> = [];
|
||||
@@ -2384,7 +2381,7 @@ export class WebServer extends EventEmitter {
|
||||
mkdirSync(join(casePath, 'src'), { recursive: true });
|
||||
|
||||
// Read settings to get custom template path
|
||||
const templatePath = this.getDefaultClaudeMdPath();
|
||||
const templatePath = await this.getDefaultClaudeMdPath();
|
||||
const claudeMd = generateClaudeMd(caseName, '', templatePath);
|
||||
writeFileSync(join(casePath, 'CLAUDE.md'), claudeMd);
|
||||
|
||||
@@ -2399,7 +2396,7 @@ export class WebServer extends EventEmitter {
|
||||
|
||||
// Create a new session with the case as working directory
|
||||
// Apply global Nice priority config if enabled in settings
|
||||
const niceConfig = this.getGlobalNiceConfig();
|
||||
const niceConfig = await this.getGlobalNiceConfig();
|
||||
const session = new Session({
|
||||
workingDir: casePath,
|
||||
screenManager: this.mux,
|
||||
@@ -2421,7 +2418,7 @@ export class WebServer extends EventEmitter {
|
||||
this.sessions.set(session.id, session);
|
||||
this.store.incrementSessionsCreated();
|
||||
this.persistSessionState(session);
|
||||
this.setupSessionListeners(session);
|
||||
await this.setupSessionListeners(session);
|
||||
this.broadcast('session:created', session.toDetailedState());
|
||||
|
||||
// Start in the appropriate mode
|
||||
@@ -2439,8 +2436,10 @@ export class WebServer extends EventEmitter {
|
||||
try {
|
||||
const settingsFilePath = join(homedir(), '.claudeman', 'settings.json');
|
||||
let settings: Record<string, unknown> = {};
|
||||
if (existsSync(settingsFilePath)) {
|
||||
settings = JSON.parse(readFileSync(settingsFilePath, 'utf-8'));
|
||||
try {
|
||||
settings = JSON.parse(await fs.readFile(settingsFilePath, 'utf-8'));
|
||||
} catch (err) {
|
||||
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err;
|
||||
}
|
||||
settings.lastUsedCase = caseName;
|
||||
const dir = dirname(settingsFilePath);
|
||||
@@ -2884,11 +2883,15 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
|
||||
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid file path');
|
||||
}
|
||||
|
||||
if (!existsSync(fullPath)) {
|
||||
return createErrorResponse(ApiErrorCode.NOT_FOUND, 'File not found');
|
||||
let content: string;
|
||||
try {
|
||||
content = await fs.readFile(fullPath, 'utf-8');
|
||||
} catch (err) {
|
||||
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
|
||||
return createErrorResponse(ApiErrorCode.NOT_FOUND, 'File not found');
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
const content = readFileSync(fullPath, 'utf-8');
|
||||
const isJson = filePath.endsWith('.json');
|
||||
|
||||
// Parse JSON content safely (may contain invalid JSON)
|
||||
@@ -3034,12 +3037,12 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
|
||||
|
||||
this.app.get('/api/settings', async () => {
|
||||
try {
|
||||
if (existsSync(settingsPath)) {
|
||||
const content = readFileSync(settingsPath, 'utf-8');
|
||||
return JSON.parse(content);
|
||||
}
|
||||
const content = await fs.readFile(settingsPath, 'utf-8');
|
||||
return JSON.parse(content);
|
||||
} catch (err) {
|
||||
console.error('Failed to read settings:', err);
|
||||
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
console.error('Failed to read settings:', err);
|
||||
}
|
||||
}
|
||||
return {};
|
||||
});
|
||||
@@ -3136,12 +3139,12 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
|
||||
|
||||
this.app.get('/api/subagent-window-states', async () => {
|
||||
try {
|
||||
if (existsSync(windowStatesPath)) {
|
||||
const content = readFileSync(windowStatesPath, 'utf-8');
|
||||
return JSON.parse(content);
|
||||
}
|
||||
const content = await fs.readFile(windowStatesPath, 'utf-8');
|
||||
return JSON.parse(content);
|
||||
} catch (err) {
|
||||
console.error('Failed to read subagent window states:', err);
|
||||
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
console.error('Failed to read subagent window states:', err);
|
||||
}
|
||||
}
|
||||
return { minimized: {}, open: [] };
|
||||
});
|
||||
@@ -3167,12 +3170,12 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
|
||||
|
||||
this.app.get('/api/subagent-parents', async () => {
|
||||
try {
|
||||
if (existsSync(parentMapPath)) {
|
||||
const content = readFileSync(parentMapPath, 'utf-8');
|
||||
return JSON.parse(content);
|
||||
}
|
||||
const content = await fs.readFile(parentMapPath, 'utf-8');
|
||||
return JSON.parse(content);
|
||||
} catch (err) {
|
||||
console.error('Failed to read subagent parent map:', err);
|
||||
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
console.error('Failed to read subagent parent map:', err);
|
||||
}
|
||||
}
|
||||
return {};
|
||||
});
|
||||
@@ -3668,7 +3671,7 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
|
||||
this.broadcast('session:deleted', { id: sessionId });
|
||||
}
|
||||
|
||||
private setupSessionListeners(session: Session): void {
|
||||
private async setupSessionListeners(session: Session): Promise<void> {
|
||||
// Create run summary tracker for this session
|
||||
const summaryTracker = new RunSummaryTracker(session.id, session.name);
|
||||
this.runSummaryTrackers.set(session.id, summaryTracker);
|
||||
@@ -3678,7 +3681,7 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
|
||||
session.ralphTracker.setWorkingDir(session.workingDir);
|
||||
|
||||
// Start watching for new images in this session's working directory (if enabled globally and per-session)
|
||||
if (this.isImageWatcherEnabled() && session.imageWatcherEnabled) {
|
||||
if (await this.isImageWatcherEnabled() && session.imageWatcherEnabled) {
|
||||
imageWatcher.watchSession(session.id, session.workingDir);
|
||||
}
|
||||
|
||||
@@ -4134,40 +4137,40 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
|
||||
}
|
||||
|
||||
// Helper to get custom CLAUDE.md template path from settings
|
||||
private getDefaultClaudeMdPath(): string | undefined {
|
||||
private async getDefaultClaudeMdPath(): Promise<string | undefined> {
|
||||
const settingsPath = join(homedir(), '.claudeman', 'settings.json');
|
||||
|
||||
try {
|
||||
if (existsSync(settingsPath)) {
|
||||
const content = readFileSync(settingsPath, 'utf-8');
|
||||
const settings = JSON.parse(content);
|
||||
if (settings.defaultClaudeMdPath) {
|
||||
return settings.defaultClaudeMdPath;
|
||||
}
|
||||
const content = await fs.readFile(settingsPath, 'utf-8');
|
||||
const settings = JSON.parse(content);
|
||||
if (settings.defaultClaudeMdPath) {
|
||||
return settings.defaultClaudeMdPath;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to read settings:', err);
|
||||
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
console.error('Failed to read settings:', err);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Helper to get global Nice priority config from settings
|
||||
private getGlobalNiceConfig(): NiceConfig | undefined {
|
||||
private async getGlobalNiceConfig(): Promise<NiceConfig | undefined> {
|
||||
const settingsPath = join(homedir(), '.claudeman', 'settings.json');
|
||||
|
||||
try {
|
||||
if (existsSync(settingsPath)) {
|
||||
const content = readFileSync(settingsPath, 'utf-8');
|
||||
const settings = JSON.parse(content);
|
||||
if (settings.nice && settings.nice.enabled) {
|
||||
return {
|
||||
enabled: settings.nice.enabled ?? false,
|
||||
niceValue: settings.nice.niceValue ?? DEFAULT_NICE_CONFIG.niceValue,
|
||||
};
|
||||
}
|
||||
const content = await fs.readFile(settingsPath, 'utf-8');
|
||||
const settings = JSON.parse(content);
|
||||
if (settings.nice && settings.nice.enabled) {
|
||||
return {
|
||||
enabled: settings.nice.enabled ?? false,
|
||||
niceValue: settings.nice.niceValue ?? DEFAULT_NICE_CONFIG.niceValue,
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to read Nice priority settings:', err);
|
||||
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
console.error('Failed to read Nice priority settings:', err);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -4231,7 +4234,7 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
|
||||
this.sessions.set(session.id, session);
|
||||
this.store.incrementSessionsCreated();
|
||||
this.persistSessionState(session);
|
||||
this.setupSessionListeners(session);
|
||||
await this.setupSessionListeners(session);
|
||||
run.sessionId = session.id;
|
||||
|
||||
addLog(`Starting task iteration with session ${session.id.slice(0, 8)}`);
|
||||
@@ -4464,6 +4467,10 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
|
||||
sessionInterval = TERMINAL_BATCH_INTERVAL;
|
||||
}
|
||||
this.adaptiveBatchIntervals.set(sessionId, sessionInterval);
|
||||
// Track minimum to avoid O(n) spread on every batch event
|
||||
if (sessionInterval < this._minBatchInterval) {
|
||||
this._minBatchInterval = sessionInterval;
|
||||
}
|
||||
|
||||
// Flush immediately if batch is large for responsiveness
|
||||
if (newBatch.length > BATCH_FLUSH_THRESHOLD) {
|
||||
@@ -4478,16 +4485,13 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
|
||||
// Start batch timer if not already running (uses adaptive interval)
|
||||
// Pick the minimum interval across all pending sessions for responsiveness
|
||||
if (!this.terminalBatchTimer) {
|
||||
const batchInterval = Math.min(
|
||||
...Array.from(this.adaptiveBatchIntervals.values()),
|
||||
TERMINAL_BATCH_INTERVAL
|
||||
);
|
||||
this.terminalBatchTimer = setTimeout(() => {
|
||||
this.flushTerminalBatches();
|
||||
this.terminalBatchTimer = null;
|
||||
// Clear per-session intervals after flush (they'll be recalculated on next event)
|
||||
this.adaptiveBatchIntervals.clear();
|
||||
}, batchInterval);
|
||||
this._minBatchInterval = TERMINAL_BATCH_INTERVAL;
|
||||
}, this._minBatchInterval);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4696,7 +4700,7 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
|
||||
}, 5 * 60 * 1000);
|
||||
|
||||
// Start subagent watcher for Claude Code background agent visibility (if enabled)
|
||||
if (this.isSubagentTrackingEnabled()) {
|
||||
if (await this.isSubagentTrackingEnabled()) {
|
||||
subagentWatcher.start();
|
||||
console.log('Subagent watcher started - monitoring ~/.claude/projects for background agent activity');
|
||||
} else {
|
||||
@@ -4704,7 +4708,7 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
|
||||
}
|
||||
|
||||
// Start image watcher for auto-popup of screenshots (if enabled)
|
||||
if (this.isImageWatcherEnabled()) {
|
||||
if (await this.isImageWatcherEnabled()) {
|
||||
imageWatcher.start();
|
||||
console.log('Image watcher started - monitoring session directories for new images');
|
||||
} else {
|
||||
@@ -4716,17 +4720,17 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
|
||||
/**
|
||||
* Check if subagent tracking is enabled in settings (default: true)
|
||||
*/
|
||||
private isSubagentTrackingEnabled(): boolean {
|
||||
private async isSubagentTrackingEnabled(): Promise<boolean> {
|
||||
const settingsPath = join(homedir(), '.claudeman', 'settings.json');
|
||||
try {
|
||||
if (existsSync(settingsPath)) {
|
||||
const content = readFileSync(settingsPath, 'utf-8');
|
||||
const settings = JSON.parse(content);
|
||||
// Default to true if not explicitly set
|
||||
return settings.subagentTrackingEnabled ?? true;
|
||||
}
|
||||
const content = await fs.readFile(settingsPath, 'utf-8');
|
||||
const settings = JSON.parse(content);
|
||||
// Default to true if not explicitly set
|
||||
return settings.subagentTrackingEnabled ?? true;
|
||||
} catch (err) {
|
||||
console.error('Failed to read subagent tracking setting:', err);
|
||||
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
console.error('Failed to read subagent tracking setting:', err);
|
||||
}
|
||||
}
|
||||
return true; // Default enabled
|
||||
}
|
||||
@@ -4734,17 +4738,17 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
|
||||
/**
|
||||
* Check if image watcher is enabled in settings (default: true)
|
||||
*/
|
||||
private isImageWatcherEnabled(): boolean {
|
||||
private async isImageWatcherEnabled(): Promise<boolean> {
|
||||
const settingsPath = join(homedir(), '.claudeman', 'settings.json');
|
||||
try {
|
||||
if (existsSync(settingsPath)) {
|
||||
const content = readFileSync(settingsPath, 'utf-8');
|
||||
const settings = JSON.parse(content);
|
||||
// Default to true if not explicitly set
|
||||
return settings.imageWatcherEnabled ?? true;
|
||||
}
|
||||
const content = await fs.readFile(settingsPath, 'utf-8');
|
||||
const settings = JSON.parse(content);
|
||||
// Default to true if not explicitly set
|
||||
return settings.imageWatcherEnabled ?? true;
|
||||
} catch (err) {
|
||||
console.error('Failed to read image watcher setting:', err);
|
||||
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
console.error('Failed to read image watcher setting:', err);
|
||||
}
|
||||
}
|
||||
return true; // Default enabled
|
||||
}
|
||||
@@ -4885,7 +4889,7 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
|
||||
}
|
||||
|
||||
this.sessions.set(session.id, session);
|
||||
this.setupSessionListeners(session);
|
||||
await this.setupSessionListeners(session);
|
||||
this.persistSessionState(session);
|
||||
|
||||
// Mark it as restored (not started yet - user needs to attach)
|
||||
@@ -4983,16 +4987,18 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
|
||||
this.respawnControllers.clear();
|
||||
|
||||
// Stop all scheduled runs first (they have their own session cleanup)
|
||||
for (const [id] of this.scheduledRuns) {
|
||||
await this.stopScheduledRun(id);
|
||||
}
|
||||
await Promise.allSettled(
|
||||
Array.from(this.scheduledRuns.keys()).map(id => this.stopScheduledRun(id))
|
||||
);
|
||||
|
||||
// Properly clean up all remaining sessions (removes listeners, clears state, etc.)
|
||||
// Properly clean up all remaining sessions in parallel (removes listeners, clears state, etc.)
|
||||
// Don't kill screens on server stop - they can be reattached on restart
|
||||
const sessionIds = Array.from(this.sessions.keys());
|
||||
for (const sessionId of sessionIds) {
|
||||
await this.cleanupSession(sessionId, false);
|
||||
}
|
||||
// Use Promise.race with a 30s timeout to prevent shutdown from hanging indefinitely
|
||||
const sessionCleanup = Promise.allSettled(
|
||||
Array.from(this.sessions.keys()).map(id => this.cleanupSession(id, false))
|
||||
);
|
||||
const shutdownTimeout = new Promise<void>(resolve => setTimeout(resolve, 30_000));
|
||||
await Promise.race([sessionCleanup, shutdownTimeout]);
|
||||
|
||||
// Flush state store to prevent data loss from debounced saves
|
||||
this.store.flushAll();
|
||||
@@ -5031,6 +5037,7 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
|
||||
// Dispose StaleExpirationMap (stops internal cleanup timer)
|
||||
this.lastTerminalEventTime.dispose();
|
||||
this.adaptiveBatchIntervals.clear();
|
||||
this._minBatchInterval = TERMINAL_BATCH_INTERVAL;
|
||||
this.activePlanOrchestrators.clear();
|
||||
this.cleaningUp.clear();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user