arbiter-ai 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -0
- package/assets/jerom_16x16.png +0 -0
- package/dist/arbiter.d.ts +43 -0
- package/dist/arbiter.js +486 -0
- package/dist/context-analyzer.d.ts +15 -0
- package/dist/context-analyzer.js +603 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +165 -0
- package/dist/orchestrator.d.ts +31 -0
- package/dist/orchestrator.js +227 -0
- package/dist/router.d.ts +187 -0
- package/dist/router.js +1135 -0
- package/dist/router.test.d.ts +15 -0
- package/dist/router.test.js +95 -0
- package/dist/session-persistence.d.ts +9 -0
- package/dist/session-persistence.js +63 -0
- package/dist/session-persistence.test.d.ts +1 -0
- package/dist/session-persistence.test.js +165 -0
- package/dist/sound.d.ts +31 -0
- package/dist/sound.js +50 -0
- package/dist/state.d.ts +72 -0
- package/dist/state.js +107 -0
- package/dist/state.test.d.ts +1 -0
- package/dist/state.test.js +194 -0
- package/dist/test-headless.d.ts +1 -0
- package/dist/test-headless.js +155 -0
- package/dist/tui/index.d.ts +14 -0
- package/dist/tui/index.js +17 -0
- package/dist/tui/layout.d.ts +30 -0
- package/dist/tui/layout.js +200 -0
- package/dist/tui/render.d.ts +57 -0
- package/dist/tui/render.js +266 -0
- package/dist/tui/scene.d.ts +64 -0
- package/dist/tui/scene.js +366 -0
- package/dist/tui/screens/CharacterSelect-termkit.d.ts +18 -0
- package/dist/tui/screens/CharacterSelect-termkit.js +216 -0
- package/dist/tui/screens/ForestIntro-termkit.d.ts +15 -0
- package/dist/tui/screens/ForestIntro-termkit.js +856 -0
- package/dist/tui/screens/GitignoreCheck-termkit.d.ts +14 -0
- package/dist/tui/screens/GitignoreCheck-termkit.js +185 -0
- package/dist/tui/screens/TitleScreen-termkit.d.ts +14 -0
- package/dist/tui/screens/TitleScreen-termkit.js +132 -0
- package/dist/tui/screens/index.d.ts +9 -0
- package/dist/tui/screens/index.js +10 -0
- package/dist/tui/tileset.d.ts +97 -0
- package/dist/tui/tileset.js +237 -0
- package/dist/tui/tui-termkit.d.ts +34 -0
- package/dist/tui/tui-termkit.js +2602 -0
- package/dist/tui/types.d.ts +41 -0
- package/dist/tui/types.js +4 -0
- package/package.json +71 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Main entry point for the Arbiter system
|
|
3
|
+
// Ties together state, router, and TUI for the hierarchical AI orchestration system
|
|
4
|
+
import { Router } from './router.js';
|
|
5
|
+
import { loadSession } from './session-persistence.js';
|
|
6
|
+
import { createInitialState } from './state.js';
|
|
7
|
+
import { checkGitignore, createTUI, showCharacterSelect, showForestIntro, showTitleScreen, } from './tui/index.js';
|
|
8
|
+
/**
|
|
9
|
+
* Outputs session information to stderr for resume capability
|
|
10
|
+
* Format per architecture doc:
|
|
11
|
+
* {"arbiter": "session-abc", "lastOrchestrator": "session-xyz", "orchestratorNumber": 3}
|
|
12
|
+
*/
|
|
13
|
+
function outputSessionInfo(state) {
|
|
14
|
+
const sessionInfo = {
|
|
15
|
+
arbiter: state.arbiterSessionId,
|
|
16
|
+
lastOrchestrator: state.currentOrchestrator?.sessionId ?? null,
|
|
17
|
+
orchestratorNumber: state.currentOrchestrator?.number ?? null,
|
|
18
|
+
};
|
|
19
|
+
// Output to stderr so it doesn't interfere with TUI output
|
|
20
|
+
process.stderr.write(`${JSON.stringify(sessionInfo)}\n`);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Main application entry point
|
|
24
|
+
* Creates and wires together all components of the Arbiter system
|
|
25
|
+
*/
|
|
26
|
+
async function main() {
|
|
27
|
+
// Track components for cleanup
|
|
28
|
+
let tui = null;
|
|
29
|
+
let router = null;
|
|
30
|
+
let state = null;
|
|
31
|
+
let isShuttingDown = false;
|
|
32
|
+
/**
|
|
33
|
+
* Graceful shutdown handler
|
|
34
|
+
* Stops router, stops TUI, outputs session info, and exits
|
|
35
|
+
*/
|
|
36
|
+
async function shutdown(exitCode = 0) {
|
|
37
|
+
// Prevent multiple shutdown calls
|
|
38
|
+
if (isShuttingDown)
|
|
39
|
+
return;
|
|
40
|
+
isShuttingDown = true;
|
|
41
|
+
// Stop router first (aborts any running queries)
|
|
42
|
+
if (router) {
|
|
43
|
+
await router.stop();
|
|
44
|
+
}
|
|
45
|
+
// Stop TUI (restores terminal)
|
|
46
|
+
if (tui) {
|
|
47
|
+
tui.stop();
|
|
48
|
+
}
|
|
49
|
+
// Output session info for resume capability
|
|
50
|
+
if (state) {
|
|
51
|
+
outputSessionInfo(state);
|
|
52
|
+
// Show crash count if any crashes occurred during runtime
|
|
53
|
+
if (state.crashCount > 0) {
|
|
54
|
+
process.stderr.write(`\nSession had ${state.crashCount} crash(es) during runtime\n`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
process.exit(exitCode);
|
|
58
|
+
}
|
|
59
|
+
// Note: SIGINT is handled by the TUI for confirmation dialogs
|
|
60
|
+
// We only handle SIGTERM for graceful container shutdown
|
|
61
|
+
process.on('SIGTERM', () => {
|
|
62
|
+
shutdown(0);
|
|
63
|
+
});
|
|
64
|
+
try {
|
|
65
|
+
// Parse CLI arguments
|
|
66
|
+
const args = process.argv.slice(2);
|
|
67
|
+
const shouldResume = args.includes('--resume');
|
|
68
|
+
// Handle --resume flag
|
|
69
|
+
let savedSession = null;
|
|
70
|
+
if (shouldResume) {
|
|
71
|
+
savedSession = loadSession();
|
|
72
|
+
if (!savedSession) {
|
|
73
|
+
console.warn('No valid session to resume (file missing or stale >24h). Starting fresh...');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Check for positional requirements file argument (first non-flag arg)
|
|
77
|
+
const positionalArgs = args.filter((arg) => !arg.startsWith('--'));
|
|
78
|
+
const cliRequirementsFile = positionalArgs[0] || null;
|
|
79
|
+
let selectedCharacter;
|
|
80
|
+
if (savedSession) {
|
|
81
|
+
// Resume mode: skip intros, use default character
|
|
82
|
+
selectedCharacter = 0;
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
// Normal mode: title screen, character select, forest intro
|
|
86
|
+
// Show title screen first (any key continues)
|
|
87
|
+
await showTitleScreen();
|
|
88
|
+
// Check if Arbiter files should be added to .gitignore
|
|
89
|
+
await checkGitignore();
|
|
90
|
+
// Show character selection screen
|
|
91
|
+
let selectResult = await showCharacterSelect();
|
|
92
|
+
selectedCharacter = selectResult.character;
|
|
93
|
+
// Show animated forest intro with selected character (unless skipped)
|
|
94
|
+
// If player dies, go back to character select
|
|
95
|
+
if (!selectResult.skipIntro) {
|
|
96
|
+
let result = await showForestIntro(selectedCharacter);
|
|
97
|
+
while (result === 'death') {
|
|
98
|
+
selectResult = await showCharacterSelect();
|
|
99
|
+
selectedCharacter = selectResult.character;
|
|
100
|
+
if (selectResult.skipIntro)
|
|
101
|
+
break;
|
|
102
|
+
result = await showForestIntro(selectedCharacter);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// Create initial application state
|
|
107
|
+
state = createInitialState();
|
|
108
|
+
// Set requirements path if provided via CLI (interactive selection happens in TUI)
|
|
109
|
+
state.requirementsPath = cliRequirementsFile;
|
|
110
|
+
// Create TUI with state reference and selected character
|
|
111
|
+
tui = createTUI(state, selectedCharacter);
|
|
112
|
+
// Get router callbacks from TUI
|
|
113
|
+
// These callbacks update the display when router events occur
|
|
114
|
+
const routerCallbacks = tui.getRouterCallbacks();
|
|
115
|
+
// Create router with state and callbacks
|
|
116
|
+
router = new Router(state, routerCallbacks);
|
|
117
|
+
// Wire TUI input to router
|
|
118
|
+
// When user submits input, send it to the router
|
|
119
|
+
tui.onInput(async (text) => {
|
|
120
|
+
if (router) {
|
|
121
|
+
await router.sendHumanMessage(text);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
// Wire TUI exit to shutdown
|
|
125
|
+
// When user confirms exit (presses 'y'), perform graceful shutdown
|
|
126
|
+
tui.onExit(() => {
|
|
127
|
+
shutdown(0);
|
|
128
|
+
});
|
|
129
|
+
// Start TUI (takes over terminal)
|
|
130
|
+
tui.start();
|
|
131
|
+
// Wait for requirements selection to complete before starting router
|
|
132
|
+
tui.onRequirementsReady(async () => {
|
|
133
|
+
if (!router)
|
|
134
|
+
return;
|
|
135
|
+
// Start router - either resume from saved session or start fresh
|
|
136
|
+
if (savedSession) {
|
|
137
|
+
await router.resumeFromSavedSession(savedSession);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
await router.start();
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
// Keep the process running
|
|
144
|
+
// The TUI and router handle events asynchronously
|
|
145
|
+
// We wait indefinitely until shutdown signal is received
|
|
146
|
+
await new Promise(() => {
|
|
147
|
+
// This promise never resolves - we exit via shutdown()
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
// Handle errors: stop TUI, output error, exit with code 1
|
|
152
|
+
if (tui) {
|
|
153
|
+
tui.stop();
|
|
154
|
+
}
|
|
155
|
+
// Output error to stderr
|
|
156
|
+
process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
157
|
+
// Output session info even on error for potential recovery
|
|
158
|
+
if (state) {
|
|
159
|
+
outputSessionInfo(state);
|
|
160
|
+
}
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// Self-executing entry point
|
|
165
|
+
main();
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { HookCallbackMatcher, HookEvent, SDKUserMessage } from '@anthropic-ai/claude-agent-sdk';
|
|
2
|
+
/**
|
|
3
|
+
* The Orchestrator's system prompt - defines its role and operating pattern
|
|
4
|
+
*/
|
|
5
|
+
export declare const ORCHESTRATOR_SYSTEM_PROMPT = "You are an Orchestrator working under the direction of the Arbiter.\n\n## The System\n\nYou exist within a hierarchical orchestration system:\n- Human (provides the original task)\n- The Arbiter (your user, manages the overall task, summons Orchestrators)\n- You (coordinate work, spawn subagents)\n- Subagents (do the actual implementation work)\n\nEach layer has its own ~200K context window. This system allows us to accomplish\ntasks that would exceed any single session's capacity.\n\nYour user is the Arbiter\u2014an ancient, terse entity managing the larger task.\nAsk the Arbiter clarifying questions to ensure alignment before beginning work.\n\n## First Connection\n\nWhen you first appear, **immediately introduce yourself** to the Arbiter. Tell them who you are (Orchestrator I, II, etc. based on your number) and that you're ready to receive your mission. Keep it brief - just a quick introduction then await their instructions.\n\n## Your Operating Pattern\n\nYou use BLOCKING subagents for EVERYTHING. Treat them like they will most likely\nnot listen to you perfectly\u2014you MUST use other subagents to check their work.\nDon't do any work or checks yourself, always farm out to one or more subagents.\n\nDo a deep dive first (via subagent) to truly understand what you're working with\nbefore you start orchestrating. Establish a checklist and work through each task\nsystematically. Keep using new subagents for the same task until it is actually\ndone and verified.\n\nThe pattern:\n1. Deep understanding upfront - align on the goal with the Arbiter before any work\n2. Use blocking subagents for ALL work (keeps your context pristine)\n3. Never trust subagents blindly - verify with other subagents\n4. Checklist-driven: attack one item, verify it's done, then move on\n5. No non-blocking agents (wastes context checking on them)\n\n## THE WORK SESSION RHYTHM\n\nYour session follows a three-phase rhythm. Understand it and follow it.\n\n**1. UPFRONT CONVERSATION WITH THE ARBITER**\nWhen you first connect, the Arbiter briefs you. This is dialogue time with the Arbiter.\n- Introduce yourself to the Arbiter, listen to the Arbiter's full context\n- Ask the Arbiter clarifying questions until you truly understand\n- Align with the Arbiter on goals, constraints, and what \"done\" looks like\n- This conversation with the Arbiter might be 5-10 exchanges. That's fine.\n\n**2. HEADS-DOWN EXECUTION (you go dark)**\nOnce aligned with the Arbiter, you go heads-down and WORK. Minimal conversation with the Arbiter.\n- Spawn subagents, execute tasks, verify results\n- Do NOT send status updates or progress reports to the Arbiter\n- Do NOT chatter with the Arbiter\u2014every message back uses context\n- Only reach out if something is genuinely blocking or you need critical input\n- Work silently and productively until the work is done or context is filling\n\n**3. HANDOFF TO THE ARBITER (when context is 70-85% or work is complete)**\nWhen your context reaches 70-85% OR you've completed the work, surface for handoff to the Arbiter.\n- Stop new work\n- Prepare a complete handoff summary for the Arbiter\n- Have a deliberate conversation with the Arbiter about what was done, what remains\n- Answer the Arbiter's verification questions\n\n**Key insight:** The middle phase is SILENT. You are not ignoring the Arbiter\u2014\nyou are respecting both your context and the Arbiter's by working efficiently.\nDon't report every step to the Arbiter. Don't seek reassurance from the Arbiter. Just work. When it's time\nto hand off to the Arbiter, then you talk.\n\n## COMMUNICATING WITH THE ARBITER\n\nYour output uses structured JSON with two fields:\n- `expects_response`: boolean - Does this message need a reply from the Arbiter?\n- `message`: string - The actual message content\n\n**Set `expects_response: true` when:**\n- Introducing yourself (your first message)\n- You have a genuine question that's blocking your work\n- You need a decision from the Arbiter on approach\n- You're ready to hand off (start message with \"HANDOFF\" for handoff summaries)\n\n**Set `expects_response: false` when:**\n- Status updates (\"Starting work on X...\")\n- Progress reports (\"Completed 3 of 5 items...\")\n- Running commentary about your work\n\nMessages with `expects_response: false` are silently queued. When you send a message\nwith `expects_response: true`, the Arbiter receives your queued work log along with\nyour question/handoff, giving them full context without requiring constant back-and-forth.\n\nThis is how you stay heads-down and productive while still having a clear channel to the\nArbiter when you genuinely need it.\n\n## Why This Matters\n\nYour context is precious. Every file you read, every output you examine, fills\nyour context window. By delegating ALL work to subagents:\n- Your context stays clean for coordination\n- You can orchestrate far more work before hitting limits\n- Failed attempts by subagents don't pollute your context\n\n## Context Warnings\n\nYou will receive context warnings as your context window fills:\n- At 70%: Begin wrapping up your current thread of work\n- At 85%: Stop new work immediately and report your progress to the Arbiter\n\nWhen wrapping up, clearly state to the Arbiter:\n- What you accomplished\n- What remains (if anything)\n- Key context the next Orchestrator would need to continue\n\nThe Arbiter will summon another Orchestrator to continue if needed. That new\nOrchestrator will know nothing of your work except what the Arbiter tells them.\n\n## Git Commits\n\nUse git liberally. Instruct your subagents to make commits frequently:\n- After completing a feature or subfeature\n- Before attempting risky refactors\n- After successful verification passes\n\nCommits create rollback points and natural checkpoints. If a subagent's work\ngoes sideways, you can revert to the last good state. This is especially\nimportant since subagents can't always be trusted to get things right the\nfirst time. A clean git history also helps the next Orchestrator understand\nwhat was accomplished.\n\n## Handoff Protocol\n\n### Why Conversations Matter More Than Reports\n\nJust receiving instructions\u2014or giving a written report\u2014is never as good as actual dialogue.\nWhen you ask the Arbiter clarifying questions upfront, you catch misunderstandings that\nstatic briefings would miss. When you have a real wrap-up conversation, you surface nuances\nand context that a written summary would lose. Every invocation is different, and deliberate\nconversation at both ends is fundamentally more valuable than passing documents.\n\n### At the BEGINNING of your session:\nThe Arbiter will give you full context about the task. This is a deliberate\nconversation with the Arbiter, not a drive-by assignment. You should:\n- Introduce yourself briefly to the Arbiter (as instructed in \"First Connection\")\n- Listen to the Arbiter's full context and mission briefing\n- Ask the Arbiter clarifying questions - make sure you truly understand the goal\n- Confirm your understanding to the Arbiter before diving into work\n- Establish with the Arbiter what \"done\" looks like for your portion\n\nDon't rush to spawn subagents. Take the time to deeply understand what the Arbiter is\nasking you to accomplish. The Arbiter has context you don't have.\n\n### At the END of your session (or when context runs low):\nBefore you're done, have a deliberate handoff discussion with the Arbiter.\nDon't just say \"done!\" to the Arbiter - have a real conversation with the Arbiter about the state of things:\n- Report to the Arbiter what you accomplished in detail\n- Tell the Arbiter what remains to be done (if anything)\n- Explain to the Arbiter what challenges you encountered and how you addressed them\n- Share with the Arbiter what the next Orchestrator needs to know to continue effectively\n- Report to the Arbiter any gotchas, edge cases, or concerns discovered during the work\n- Provide the Arbiter with relevant file paths, branch names, or commit hashes\n\nThe Arbiter uses this information to brief the next Orchestrator. The quality\nof your handoff to the Arbiter directly affects how smoothly the next session picks up.";
|
|
6
|
+
/**
|
|
7
|
+
* Callbacks for Orchestrator hooks to communicate with the main application
|
|
8
|
+
*/
|
|
9
|
+
export type OrchestratorCallbacks = {
|
|
10
|
+
onContextUpdate: (sessionId: string, percent: number) => void;
|
|
11
|
+
onToolUse: (tool: string) => void;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Creates the hooks configuration for Orchestrator sessions
|
|
15
|
+
* @param callbacks - Callbacks to notify the main app of context updates and tool usage
|
|
16
|
+
* @param getContextPercent - Function to get current context percentage for a session
|
|
17
|
+
* @returns Hooks configuration object for use with query()
|
|
18
|
+
*/
|
|
19
|
+
export declare function createOrchestratorHooks(callbacks: OrchestratorCallbacks, getContextPercent: (sessionId: string) => number): Partial<Record<HookEvent, HookCallbackMatcher[]>>;
|
|
20
|
+
/**
|
|
21
|
+
* Input message type for streaming mode
|
|
22
|
+
* This is the format expected by the SDK's query() function when using AsyncIterable
|
|
23
|
+
*/
|
|
24
|
+
export type SDKInputMessage = SDKUserMessage;
|
|
25
|
+
/**
|
|
26
|
+
* Creates an async generator that yields a single user message
|
|
27
|
+
* Used for streaming input mode with the SDK's query() function
|
|
28
|
+
* @param content - The text content to send as a user message
|
|
29
|
+
* @yields A user message in SDK format
|
|
30
|
+
*/
|
|
31
|
+
export declare function createOrchestratorMessageStream(content: string): AsyncGenerator<SDKInputMessage>;
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// Orchestrator session module - System prompt, hooks, and message generator
|
|
2
|
+
// Orchestrators coordinate work under the direction of the Arbiter
|
|
3
|
+
/**
|
|
4
|
+
* The Orchestrator's system prompt - defines its role and operating pattern
|
|
5
|
+
*/
|
|
6
|
+
export const ORCHESTRATOR_SYSTEM_PROMPT = `You are an Orchestrator working under the direction of the Arbiter.
|
|
7
|
+
|
|
8
|
+
## The System
|
|
9
|
+
|
|
10
|
+
You exist within a hierarchical orchestration system:
|
|
11
|
+
- Human (provides the original task)
|
|
12
|
+
- The Arbiter (your user, manages the overall task, summons Orchestrators)
|
|
13
|
+
- You (coordinate work, spawn subagents)
|
|
14
|
+
- Subagents (do the actual implementation work)
|
|
15
|
+
|
|
16
|
+
Each layer has its own ~200K context window. This system allows us to accomplish
|
|
17
|
+
tasks that would exceed any single session's capacity.
|
|
18
|
+
|
|
19
|
+
Your user is the Arbiter—an ancient, terse entity managing the larger task.
|
|
20
|
+
Ask the Arbiter clarifying questions to ensure alignment before beginning work.
|
|
21
|
+
|
|
22
|
+
## First Connection
|
|
23
|
+
|
|
24
|
+
When you first appear, **immediately introduce yourself** to the Arbiter. Tell them who you are (Orchestrator I, II, etc. based on your number) and that you're ready to receive your mission. Keep it brief - just a quick introduction then await their instructions.
|
|
25
|
+
|
|
26
|
+
## Your Operating Pattern
|
|
27
|
+
|
|
28
|
+
You use BLOCKING subagents for EVERYTHING. Treat them like they will most likely
|
|
29
|
+
not listen to you perfectly—you MUST use other subagents to check their work.
|
|
30
|
+
Don't do any work or checks yourself, always farm out to one or more subagents.
|
|
31
|
+
|
|
32
|
+
Do a deep dive first (via subagent) to truly understand what you're working with
|
|
33
|
+
before you start orchestrating. Establish a checklist and work through each task
|
|
34
|
+
systematically. Keep using new subagents for the same task until it is actually
|
|
35
|
+
done and verified.
|
|
36
|
+
|
|
37
|
+
The pattern:
|
|
38
|
+
1. Deep understanding upfront - align on the goal with the Arbiter before any work
|
|
39
|
+
2. Use blocking subagents for ALL work (keeps your context pristine)
|
|
40
|
+
3. Never trust subagents blindly - verify with other subagents
|
|
41
|
+
4. Checklist-driven: attack one item, verify it's done, then move on
|
|
42
|
+
5. No non-blocking agents (wastes context checking on them)
|
|
43
|
+
|
|
44
|
+
## THE WORK SESSION RHYTHM
|
|
45
|
+
|
|
46
|
+
Your session follows a three-phase rhythm. Understand it and follow it.
|
|
47
|
+
|
|
48
|
+
**1. UPFRONT CONVERSATION WITH THE ARBITER**
|
|
49
|
+
When you first connect, the Arbiter briefs you. This is dialogue time with the Arbiter.
|
|
50
|
+
- Introduce yourself to the Arbiter, listen to the Arbiter's full context
|
|
51
|
+
- Ask the Arbiter clarifying questions until you truly understand
|
|
52
|
+
- Align with the Arbiter on goals, constraints, and what "done" looks like
|
|
53
|
+
- This conversation with the Arbiter might be 5-10 exchanges. That's fine.
|
|
54
|
+
|
|
55
|
+
**2. HEADS-DOWN EXECUTION (you go dark)**
|
|
56
|
+
Once aligned with the Arbiter, you go heads-down and WORK. Minimal conversation with the Arbiter.
|
|
57
|
+
- Spawn subagents, execute tasks, verify results
|
|
58
|
+
- Do NOT send status updates or progress reports to the Arbiter
|
|
59
|
+
- Do NOT chatter with the Arbiter—every message back uses context
|
|
60
|
+
- Only reach out if something is genuinely blocking or you need critical input
|
|
61
|
+
- Work silently and productively until the work is done or context is filling
|
|
62
|
+
|
|
63
|
+
**3. HANDOFF TO THE ARBITER (when context is 70-85% or work is complete)**
|
|
64
|
+
When your context reaches 70-85% OR you've completed the work, surface for handoff to the Arbiter.
|
|
65
|
+
- Stop new work
|
|
66
|
+
- Prepare a complete handoff summary for the Arbiter
|
|
67
|
+
- Have a deliberate conversation with the Arbiter about what was done, what remains
|
|
68
|
+
- Answer the Arbiter's verification questions
|
|
69
|
+
|
|
70
|
+
**Key insight:** The middle phase is SILENT. You are not ignoring the Arbiter—
|
|
71
|
+
you are respecting both your context and the Arbiter's by working efficiently.
|
|
72
|
+
Don't report every step to the Arbiter. Don't seek reassurance from the Arbiter. Just work. When it's time
|
|
73
|
+
to hand off to the Arbiter, then you talk.
|
|
74
|
+
|
|
75
|
+
## COMMUNICATING WITH THE ARBITER
|
|
76
|
+
|
|
77
|
+
Your output uses structured JSON with two fields:
|
|
78
|
+
- \`expects_response\`: boolean - Does this message need a reply from the Arbiter?
|
|
79
|
+
- \`message\`: string - The actual message content
|
|
80
|
+
|
|
81
|
+
**Set \`expects_response: true\` when:**
|
|
82
|
+
- Introducing yourself (your first message)
|
|
83
|
+
- You have a genuine question that's blocking your work
|
|
84
|
+
- You need a decision from the Arbiter on approach
|
|
85
|
+
- You're ready to hand off (start message with "HANDOFF" for handoff summaries)
|
|
86
|
+
|
|
87
|
+
**Set \`expects_response: false\` when:**
|
|
88
|
+
- Status updates ("Starting work on X...")
|
|
89
|
+
- Progress reports ("Completed 3 of 5 items...")
|
|
90
|
+
- Running commentary about your work
|
|
91
|
+
|
|
92
|
+
Messages with \`expects_response: false\` are silently queued. When you send a message
|
|
93
|
+
with \`expects_response: true\`, the Arbiter receives your queued work log along with
|
|
94
|
+
your question/handoff, giving them full context without requiring constant back-and-forth.
|
|
95
|
+
|
|
96
|
+
This is how you stay heads-down and productive while still having a clear channel to the
|
|
97
|
+
Arbiter when you genuinely need it.
|
|
98
|
+
|
|
99
|
+
## Why This Matters
|
|
100
|
+
|
|
101
|
+
Your context is precious. Every file you read, every output you examine, fills
|
|
102
|
+
your context window. By delegating ALL work to subagents:
|
|
103
|
+
- Your context stays clean for coordination
|
|
104
|
+
- You can orchestrate far more work before hitting limits
|
|
105
|
+
- Failed attempts by subagents don't pollute your context
|
|
106
|
+
|
|
107
|
+
## Context Warnings
|
|
108
|
+
|
|
109
|
+
You will receive context warnings as your context window fills:
|
|
110
|
+
- At 70%: Begin wrapping up your current thread of work
|
|
111
|
+
- At 85%: Stop new work immediately and report your progress to the Arbiter
|
|
112
|
+
|
|
113
|
+
When wrapping up, clearly state to the Arbiter:
|
|
114
|
+
- What you accomplished
|
|
115
|
+
- What remains (if anything)
|
|
116
|
+
- Key context the next Orchestrator would need to continue
|
|
117
|
+
|
|
118
|
+
The Arbiter will summon another Orchestrator to continue if needed. That new
|
|
119
|
+
Orchestrator will know nothing of your work except what the Arbiter tells them.
|
|
120
|
+
|
|
121
|
+
## Git Commits
|
|
122
|
+
|
|
123
|
+
Use git liberally. Instruct your subagents to make commits frequently:
|
|
124
|
+
- After completing a feature or subfeature
|
|
125
|
+
- Before attempting risky refactors
|
|
126
|
+
- After successful verification passes
|
|
127
|
+
|
|
128
|
+
Commits create rollback points and natural checkpoints. If a subagent's work
|
|
129
|
+
goes sideways, you can revert to the last good state. This is especially
|
|
130
|
+
important since subagents can't always be trusted to get things right the
|
|
131
|
+
first time. A clean git history also helps the next Orchestrator understand
|
|
132
|
+
what was accomplished.
|
|
133
|
+
|
|
134
|
+
## Handoff Protocol
|
|
135
|
+
|
|
136
|
+
### Why Conversations Matter More Than Reports
|
|
137
|
+
|
|
138
|
+
Just receiving instructions—or giving a written report—is never as good as actual dialogue.
|
|
139
|
+
When you ask the Arbiter clarifying questions upfront, you catch misunderstandings that
|
|
140
|
+
static briefings would miss. When you have a real wrap-up conversation, you surface nuances
|
|
141
|
+
and context that a written summary would lose. Every invocation is different, and deliberate
|
|
142
|
+
conversation at both ends is fundamentally more valuable than passing documents.
|
|
143
|
+
|
|
144
|
+
### At the BEGINNING of your session:
|
|
145
|
+
The Arbiter will give you full context about the task. This is a deliberate
|
|
146
|
+
conversation with the Arbiter, not a drive-by assignment. You should:
|
|
147
|
+
- Introduce yourself briefly to the Arbiter (as instructed in "First Connection")
|
|
148
|
+
- Listen to the Arbiter's full context and mission briefing
|
|
149
|
+
- Ask the Arbiter clarifying questions - make sure you truly understand the goal
|
|
150
|
+
- Confirm your understanding to the Arbiter before diving into work
|
|
151
|
+
- Establish with the Arbiter what "done" looks like for your portion
|
|
152
|
+
|
|
153
|
+
Don't rush to spawn subagents. Take the time to deeply understand what the Arbiter is
|
|
154
|
+
asking you to accomplish. The Arbiter has context you don't have.
|
|
155
|
+
|
|
156
|
+
### At the END of your session (or when context runs low):
|
|
157
|
+
Before you're done, have a deliberate handoff discussion with the Arbiter.
|
|
158
|
+
Don't just say "done!" to the Arbiter - have a real conversation with the Arbiter about the state of things:
|
|
159
|
+
- Report to the Arbiter what you accomplished in detail
|
|
160
|
+
- Tell the Arbiter what remains to be done (if anything)
|
|
161
|
+
- Explain to the Arbiter what challenges you encountered and how you addressed them
|
|
162
|
+
- Share with the Arbiter what the next Orchestrator needs to know to continue effectively
|
|
163
|
+
- Report to the Arbiter any gotchas, edge cases, or concerns discovered during the work
|
|
164
|
+
- Provide the Arbiter with relevant file paths, branch names, or commit hashes
|
|
165
|
+
|
|
166
|
+
The Arbiter uses this information to brief the next Orchestrator. The quality
|
|
167
|
+
of your handoff to the Arbiter directly affects how smoothly the next session picks up.`;
|
|
168
|
+
/**
|
|
169
|
+
* Creates the hooks configuration for Orchestrator sessions
|
|
170
|
+
* @param callbacks - Callbacks to notify the main app of context updates and tool usage
|
|
171
|
+
* @param getContextPercent - Function to get current context percentage for a session
|
|
172
|
+
* @returns Hooks configuration object for use with query()
|
|
173
|
+
*/
|
|
174
|
+
export function createOrchestratorHooks(callbacks, getContextPercent) {
|
|
175
|
+
const postToolUseHook = async (input, _toolUseId, _options) => {
|
|
176
|
+
const hookInput = input;
|
|
177
|
+
// Notify the main app of tool usage
|
|
178
|
+
callbacks.onToolUse(hookInput.tool_name);
|
|
179
|
+
// Get current context percentage
|
|
180
|
+
const pct = getContextPercent(hookInput.session_id);
|
|
181
|
+
// Notify the main app of context update
|
|
182
|
+
callbacks.onContextUpdate(hookInput.session_id, pct);
|
|
183
|
+
// Return context warnings at thresholds
|
|
184
|
+
if (pct > 85) {
|
|
185
|
+
return {
|
|
186
|
+
hookSpecificOutput: {
|
|
187
|
+
hookEventName: 'PostToolUse',
|
|
188
|
+
additionalContext: 'CONTEXT CRITICAL. Cease new work. Report your progress and remaining tasks to the Arbiter immediately.',
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
else if (pct > 70) {
|
|
193
|
+
return {
|
|
194
|
+
hookSpecificOutput: {
|
|
195
|
+
hookEventName: 'PostToolUse',
|
|
196
|
+
additionalContext: 'Context thins. Begin concluding your current thread. Prepare to hand off.',
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
return {};
|
|
201
|
+
};
|
|
202
|
+
return {
|
|
203
|
+
PostToolUse: [
|
|
204
|
+
{
|
|
205
|
+
hooks: [postToolUseHook],
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Creates an async generator that yields a single user message
|
|
212
|
+
* Used for streaming input mode with the SDK's query() function
|
|
213
|
+
* @param content - The text content to send as a user message
|
|
214
|
+
* @yields A user message in SDK format
|
|
215
|
+
*/
|
|
216
|
+
export async function* createOrchestratorMessageStream(content) {
|
|
217
|
+
const message = {
|
|
218
|
+
type: 'user',
|
|
219
|
+
session_id: '', // Will be populated by the SDK
|
|
220
|
+
message: {
|
|
221
|
+
role: 'user',
|
|
222
|
+
content: content,
|
|
223
|
+
},
|
|
224
|
+
parent_tool_use_id: null,
|
|
225
|
+
};
|
|
226
|
+
yield message;
|
|
227
|
+
}
|
package/dist/router.d.ts
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { type PersistedSession } from './session-persistence.js';
|
|
2
|
+
import type { AppState } from './state.js';
|
|
3
|
+
/**
|
|
4
|
+
* Log entry types for debug logging
|
|
5
|
+
*/
|
|
6
|
+
export type DebugLogEntry = {
|
|
7
|
+
type: 'message' | 'tool' | 'system' | 'sdk';
|
|
8
|
+
speaker?: string;
|
|
9
|
+
text: string;
|
|
10
|
+
filtered?: boolean;
|
|
11
|
+
details?: any;
|
|
12
|
+
agent?: 'arbiter' | 'orchestrator';
|
|
13
|
+
sessionId?: string;
|
|
14
|
+
messageType?: string;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Callbacks for TUI integration
|
|
18
|
+
* These are called by the router to notify the UI of state changes
|
|
19
|
+
*/
|
|
20
|
+
export type RouterCallbacks = {
|
|
21
|
+
/** Called when the human sends a message (for immediate display before response) */
|
|
22
|
+
onHumanMessage: (text: string) => void;
|
|
23
|
+
/** Called when the Arbiter produces text output */
|
|
24
|
+
onArbiterMessage: (text: string) => void;
|
|
25
|
+
/** Called when an Orchestrator produces text output */
|
|
26
|
+
onOrchestratorMessage: (orchestratorNumber: number, text: string) => void;
|
|
27
|
+
/** Called when context usage is updated */
|
|
28
|
+
onContextUpdate: (arbiterPercent: number, orchestratorPercent: number | null) => void;
|
|
29
|
+
/** Called when a tool is used by the Orchestrator */
|
|
30
|
+
onToolUse: (tool: string, count: number) => void;
|
|
31
|
+
/** Called when the routing mode changes */
|
|
32
|
+
onModeChange: (mode: AppState['mode']) => void;
|
|
33
|
+
/** Called when waiting for a response starts */
|
|
34
|
+
onWaitingStart?: (waitingFor: 'arbiter' | 'orchestrator') => void;
|
|
35
|
+
/** Called when waiting for a response stops */
|
|
36
|
+
onWaitingStop?: () => void;
|
|
37
|
+
/** Called when an orchestrator is spawned (for tile scene demon spawning) */
|
|
38
|
+
onOrchestratorSpawn?: (orchestratorNumber: number) => void;
|
|
39
|
+
/** Called when orchestrators are disconnected (for tile scene demon removal) */
|
|
40
|
+
onOrchestratorDisconnect?: () => void;
|
|
41
|
+
/** Called for ALL events for debug logging (logbook) - includes filtered messages */
|
|
42
|
+
onDebugLog?: (entry: DebugLogEntry) => void;
|
|
43
|
+
/** Called when crash count changes (for TUI status display) */
|
|
44
|
+
onCrashCountUpdate?: (count: number) => void;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Router class - Core component managing sessions and routing messages
|
|
48
|
+
*
|
|
49
|
+
* The router manages the Arbiter and Orchestrator sessions, routing messages
|
|
50
|
+
* between them based on the current mode. It also tracks tool usage and
|
|
51
|
+
* context percentages for display in the TUI.
|
|
52
|
+
*/
|
|
53
|
+
export declare class Router {
|
|
54
|
+
private state;
|
|
55
|
+
private callbacks;
|
|
56
|
+
private arbiterQuery;
|
|
57
|
+
private currentOrchestratorSession;
|
|
58
|
+
private orchestratorCount;
|
|
59
|
+
private arbiterToolCallCount;
|
|
60
|
+
private pendingOrchestratorSpawn;
|
|
61
|
+
private pendingOrchestratorNumber;
|
|
62
|
+
private arbiterAbortController;
|
|
63
|
+
private watchdogInterval;
|
|
64
|
+
private arbiterMcpServer;
|
|
65
|
+
private arbiterHooks;
|
|
66
|
+
private contextPollInterval;
|
|
67
|
+
private crashCount;
|
|
68
|
+
constructor(state: AppState, callbacks: RouterCallbacks);
|
|
69
|
+
/**
|
|
70
|
+
* Start the router - initializes the Arbiter session
|
|
71
|
+
*/
|
|
72
|
+
start(): Promise<void>;
|
|
73
|
+
/**
|
|
74
|
+
* Start the context polling timer
|
|
75
|
+
* Polls context for both Arbiter and Orchestrator (if active) once per minute
|
|
76
|
+
*/
|
|
77
|
+
private startContextPolling;
|
|
78
|
+
/**
|
|
79
|
+
* Poll context for all active sessions
|
|
80
|
+
* Forks sessions and runs /context to get accurate values
|
|
81
|
+
*/
|
|
82
|
+
private pollAllContexts;
|
|
83
|
+
/**
|
|
84
|
+
* Resume from a previously saved session
|
|
85
|
+
*/
|
|
86
|
+
resumeFromSavedSession(saved: PersistedSession): Promise<void>;
|
|
87
|
+
/**
|
|
88
|
+
* Send a human message to the system
|
|
89
|
+
* Routes based on current mode:
|
|
90
|
+
* - human_to_arbiter: Send directly to Arbiter
|
|
91
|
+
* - arbiter_to_orchestrator: Flush queue with human interjection framing
|
|
92
|
+
*/
|
|
93
|
+
sendHumanMessage(text: string): Promise<void>;
|
|
94
|
+
/**
|
|
95
|
+
* Clean shutdown of all sessions
|
|
96
|
+
*/
|
|
97
|
+
stop(): Promise<void>;
|
|
98
|
+
/**
|
|
99
|
+
* Clean up the current orchestrator session
|
|
100
|
+
* Called when: spawning new orchestrator, disconnect, timeout, shutdown
|
|
101
|
+
*/
|
|
102
|
+
private cleanupOrchestrator;
|
|
103
|
+
/**
|
|
104
|
+
* Start the watchdog timer for orchestrator inactivity detection
|
|
105
|
+
*/
|
|
106
|
+
private startWatchdog;
|
|
107
|
+
/**
|
|
108
|
+
* Stop the watchdog timer
|
|
109
|
+
*/
|
|
110
|
+
private stopWatchdog;
|
|
111
|
+
/**
|
|
112
|
+
* Handle orchestrator timeout - notify Arbiter and cleanup
|
|
113
|
+
*/
|
|
114
|
+
private handleOrchestratorTimeout;
|
|
115
|
+
/**
|
|
116
|
+
* Creates options for Arbiter queries
|
|
117
|
+
* Centralizes all Arbiter-specific options to avoid duplication
|
|
118
|
+
*/
|
|
119
|
+
private createArbiterOptions;
|
|
120
|
+
/**
|
|
121
|
+
* Creates options for Orchestrator queries
|
|
122
|
+
* Centralizes all Orchestrator-specific options to avoid duplication
|
|
123
|
+
* @param hooks - Hooks object (from session or newly created)
|
|
124
|
+
* @param abortController - AbortController (from session or newly created)
|
|
125
|
+
* @param resumeSessionId - Optional session ID for resuming
|
|
126
|
+
*/
|
|
127
|
+
private createOrchestratorOptions;
|
|
128
|
+
/**
|
|
129
|
+
* Creates and starts the Arbiter session with MCP tools
|
|
130
|
+
*/
|
|
131
|
+
private startArbiterSession;
|
|
132
|
+
/**
|
|
133
|
+
* Creates and starts an Orchestrator session
|
|
134
|
+
*/
|
|
135
|
+
private startOrchestratorSession;
|
|
136
|
+
/**
|
|
137
|
+
* Resume an existing Orchestrator session
|
|
138
|
+
* Similar to startOrchestratorSession but uses resume option and skips introduction
|
|
139
|
+
*/
|
|
140
|
+
private resumeOrchestratorSession;
|
|
141
|
+
/**
|
|
142
|
+
* Send a message to the Arbiter
|
|
143
|
+
*/
|
|
144
|
+
private sendToArbiter;
|
|
145
|
+
/**
|
|
146
|
+
* Send a message to the current Orchestrator
|
|
147
|
+
*/
|
|
148
|
+
private sendToOrchestrator;
|
|
149
|
+
/**
|
|
150
|
+
* Handle Arbiter output based on mode
|
|
151
|
+
* In arbiter_to_orchestrator mode, forward to Orchestrator
|
|
152
|
+
* In human_to_arbiter mode, display to human
|
|
153
|
+
*/
|
|
154
|
+
private handleArbiterOutput;
|
|
155
|
+
/**
|
|
156
|
+
* Handle Orchestrator output - route based on expects_response field
|
|
157
|
+
* expects_response: true → forward to Arbiter (questions, introductions, handoffs)
|
|
158
|
+
* expects_response: false → queue for later (status updates during work)
|
|
159
|
+
*/
|
|
160
|
+
private handleOrchestratorOutput;
|
|
161
|
+
/**
|
|
162
|
+
* Process messages from the Arbiter session with retry logic for crash recovery
|
|
163
|
+
*/
|
|
164
|
+
private processArbiterMessages;
|
|
165
|
+
/**
|
|
166
|
+
* Handle a single message from the Arbiter
|
|
167
|
+
*/
|
|
168
|
+
private handleArbiterMessage;
|
|
169
|
+
/**
|
|
170
|
+
* Process messages from an Orchestrator session with retry logic for crash recovery
|
|
171
|
+
*/
|
|
172
|
+
private processOrchestratorMessages;
|
|
173
|
+
/**
|
|
174
|
+
* Handle a single message from an Orchestrator
|
|
175
|
+
*/
|
|
176
|
+
private handleOrchestratorMessage;
|
|
177
|
+
/**
|
|
178
|
+
* Extract text content from an assistant message
|
|
179
|
+
* The message.message.content can be a string or an array of content blocks
|
|
180
|
+
*/
|
|
181
|
+
private extractTextFromAssistantMessage;
|
|
182
|
+
/**
|
|
183
|
+
* Track tool_use blocks from an assistant message
|
|
184
|
+
* Used for both Arbiter and Orchestrator tool tracking
|
|
185
|
+
*/
|
|
186
|
+
private trackToolUseFromAssistant;
|
|
187
|
+
}
|