tycono 0.1.93-beta.2 → 0.1.93

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tycono",
3
- "version": "0.1.93-beta.2",
3
+ "version": "0.1.93",
4
4
  "description": "Build an AI company. Watch them work.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -32,6 +32,7 @@ import { skillsRouter } from './routes/skills.js';
32
32
  import { questsRouter } from './routes/quests.js';
33
33
  import { coinsRouter } from './routes/coins.js';
34
34
  import { activeSessionsRouter } from './routes/active-sessions.js';
35
+ import { supervisionRouter } from './routes/supervision.js';
35
36
  import { importKnowledge } from './services/knowledge-importer.js';
36
37
  import { AnthropicProvider, type LLMProvider } from './engine/llm-adapter.js';
37
38
  import { readConfig } from './services/company-config.js';
@@ -209,6 +210,7 @@ export function createExpressApp(): express.Application {
209
210
  app.use('/api/quests', questsRouter);
210
211
  app.use('/api/coins', coinsRouter);
211
212
  app.use('/api/active-sessions', activeSessionsRouter);
213
+ app.use('/api/supervision', supervisionRouter);
212
214
 
213
215
  app.get('/api/health', (_req, res) => {
214
216
  res.json({ status: 'ok', companyRoot: COMPANY_ROOT });
@@ -37,6 +37,10 @@ export interface AgentConfig {
37
37
  onTurnComplete?: (turn: number) => void;
38
38
  /** Trace: emitted when system prompt is assembled */
39
39
  onPromptAssembled?: (systemPrompt: string, userTask: string) => void;
40
+ /** Supervision: abort a running session */
41
+ onAbortSession?: (sessionId: string) => boolean;
42
+ /** Supervision: amend a running session with new instructions */
43
+ onAmendSession?: (sessionId: string, instruction: string) => boolean;
40
44
  }
41
45
 
42
46
  export interface AgentResult {
@@ -51,19 +55,48 @@ export interface AgentResult {
51
55
 
52
56
  /**
53
57
  * Compress older messages to reduce token usage.
54
- * Strategy: Keep first 2 messages (initial task) and last 4 messages (recent context).
55
- * Middle messages: truncate long tool_result content, collapse text blocks.
58
+ *
59
+ * SV-9 Enhancement: Zone-based compression for supervision sessions.
60
+ * - Zone A (pinned): first 2 messages (system prompt + original task + plan) — never compress
61
+ * - Zone B (rolling): middle messages — heartbeat ticks get aggressive compression
62
+ * - Zone C (recent): last 4 messages — preserve for LLM context
63
+ *
64
+ * Heartbeat-specific: consecutive quiet ticks are merged into a single line.
56
65
  */
57
66
  function compressMessages(messages: LLMMessage[]): void {
58
67
  if (messages.length <= 6) return;
59
68
 
60
- // Keep first 2 (task setup) and last 4 (recent context)
69
+ // Zone A: first 2, Zone C: last 4
61
70
  const keepHead = 2;
62
71
  const keepTail = 4;
63
72
  const compressRange = messages.slice(keepHead, messages.length - keepTail);
64
73
 
65
- for (const msg of compressRange) {
74
+ // Track consecutive quiet heartbeat ticks for merging
75
+ let quietTickStart = -1;
76
+ let quietTickCount = 0;
77
+
78
+ for (let idx = 0; idx < compressRange.length; idx++) {
79
+ const msg = compressRange[idx];
80
+
66
81
  if (typeof msg.content === 'string') {
82
+ // Check if this is a heartbeat quiet tick result
83
+ const isQuietTick = msg.content.includes('sessions progressing normally') && msg.content.includes('No anomalies');
84
+
85
+ if (isQuietTick) {
86
+ quietTickCount++;
87
+ if (quietTickStart === -1) quietTickStart = idx;
88
+
89
+ // Merge consecutive quiet ticks
90
+ if (quietTickCount > 1) {
91
+ msg.content = `[Quiet ticks merged: ${quietTickCount} ticks, no anomalies]`;
92
+ }
93
+ continue;
94
+ }
95
+
96
+ // Reset quiet tick counter on non-quiet content
97
+ quietTickStart = -1;
98
+ quietTickCount = 0;
99
+
67
100
  // Truncate long text content
68
101
  if (msg.content.length > 500) {
69
102
  msg.content = msg.content.slice(0, 300) + '\n\n[... compressed ...]';
@@ -73,8 +106,13 @@ function compressMessages(messages: LLMMessage[]): void {
73
106
  const block = msg.content[i] as Record<string, unknown>;
74
107
  if (block.type === 'tool_result') {
75
108
  const content = typeof block.content === 'string' ? block.content : '';
76
- if (content.length > 300) {
77
- block.content = content.slice(0, 200) + '\n[... compressed, was ' + content.length + ' chars]';
109
+
110
+ // Heartbeat digest results: compress more aggressively
111
+ const isDigest = content.includes('Supervision Digest') || content.includes('sessions progressing normally');
112
+ const maxLen = isDigest ? 150 : 300;
113
+
114
+ if (content.length > maxLen) {
115
+ block.content = content.slice(0, maxLen - 50) + '\n[... compressed, was ' + content.length + ' chars]';
78
116
  }
79
117
  } else if (block.type === 'text' && typeof block.text === 'string' && block.text.length > 500) {
80
118
  block.text = (block.text as string).slice(0, 300) + '\n[... compressed ...]';
@@ -132,7 +170,9 @@ export async function runAgentLoop(config: AgentConfig): Promise<AgentResult> {
132
170
  // 2. Determine tools
133
171
  const subordinates = getSubordinates(orgTree, roleId);
134
172
  const hasBash = !readOnly && !!config.codeRoot;
135
- const tools = getToolsForRole(subordinates.length > 0, readOnly, hasBash);
173
+ const node = orgTree.nodes.get(roleId);
174
+ const heartbeatEnabled = node?.heartbeat?.enabled === true && subordinates.length > 0;
175
+ const tools = getToolsForRole(subordinates.length > 0, readOnly, hasBash, heartbeatEnabled);
136
176
 
137
177
  // 3. Set up tool executor
138
178
  const toolExecOptions: ToolExecutorOptions = {
@@ -140,7 +180,10 @@ export async function runAgentLoop(config: AgentConfig): Promise<AgentResult> {
140
180
  roleId,
141
181
  orgTree,
142
182
  codeRoot: config.codeRoot,
183
+ sessionId: config.sessionId,
143
184
  onToolExec,
185
+ onAbortSession: config.onAbortSession,
186
+ onAmendSession: config.onAmendSession,
144
187
  onDispatch: async (targetRoleId: string, subTask: string) => {
145
188
  // Recursive dispatch — validate, then run sub-agent
146
189
  const authResult = validateDispatch(orgTree, roleId, targetRoleId);
@@ -318,9 +361,9 @@ export async function runAgentLoop(config: AgentConfig): Promise<AgentResult> {
318
361
  );
319
362
 
320
363
  // EG-004: Parallel tool execution for independent tools
321
- // dispatch/consult run sequentially (recursive agent calls)
364
+ // dispatch/consult/heartbeat run sequentially (recursive agent calls / blocking)
322
365
  // All other tools run in parallel via Promise.all()
323
- const sequentialTools = new Set(['dispatch', 'consult']);
366
+ const sequentialTools = new Set(['dispatch', 'consult', 'heartbeat_watch']);
324
367
  const parallelCalls = toolCalls.filter(tc => !sequentialTools.has(tc.name));
325
368
  const sequentialCalls = toolCalls.filter(tc => sequentialTools.has(tc.name));
326
369
 
@@ -132,6 +132,12 @@ Use the code repository path for all source code work (reading, writing, buildin
132
132
  subordinates = subordinates.filter(id => options.targetRoles!.includes(id));
133
133
  }
134
134
 
135
+ // Supervision prompt (SV-11, SV-12: C-Level heartbeat mode)
136
+ const heartbeatEnabled = node.heartbeat?.enabled === true;
137
+ if (heartbeatEnabled && subordinates.length > 0) {
138
+ sections.push(buildSupervisionSection(node));
139
+ }
140
+
135
141
  // Dispatch 도구 안내 (하위 Role이 있는 경우)
136
142
  if (subordinates.length > 0) {
137
143
  sections.push(buildDispatchSection(orgTree, roleId, subordinates, options?.teamStatus));
@@ -697,6 +703,59 @@ Your final report MUST include a **Change Summary** with files changed and commi
697
703
  return section;
698
704
  }
699
705
 
706
+ function buildSupervisionSection(node: OrgNode): string {
707
+ const hb = node.heartbeat ?? { enabled: true, intervalSec: 120, maxTicks: 60 };
708
+ return `# Supervision Mode (Heartbeat)
709
+
710
+ ⛔ **When you dispatch subordinates, you MUST enter supervision mode using heartbeat_watch.**
711
+ ⛔ **Do NOT use sleep+curl polling. heartbeat_watch blocks server-side at zero cost.**
712
+
713
+ ## Supervision Protocol
714
+
715
+ 1. **Dispatch** subordinates with clear task descriptions
716
+ 2. **Call heartbeat_watch** with the returned session IDs:
717
+ \`heartbeat_watch(sessionIds=[...], durationSec=${hb.intervalSec})\`
718
+ 3. **Analyze the digest** against your plan:
719
+ - On track → call heartbeat_watch again (keep watching)
720
+ - Off track → \`amend_session(sessionId, instruction)\` to course-correct
721
+ - Seriously wrong → \`abort_session(sessionId)\` + re-dispatch with different instructions
722
+ - Need peer input → \`consult(peer_role_id, question)\`
723
+ - All done → compile results and report to your superior
724
+ 4. **Repeat** heartbeat_watch until all subordinates complete
725
+
726
+ ## Available Supervision Tools
727
+
728
+ | Tool | When to Use |
729
+ |------|-------------|
730
+ | \`heartbeat_watch\` | Watch subordinate sessions (blocks ${hb.intervalSec}s, $0 LLM cost) |
731
+ | \`amend_session\` | Inject new instructions into a running session |
732
+ | \`abort_session\` | Kill a session that's going wrong |
733
+ | \`consult\` | Ask a peer C-Level for their perspective |
734
+
735
+ ## Digest Response
736
+
737
+ heartbeat_watch returns a digest with:
738
+ - **Significance score** (0-10): How much attention this tick needs
739
+ - **Anomalies**: Errors, stalls (3min+), sessions awaiting input
740
+ - **Per-session activity**: What each subordinate has been doing
741
+ - **Peer activity** (if peers are also in supervision mode)
742
+
743
+ Quiet ticks (score 0-1) return a single line: "All N sessions progressing normally."
744
+
745
+ ## Budget
746
+
747
+ - Max ticks: ${hb.maxTicks} (${Math.round(hb.maxTicks * hb.intervalSec / 60)} minutes total)
748
+ - Quiet tick cost: ~$0.001 (minimal LLM analysis)
749
+ - Alert tick cost: ~$0.02-0.05 (intervention decision)
750
+
751
+ ## ⛔ Anti-Patterns
752
+
753
+ - ❌ Using \`bash_execute\` with sleep/curl to poll — use heartbeat_watch instead
754
+ - ❌ Calling \`--check\` in a loop — heartbeat_watch handles this automatically
755
+ - ❌ Ignoring digest anomalies — always address errors and stalls
756
+ - ❌ Not re-watching after a quiet tick — keep the loop going until all done`;
757
+ }
758
+
700
759
  function buildConsultSection(orgTree: OrgTree, roleId: string): string | null {
701
760
  // Build list of roles this agent can consult
702
761
  const consultable: string[] = [];
@@ -21,6 +21,12 @@ export interface RoleSource {
21
21
  upstream_version?: string;
22
22
  }
23
23
 
24
+ export interface HeartbeatConfig {
25
+ enabled: boolean;
26
+ intervalSec: number; // default 120
27
+ maxTicks: number; // default 60
28
+ }
29
+
24
30
  export interface OrgNode {
25
31
  id: string;
26
32
  name: string;
@@ -34,6 +40,7 @@ export interface OrgNode {
34
40
  skills?: string[];
35
41
  model?: string;
36
42
  source?: RoleSource;
43
+ heartbeat?: HeartbeatConfig;
37
44
  }
38
45
 
39
46
  export interface OrgTree {
@@ -69,6 +76,11 @@ interface RawRoleYaml {
69
76
  forked_at?: string;
70
77
  upstream_version?: string;
71
78
  };
79
+ heartbeat?: {
80
+ enabled?: boolean;
81
+ intervalSec?: number;
82
+ maxTicks?: number;
83
+ };
72
84
  }
73
85
 
74
86
  /* ─── Build ──────────────────────────────────── */
@@ -128,6 +140,11 @@ export function buildOrgTree(companyRoot: string): OrgTree {
128
140
  forked_at: raw.source.forked_at,
129
141
  upstream_version: raw.source.upstream_version,
130
142
  } : undefined,
143
+ heartbeat: raw.heartbeat ? {
144
+ enabled: raw.heartbeat.enabled ?? false,
145
+ intervalSec: raw.heartbeat.intervalSec ?? 120,
146
+ maxTicks: raw.heartbeat.maxTicks ?? 60,
147
+ } : undefined,
131
148
  };
132
149
  tree.nodes.set(node.id, node);
133
150
  } catch {
@@ -249,6 +249,152 @@ log(f'')
249
249
  log(f'Poll every 10s until status is DONE.')
250
250
  `;
251
251
 
252
+ /* ─── Supervision Bridge Script (Python3) — SV-14 ────── */
253
+
254
+ const SUPERVISION_SCRIPT = `#!/usr/bin/env python3
255
+ """supervision-bridge: C-Level이 부하 세션을 감시하는 브릿지 스크립트.
256
+
257
+ 사용법:
258
+ supervision watch ses-001,ses-002 --duration 120 — Long-poll watch (blocking)
259
+ supervision peers --wave xxx --role cto — Peer session discovery
260
+ supervision abort ses-001 --reason "Wrong direction" — Abort session
261
+ supervision amend ses-001 "New instructions here" — Amend session
262
+
263
+ 환경변수:
264
+ DISPATCH_API_URL — API 서버 URL (default: http://localhost:3001)
265
+ """
266
+ import sys, os, json, urllib.request, urllib.error
267
+ sys.stdout.reconfigure(line_buffering=True)
268
+
269
+ api = os.environ.get('DISPATCH_API_URL', 'http://localhost:3001')
270
+
271
+ def log(msg):
272
+ print(msg, flush=True)
273
+
274
+ if len(sys.argv) < 2:
275
+ log('Usage: supervision <watch|peers|abort|amend> [args...]')
276
+ sys.exit(1)
277
+
278
+ cmd = sys.argv[1]
279
+
280
+ if cmd == 'watch':
281
+ sessions = sys.argv[2] if len(sys.argv) > 2 else ''
282
+ duration = '120'
283
+ alert_on = 'msg:done,msg:error'
284
+ i = 3
285
+ while i < len(sys.argv):
286
+ if sys.argv[i] == '--duration' and i + 1 < len(sys.argv):
287
+ duration = sys.argv[i + 1]
288
+ i += 2
289
+ elif sys.argv[i] == '--alert-on' and i + 1 < len(sys.argv):
290
+ alert_on = sys.argv[i + 1]
291
+ i += 2
292
+ else:
293
+ i += 1
294
+
295
+ if not sessions:
296
+ log('Error: session IDs required (comma-separated)')
297
+ sys.exit(1)
298
+
299
+ url = f'{api}/api/supervision/watch?sessions={sessions}&duration={duration}&alertOn={alert_on}'
300
+ try:
301
+ resp = json.loads(urllib.request.urlopen(url, timeout=int(duration) + 10).read())
302
+ log(resp.get('text', '(no digest)'))
303
+ if resp.get('anomalies'):
304
+ log(f'\\nAnomalies: {len(resp["anomalies"])}')
305
+ for a in resp['anomalies']:
306
+ log(f' [{a["type"]}] {a["message"]}')
307
+ except Exception as e:
308
+ log(f'ERROR: {e}')
309
+ sys.exit(0)
310
+
311
+ elif cmd == 'peers':
312
+ wave_id = ''
313
+ role_id = ''
314
+ i = 2
315
+ while i < len(sys.argv):
316
+ if sys.argv[i] == '--wave' and i + 1 < len(sys.argv):
317
+ wave_id = sys.argv[i + 1]
318
+ i += 2
319
+ elif sys.argv[i] == '--role' and i + 1 < len(sys.argv):
320
+ role_id = sys.argv[i + 1]
321
+ i += 2
322
+ else:
323
+ i += 1
324
+
325
+ if not wave_id or not role_id:
326
+ log('Usage: supervision peers --wave <waveId> --role <roleId>')
327
+ sys.exit(1)
328
+
329
+ try:
330
+ url = f'{api}/api/supervision/peers?waveId={wave_id}&roleId={role_id}'
331
+ resp = json.loads(urllib.request.urlopen(url, timeout=10).read())
332
+ peers = resp.get('peers', [])
333
+ if not peers:
334
+ log('No peer C-Level sessions found in this wave.')
335
+ else:
336
+ for p in peers:
337
+ log(f'[{p["roleId"]}] {p["sessionId"]} — {p["status"]} — {p["task"][:80]}')
338
+ except Exception as e:
339
+ log(f'ERROR: {e}')
340
+ sys.exit(0)
341
+
342
+ elif cmd == 'abort':
343
+ session_id = sys.argv[2] if len(sys.argv) > 2 else ''
344
+ reason = 'Aborted by supervisor'
345
+ i = 3
346
+ while i < len(sys.argv):
347
+ if sys.argv[i] == '--reason' and i + 1 < len(sys.argv):
348
+ reason = sys.argv[i + 1]
349
+ i += 2
350
+ else:
351
+ i += 1
352
+
353
+ if not session_id:
354
+ log('Usage: supervision abort <sessionId> [--reason "..."]')
355
+ sys.exit(1)
356
+
357
+ try:
358
+ body = json.dumps({'sessionId': session_id, 'reason': reason}).encode()
359
+ req = urllib.request.Request(f'{api}/api/jobs/{session_id}', method='DELETE')
360
+ urllib.request.urlopen(req, timeout=10)
361
+ log(f'Session {session_id} aborted. Reason: {reason}')
362
+ except Exception as e:
363
+ log(f'ERROR: {e}')
364
+ sys.exit(0)
365
+
366
+ elif cmd == 'amend':
367
+ session_id = sys.argv[2] if len(sys.argv) > 2 else ''
368
+ instruction = ' '.join(sys.argv[3:]) if len(sys.argv) > 3 else ''
369
+
370
+ if not session_id or not instruction:
371
+ log('Usage: supervision amend <sessionId> "<instruction>"')
372
+ sys.exit(1)
373
+
374
+ # Amend uses continue-session with amended context
375
+ body = json.dumps({
376
+ 'response': f'[SUPERVISION AMENDMENT] {instruction}',
377
+ 'responderRole': os.environ.get('DISPATCH_SOURCE_ROLE', 'ceo'),
378
+ }).encode()
379
+
380
+ try:
381
+ req = urllib.request.Request(
382
+ f'{api}/api/exec/session/{session_id}/message',
383
+ body,
384
+ {'Content-Type': 'application/json'},
385
+ )
386
+ resp = json.loads(urllib.request.urlopen(req, timeout=10).read())
387
+ log(f'Session {session_id} amended with new instructions.')
388
+ except Exception as e:
389
+ log(f'ERROR: {e}')
390
+ sys.exit(0)
391
+
392
+ else:
393
+ log(f'Unknown command: {cmd}')
394
+ log('Usage: supervision <watch|peers|abort|amend> [args...]')
395
+ sys.exit(1)
396
+ `;
397
+
252
398
  /* ─── Claude CLI Runner ──────────────────────── */
253
399
 
254
400
  /**
@@ -303,6 +449,12 @@ export class ClaudeCliRunner implements ExecutionRunner {
303
449
  const consultScript = path.join(tmpDir, `consult-${roleId}-${Date.now()}.py`);
304
450
  fs.writeFileSync(consultScript, CONSULT_SCRIPT, { mode: 0o755 });
305
451
 
452
+ // Supervision Bridge — for C-Level roles with subordinates + heartbeat enabled
453
+ const supervisionScript = path.join(tmpDir, `supervision-${roleId}-${Date.now()}.py`);
454
+ if (subordinates.length > 0) {
455
+ fs.writeFileSync(supervisionScript, SUPERVISION_SCRIPT, { mode: 0o755 });
456
+ }
457
+
306
458
  // 5. Playwright MCP 설정 — 각 runner 인스턴스가 독립 브라우저 사용
307
459
  const runnerOutputDir = path.join(tmpDir, `playwright-${roleId}-${Date.now()}`);
308
460
  fs.mkdirSync(runnerOutputDir, { recursive: true });
@@ -361,6 +513,9 @@ export class ClaudeCliRunner implements ExecutionRunner {
361
513
  cleanEnv.DISPATCH_CMD = dispatchScript;
362
514
  cleanEnv.CONSULT_CMD = consultScript;
363
515
  cleanEnv.CONSULT_SOURCE_ROLE = roleId;
516
+ if (subordinates.length > 0) {
517
+ cleanEnv.SUPERVISION_CMD = supervisionScript;
518
+ }
364
519
 
365
520
  const modelName = config.model ?? 'claude-sonnet-4-5';
366
521
  // Use codeRoot as cwd — auto-creates ../{name}-code/ if not configured
@@ -499,6 +654,7 @@ export class ClaudeCliRunner implements ExecutionRunner {
499
654
  try { fs.unlinkSync(promptFile); } catch { /* ignore */ }
500
655
  try { fs.unlinkSync(dispatchScript); } catch { /* ignore */ }
501
656
  try { fs.unlinkSync(consultScript); } catch { /* ignore */ }
657
+ try { fs.unlinkSync(supervisionScript); } catch { /* ignore */ }
502
658
  try { fs.rmSync(runnerOutputDir, { recursive: true, force: true }); } catch { /* ignore */ }
503
659
 
504
660
  // 비정상 종료 시에도 결과 반환 (output이 있을 수 있으므로)
@@ -517,6 +673,7 @@ export class ClaudeCliRunner implements ExecutionRunner {
517
673
  try { fs.unlinkSync(promptFile); } catch { /* ignore */ }
518
674
  try { fs.unlinkSync(dispatchScript); } catch { /* ignore */ }
519
675
  try { fs.unlinkSync(consultScript); } catch { /* ignore */ }
676
+ try { fs.unlinkSync(supervisionScript); } catch { /* ignore */ }
520
677
  try { fs.rmSync(runnerOutputDir, { recursive: true, force: true }); } catch { /* ignore */ }
521
678
  reject(err);
522
679
  });
@@ -48,6 +48,8 @@ export class DirectApiRunner implements ExecutionRunner {
48
48
  onConsult: (roleId, question) => callbacks.onConsult?.(roleId, question),
49
49
  onTurnComplete: (turn) => callbacks.onTurnComplete?.(turn),
50
50
  onPromptAssembled: (systemPrompt, userTask) => callbacks.onPromptAssembled?.(systemPrompt, userTask),
51
+ onAbortSession: config.onAbortSession,
52
+ onAmendSession: config.onAmendSession,
51
53
  }).then((agentResult): RunnerResult => ({
52
54
  output: agentResult.output,
53
55
  turns: agentResult.turns,
@@ -45,6 +45,10 @@ export interface RunnerConfig {
45
45
  codeRoot?: string;
46
46
  /** PSM-004: Environment variables to inject (e.g., port assignments) */
47
47
  env?: Record<string, string>;
48
+ /** SV-7: Supervision — abort a running session */
49
+ onAbortSession?: (sessionId: string) => boolean;
50
+ /** SV-6: Supervision — amend a running session */
51
+ onAmendSession?: (sessionId: string, instruction: string) => boolean;
48
52
  }
49
53
 
50
54
  /* ─── Callbacks ───────────────────────────────── */
@@ -106,6 +106,54 @@ export const BASH_TOOL: ToolDefinition = {
106
106
  },
107
107
  };
108
108
 
109
+ /**
110
+ * Supervision 도구 — C-Level에게만 제공 (부하/동료 세션 감시)
111
+ */
112
+ export const HEARTBEAT_WATCH_TOOL: ToolDefinition = {
113
+ name: 'heartbeat_watch',
114
+ description: 'Block and watch activity streams of subordinates (or peers). Returns a digest of events after the specified duration or when an alert event occurs. Use this to supervise running dispatches at zero LLM cost during the wait period.',
115
+ input_schema: {
116
+ type: 'object',
117
+ properties: {
118
+ sessionIds: { type: 'array', items: { type: 'string' }, description: 'Session IDs to watch (subordinates or peers)' },
119
+ durationSec: { type: 'number', description: 'Watch duration in seconds (default 120, max 300)', default: 120 },
120
+ alertOn: {
121
+ type: 'array',
122
+ items: { type: 'string' },
123
+ description: 'Event types that trigger early return (default: msg:done, msg:error)',
124
+ default: ['msg:done', 'msg:error'],
125
+ },
126
+ },
127
+ required: ['sessionIds'],
128
+ },
129
+ };
130
+
131
+ export const AMEND_SESSION_TOOL: ToolDefinition = {
132
+ name: 'amend_session',
133
+ description: 'Send additional instructions to a running subordinate session. The instructions will be injected at the next turn boundary. Use when a subordinate is going in the wrong direction and needs course correction.',
134
+ input_schema: {
135
+ type: 'object',
136
+ properties: {
137
+ sessionId: { type: 'string', description: 'Target session ID to amend' },
138
+ instruction: { type: 'string', description: 'Additional instruction to inject into the session' },
139
+ },
140
+ required: ['sessionId', 'instruction'],
141
+ },
142
+ };
143
+
144
+ export const ABORT_SESSION_TOOL: ToolDefinition = {
145
+ name: 'abort_session',
146
+ description: 'Abort a running subordinate session immediately. Use when the subordinate is clearly doing the wrong thing and needs to be stopped. You can re-dispatch with different instructions afterwards.',
147
+ input_schema: {
148
+ type: 'object',
149
+ properties: {
150
+ sessionId: { type: 'string', description: 'Target session ID to abort' },
151
+ reason: { type: 'string', description: 'Reason for aborting (logged in activity stream)' },
152
+ },
153
+ required: ['sessionId'],
154
+ },
155
+ };
156
+
109
157
  /**
110
158
  * 상담 도구 — 모든 Role에게 제공 (동료/상관/부하에게 질문)
111
159
  */
@@ -124,8 +172,9 @@ export const CONSULT_TOOL: ToolDefinition = {
124
172
 
125
173
  /**
126
174
  * Role에 따른 도구 목록 반환
175
+ * @param heartbeatEnabled - C-Level supervision mode enabled (provides heartbeat_watch, amend_session, abort_session)
127
176
  */
128
- export function getToolsForRole(hasSubordinates: boolean, readOnly: boolean, hasBash = false): ToolDefinition[] {
177
+ export function getToolsForRole(hasSubordinates: boolean, readOnly: boolean, hasBash = false, heartbeatEnabled = false): ToolDefinition[] {
129
178
  if (readOnly) {
130
179
  return [...READ_TOOLS];
131
180
  }
@@ -138,6 +187,11 @@ export function getToolsForRole(hasSubordinates: boolean, readOnly: boolean, has
138
187
 
139
188
  if (hasSubordinates) {
140
189
  tools.push(DISPATCH_TOOL);
190
+
191
+ // Supervision tools — only for roles with subordinates AND heartbeat enabled
192
+ if (heartbeatEnabled) {
193
+ tools.push(HEARTBEAT_WATCH_TOOL, AMEND_SESSION_TOOL, ABORT_SESSION_TOOL);
194
+ }
141
195
  }
142
196
 
143
197
  return tools;