feat: auto-detect completion phrase from CLAUDE.md

- Extract <promise>PHRASE</promise> from CLAUDE.md on session start
- Configure inner loop tracker before first broadcast so UI shows phrase immediately
- More lenient bare phrase detection (triggers when loop is active)
- Auto-detect on session restoration from screens
- Add logging for auto-detection debugging

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-01-21 19:49:44 +01:00
co-authored by Claude Opus 4.5
parent e469b30ddb
commit 403c7e77fb
4 changed files with 115 additions and 9 deletions
+82
View File
@@ -73,6 +73,63 @@ const SCHEDULED_RUN_MAX_AGE = 60 * 60 * 1000;
// Maximum concurrent sessions to prevent resource exhaustion
const MAX_CONCURRENT_SESSIONS = 50;
// Pattern to extract completion phrase from CLAUDE.md
// Matches <promise>PHRASE</promise> with optional whitespace
const CLAUDE_MD_PROMISE_PATTERN = /<promise>\s*([A-Z0-9_]+)\s*<\/promise>/gi;
/**
* Extract completion phrase from CLAUDE.md content.
* Looks for <promise>PHRASE</promise> pattern.
*
* Handles multiple variations:
* - Raw text: <promise>PHRASE</promise>
* - In backticks: `<promise>PHRASE</promise>`
* - With whitespace: <promise> PHRASE </promise>
* - Multiple occurrences: returns the first one
*
* @param claudeMdPath - Path to CLAUDE.md file
* @returns The completion phrase (uppercase), or null if not found
*/
function extractCompletionPhrase(claudeMdPath: string): string | null {
try {
if (!existsSync(claudeMdPath)) return null;
const content = readFileSync(claudeMdPath, 'utf-8');
// Reset regex state (global flag)
CLAUDE_MD_PROMISE_PATTERN.lastIndex = 0;
// Find all matches and return the first one
const match = CLAUDE_MD_PROMISE_PATTERN.exec(content);
if (match && match[1]) {
const phrase = match[1].trim().toUpperCase();
console.log(`[auto-detect] Found completion phrase in CLAUDE.md: ${phrase}`);
return phrase;
}
return null;
} catch (err) {
console.error(`[auto-detect] Error reading CLAUDE.md: ${err}`);
return null;
}
}
/**
* Auto-configure inner loop tracker for a session based on CLAUDE.md
*/
function autoConfigureInnerLoop(session: Session, workingDir: string, broadcast: (event: string, data: unknown) => void): void {
const claudeMdPath = join(workingDir, 'CLAUDE.md');
const completionPhrase = extractCompletionPhrase(claudeMdPath);
if (completionPhrase) {
session.innerLoopTracker.enable();
session.innerLoopTracker.startLoop(completionPhrase);
console.log(`[auto-detect] Configured inner loop for session ${session.id} with phrase: ${completionPhrase}`);
broadcast('session:innerLoopUpdate', {
sessionId: session.id,
state: session.innerLoopTracker.loopState,
});
}
}
export class WebServer extends EventEmitter {
private app: FastifyInstance;
private sessions: Map<string, Session> = new Map();
@@ -377,9 +434,13 @@ export class WebServer extends EventEmitter {
}
try {
// Auto-detect completion phrase from CLAUDE.md BEFORE starting
autoConfigureInnerLoop(session, session.workingDir, () => {});
await session.startInteractive();
this.broadcast('session:interactive', { id });
this.broadcast('session:updated', { session: session.toDetailedState() });
return { success: true, message: 'Interactive session started' };
} catch (err) {
return { error: getErrorMessage(err) };
@@ -582,6 +643,9 @@ export class WebServer extends EventEmitter {
}
try {
// Auto-detect completion phrase from CLAUDE.md BEFORE starting
autoConfigureInnerLoop(session, session.workingDir, () => {});
// Start interactive session
await session.startInteractive();
this.broadcast('session:interactive', { id });
@@ -984,6 +1048,13 @@ export class WebServer extends EventEmitter {
useScreen: true,
mode: mode,
});
// Auto-detect completion phrase from CLAUDE.md BEFORE broadcasting
// so the initial state already has the phrase configured
if (mode === 'claude') {
autoConfigureInnerLoop(session, casePath, () => {}); // no broadcast yet
}
this.sessions.set(session.id, session);
this.setupSessionListeners(session);
this.broadcast('session:created', session.toDetailedState());
@@ -1729,6 +1800,17 @@ export class WebServer extends EventEmitter {
console.log(`[Server] Enabled inner loop tracker for session ${session.id} from screen config`);
}
// Auto-detect completion phrase from CLAUDE.md if not already set
if (!session.innerLoopTracker.loopState.completionPhrase) {
const claudeMdPath = join(session.workingDir, 'CLAUDE.md');
const completionPhrase = extractCompletionPhrase(claudeMdPath);
if (completionPhrase) {
session.innerLoopTracker.enable();
session.innerLoopTracker.startLoop(completionPhrase);
console.log(`[Server] Auto-detected completion phrase for session ${session.id}: ${completionPhrase}`);
}
}
// Restore respawn controller if it was enabled
if (screen.respawnConfig?.enabled) {
try {