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.
Files changed (51) hide show
  1. package/README.md +41 -0
  2. package/assets/jerom_16x16.png +0 -0
  3. package/dist/arbiter.d.ts +43 -0
  4. package/dist/arbiter.js +486 -0
  5. package/dist/context-analyzer.d.ts +15 -0
  6. package/dist/context-analyzer.js +603 -0
  7. package/dist/index.d.ts +2 -0
  8. package/dist/index.js +165 -0
  9. package/dist/orchestrator.d.ts +31 -0
  10. package/dist/orchestrator.js +227 -0
  11. package/dist/router.d.ts +187 -0
  12. package/dist/router.js +1135 -0
  13. package/dist/router.test.d.ts +15 -0
  14. package/dist/router.test.js +95 -0
  15. package/dist/session-persistence.d.ts +9 -0
  16. package/dist/session-persistence.js +63 -0
  17. package/dist/session-persistence.test.d.ts +1 -0
  18. package/dist/session-persistence.test.js +165 -0
  19. package/dist/sound.d.ts +31 -0
  20. package/dist/sound.js +50 -0
  21. package/dist/state.d.ts +72 -0
  22. package/dist/state.js +107 -0
  23. package/dist/state.test.d.ts +1 -0
  24. package/dist/state.test.js +194 -0
  25. package/dist/test-headless.d.ts +1 -0
  26. package/dist/test-headless.js +155 -0
  27. package/dist/tui/index.d.ts +14 -0
  28. package/dist/tui/index.js +17 -0
  29. package/dist/tui/layout.d.ts +30 -0
  30. package/dist/tui/layout.js +200 -0
  31. package/dist/tui/render.d.ts +57 -0
  32. package/dist/tui/render.js +266 -0
  33. package/dist/tui/scene.d.ts +64 -0
  34. package/dist/tui/scene.js +366 -0
  35. package/dist/tui/screens/CharacterSelect-termkit.d.ts +18 -0
  36. package/dist/tui/screens/CharacterSelect-termkit.js +216 -0
  37. package/dist/tui/screens/ForestIntro-termkit.d.ts +15 -0
  38. package/dist/tui/screens/ForestIntro-termkit.js +856 -0
  39. package/dist/tui/screens/GitignoreCheck-termkit.d.ts +14 -0
  40. package/dist/tui/screens/GitignoreCheck-termkit.js +185 -0
  41. package/dist/tui/screens/TitleScreen-termkit.d.ts +14 -0
  42. package/dist/tui/screens/TitleScreen-termkit.js +132 -0
  43. package/dist/tui/screens/index.d.ts +9 -0
  44. package/dist/tui/screens/index.js +10 -0
  45. package/dist/tui/tileset.d.ts +97 -0
  46. package/dist/tui/tileset.js +237 -0
  47. package/dist/tui/tui-termkit.d.ts +34 -0
  48. package/dist/tui/tui-termkit.js +2602 -0
  49. package/dist/tui/types.d.ts +41 -0
  50. package/dist/tui/types.js +4 -0
  51. 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
+ }
@@ -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
+ }