mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
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:
@@ -34,3 +34,4 @@ Thumbs.db
|
|||||||
*.tmp
|
*.tmp
|
||||||
*.temp
|
*.temp
|
||||||
.claude/
|
.claude/
|
||||||
|
media-assets/
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ npx vitest run -t "should create session" # By pattern
|
|||||||
|
|
||||||
# Tests mock PTY - no real Claude CLI spawned
|
# Tests mock PTY - no real Claude CLI spawned
|
||||||
# Test timeout: 30s (configured in vitest.config.ts)
|
# Test timeout: 30s (configured in vitest.config.ts)
|
||||||
|
# Global test utilities (describe/it/expect) available without imports (globals: true)
|
||||||
|
|
||||||
# TypeScript checking
|
# TypeScript checking
|
||||||
npm run typecheck # Type check without building (or: npx tsc --noEmit)
|
npm run typecheck # Type check without building (or: npx tsc --noEmit)
|
||||||
@@ -107,6 +108,7 @@ pkill -f "SCREEN.*claudeman" # Force kill all claudeman screens
|
|||||||
| `src/tui/components/*.tsx` | TUI components: StartScreen, TabBar, TerminalView, StatusBar, RalphPanel, HelpOverlay |
|
| `src/tui/components/*.tsx` | TUI components: StartScreen, TabBar, TerminalView, StatusBar, RalphPanel, HelpOverlay |
|
||||||
| `src/tui/hooks/useSessionManager.ts` | TUI session state, screen polling, input handling |
|
| `src/tui/hooks/useSessionManager.ts` | TUI session state, screen polling, input handling |
|
||||||
| `src/types.ts` | All TypeScript interfaces |
|
| `src/types.ts` | All TypeScript interfaces |
|
||||||
|
| `src/templates/claude-md.ts` | CLAUDE.md template generation with placeholder support |
|
||||||
|
|
||||||
### Data Flow
|
### Data Flow
|
||||||
|
|
||||||
@@ -345,7 +347,17 @@ Writes debounced to `~/.claudeman/state.json`. Batches rapid changes.
|
|||||||
|
|
||||||
### TypeScript Config
|
### TypeScript Config
|
||||||
|
|
||||||
Module resolution: NodeNext. Target: ES2022. Strict mode enabled. See `tsconfig.json` for full settings.
|
Module resolution: NodeNext. Target: ES2022. Strict mode with additional checks:
|
||||||
|
|
||||||
|
| Setting | Effect |
|
||||||
|
|---------|--------|
|
||||||
|
| `noUnusedLocals` | Error on unused local variables |
|
||||||
|
| `noUnusedParameters` | Error on unused function parameters |
|
||||||
|
| `noImplicitReturns` | All code paths must return a value |
|
||||||
|
| `noFallthroughCasesInSwitch` | Require break/return in switch cases |
|
||||||
|
| `allowUnreachableCode: false` | Error on unreachable code |
|
||||||
|
|
||||||
|
TUI uses React JSX (`jsxImportSource: react`) for Ink components.
|
||||||
|
|
||||||
## Adding New Features
|
## Adding New Features
|
||||||
|
|
||||||
@@ -471,6 +483,13 @@ Long-running sessions are supported with automatic trimming:
|
|||||||
|
|
||||||
Cases created in `~/claudeman-cases/` by default.
|
Cases created in `~/claudeman-cases/` by default.
|
||||||
|
|
||||||
|
### Custom CLAUDE.md Templates
|
||||||
|
|
||||||
|
New cases can use custom CLAUDE.md templates via `generateClaudeMd()` in `src/templates/claude-md.ts`. Placeholders:
|
||||||
|
- `[PROJECT_NAME]` → Case name
|
||||||
|
- `[PROJECT_DESCRIPTION]` → Description
|
||||||
|
- `[DATE]` → Current date (YYYY-MM-DD)
|
||||||
|
|
||||||
## Screen Session Manager (CLI Tool)
|
## Screen Session Manager (CLI Tool)
|
||||||
|
|
||||||
Interactive bash tool for managing claudeman screen sessions directly from the terminal.
|
Interactive bash tool for managing claudeman screen sessions directly from the terminal.
|
||||||
|
|||||||
@@ -786,14 +786,14 @@ export class InnerLoopTracker extends EventEmitter {
|
|||||||
// If we have an expected completion phrase, also check for bare phrase
|
// If we have an expected completion phrase, also check for bare phrase
|
||||||
// This handles cases where Claude outputs "ALL_TASKS_DONE" without the tags
|
// This handles cases where Claude outputs "ALL_TASKS_DONE" without the tags
|
||||||
const expectedPhrase = this._loopState.completionPhrase;
|
const expectedPhrase = this._loopState.completionPhrase;
|
||||||
if (expectedPhrase && line.includes(expectedPhrase)) {
|
if (expectedPhrase && line.toUpperCase().includes(expectedPhrase.toUpperCase())) {
|
||||||
// Avoid false positives: don't trigger on the original prompt echo
|
// Avoid false positives: don't trigger on prompt context
|
||||||
// Only trigger if line looks like completion output (standalone or at end)
|
|
||||||
const isStandalone = line.trim() === expectedPhrase;
|
|
||||||
const isAtEnd = line.trim().endsWith(expectedPhrase);
|
|
||||||
const isNotInPromptContext = !line.includes('<promise>') && !line.includes('output:');
|
const isNotInPromptContext = !line.includes('<promise>') && !line.includes('output:');
|
||||||
|
// Also avoid triggering on "completion phrase is X" explanatory text
|
||||||
|
const isNotExplanation = !line.toLowerCase().includes('completion phrase') &&
|
||||||
|
!line.toLowerCase().includes('output exactly');
|
||||||
|
|
||||||
if ((isStandalone || isAtEnd) && isNotInPromptContext) {
|
if (isNotInPromptContext && isNotExplanation) {
|
||||||
this.handleBareCompletionPhrase(expectedPhrase);
|
this.handleBareCompletionPhrase(expectedPhrase);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -817,9 +817,13 @@ export class InnerLoopTracker extends EventEmitter {
|
|||||||
* @fires loopUpdate - When loop state changes
|
* @fires loopUpdate - When loop state changes
|
||||||
*/
|
*/
|
||||||
private handleBareCompletionPhrase(phrase: string): void {
|
private handleBareCompletionPhrase(phrase: string): void {
|
||||||
// Only count if this phrase was already seen in tagged form (from the prompt)
|
// Allow bare phrase detection if:
|
||||||
|
// 1. Loop is explicitly active (via startLoop()) - phrase was set programmatically
|
||||||
|
// 2. OR phrase was seen in tagged form (from terminal output)
|
||||||
const taggedCount = this._completionPhraseCount.get(phrase) || 0;
|
const taggedCount = this._completionPhraseCount.get(phrase) || 0;
|
||||||
if (taggedCount === 0) return;
|
const loopExplicitlyActive = this._loopState.active;
|
||||||
|
|
||||||
|
if (taggedCount === 0 && !loopExplicitlyActive) return;
|
||||||
|
|
||||||
// Track bare occurrences to avoid double-firing
|
// Track bare occurrences to avoid double-firing
|
||||||
const bareKey = `bare:${phrase}`;
|
const bareKey = `bare:${phrase}`;
|
||||||
|
|||||||
@@ -73,6 +73,63 @@ const SCHEDULED_RUN_MAX_AGE = 60 * 60 * 1000;
|
|||||||
// Maximum concurrent sessions to prevent resource exhaustion
|
// Maximum concurrent sessions to prevent resource exhaustion
|
||||||
const MAX_CONCURRENT_SESSIONS = 50;
|
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 {
|
export class WebServer extends EventEmitter {
|
||||||
private app: FastifyInstance;
|
private app: FastifyInstance;
|
||||||
private sessions: Map<string, Session> = new Map();
|
private sessions: Map<string, Session> = new Map();
|
||||||
@@ -377,9 +434,13 @@ export class WebServer extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Auto-detect completion phrase from CLAUDE.md BEFORE starting
|
||||||
|
autoConfigureInnerLoop(session, session.workingDir, () => {});
|
||||||
|
|
||||||
await session.startInteractive();
|
await session.startInteractive();
|
||||||
this.broadcast('session:interactive', { id });
|
this.broadcast('session:interactive', { id });
|
||||||
this.broadcast('session:updated', { session: session.toDetailedState() });
|
this.broadcast('session:updated', { session: session.toDetailedState() });
|
||||||
|
|
||||||
return { success: true, message: 'Interactive session started' };
|
return { success: true, message: 'Interactive session started' };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return { error: getErrorMessage(err) };
|
return { error: getErrorMessage(err) };
|
||||||
@@ -582,6 +643,9 @@ export class WebServer extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Auto-detect completion phrase from CLAUDE.md BEFORE starting
|
||||||
|
autoConfigureInnerLoop(session, session.workingDir, () => {});
|
||||||
|
|
||||||
// Start interactive session
|
// Start interactive session
|
||||||
await session.startInteractive();
|
await session.startInteractive();
|
||||||
this.broadcast('session:interactive', { id });
|
this.broadcast('session:interactive', { id });
|
||||||
@@ -984,6 +1048,13 @@ export class WebServer extends EventEmitter {
|
|||||||
useScreen: true,
|
useScreen: true,
|
||||||
mode: mode,
|
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.sessions.set(session.id, session);
|
||||||
this.setupSessionListeners(session);
|
this.setupSessionListeners(session);
|
||||||
this.broadcast('session:created', session.toDetailedState());
|
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`);
|
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
|
// Restore respawn controller if it was enabled
|
||||||
if (screen.respawnConfig?.enabled) {
|
if (screen.respawnConfig?.enabled) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user