feat: implement spawn1337 autonomous agent protocol

Add full lifecycle management for spawning autonomous Claude sessions as
screen-based agents. Agents communicate via filesystem message bus, signal
completion via RalphTracker <promise> mechanism, and enforce resource budgets
(tokens, cost, timeout, depth limits).

New files:
- spawn-types.ts: Types, YAML parser, factory functions, serialization
- spawn-detector.ts: Terminal pattern detection for spawn1337 tags
- spawn-orchestrator.ts: Agent lifecycle (spawn, monitor, queue, cleanup)
- spawn-claude-md.ts: CLAUDE.md generator for agent sessions

Modified:
- session.ts: SpawnDetector integration, parent/child tracking
- server.ts: Orchestrator wiring, 11 API endpoints, SSE events
- types.ts: Re-exports, SessionState additions

Tests: 80 new tests across 3 test files (all passing)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-01-23 10:20:37 +01:00
co-authored by Claude Opus 4.5
parent 33a8febf30
commit 1705b7a67d
11 changed files with 3591 additions and 2 deletions
+78
View File
@@ -21,6 +21,7 @@ import * as pty from 'node-pty';
import { SessionState, SessionStatus, SessionConfig, ScreenSession, RalphTrackerState, RalphTodoItem } from './types.js';
import { TaskTracker, type BackgroundTask } from './task-tracker.js';
import { RalphTracker } from './ralph-tracker.js';
import { SpawnDetector } from './spawn-detector.js';
import { ScreenManager } from './screen-manager.js';
export type { BackgroundTask } from './task-tracker.js';
@@ -225,6 +226,14 @@ export interface SessionEvents {
ralphTodoUpdate: (todos: RalphTodoItem[]) => void;
/** Ralph completion phrase detected */
ralphCompletionDetected: (phrase: string) => void;
/** Spawn1337 agent spawn requested */
spawnRequested: (filePath: string, rawLine: string) => void;
/** Spawn1337 agent status query */
spawnStatusRequested: (agentId: string) => void;
/** Spawn1337 agent cancel request */
spawnCancelRequested: (agentId: string) => void;
/** Spawn1337 message to child agent */
spawnMessageToChild: (agentId: string, content: string) => void;
}
/**
@@ -325,6 +334,13 @@ export class Session extends EventEmitter {
// Ralph tracking (Ralph Wiggum loops and todo lists inside Claude Code)
private _ralphTracker: RalphTracker;
// Spawn1337 detection (agent spawning protocol)
private _spawnDetector: SpawnDetector;
// Agent tree tracking
private _parentAgentId: string | null = null;
private _childAgentIds: string[] = [];
// Store handler references for cleanup (prevents memory leaks)
private _taskTrackerHandlers: {
taskCreated: (task: BackgroundTask) => void;
@@ -339,6 +355,13 @@ export class Session extends EventEmitter {
completionDetected: (phrase: string) => void;
} | null = null;
private _spawnHandlers: {
spawnRequested: (filePath: string, rawLine: string) => void;
statusRequested: (agentId: string) => void;
cancelRequested: (agentId: string) => void;
messageToChild: (agentId: string, content: string) => void;
} | null = null;
constructor(config: Partial<SessionConfig> & {
workingDir: string;
mode?: SessionMode;
@@ -381,6 +404,19 @@ export class Session extends EventEmitter {
this._ralphTracker.on('loopUpdate', this._ralphHandlers.loopUpdate);
this._ralphTracker.on('todoUpdate', this._ralphHandlers.todoUpdate);
this._ralphTracker.on('completionDetected', this._ralphHandlers.completionDetected);
// Initialize Spawn detector and forward events (store handlers for cleanup)
this._spawnDetector = new SpawnDetector();
this._spawnHandlers = {
spawnRequested: (filePath, rawLine) => this.emit('spawnRequested', filePath, rawLine),
statusRequested: (agentId) => this.emit('spawnStatusRequested', agentId),
cancelRequested: (agentId) => this.emit('spawnCancelRequested', agentId),
messageToChild: (agentId, content) => this.emit('spawnMessageToChild', agentId, content),
};
this._spawnDetector.on('spawnRequested', this._spawnHandlers.spawnRequested);
this._spawnDetector.on('statusRequested', this._spawnHandlers.statusRequested);
this._spawnDetector.on('cancelRequested', this._spawnHandlers.cancelRequested);
this._spawnDetector.on('messageToChild', this._spawnHandlers.messageToChild);
}
get status(): SessionStatus {
@@ -468,6 +504,34 @@ export class Session extends EventEmitter {
return this._ralphTracker.getTodoStats();
}
// Spawn1337 tracking getters
get spawnDetector(): SpawnDetector {
return this._spawnDetector;
}
get parentAgentId(): string | null {
return this._parentAgentId;
}
set parentAgentId(value: string | null) {
this._parentAgentId = value;
}
get childAgentIds(): string[] {
return [...this._childAgentIds];
}
addChildAgentId(agentId: string): void {
if (!this._childAgentIds.includes(agentId)) {
this._childAgentIds.push(agentId);
}
}
removeChildAgentId(agentId: string): void {
const idx = this._childAgentIds.indexOf(agentId);
if (idx >= 0) this._childAgentIds.splice(idx, 1);
}
// Token tracking getters and setters
get totalTokens(): number {
return this._totalInputTokens + this._totalOutputTokens;
@@ -559,6 +623,8 @@ export class Session extends EventEmitter {
outputTokens: this._totalOutputTokens,
ralphEnabled: this._ralphTracker.enabled,
ralphCompletionPhrase: this._ralphTracker.loopState.completionPhrase || undefined,
parentAgentId: this._parentAgentId || undefined,
childAgentIds: this._childAgentIds.length > 0 ? this._childAgentIds : undefined,
};
}
@@ -743,6 +809,9 @@ export class Session extends EventEmitter {
// Forward to Ralph tracker to detect Ralph loops and todos
this._ralphTracker.processTerminalData(data);
// Forward to Spawn detector to detect spawn1337 protocol tags
this._spawnDetector.processTerminalData(data);
// Parse token count from status line (e.g., "123.4k tokens" or "5234 tokens")
this.parseTokensFromStatusLine(data);
@@ -1390,6 +1459,15 @@ export class Session extends EventEmitter {
this._ralphTracker.off('completionDetected', this._ralphHandlers.completionDetected);
this._ralphHandlers = null;
}
// Remove SpawnDetector handlers
if (this._spawnHandlers) {
this._spawnDetector.off('spawnRequested', this._spawnHandlers.spawnRequested);
this._spawnDetector.off('statusRequested', this._spawnHandlers.statusRequested);
this._spawnDetector.off('cancelRequested', this._spawnHandlers.cancelRequested);
this._spawnDetector.off('messageToChild', this._spawnHandlers.messageToChild);
this._spawnHandlers = null;
}
}
/**