oh-my-codex 0.8.0 → 0.8.2

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 (169) hide show
  1. package/README.de.md +7 -2
  2. package/README.es.md +7 -2
  3. package/README.fr.md +7 -2
  4. package/README.it.md +7 -2
  5. package/README.ja.md +7 -2
  6. package/README.ko.md +7 -2
  7. package/README.md +61 -11
  8. package/README.pt.md +7 -2
  9. package/README.ru.md +7 -2
  10. package/README.tr.md +7 -2
  11. package/README.vi.md +7 -2
  12. package/README.zh-TW.md +366 -0
  13. package/README.zh.md +7 -2
  14. package/dist/cli/__tests__/index.test.js +70 -4
  15. package/dist/cli/__tests__/index.test.js.map +1 -1
  16. package/dist/cli/__tests__/setup-skills-overwrite.test.js +100 -1
  17. package/dist/cli/__tests__/setup-skills-overwrite.test.js.map +1 -1
  18. package/dist/cli/__tests__/team.test.js +219 -1
  19. package/dist/cli/__tests__/team.test.js.map +1 -1
  20. package/dist/cli/catalog-contract.d.ts.map +1 -1
  21. package/dist/cli/catalog-contract.js +8 -2
  22. package/dist/cli/catalog-contract.js.map +1 -1
  23. package/dist/cli/index.d.ts +7 -1
  24. package/dist/cli/index.d.ts.map +1 -1
  25. package/dist/cli/index.js +58 -12
  26. package/dist/cli/index.js.map +1 -1
  27. package/dist/cli/setup.d.ts.map +1 -1
  28. package/dist/cli/setup.js +50 -17
  29. package/dist/cli/setup.js.map +1 -1
  30. package/dist/cli/team.d.ts.map +1 -1
  31. package/dist/cli/team.js +257 -0
  32. package/dist/cli/team.js.map +1 -1
  33. package/dist/config/__tests__/models.test.js +11 -11
  34. package/dist/config/__tests__/models.test.js.map +1 -1
  35. package/dist/config/models.d.ts +4 -3
  36. package/dist/config/models.d.ts.map +1 -1
  37. package/dist/config/models.js +6 -5
  38. package/dist/config/models.js.map +1 -1
  39. package/dist/hooks/__tests__/keyword-detector.test.js +46 -3
  40. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  41. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js +23 -7
  42. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +1 -1
  43. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +176 -1
  44. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
  45. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +61 -1
  46. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
  47. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js +17 -7
  48. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
  49. package/dist/hooks/__tests__/openclaw-setup-contract.test.js +26 -16
  50. package/dist/hooks/__tests__/openclaw-setup-contract.test.js.map +1 -1
  51. package/dist/hooks/keyword-detector.d.ts +2 -1
  52. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  53. package/dist/hooks/keyword-detector.js +41 -4
  54. package/dist/hooks/keyword-detector.js.map +1 -1
  55. package/dist/hooks/keyword-registry.d.ts.map +1 -1
  56. package/dist/hooks/keyword-registry.js +5 -0
  57. package/dist/hooks/keyword-registry.js.map +1 -1
  58. package/dist/mcp/__tests__/path-traversal.test.js +9 -227
  59. package/dist/mcp/__tests__/path-traversal.test.js.map +1 -1
  60. package/dist/mcp/__tests__/state-server-schema.test.js +16 -20
  61. package/dist/mcp/__tests__/state-server-schema.test.js.map +1 -1
  62. package/dist/mcp/__tests__/state-server-team-tools.test.js +30 -487
  63. package/dist/mcp/__tests__/state-server-team-tools.test.js.map +1 -1
  64. package/dist/mcp/state-server.d.ts +179 -0
  65. package/dist/mcp/state-server.d.ts.map +1 -1
  66. package/dist/mcp/state-server.js +217 -1111
  67. package/dist/mcp/state-server.js.map +1 -1
  68. package/dist/mcp/team-server.d.ts.map +1 -1
  69. package/dist/mcp/team-server.js +28 -7
  70. package/dist/mcp/team-server.js.map +1 -1
  71. package/dist/notifications/__tests__/dispatch-cooldown.test.d.ts +5 -0
  72. package/dist/notifications/__tests__/dispatch-cooldown.test.d.ts.map +1 -0
  73. package/dist/notifications/__tests__/dispatch-cooldown.test.js +100 -0
  74. package/dist/notifications/__tests__/dispatch-cooldown.test.js.map +1 -0
  75. package/dist/notifications/__tests__/temp-mode.test.d.ts +2 -0
  76. package/dist/notifications/__tests__/temp-mode.test.d.ts.map +1 -0
  77. package/dist/notifications/__tests__/temp-mode.test.js +172 -0
  78. package/dist/notifications/__tests__/temp-mode.test.js.map +1 -0
  79. package/dist/notifications/config.d.ts.map +1 -1
  80. package/dist/notifications/config.js +59 -6
  81. package/dist/notifications/config.js.map +1 -1
  82. package/dist/notifications/dispatch-cooldown.d.ts +36 -0
  83. package/dist/notifications/dispatch-cooldown.d.ts.map +1 -0
  84. package/dist/notifications/dispatch-cooldown.js +109 -0
  85. package/dist/notifications/dispatch-cooldown.js.map +1 -0
  86. package/dist/notifications/index.d.ts +5 -0
  87. package/dist/notifications/index.d.ts.map +1 -1
  88. package/dist/notifications/index.js +39 -8
  89. package/dist/notifications/index.js.map +1 -1
  90. package/dist/notifications/temp-contract.d.ts +22 -0
  91. package/dist/notifications/temp-contract.d.ts.map +1 -0
  92. package/dist/notifications/temp-contract.js +147 -0
  93. package/dist/notifications/temp-contract.js.map +1 -0
  94. package/dist/notifications/types.d.ts +18 -0
  95. package/dist/notifications/types.d.ts.map +1 -1
  96. package/dist/openclaw/__tests__/config.test.js +81 -0
  97. package/dist/openclaw/__tests__/config.test.js.map +1 -1
  98. package/dist/openclaw/__tests__/dispatcher.test.js +50 -7
  99. package/dist/openclaw/__tests__/dispatcher.test.js.map +1 -1
  100. package/dist/openclaw/config.d.ts +4 -0
  101. package/dist/openclaw/config.d.ts.map +1 -1
  102. package/dist/openclaw/config.js +110 -16
  103. package/dist/openclaw/config.js.map +1 -1
  104. package/dist/openclaw/dispatcher.d.ts +10 -4
  105. package/dist/openclaw/dispatcher.d.ts.map +1 -1
  106. package/dist/openclaw/dispatcher.js +40 -10
  107. package/dist/openclaw/dispatcher.js.map +1 -1
  108. package/dist/openclaw/types.d.ts +5 -1
  109. package/dist/openclaw/types.d.ts.map +1 -1
  110. package/dist/team/__tests__/api-interop.test.d.ts +2 -0
  111. package/dist/team/__tests__/api-interop.test.d.ts.map +1 -0
  112. package/dist/team/__tests__/api-interop.test.js +1052 -0
  113. package/dist/team/__tests__/api-interop.test.js.map +1 -0
  114. package/dist/team/__tests__/mcp-comm.test.js +30 -0
  115. package/dist/team/__tests__/mcp-comm.test.js.map +1 -1
  116. package/dist/team/__tests__/runtime-cli.test.js +6 -0
  117. package/dist/team/__tests__/runtime-cli.test.js.map +1 -1
  118. package/dist/team/__tests__/runtime.test.js +52 -22
  119. package/dist/team/__tests__/runtime.test.js.map +1 -1
  120. package/dist/team/__tests__/tmux-claude-workers-demo.test.d.ts +2 -0
  121. package/dist/team/__tests__/tmux-claude-workers-demo.test.d.ts.map +1 -0
  122. package/dist/team/__tests__/tmux-claude-workers-demo.test.js +190 -0
  123. package/dist/team/__tests__/tmux-claude-workers-demo.test.js.map +1 -0
  124. package/dist/team/__tests__/tmux-session.test.js +45 -2
  125. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  126. package/dist/team/__tests__/worker-bootstrap.test.js +20 -12
  127. package/dist/team/__tests__/worker-bootstrap.test.js.map +1 -1
  128. package/dist/team/api-interop.d.ts +19 -0
  129. package/dist/team/api-interop.d.ts.map +1 -0
  130. package/dist/team/api-interop.js +578 -0
  131. package/dist/team/api-interop.js.map +1 -0
  132. package/dist/team/mcp-comm.d.ts.map +1 -1
  133. package/dist/team/mcp-comm.js +26 -0
  134. package/dist/team/mcp-comm.js.map +1 -1
  135. package/dist/team/runtime-cli.d.ts +3 -0
  136. package/dist/team/runtime-cli.d.ts.map +1 -1
  137. package/dist/team/runtime-cli.js +24 -2
  138. package/dist/team/runtime-cli.js.map +1 -1
  139. package/dist/team/runtime.d.ts.map +1 -1
  140. package/dist/team/runtime.js +67 -11
  141. package/dist/team/runtime.js.map +1 -1
  142. package/dist/team/scaling.js.map +1 -1
  143. package/dist/team/state/types.d.ts +1 -1
  144. package/dist/team/state/types.d.ts.map +1 -1
  145. package/dist/team/state.d.ts +1 -1
  146. package/dist/team/state.d.ts.map +1 -1
  147. package/dist/team/tmux-session.d.ts +1 -1
  148. package/dist/team/tmux-session.d.ts.map +1 -1
  149. package/dist/team/tmux-session.js +17 -5
  150. package/dist/team/tmux-session.js.map +1 -1
  151. package/dist/team/worker-bootstrap.d.ts.map +1 -1
  152. package/dist/team/worker-bootstrap.js +48 -19
  153. package/dist/team/worker-bootstrap.js.map +1 -1
  154. package/package.json +1 -1
  155. package/scripts/demo-claude-workers.sh +241 -0
  156. package/scripts/demo-team-e2e.sh +179 -0
  157. package/scripts/notify-hook/team-dispatch.js +186 -12
  158. package/scripts/notify-hook/team-leader-nudge.js +42 -2
  159. package/scripts/notify-hook/team-worker.js +63 -4
  160. package/skills/configure-notifications/SKILL.md +193 -185
  161. package/skills/omx-setup/SKILL.md +1 -1
  162. package/skills/team/SKILL.md +47 -5
  163. package/skills/worker/SKILL.md +40 -10
  164. package/templates/AGENTS.md +7 -3
  165. package/templates/catalog-manifest.json +26 -3
  166. package/skills/configure-discord/SKILL.md +0 -256
  167. package/skills/configure-openclaw/SKILL.md +0 -264
  168. package/skills/configure-slack/SKILL.md +0 -226
  169. package/skills/configure-telegram/SKILL.md +0 -232
@@ -3,62 +3,34 @@
3
3
  * Provides state read/write/clear/list tools for workflow modes
4
4
  * Storage: .omx/state/{mode}-state.json
5
5
  */
6
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
7
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
8
- import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
9
- import { readFile, writeFile, readdir, mkdir, unlink, rename } from 'fs/promises';
10
- import { existsSync, readFileSync } from 'fs';
11
- import { dirname, join, resolve as resolvePath } from 'path';
12
- import { getAllScopedStatePaths, getReadScopedStateDirs, getReadScopedStatePaths, resolveStateScope, getStateDir, getStatePath, resolveWorkingDirectoryForState, validateSessionId, } from './state-paths.js';
13
- import { withModeRuntimeContext } from '../state/mode-state-context.js';
14
- import { RALPH_PHASES, validateAndNormalizeRalphState } from '../ralph/contract.js';
15
- import { ensureCanonicalRalphArtifacts } from '../ralph/persistence.js';
16
- import { shouldAutoStartMcpServer } from './bootstrap.js';
17
- import { TEAM_NAME_SAFE_PATTERN, WORKER_NAME_SAFE_PATTERN, TASK_ID_SAFE_PATTERN, TEAM_TASK_STATUSES, TEAM_EVENT_TYPES, TEAM_TASK_APPROVAL_STATUSES, } from '../team/contracts.js';
18
- import { teamSendMessage as sendDirectMessage, teamBroadcast as broadcastMessage, teamListMailbox as listMailboxMessages, teamMarkMessageDelivered as markMessageDelivered, teamMarkMessageNotified as markMessageNotified, teamCreateTask, teamReadTask, teamListTasks, teamUpdateTask, teamClaimTask, teamTransitionTaskStatus, teamReleaseTaskClaim, teamReadConfig, teamReadManifest, teamReadWorkerStatus, teamReadWorkerHeartbeat, teamUpdateWorkerHeartbeat, teamWriteWorkerInbox, teamWriteWorkerIdentity, teamAppendEvent, teamGetSummary, teamCleanup, teamWriteShutdownRequest, teamReadShutdownAck, teamReadMonitorSnapshot, teamWriteMonitorSnapshot, teamReadTaskApproval, teamWriteTaskApproval, } from '../team/team-ops.js';
6
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
7
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
9
+ import { readFile, writeFile, readdir, mkdir, unlink, rename, } from "fs/promises";
10
+ import { existsSync } from "fs";
11
+ import { join } from "path";
12
+ import { getAllScopedStatePaths, getReadScopedStateDirs, getReadScopedStatePaths, resolveStateScope, getStateDir, getStatePath, resolveWorkingDirectoryForState, validateSessionId, } from "./state-paths.js";
13
+ import { withModeRuntimeContext } from "../state/mode-state-context.js";
14
+ import { RALPH_PHASES, validateAndNormalizeRalphState, } from "../ralph/contract.js";
15
+ import { ensureCanonicalRalphArtifacts } from "../ralph/persistence.js";
16
+ import { shouldAutoStartMcpServer } from "./bootstrap.js";
17
+ import { LEGACY_TEAM_MCP_TOOLS, buildLegacyTeamDeprecationHint, } from "../team/api-interop.js";
19
18
  const SUPPORTED_MODES = [
20
- 'autopilot', 'team',
21
- 'ralph', 'ultrawork', 'ultraqa', 'ralplan',
19
+ "autopilot",
20
+ "team",
21
+ "ralph",
22
+ "ultrawork",
23
+ "ultraqa",
24
+ "ralplan",
22
25
  ];
23
26
  const STATE_TOOL_NAMES = new Set([
24
- 'state_read',
25
- 'state_write',
26
- 'state_clear',
27
- 'state_list_active',
28
- 'state_get_status',
27
+ "state_read",
28
+ "state_write",
29
+ "state_clear",
30
+ "state_list_active",
31
+ "state_get_status",
29
32
  ]);
30
- const TEAM_COMM_TOOL_NAMES = new Set([
31
- 'team_send_message',
32
- 'team_broadcast',
33
- 'team_mailbox_list',
34
- 'team_mailbox_mark_delivered',
35
- 'team_mailbox_mark_notified',
36
- 'team_create_task',
37
- 'team_read_task',
38
- 'team_list_tasks',
39
- 'team_update_task',
40
- 'team_claim_task',
41
- 'team_transition_task_status',
42
- 'team_release_task_claim',
43
- 'team_read_config',
44
- 'team_read_manifest',
45
- 'team_read_worker_status',
46
- 'team_read_worker_heartbeat',
47
- 'team_update_worker_heartbeat',
48
- 'team_write_worker_inbox',
49
- 'team_write_worker_identity',
50
- 'team_append_event',
51
- 'team_get_summary',
52
- 'team_cleanup',
53
- 'team_write_shutdown_request',
54
- 'team_read_shutdown_ack',
55
- 'team_read_monitor_snapshot',
56
- 'team_write_monitor_snapshot',
57
- 'team_read_task_approval',
58
- 'team_write_task_approval',
59
- ]);
60
- const TEAM_UPDATE_TASK_MUTABLE_FIELDS = new Set(['subject', 'description', 'blocked_by', 'requires_code_change']);
61
- const TEAM_UPDATE_TASK_REQUEST_FIELDS = new Set(['team_name', 'task_id', 'workingDirectory', ...TEAM_UPDATE_TASK_MUTABLE_FIELDS]);
33
+ const TEAM_COMM_TOOL_NAMES = new Set([...LEGACY_TEAM_MCP_TOOLS]);
62
34
  const stateWriteQueues = new Map();
63
35
  async function withStateWriteLock(path, fn) {
64
36
  const tail = stateWriteQueues.get(path) ?? Promise.resolve();
@@ -81,7 +53,7 @@ async function withStateWriteLock(path, fn) {
81
53
  }
82
54
  async function writeAtomicFile(path, data) {
83
55
  const tmpPath = `${path}.tmp.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}`;
84
- await writeFile(tmpPath, data, 'utf-8');
56
+ await writeFile(tmpPath, data, "utf-8");
85
57
  try {
86
58
  await rename(tmpPath, path);
87
59
  }
@@ -90,585 +62,110 @@ async function writeAtomicFile(path, data) {
90
62
  throw error;
91
63
  }
92
64
  }
93
- function isFiniteInteger(value) {
94
- return typeof value === 'number' && Number.isInteger(value) && Number.isFinite(value);
95
- }
96
- function parseValidatedTaskIdArray(value, fieldName) {
97
- if (!Array.isArray(value)) {
98
- throw new Error(`${fieldName} must be an array of task IDs (strings)`);
99
- }
100
- const taskIds = [];
101
- for (const item of value) {
102
- if (typeof item !== 'string') {
103
- throw new Error(`${fieldName} entries must be strings`);
104
- }
105
- const normalized = item.trim();
106
- if (!TASK_ID_SAFE_PATTERN.test(normalized)) {
107
- throw new Error(`${fieldName} contains invalid task ID: "${item}"`);
108
- }
109
- taskIds.push(normalized);
110
- }
111
- return taskIds;
112
- }
113
- function teamStateExists(teamName, candidateCwd) {
114
- if (!TEAM_NAME_SAFE_PATTERN.test(teamName))
115
- return false;
116
- const teamRoot = join(candidateCwd, '.omx', 'state', 'team', teamName);
117
- return (existsSync(join(teamRoot, 'config.json')) ||
118
- existsSync(join(teamRoot, 'tasks')) ||
119
- existsSync(teamRoot));
120
- }
121
- function parseTeamWorkerEnv(raw) {
122
- if (typeof raw !== 'string' || raw.trim() === '')
123
- return null;
124
- const match = /^([a-z0-9][a-z0-9-]{0,29})\/(worker-\d+)$/.exec(raw.trim());
125
- if (!match)
126
- return null;
127
- return { teamName: match[1], workerName: match[2] };
128
- }
129
- function readTeamStateRootFromFile(path) {
130
- if (!existsSync(path))
131
- return null;
132
- try {
133
- const parsed = JSON.parse(readFileSync(path, 'utf8'));
134
- return typeof parsed.team_state_root === 'string' && parsed.team_state_root.trim() !== ''
135
- ? parsed.team_state_root.trim()
136
- : null;
137
- }
138
- catch {
139
- return null;
140
- }
141
- }
142
- function stateRootToWorkingDirectory(stateRoot) {
143
- const absolute = resolvePath(stateRoot);
144
- return dirname(dirname(absolute));
145
- }
146
- function resolveTeamWorkingDirectoryFromMetadata(teamName, candidateCwd, workerContext) {
147
- const teamRoot = join(candidateCwd, '.omx', 'state', 'team', teamName);
148
- if (!existsSync(teamRoot))
149
- return null;
150
- if (workerContext?.teamName === teamName) {
151
- const workerRoot = readTeamStateRootFromFile(join(teamRoot, 'workers', workerContext.workerName, 'identity.json'));
152
- if (workerRoot)
153
- return stateRootToWorkingDirectory(workerRoot);
154
- }
155
- const fromManifest = readTeamStateRootFromFile(join(teamRoot, 'manifest.v2.json'));
156
- if (fromManifest)
157
- return stateRootToWorkingDirectory(fromManifest);
158
- const fromConfig = readTeamStateRootFromFile(join(teamRoot, 'config.json'));
159
- if (fromConfig)
160
- return stateRootToWorkingDirectory(fromConfig);
161
- return null;
162
- }
163
- function resolveTeamWorkingDirectory(teamName, preferredCwd) {
164
- const normalizedTeamName = String(teamName || '').trim();
165
- if (!normalizedTeamName)
166
- return preferredCwd;
167
- const envTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
168
- if (typeof envTeamStateRoot === 'string' && envTeamStateRoot.trim() !== '') {
169
- return stateRootToWorkingDirectory(envTeamStateRoot.trim());
170
- }
171
- const seeds = [];
172
- for (const seed of [preferredCwd, process.cwd()]) {
173
- if (typeof seed !== 'string' || seed.trim() === '')
174
- continue;
175
- if (!seeds.includes(seed))
176
- seeds.push(seed);
177
- }
178
- const workerContext = parseTeamWorkerEnv(process.env.OMX_TEAM_WORKER);
179
- for (const seed of seeds) {
180
- let cursor = seed;
181
- while (cursor) {
182
- if (teamStateExists(normalizedTeamName, cursor)) {
183
- return resolveTeamWorkingDirectoryFromMetadata(normalizedTeamName, cursor, workerContext) ?? cursor;
184
- }
185
- const parent = dirname(cursor);
186
- if (!parent || parent === cursor)
187
- break;
188
- cursor = parent;
189
- }
190
- }
191
- return preferredCwd;
192
- }
193
- const server = new Server({ name: 'omx-state', version: '0.1.0' }, { capabilities: { tools: {} } });
194
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
195
- tools: [
196
- {
197
- name: 'state_read',
198
- description: 'Read state for a specific mode. Returns JSON state data or indicates no state exists.',
199
- inputSchema: {
200
- type: 'object',
201
- properties: {
202
- mode: { type: 'string', enum: [...SUPPORTED_MODES], description: 'The mode to read state for' },
203
- workingDirectory: { type: 'string', description: 'Working directory override' },
204
- session_id: { type: 'string', description: 'Optional session scope ID' },
205
- },
206
- required: ['mode'],
207
- },
208
- },
209
- {
210
- name: 'state_write',
211
- description: 'Write/update state for a specific mode. Creates directories if needed.',
212
- inputSchema: {
213
- type: 'object',
214
- properties: {
215
- mode: { type: 'string', enum: [...SUPPORTED_MODES] },
216
- active: { type: 'boolean' },
217
- iteration: { type: 'number' },
218
- max_iterations: { type: 'number' },
219
- current_phase: { type: 'string' },
220
- task_description: { type: 'string' },
221
- started_at: { type: 'string' },
222
- completed_at: { type: 'string' },
223
- error: { type: 'string' },
224
- state: { type: 'object', description: 'Additional custom fields' },
225
- workingDirectory: { type: 'string' },
226
- session_id: { type: 'string', description: 'Optional session scope ID' },
227
- },
228
- required: ['mode'],
229
- },
230
- },
231
- {
232
- name: 'state_clear',
233
- description: 'Clear/delete state for a specific mode.',
234
- inputSchema: {
235
- type: 'object',
236
- properties: {
237
- mode: { type: 'string', enum: [...SUPPORTED_MODES] },
238
- workingDirectory: { type: 'string' },
239
- session_id: { type: 'string', description: 'Optional session scope ID' },
240
- all_sessions: { type: 'boolean', description: 'Clear matching mode in global and all session scopes' },
241
- },
242
- required: ['mode'],
243
- },
244
- },
245
- {
246
- name: 'state_list_active',
247
- description: 'List all currently active modes.',
248
- inputSchema: {
249
- type: 'object',
250
- properties: {
251
- workingDirectory: { type: 'string' },
252
- session_id: { type: 'string', description: 'Optional session scope ID' },
253
- },
254
- },
255
- },
256
- {
257
- name: 'state_get_status',
258
- description: 'Get detailed status for a specific mode or all modes.',
259
- inputSchema: {
260
- type: 'object',
261
- properties: {
262
- mode: { type: 'string', enum: [...SUPPORTED_MODES] },
263
- workingDirectory: { type: 'string' },
264
- session_id: { type: 'string', description: 'Optional session scope ID' },
265
- },
266
- },
267
- },
268
- {
269
- name: 'team_send_message',
270
- description: 'Send a direct team mailbox message from one worker to another worker/leader.',
271
- inputSchema: {
272
- type: 'object',
273
- properties: {
274
- team_name: { type: 'string', description: 'Sanitized team name (e.g., my-team)' },
275
- from_worker: { type: 'string', description: 'Sender worker id (e.g., worker-1)' },
276
- to_worker: { type: 'string', description: 'Recipient worker id (e.g., worker-2 or leader-fixed)' },
277
- body: { type: 'string', description: 'Message content' },
278
- workingDirectory: { type: 'string' },
279
- },
280
- required: ['team_name', 'from_worker', 'to_worker', 'body'],
281
- },
282
- },
283
- {
284
- name: 'team_broadcast',
285
- description: 'Broadcast a message from one worker to all other workers in the team.',
286
- inputSchema: {
287
- type: 'object',
288
- properties: {
289
- team_name: { type: 'string', description: 'Sanitized team name (e.g., my-team)' },
290
- from_worker: { type: 'string', description: 'Sender worker id (e.g., worker-1)' },
291
- body: { type: 'string', description: 'Message content' },
292
- workingDirectory: { type: 'string' },
293
- },
294
- required: ['team_name', 'from_worker', 'body'],
295
- },
296
- },
297
- {
298
- name: 'team_mailbox_list',
299
- description: 'List mailbox messages for a specific worker (including leader-fixed).',
300
- inputSchema: {
301
- type: 'object',
302
- properties: {
303
- team_name: { type: 'string', description: 'Sanitized team name (e.g., my-team)' },
304
- worker: { type: 'string', description: 'Mailbox owner worker id' },
305
- include_delivered: { type: 'boolean', description: 'Include delivered messages (default: true)' },
306
- workingDirectory: { type: 'string' },
307
- },
308
- required: ['team_name', 'worker'],
309
- },
310
- },
311
- {
312
- name: 'team_mailbox_mark_delivered',
313
- description: 'Mark a mailbox message as delivered for a worker.',
314
- inputSchema: {
315
- type: 'object',
316
- properties: {
317
- team_name: { type: 'string', description: 'Sanitized team name (e.g., my-team)' },
318
- worker: { type: 'string', description: 'Mailbox owner worker id' },
319
- message_id: { type: 'string', description: 'Message ID to mark delivered' },
320
- workingDirectory: { type: 'string' },
321
- },
322
- required: ['team_name', 'worker', 'message_id'],
323
- },
324
- },
325
- {
326
- name: 'team_mailbox_mark_notified',
327
- description: 'Mark a mailbox message as notified (tmux trigger sent) for a worker.',
328
- inputSchema: {
329
- type: 'object',
330
- properties: {
331
- team_name: { type: 'string', description: 'Sanitized team name' },
332
- worker: { type: 'string', description: 'Mailbox owner worker id' },
333
- message_id: { type: 'string', description: 'Message ID to mark notified' },
334
- workingDirectory: { type: 'string' },
335
- },
336
- required: ['team_name', 'worker', 'message_id'],
337
- },
338
- },
339
- {
340
- name: 'team_create_task',
341
- description: 'Create a new task in the team task list. Returns the created task with auto-incremented ID.',
342
- inputSchema: {
343
- type: 'object',
344
- properties: {
345
- team_name: { type: 'string', description: 'Sanitized team name' },
346
- subject: { type: 'string', description: 'Task subject/title' },
347
- description: { type: 'string', description: 'Task description' },
348
- owner: { type: 'string', description: 'Worker name to assign (optional)' },
349
- blocked_by: { type: 'array', items: { type: 'string' }, description: 'Task IDs this task depends on' },
350
- requires_code_change: { type: 'boolean', description: 'Whether the task involves code changes' },
351
- workingDirectory: { type: 'string' },
352
- },
353
- required: ['team_name', 'subject', 'description'],
354
- },
355
- },
356
- {
357
- name: 'team_read_task',
358
- description: 'Read a single task by ID.',
359
- inputSchema: {
360
- type: 'object',
361
- properties: {
362
- team_name: { type: 'string', description: 'Sanitized team name' },
363
- task_id: { type: 'string', description: 'Task ID to read' },
364
- workingDirectory: { type: 'string' },
365
- },
366
- required: ['team_name', 'task_id'],
367
- },
368
- },
369
- {
370
- name: 'team_list_tasks',
371
- description: 'List all tasks in a team, sorted by numeric ID.',
372
- inputSchema: {
373
- type: 'object',
374
- properties: {
375
- team_name: { type: 'string', description: 'Sanitized team name' },
376
- workingDirectory: { type: 'string' },
377
- },
378
- required: ['team_name'],
379
- },
380
- },
381
- {
382
- name: 'team_update_task',
383
- description: 'Update non-lifecycle task metadata (subject, description, blocked_by, requires_code_change). Status/owner/result/error are lifecycle fields that must be changed via team_claim_task + team_transition_task_status.',
384
- inputSchema: {
385
- type: 'object',
386
- properties: {
387
- team_name: { type: 'string', description: 'Sanitized team name' },
388
- task_id: { type: 'string', description: 'Task ID to update' },
389
- subject: { type: 'string', description: 'Task subject/title' },
390
- description: { type: 'string', description: 'Task description' },
391
- blocked_by: { type: 'array', items: { type: 'string' }, description: 'Task IDs this task depends on' },
392
- requires_code_change: { type: 'boolean', description: 'Whether this task requires a code change' },
393
- workingDirectory: { type: 'string' },
394
- },
395
- required: ['team_name', 'task_id'],
396
- },
397
- },
398
- {
399
- name: 'team_claim_task',
400
- description: 'Atomically claim a task for a worker. Checks dependencies and version.',
401
- inputSchema: {
402
- type: 'object',
403
- properties: {
404
- team_name: { type: 'string', description: 'Sanitized team name' },
405
- task_id: { type: 'string', description: 'Task ID to claim' },
406
- worker: { type: 'string', description: 'Worker name claiming the task' },
407
- expected_version: { type: 'number', description: 'Expected task version for optimistic locking. Omitting does not allow claiming an already in-progress task.' },
408
- workingDirectory: { type: 'string' },
409
- },
410
- required: ['team_name', 'task_id', 'worker'],
411
- },
412
- },
413
- {
414
- name: 'team_transition_task_status',
415
- description: 'Atomically transition task status with claim token validation.',
416
- inputSchema: {
417
- type: 'object',
418
- properties: {
419
- team_name: { type: 'string', description: 'Sanitized team name' },
420
- task_id: { type: 'string', description: 'Task ID to transition' },
421
- from: { type: 'string', enum: [...TEAM_TASK_STATUSES] },
422
- to: { type: 'string', enum: [...TEAM_TASK_STATUSES] },
423
- claim_token: { type: 'string', description: 'Claim token from team_claim_task' },
424
- workingDirectory: { type: 'string' },
425
- },
426
- required: ['team_name', 'task_id', 'from', 'to', 'claim_token'],
427
- },
428
- },
429
- {
430
- name: 'team_release_task_claim',
431
- description: 'Release a task claim, returning task to pending status.',
432
- inputSchema: {
433
- type: 'object',
434
- properties: {
435
- team_name: { type: 'string', description: 'Sanitized team name' },
436
- task_id: { type: 'string', description: 'Task ID' },
437
- claim_token: { type: 'string', description: 'Claim token from the claim operation' },
438
- worker: { type: 'string', description: 'Worker name that holds the claim' },
439
- workingDirectory: { type: 'string' },
440
- },
441
- required: ['team_name', 'task_id', 'claim_token', 'worker'],
442
- },
443
- },
444
- {
445
- name: 'team_read_config',
446
- description: 'Read team configuration (workers, tmux session, task counter).',
447
- inputSchema: {
448
- type: 'object',
449
- properties: {
450
- team_name: { type: 'string', description: 'Sanitized team name' },
451
- workingDirectory: { type: 'string' },
452
- },
453
- required: ['team_name'],
454
- },
455
- },
456
- {
457
- name: 'team_read_manifest',
458
- description: 'Read team manifest v2 (leader, policy, permissions snapshot).',
459
- inputSchema: {
460
- type: 'object',
461
- properties: {
462
- team_name: { type: 'string', description: 'Sanitized team name' },
463
- workingDirectory: { type: 'string' },
464
- },
465
- required: ['team_name'],
466
- },
467
- },
468
- {
469
- name: 'team_read_worker_status',
470
- description: 'Read current worker status (state, current_task_id, reason).',
471
- inputSchema: {
472
- type: 'object',
473
- properties: {
474
- team_name: { type: 'string', description: 'Sanitized team name' },
475
- worker: { type: 'string', description: 'Worker name (e.g., worker-1)' },
476
- workingDirectory: { type: 'string' },
477
- },
478
- required: ['team_name', 'worker'],
479
- },
480
- },
481
- {
482
- name: 'team_read_worker_heartbeat',
483
- description: 'Read worker heartbeat (pid, turn count, alive flag).',
484
- inputSchema: {
485
- type: 'object',
486
- properties: {
487
- team_name: { type: 'string', description: 'Sanitized team name' },
488
- worker: { type: 'string', description: 'Worker name' },
489
- workingDirectory: { type: 'string' },
490
- },
491
- required: ['team_name', 'worker'],
492
- },
493
- },
65
+ const server = new Server({ name: "omx-state", version: "0.1.0" }, { capabilities: { tools: {} } });
66
+ export function buildStateServerTools() {
67
+ return [
494
68
  {
495
- name: 'team_update_worker_heartbeat',
496
- description: 'Write/update a worker heartbeat.',
69
+ name: "state_read",
70
+ description: "Read state for a specific mode. Returns JSON state data or indicates no state exists.",
497
71
  inputSchema: {
498
- type: 'object',
72
+ type: "object",
499
73
  properties: {
500
- team_name: { type: 'string', description: 'Sanitized team name' },
501
- worker: { type: 'string', description: 'Worker name' },
502
- pid: { type: 'number', description: 'Worker process ID' },
503
- turn_count: { type: 'number', description: 'Cumulative turn count' },
504
- alive: { type: 'boolean', description: 'Whether the worker is alive' },
505
- workingDirectory: { type: 'string' },
74
+ mode: {
75
+ type: "string",
76
+ enum: [...SUPPORTED_MODES],
77
+ description: "The mode to read state for",
78
+ },
79
+ workingDirectory: {
80
+ type: "string",
81
+ description: "Working directory override",
82
+ },
83
+ session_id: {
84
+ type: "string",
85
+ description: "Optional session scope ID",
86
+ },
506
87
  },
507
- required: ['team_name', 'worker', 'pid', 'turn_count', 'alive'],
88
+ required: ["mode"],
508
89
  },
509
90
  },
510
91
  {
511
- name: 'team_write_worker_inbox',
512
- description: 'Write a prompt/instruction to a worker inbox file.',
92
+ name: "state_write",
93
+ description: "Write/update state for a specific mode. Creates directories if needed.",
513
94
  inputSchema: {
514
- type: 'object',
95
+ type: "object",
515
96
  properties: {
516
- team_name: { type: 'string', description: 'Sanitized team name' },
517
- worker: { type: 'string', description: 'Worker name' },
518
- content: { type: 'string', description: 'Inbox content (markdown)' },
519
- workingDirectory: { type: 'string' },
97
+ mode: { type: "string", enum: [...SUPPORTED_MODES] },
98
+ active: { type: "boolean" },
99
+ iteration: { type: "number" },
100
+ max_iterations: { type: "number" },
101
+ current_phase: { type: "string" },
102
+ task_description: { type: "string" },
103
+ started_at: { type: "string" },
104
+ completed_at: { type: "string" },
105
+ error: { type: "string" },
106
+ state: { type: "object", description: "Additional custom fields" },
107
+ workingDirectory: { type: "string" },
108
+ session_id: {
109
+ type: "string",
110
+ description: "Optional session scope ID",
111
+ },
520
112
  },
521
- required: ['team_name', 'worker', 'content'],
113
+ required: ["mode"],
522
114
  },
523
115
  },
524
116
  {
525
- name: 'team_write_worker_identity',
526
- description: 'Write worker identity file (name, index, role, assigned tasks).',
117
+ name: "state_clear",
118
+ description: "Clear/delete state for a specific mode.",
527
119
  inputSchema: {
528
- type: 'object',
120
+ type: "object",
529
121
  properties: {
530
- team_name: { type: 'string', description: 'Sanitized team name' },
531
- worker: { type: 'string', description: 'Worker name' },
532
- index: { type: 'number', description: 'Worker index (1-based)' },
533
- role: { type: 'string', description: 'Agent role/type' },
534
- assigned_tasks: { type: 'array', items: { type: 'string' }, description: 'Assigned task IDs' },
535
- pid: { type: 'number', description: 'Worker process ID (optional)' },
536
- pane_id: { type: 'string', description: 'Tmux pane ID (optional)' },
537
- working_dir: { type: 'string', description: 'Worker working directory (optional)' },
538
- worktree_path: { type: 'string', description: 'Worker git worktree path (optional)' },
539
- worktree_branch: { type: 'string', description: 'Worker git worktree branch (optional)' },
540
- worktree_detached: { type: 'boolean', description: 'Whether worker worktree is detached (optional)' },
541
- team_state_root: { type: 'string', description: 'Canonical team state root path (optional)' },
542
- workingDirectory: { type: 'string' },
122
+ mode: { type: "string", enum: [...SUPPORTED_MODES] },
123
+ workingDirectory: { type: "string" },
124
+ session_id: {
125
+ type: "string",
126
+ description: "Optional session scope ID",
127
+ },
128
+ all_sessions: {
129
+ type: "boolean",
130
+ description: "Clear matching mode in global and all session scopes",
131
+ },
543
132
  },
544
- required: ['team_name', 'worker', 'index', 'role'],
133
+ required: ["mode"],
545
134
  },
546
135
  },
547
136
  {
548
- name: 'team_append_event',
549
- description: 'Append an event to the team event log (ndjson).',
137
+ name: "state_list_active",
138
+ description: "List all currently active modes.",
550
139
  inputSchema: {
551
- type: 'object',
140
+ type: "object",
552
141
  properties: {
553
- team_name: { type: 'string', description: 'Sanitized team name' },
554
- type: { type: 'string', enum: [...TEAM_EVENT_TYPES] },
555
- worker: { type: 'string', description: 'Worker name associated with the event' },
556
- task_id: { type: 'string', description: 'Related task ID (optional)' },
557
- message_id: { type: 'string', description: 'Related message ID (optional)' },
558
- reason: { type: 'string', description: 'Event reason (optional)' },
559
- workingDirectory: { type: 'string' },
142
+ workingDirectory: { type: "string" },
143
+ session_id: {
144
+ type: "string",
145
+ description: "Optional session scope ID",
146
+ },
560
147
  },
561
- required: ['team_name', 'type', 'worker'],
562
148
  },
563
149
  },
564
150
  {
565
- name: 'team_get_summary',
566
- description: 'Get team summary with task counts, worker status, and non-reporting detection.',
151
+ name: "state_get_status",
152
+ description: "Get detailed status for a specific mode or all modes.",
567
153
  inputSchema: {
568
- type: 'object',
154
+ type: "object",
569
155
  properties: {
570
- team_name: { type: 'string', description: 'Sanitized team name' },
571
- workingDirectory: { type: 'string' },
156
+ mode: { type: "string", enum: [...SUPPORTED_MODES] },
157
+ workingDirectory: { type: "string" },
158
+ session_id: {
159
+ type: "string",
160
+ description: "Optional session scope ID",
161
+ },
572
162
  },
573
- required: ['team_name'],
574
163
  },
575
164
  },
576
- {
577
- name: 'team_cleanup',
578
- description: 'Delete all team state files on disk (config, tasks, workers, events). Does NOT kill tmux panes -- use omx_run_team_cleanup for that.',
579
- inputSchema: {
580
- type: 'object',
581
- properties: {
582
- team_name: { type: 'string', description: 'Sanitized team name' },
583
- workingDirectory: { type: 'string' },
584
- },
585
- required: ['team_name'],
586
- },
587
- },
588
- {
589
- name: 'team_write_shutdown_request',
590
- description: 'Write a shutdown request for a worker.',
591
- inputSchema: {
592
- type: 'object',
593
- properties: {
594
- team_name: { type: 'string', description: 'Sanitized team name' },
595
- worker: { type: 'string', description: 'Worker name to shut down' },
596
- requested_by: { type: 'string', description: 'Requester identity (e.g., leader-fixed)' },
597
- workingDirectory: { type: 'string' },
598
- },
599
- required: ['team_name', 'worker', 'requested_by'],
600
- },
601
- },
602
- {
603
- name: 'team_read_shutdown_ack',
604
- description: 'Read a worker shutdown acknowledgment.',
605
- inputSchema: {
606
- type: 'object',
607
- properties: {
608
- team_name: { type: 'string', description: 'Sanitized team name' },
609
- worker: { type: 'string', description: 'Worker name' },
610
- min_updated_at: { type: 'string', description: 'ISO timestamp - ignore acks older than this' },
611
- workingDirectory: { type: 'string' },
612
- },
613
- required: ['team_name', 'worker'],
614
- },
615
- },
616
- {
617
- name: 'team_read_monitor_snapshot',
618
- description: 'Read the monitor snapshot (task/worker state from last poll).',
619
- inputSchema: {
620
- type: 'object',
621
- properties: {
622
- team_name: { type: 'string', description: 'Sanitized team name' },
623
- workingDirectory: { type: 'string' },
624
- },
625
- required: ['team_name'],
626
- },
627
- },
628
- {
629
- name: 'team_write_monitor_snapshot',
630
- description: 'Write the monitor snapshot for change detection across poll cycles.',
631
- inputSchema: {
632
- type: 'object',
633
- properties: {
634
- team_name: { type: 'string', description: 'Sanitized team name' },
635
- snapshot: { type: 'object', description: 'Monitor snapshot data' },
636
- workingDirectory: { type: 'string' },
637
- },
638
- required: ['team_name', 'snapshot'],
639
- },
640
- },
641
- {
642
- name: 'team_read_task_approval',
643
- description: 'Read task approval record (for plan-approval-required policy).',
644
- inputSchema: {
645
- type: 'object',
646
- properties: {
647
- team_name: { type: 'string', description: 'Sanitized team name' },
648
- task_id: { type: 'string', description: 'Task ID' },
649
- workingDirectory: { type: 'string' },
650
- },
651
- required: ['team_name', 'task_id'],
652
- },
653
- },
654
- {
655
- name: 'team_write_task_approval',
656
- description: 'Write a task approval decision.',
657
- inputSchema: {
658
- type: 'object',
659
- properties: {
660
- team_name: { type: 'string', description: 'Sanitized team name' },
661
- task_id: { type: 'string', description: 'Task ID' },
662
- required: { type: 'boolean', description: 'Whether approval was required' },
663
- status: { type: 'string', enum: [...TEAM_TASK_APPROVAL_STATUSES] },
664
- reviewer: { type: 'string', description: 'Reviewer identity' },
665
- decision_reason: { type: 'string', description: 'Reason for the decision' },
666
- workingDirectory: { type: 'string' },
667
- },
668
- required: ['team_name', 'task_id', 'status', 'reviewer', 'decision_reason'],
669
- },
670
- },
671
- ],
165
+ ];
166
+ }
167
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
168
+ tools: buildStateServerTools(),
672
169
  }));
673
170
  export async function handleStateToolCall(request) {
674
171
  const { name, arguments: args } = request.params;
@@ -679,7 +176,12 @@ export async function handleStateToolCall(request) {
679
176
  }
680
177
  catch (error) {
681
178
  return {
682
- content: [{ type: 'text', text: JSON.stringify({ error: error.message }) }],
179
+ content: [
180
+ {
181
+ type: "text",
182
+ text: JSON.stringify({ error: error.message }),
183
+ },
184
+ ],
683
185
  isError: true,
684
186
  };
685
187
  }
@@ -690,7 +192,12 @@ export async function handleStateToolCall(request) {
690
192
  }
691
193
  catch (error) {
692
194
  return {
693
- content: [{ type: 'text', text: JSON.stringify({ error: error.message }) }],
195
+ content: [
196
+ {
197
+ type: "text",
198
+ text: JSON.stringify({ error: error.message }),
199
+ },
200
+ ],
694
201
  isError: true,
695
202
  };
696
203
  }
@@ -704,56 +211,54 @@ export async function handleStateToolCall(request) {
704
211
  if (effectiveSessionId) {
705
212
  await mkdir(getStateDir(cwd, effectiveSessionId), { recursive: true });
706
213
  }
707
- const { ensureTmuxHookInitialized } = await import('../cli/tmux-hook.js');
214
+ const { ensureTmuxHookInitialized } = await import("../cli/tmux-hook.js");
708
215
  await ensureTmuxHookInitialized(cwd);
709
216
  }
710
217
  if (TEAM_COMM_TOOL_NAMES.has(name)) {
711
- const teamName = String(args?.team_name || '').trim();
712
- if (teamName && !TEAM_NAME_SAFE_PATTERN.test(teamName)) {
713
- return {
714
- content: [{ type: 'text', text: JSON.stringify({ error: `Invalid team_name: "${teamName}". Must match /^[a-z0-9][a-z0-9-]{0,29}$/ (lowercase alphanumeric + hyphens, max 30 chars).` }) }],
715
- isError: true,
716
- };
717
- }
718
- for (const workerField of ['worker', 'from_worker', 'to_worker']) {
719
- const workerVal = String(args?.[workerField] || '').trim();
720
- if (workerVal && !WORKER_NAME_SAFE_PATTERN.test(workerVal)) {
721
- return {
722
- content: [{ type: 'text', text: JSON.stringify({ error: `Invalid ${workerField}: "${workerVal}". Must match /^[a-z0-9][a-z0-9-]{0,63}$/ (lowercase alphanumeric + hyphens, max 64 chars).` }) }],
723
- isError: true,
724
- };
725
- }
726
- }
727
- const rawTaskId = String(args?.task_id || '').trim();
728
- if (rawTaskId && !TASK_ID_SAFE_PATTERN.test(rawTaskId)) {
729
- return {
730
- content: [{ type: 'text', text: JSON.stringify({ error: `Invalid task_id: "${rawTaskId}". Must be a positive integer (digits only, max 20 digits).` }) }],
731
- isError: true,
732
- };
733
- }
734
- if (teamName) {
735
- cwd = resolveTeamWorkingDirectory(teamName, cwd);
736
- }
737
- await mkdir(getStateDir(cwd, explicitSessionId), { recursive: true });
218
+ const hint = buildLegacyTeamDeprecationHint(name, args ?? {});
219
+ return {
220
+ content: [
221
+ {
222
+ type: "text",
223
+ text: JSON.stringify({
224
+ error: `MCP tool "${name}" is hard-deprecated. Team mutations now require CLI interop.`,
225
+ code: "deprecated_cli_only",
226
+ hint,
227
+ }),
228
+ },
229
+ ],
230
+ isError: true,
231
+ };
738
232
  }
739
233
  switch (name) {
740
- case 'state_read': {
234
+ case "state_read": {
741
235
  const mode = args.mode;
742
236
  if (!SUPPORTED_MODES.includes(mode)) {
743
237
  return {
744
- content: [{ type: 'text', text: JSON.stringify({ error: `mode must be one of: ${SUPPORTED_MODES.join(', ')}` }) }],
238
+ content: [
239
+ {
240
+ type: "text",
241
+ text: JSON.stringify({
242
+ error: `mode must be one of: ${SUPPORTED_MODES.join(", ")}`,
243
+ }),
244
+ },
245
+ ],
745
246
  isError: true,
746
247
  };
747
248
  }
748
249
  const paths = await getReadScopedStatePaths(mode, cwd, explicitSessionId);
749
250
  const path = paths.find((candidate) => existsSync(candidate));
750
251
  if (!path) {
751
- return { content: [{ type: 'text', text: JSON.stringify({ exists: false, mode }) }] };
252
+ return {
253
+ content: [
254
+ { type: "text", text: JSON.stringify({ exists: false, mode }) },
255
+ ],
256
+ };
752
257
  }
753
- const data = await readFile(path, 'utf-8');
754
- return { content: [{ type: 'text', text: data }] };
258
+ const data = await readFile(path, "utf-8");
259
+ return { content: [{ type: "text", text: data }] };
755
260
  }
756
- case 'state_write': {
261
+ case "state_write": {
757
262
  const mode = args.mode;
758
263
  const path = getStatePath(mode, cwd, effectiveSessionId);
759
264
  const { mode: _m, workingDirectory: _w, session_id: _sid, state: customState, ...fields } = args;
@@ -762,23 +267,29 @@ export async function handleStateToolCall(request) {
762
267
  let existing = {};
763
268
  if (existsSync(path)) {
764
269
  try {
765
- existing = JSON.parse(await readFile(path, 'utf-8'));
270
+ existing = JSON.parse(await readFile(path, "utf-8"));
766
271
  }
767
272
  catch (e) {
768
- process.stderr.write('[state-server] Failed to parse state file: ' + e + '\n');
273
+ process.stderr.write("[state-server] Failed to parse state file: " + e + "\n");
769
274
  }
770
275
  }
771
- const mergedRaw = { ...existing, ...fields, ...(customState || {}) };
772
- if (mode === 'ralph') {
276
+ const mergedRaw = {
277
+ ...existing,
278
+ ...fields,
279
+ ...(customState || {}),
280
+ };
281
+ if (mode === "ralph") {
773
282
  const originalPhase = mergedRaw.current_phase;
774
283
  const validation = validateAndNormalizeRalphState(mergedRaw);
775
284
  if (!validation.ok || !validation.state) {
776
- validationError = validation.error || `ralph.current_phase must be one of: ${RALPH_PHASES.join(', ')}`;
285
+ validationError =
286
+ validation.error ||
287
+ `ralph.current_phase must be one of: ${RALPH_PHASES.join(", ")}`;
777
288
  return;
778
289
  }
779
- if (typeof originalPhase === 'string'
780
- && typeof validation.state.current_phase === 'string'
781
- && validation.state.current_phase !== originalPhase) {
290
+ if (typeof originalPhase === "string" &&
291
+ typeof validation.state.current_phase === "string" &&
292
+ validation.state.current_phase !== originalPhase) {
782
293
  validation.state.ralph_phase_normalized_from = originalPhase;
783
294
  }
784
295
  Object.assign(mergedRaw, validation.state);
@@ -789,16 +300,25 @@ export async function handleStateToolCall(request) {
789
300
  });
790
301
  if (validationError) {
791
302
  return {
792
- content: [{
793
- type: 'text',
303
+ content: [
304
+ {
305
+ type: "text",
794
306
  text: JSON.stringify({ error: validationError }),
795
- }],
307
+ },
308
+ ],
796
309
  isError: true,
797
310
  };
798
311
  }
799
- return { content: [{ type: 'text', text: JSON.stringify({ success: true, mode, path }) }] };
312
+ return {
313
+ content: [
314
+ {
315
+ type: "text",
316
+ text: JSON.stringify({ success: true, mode, path }),
317
+ },
318
+ ],
319
+ };
800
320
  }
801
- case 'state_clear': {
321
+ case "state_clear": {
802
322
  const mode = args.mode;
803
323
  const allSessions = args.all_sessions === true;
804
324
  if (!allSessions) {
@@ -806,7 +326,14 @@ export async function handleStateToolCall(request) {
806
326
  if (existsSync(path)) {
807
327
  await unlink(path);
808
328
  }
809
- return { content: [{ type: 'text', text: JSON.stringify({ cleared: true, mode, path }) }] };
329
+ return {
330
+ content: [
331
+ {
332
+ type: "text",
333
+ text: JSON.stringify({ cleared: true, mode, path }),
334
+ },
335
+ ],
336
+ };
810
337
  }
811
338
  const removedPaths = [];
812
339
  const paths = await getAllScopedStatePaths(mode, cwd);
@@ -817,20 +344,22 @@ export async function handleStateToolCall(request) {
817
344
  removedPaths.push(path);
818
345
  }
819
346
  return {
820
- content: [{
821
- type: 'text',
347
+ content: [
348
+ {
349
+ type: "text",
822
350
  text: JSON.stringify({
823
351
  cleared: true,
824
352
  mode,
825
353
  all_sessions: true,
826
354
  removed: removedPaths.length,
827
355
  paths: removedPaths,
828
- warning: 'all_sessions clears global and session-scoped state files',
356
+ warning: "all_sessions clears global and session-scoped state files",
829
357
  }),
830
- }],
358
+ },
359
+ ],
831
360
  };
832
361
  }
833
- case 'state_list_active': {
362
+ case "state_list_active": {
834
363
  const stateDirs = await getReadScopedStateDirs(cwd, explicitSessionId);
835
364
  const active = [];
836
365
  const seenModes = new Set();
@@ -839,26 +368,30 @@ export async function handleStateToolCall(request) {
839
368
  continue;
840
369
  const files = await readdir(stateDir);
841
370
  for (const f of files) {
842
- if (!f.endsWith('-state.json'))
371
+ if (!f.endsWith("-state.json"))
843
372
  continue;
844
- const mode = f.replace('-state.json', '');
373
+ const mode = f.replace("-state.json", "");
845
374
  if (seenModes.has(mode))
846
375
  continue;
847
376
  seenModes.add(mode);
848
377
  try {
849
- const data = JSON.parse(await readFile(join(stateDir, f), 'utf-8'));
378
+ const data = JSON.parse(await readFile(join(stateDir, f), "utf-8"));
850
379
  if (data.active) {
851
380
  active.push(mode);
852
381
  }
853
382
  }
854
383
  catch (e) {
855
- process.stderr.write('[state-server] Failed to parse state file: ' + e + '\n');
384
+ process.stderr.write("[state-server] Failed to parse state file: " + e + "\n");
856
385
  }
857
386
  }
858
387
  }
859
- return { content: [{ type: 'text', text: JSON.stringify({ active_modes: active }) }] };
388
+ return {
389
+ content: [
390
+ { type: "text", text: JSON.stringify({ active_modes: active }) },
391
+ ],
392
+ };
860
393
  }
861
- case 'state_get_status': {
394
+ case "state_get_status": {
862
395
  const mode = args?.mode;
863
396
  const stateDirs = await getReadScopedStateDirs(cwd, explicitSessionId);
864
397
  const statuses = {};
@@ -868,481 +401,54 @@ export async function handleStateToolCall(request) {
868
401
  continue;
869
402
  const files = await readdir(stateDir);
870
403
  for (const f of files) {
871
- if (!f.endsWith('-state.json'))
404
+ if (!f.endsWith("-state.json"))
872
405
  continue;
873
- const m = f.replace('-state.json', '');
406
+ const m = f.replace("-state.json", "");
874
407
  if (mode && m !== mode)
875
408
  continue;
876
409
  if (seenModes.has(m))
877
410
  continue;
878
411
  seenModes.add(m);
879
412
  try {
880
- const data = JSON.parse(await readFile(join(stateDir, f), 'utf-8'));
881
- statuses[m] = { active: data.active, phase: data.current_phase, path: join(stateDir, f), data };
413
+ const data = JSON.parse(await readFile(join(stateDir, f), "utf-8"));
414
+ statuses[m] = {
415
+ active: data.active,
416
+ phase: data.current_phase,
417
+ path: join(stateDir, f),
418
+ data,
419
+ };
882
420
  }
883
421
  catch {
884
- statuses[m] = { error: 'malformed state file' };
422
+ statuses[m] = { error: "malformed state file" };
885
423
  }
886
424
  }
887
425
  }
888
- return { content: [{ type: 'text', text: JSON.stringify({ statuses }) }] };
889
- }
890
- case 'team_send_message': {
891
- const teamName = String(args.team_name || '').trim();
892
- const fromWorker = String(args.from_worker || '').trim();
893
- const toWorker = String(args.to_worker || '').trim();
894
- const body = String(args.body || '').trim();
895
- if (!fromWorker) {
896
- return {
897
- content: [{ type: 'text', text: JSON.stringify({
898
- error: 'from_worker is required. You must identify yourself. Check your worker name in your inbox file or AGENTS.md overlay.',
899
- hint: 'Your worker name was set when you were spawned (e.g., "worker-1", "worker-2", or "leader-fixed").'
900
- }) }],
901
- isError: true,
902
- };
903
- }
904
- if (!teamName || !toWorker || !body) {
905
- return {
906
- content: [{ type: 'text', text: JSON.stringify({ error: 'team_name, from_worker, to_worker, body are required' }) }],
907
- isError: true,
908
- };
909
- }
910
- const message = await sendDirectMessage(teamName, fromWorker, toWorker, body, cwd);
911
- return {
912
- content: [{ type: 'text', text: JSON.stringify({ ok: true, message }) }],
913
- };
914
- }
915
- case 'team_broadcast': {
916
- const teamName = String(args.team_name || '').trim();
917
- const fromWorker = String(args.from_worker || '').trim();
918
- const body = String(args.body || '').trim();
919
- if (!teamName || !fromWorker || !body) {
920
- return {
921
- content: [{ type: 'text', text: JSON.stringify({ error: 'team_name, from_worker, body are required' }) }],
922
- isError: true,
923
- };
924
- }
925
- const messages = await broadcastMessage(teamName, fromWorker, body, cwd);
926
426
  return {
927
- content: [{ type: 'text', text: JSON.stringify({ ok: true, count: messages.length, messages }) }],
427
+ content: [{ type: "text", text: JSON.stringify({ statuses }) }],
928
428
  };
929
429
  }
930
- case 'team_mailbox_list': {
931
- const teamName = String(args.team_name || '').trim();
932
- const worker = String(args.worker || '').trim();
933
- const includeDelivered = args.include_delivered !== false;
934
- if (!teamName || !worker) {
935
- return {
936
- content: [{ type: 'text', text: JSON.stringify({ error: 'team_name and worker are required' }) }],
937
- isError: true,
938
- };
939
- }
940
- const all = await listMailboxMessages(teamName, worker, cwd);
941
- const messages = includeDelivered ? all : all.filter((m) => !m.delivered_at);
942
- return {
943
- content: [{ type: 'text', text: JSON.stringify({ ok: true, worker, count: messages.length, messages }) }],
944
- };
945
- }
946
- case 'team_mailbox_mark_delivered': {
947
- const teamName = String(args.team_name || '').trim();
948
- const worker = String(args.worker || '').trim();
949
- const messageId = String(args.message_id || '').trim();
950
- if (!teamName || !worker || !messageId) {
951
- return {
952
- content: [{ type: 'text', text: JSON.stringify({ error: 'team_name, worker, message_id are required' }) }],
953
- isError: true,
954
- };
955
- }
956
- const updated = await markMessageDelivered(teamName, worker, messageId, cwd);
430
+ default:
957
431
  return {
958
- content: [{ type: 'text', text: JSON.stringify({ ok: true, updated, worker, message_id: messageId }) }],
432
+ content: [{ type: "text", text: `Unknown tool: ${name}` }],
433
+ isError: true,
959
434
  };
960
- }
961
- case 'team_mailbox_mark_notified': {
962
- const teamName = String(args.team_name || '').trim();
963
- const worker = String(args.worker || '').trim();
964
- const messageId = String(args.message_id || '').trim();
965
- if (!teamName || !worker || !messageId) {
966
- return { content: [{ type: 'text', text: JSON.stringify({ error: 'team_name, worker, message_id are required' }) }], isError: true };
967
- }
968
- const notified = await markMessageNotified(teamName, worker, messageId, cwd);
969
- return { content: [{ type: 'text', text: JSON.stringify({ ok: true, notified, worker, message_id: messageId }) }] };
970
- }
971
- case 'team_create_task': {
972
- const teamName = String(args.team_name || '').trim();
973
- const subject = String(args.subject || '').trim();
974
- const description = String(args.description || '').trim();
975
- if (!teamName || !subject || !description) {
976
- return { content: [{ type: 'text', text: JSON.stringify({ error: 'team_name, subject, description are required' }) }], isError: true };
977
- }
978
- const owner = args.owner;
979
- const blockedBy = args.blocked_by;
980
- const requiresCodeChange = args.requires_code_change;
981
- const task = await teamCreateTask(teamName, {
982
- subject, description, status: 'pending',
983
- owner: owner || undefined,
984
- blocked_by: blockedBy,
985
- requires_code_change: requiresCodeChange,
986
- }, cwd);
987
- return { content: [{ type: 'text', text: JSON.stringify({ ok: true, task }) }] };
988
- }
989
- case 'team_read_task': {
990
- const teamName = String(args.team_name || '').trim();
991
- const taskId = String(args.task_id || '').trim();
992
- if (!teamName || !taskId) {
993
- return { content: [{ type: 'text', text: JSON.stringify({ error: 'team_name and task_id are required' }) }], isError: true };
994
- }
995
- const task = await teamReadTask(teamName, taskId, cwd);
996
- if (!task)
997
- return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: 'task_not_found' }) }] };
998
- return { content: [{ type: 'text', text: JSON.stringify({ ok: true, task }) }] };
999
- }
1000
- case 'team_list_tasks': {
1001
- const teamName = String(args.team_name || '').trim();
1002
- if (!teamName) {
1003
- return { content: [{ type: 'text', text: JSON.stringify({ error: 'team_name is required' }) }], isError: true };
1004
- }
1005
- const tasks = await teamListTasks(teamName, cwd);
1006
- return { content: [{ type: 'text', text: JSON.stringify({ ok: true, count: tasks.length, tasks }) }] };
1007
- }
1008
- case 'team_update_task': {
1009
- const teamName = String(args.team_name || '').trim();
1010
- const taskId = String(args.task_id || '').trim();
1011
- if (!teamName || !taskId) {
1012
- return { content: [{ type: 'text', text: JSON.stringify({ error: 'team_name and task_id are required' }) }], isError: true };
1013
- }
1014
- const lifecycleFields = ['status', 'owner', 'result', 'error'];
1015
- const presentLifecycleFields = lifecycleFields.filter((f) => f in args);
1016
- if (presentLifecycleFields.length > 0) {
1017
- return {
1018
- content: [{
1019
- type: 'text',
1020
- text: JSON.stringify({
1021
- error: `team_update_task cannot mutate lifecycle fields: ${presentLifecycleFields.join(', ')}. Use team_claim_task + team_transition_task_status to change task status/owner/result/error.`,
1022
- }),
1023
- }],
1024
- isError: true,
1025
- };
1026
- }
1027
- const unexpectedFields = Object.keys(args).filter((field) => !TEAM_UPDATE_TASK_REQUEST_FIELDS.has(field));
1028
- if (unexpectedFields.length > 0) {
1029
- return {
1030
- content: [{
1031
- type: 'text',
1032
- text: JSON.stringify({
1033
- error: `team_update_task received unsupported fields: ${unexpectedFields.join(', ')}. Allowed mutable fields: subject, description, blocked_by, requires_code_change.`,
1034
- }),
1035
- }],
1036
- isError: true,
1037
- };
1038
- }
1039
- const updates = {};
1040
- if ('subject' in args) {
1041
- const subject = args.subject;
1042
- if (typeof subject !== 'string') {
1043
- return { content: [{ type: 'text', text: JSON.stringify({ error: 'subject must be a string when provided' }) }], isError: true };
1044
- }
1045
- updates.subject = subject.trim();
1046
- }
1047
- if ('description' in args) {
1048
- const description = args.description;
1049
- if (typeof description !== 'string') {
1050
- return { content: [{ type: 'text', text: JSON.stringify({ error: 'description must be a string when provided' }) }], isError: true };
1051
- }
1052
- updates.description = description.trim();
1053
- }
1054
- if ('requires_code_change' in args) {
1055
- const requiresCodeChange = args.requires_code_change;
1056
- if (typeof requiresCodeChange !== 'boolean') {
1057
- return { content: [{ type: 'text', text: JSON.stringify({ error: 'requires_code_change must be a boolean when provided' }) }], isError: true };
1058
- }
1059
- updates.requires_code_change = requiresCodeChange;
1060
- }
1061
- if ('blocked_by' in args) {
1062
- try {
1063
- updates.blocked_by = parseValidatedTaskIdArray(args.blocked_by, 'blocked_by');
1064
- }
1065
- catch (error) {
1066
- return { content: [{ type: 'text', text: JSON.stringify({ error: error.message }) }], isError: true };
1067
- }
1068
- }
1069
- const task = await teamUpdateTask(teamName, taskId, updates, cwd);
1070
- if (!task)
1071
- return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: 'task_not_found' }) }] };
1072
- return { content: [{ type: 'text', text: JSON.stringify({ ok: true, task }) }] };
1073
- }
1074
- case 'team_claim_task': {
1075
- const teamName = String(args.team_name || '').trim();
1076
- const taskId = String(args.task_id || '').trim();
1077
- const worker = String(args.worker || '').trim();
1078
- if (!teamName || !taskId || !worker) {
1079
- return { content: [{ type: 'text', text: JSON.stringify({ error: 'team_name, task_id, worker are required' }) }], isError: true };
1080
- }
1081
- const rawExpectedVersion = args.expected_version;
1082
- if (rawExpectedVersion !== undefined && (!isFiniteInteger(rawExpectedVersion) || rawExpectedVersion < 1)) {
1083
- return {
1084
- content: [{ type: 'text', text: JSON.stringify({ error: 'expected_version must be a positive integer when provided' }) }],
1085
- isError: true,
1086
- };
1087
- }
1088
- const expectedVersion = rawExpectedVersion;
1089
- const result = await teamClaimTask(teamName, taskId, worker, expectedVersion ?? null, cwd);
1090
- return { content: [{ type: 'text', text: JSON.stringify(result) }] };
1091
- }
1092
- case 'team_transition_task_status': {
1093
- const teamName = String(args.team_name || '').trim();
1094
- const taskId = String(args.task_id || '').trim();
1095
- const from = String(args.from || '').trim();
1096
- const to = String(args.to || '').trim();
1097
- const claimToken = String(args.claim_token || '').trim();
1098
- if (!teamName || !taskId || !from || !to || !claimToken) {
1099
- return { content: [{ type: 'text', text: JSON.stringify({ error: 'team_name, task_id, from, to, claim_token are required' }) }], isError: true };
1100
- }
1101
- const allowed = new Set(TEAM_TASK_STATUSES);
1102
- if (!allowed.has(from) || !allowed.has(to)) {
1103
- return { content: [{ type: 'text', text: JSON.stringify({ error: 'from and to must be valid task statuses' }) }], isError: true };
1104
- }
1105
- const result = await teamTransitionTaskStatus(teamName, taskId, from, to, claimToken, cwd);
1106
- return { content: [{ type: 'text', text: JSON.stringify(result) }] };
1107
- }
1108
- case 'team_release_task_claim': {
1109
- const teamName = String(args.team_name || '').trim();
1110
- const taskId = String(args.task_id || '').trim();
1111
- const claimToken = String(args.claim_token || '').trim();
1112
- const worker = String(args.worker || '').trim();
1113
- if (!teamName || !taskId || !claimToken || !worker) {
1114
- return { content: [{ type: 'text', text: JSON.stringify({ error: 'team_name, task_id, claim_token, worker are required' }) }], isError: true };
1115
- }
1116
- const result = await teamReleaseTaskClaim(teamName, taskId, claimToken, worker, cwd);
1117
- return { content: [{ type: 'text', text: JSON.stringify(result) }] };
1118
- }
1119
- case 'team_read_config': {
1120
- const teamName = String(args.team_name || '').trim();
1121
- if (!teamName) {
1122
- return { content: [{ type: 'text', text: JSON.stringify({ error: 'team_name is required' }) }], isError: true };
1123
- }
1124
- const config = await teamReadConfig(teamName, cwd);
1125
- if (!config)
1126
- return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: 'team_not_found' }) }] };
1127
- return { content: [{ type: 'text', text: JSON.stringify({ ok: true, config }) }] };
1128
- }
1129
- case 'team_read_manifest': {
1130
- const teamName = String(args.team_name || '').trim();
1131
- if (!teamName) {
1132
- return { content: [{ type: 'text', text: JSON.stringify({ error: 'team_name is required' }) }], isError: true };
1133
- }
1134
- const manifest = await teamReadManifest(teamName, cwd);
1135
- if (!manifest)
1136
- return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: 'manifest_not_found' }) }] };
1137
- return { content: [{ type: 'text', text: JSON.stringify({ ok: true, manifest }) }] };
1138
- }
1139
- case 'team_read_worker_status': {
1140
- const teamName = String(args.team_name || '').trim();
1141
- const worker = String(args.worker || '').trim();
1142
- if (!teamName || !worker) {
1143
- return { content: [{ type: 'text', text: JSON.stringify({ error: 'team_name and worker are required' }) }], isError: true };
1144
- }
1145
- const status = await teamReadWorkerStatus(teamName, worker, cwd);
1146
- return { content: [{ type: 'text', text: JSON.stringify({ ok: true, worker, status }) }] };
1147
- }
1148
- case 'team_read_worker_heartbeat': {
1149
- const teamName = String(args.team_name || '').trim();
1150
- const worker = String(args.worker || '').trim();
1151
- if (!teamName || !worker) {
1152
- return { content: [{ type: 'text', text: JSON.stringify({ error: 'team_name and worker are required' }) }], isError: true };
1153
- }
1154
- const heartbeat = await teamReadWorkerHeartbeat(teamName, worker, cwd);
1155
- return { content: [{ type: 'text', text: JSON.stringify({ ok: true, worker, heartbeat }) }] };
1156
- }
1157
- case 'team_update_worker_heartbeat': {
1158
- const teamName = String(args.team_name || '').trim();
1159
- const worker = String(args.worker || '').trim();
1160
- const pid = args.pid;
1161
- const turnCount = args.turn_count;
1162
- const alive = args.alive;
1163
- if (!teamName || !worker || typeof pid !== 'number' || typeof turnCount !== 'number' || typeof alive !== 'boolean') {
1164
- return { content: [{ type: 'text', text: JSON.stringify({ error: 'team_name, worker, pid, turn_count, alive are required' }) }], isError: true };
1165
- }
1166
- await teamUpdateWorkerHeartbeat(teamName, worker, { pid, turn_count: turnCount, alive, last_turn_at: new Date().toISOString() }, cwd);
1167
- return { content: [{ type: 'text', text: JSON.stringify({ ok: true, worker }) }] };
1168
- }
1169
- case 'team_write_worker_inbox': {
1170
- const teamName = String(args.team_name || '').trim();
1171
- const worker = String(args.worker || '').trim();
1172
- const content = String(args.content || '').trim();
1173
- if (!teamName || !worker || !content) {
1174
- return { content: [{ type: 'text', text: JSON.stringify({ error: 'team_name, worker, content are required' }) }], isError: true };
1175
- }
1176
- await teamWriteWorkerInbox(teamName, worker, content, cwd);
1177
- return { content: [{ type: 'text', text: JSON.stringify({ ok: true, worker }) }] };
1178
- }
1179
- case 'team_write_worker_identity': {
1180
- const teamName = String(args.team_name || '').trim();
1181
- const worker = String(args.worker || '').trim();
1182
- const index = args.index;
1183
- const role = String(args.role || '').trim();
1184
- if (!teamName || !worker || typeof index !== 'number' || !role) {
1185
- return { content: [{ type: 'text', text: JSON.stringify({ error: 'team_name, worker, index, role are required' }) }], isError: true };
1186
- }
1187
- const assignedTasks = args.assigned_tasks ?? [];
1188
- const pid = args.pid;
1189
- const paneId = args.pane_id;
1190
- const workingDir = args.working_dir;
1191
- const worktreePath = args.worktree_path;
1192
- const worktreeBranch = args.worktree_branch;
1193
- const worktreeDetached = args.worktree_detached;
1194
- const teamStateRoot = args.team_state_root;
1195
- await teamWriteWorkerIdentity(teamName, worker, {
1196
- name: worker,
1197
- index,
1198
- role,
1199
- assigned_tasks: assignedTasks,
1200
- pid,
1201
- pane_id: paneId,
1202
- working_dir: workingDir,
1203
- worktree_path: worktreePath,
1204
- worktree_branch: worktreeBranch,
1205
- worktree_detached: worktreeDetached,
1206
- team_state_root: teamStateRoot,
1207
- }, cwd);
1208
- return { content: [{ type: 'text', text: JSON.stringify({ ok: true, worker }) }] };
1209
- }
1210
- case 'team_append_event': {
1211
- const teamName = String(args.team_name || '').trim();
1212
- const eventType = String(args.type || '').trim();
1213
- const worker = String(args.worker || '').trim();
1214
- if (!teamName || !eventType || !worker) {
1215
- return { content: [{ type: 'text', text: JSON.stringify({ error: 'team_name, type, worker are required' }) }], isError: true };
1216
- }
1217
- if (!TEAM_EVENT_TYPES.includes(eventType)) {
1218
- return {
1219
- content: [{
1220
- type: 'text',
1221
- text: JSON.stringify({ error: `type must be one of: ${TEAM_EVENT_TYPES.join(', ')}` }),
1222
- }],
1223
- isError: true,
1224
- };
1225
- }
1226
- const event = await teamAppendEvent(teamName, {
1227
- type: eventType,
1228
- worker,
1229
- task_id: args.task_id,
1230
- message_id: args.message_id ?? null,
1231
- reason: args.reason,
1232
- }, cwd);
1233
- return { content: [{ type: 'text', text: JSON.stringify({ ok: true, event }) }] };
1234
- }
1235
- case 'team_get_summary': {
1236
- const teamName = String(args.team_name || '').trim();
1237
- if (!teamName) {
1238
- return { content: [{ type: 'text', text: JSON.stringify({ error: 'team_name is required' }) }], isError: true };
1239
- }
1240
- const summary = await teamGetSummary(teamName, cwd);
1241
- if (!summary)
1242
- return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: 'team_not_found' }) }] };
1243
- return { content: [{ type: 'text', text: JSON.stringify({ ok: true, summary }) }] };
1244
- }
1245
- case 'team_cleanup': {
1246
- const teamName = String(args.team_name || '').trim();
1247
- if (!teamName) {
1248
- return { content: [{ type: 'text', text: JSON.stringify({ error: 'team_name is required' }) }], isError: true };
1249
- }
1250
- await teamCleanup(teamName, cwd);
1251
- return { content: [{ type: 'text', text: JSON.stringify({ ok: true, team_name: teamName }) }] };
1252
- }
1253
- case 'team_write_shutdown_request': {
1254
- const teamName = String(args.team_name || '').trim();
1255
- const worker = String(args.worker || '').trim();
1256
- const requestedBy = String(args.requested_by || '').trim();
1257
- if (!teamName || !worker || !requestedBy) {
1258
- return { content: [{ type: 'text', text: JSON.stringify({ error: 'team_name, worker, requested_by are required' }) }], isError: true };
1259
- }
1260
- await teamWriteShutdownRequest(teamName, worker, requestedBy, cwd);
1261
- return { content: [{ type: 'text', text: JSON.stringify({ ok: true, worker }) }] };
1262
- }
1263
- case 'team_read_shutdown_ack': {
1264
- const teamName = String(args.team_name || '').trim();
1265
- const worker = String(args.worker || '').trim();
1266
- if (!teamName || !worker) {
1267
- return { content: [{ type: 'text', text: JSON.stringify({ error: 'team_name and worker are required' }) }], isError: true };
1268
- }
1269
- const minUpdatedAt = args.min_updated_at;
1270
- const ack = await teamReadShutdownAck(teamName, worker, cwd, minUpdatedAt);
1271
- return { content: [{ type: 'text', text: JSON.stringify({ ok: true, worker, ack }) }] };
1272
- }
1273
- case 'team_read_monitor_snapshot': {
1274
- const teamName = String(args.team_name || '').trim();
1275
- if (!teamName) {
1276
- return { content: [{ type: 'text', text: JSON.stringify({ error: 'team_name is required' }) }], isError: true };
1277
- }
1278
- const snapshot = await teamReadMonitorSnapshot(teamName, cwd);
1279
- return { content: [{ type: 'text', text: JSON.stringify({ ok: true, snapshot }) }] };
1280
- }
1281
- case 'team_write_monitor_snapshot': {
1282
- const teamName = String(args.team_name || '').trim();
1283
- const snapshot = args.snapshot;
1284
- if (!teamName || !snapshot) {
1285
- return { content: [{ type: 'text', text: JSON.stringify({ error: 'team_name and snapshot are required' }) }], isError: true };
1286
- }
1287
- await teamWriteMonitorSnapshot(teamName, snapshot, cwd);
1288
- return { content: [{ type: 'text', text: JSON.stringify({ ok: true }) }] };
1289
- }
1290
- case 'team_read_task_approval': {
1291
- const teamName = String(args.team_name || '').trim();
1292
- const taskId = String(args.task_id || '').trim();
1293
- if (!teamName || !taskId) {
1294
- return { content: [{ type: 'text', text: JSON.stringify({ error: 'team_name and task_id are required' }) }], isError: true };
1295
- }
1296
- const approval = await teamReadTaskApproval(teamName, taskId, cwd);
1297
- return { content: [{ type: 'text', text: JSON.stringify({ ok: true, approval }) }] };
1298
- }
1299
- case 'team_write_task_approval': {
1300
- const teamName = String(args.team_name || '').trim();
1301
- const taskId = String(args.task_id || '').trim();
1302
- const status = String(args.status || '').trim();
1303
- const reviewer = String(args.reviewer || '').trim();
1304
- const decisionReason = String(args.decision_reason || '').trim();
1305
- if (!teamName || !taskId || !status || !reviewer || !decisionReason) {
1306
- return { content: [{ type: 'text', text: JSON.stringify({ error: 'team_name, task_id, status, reviewer, decision_reason are required' }) }], isError: true };
1307
- }
1308
- if (!TEAM_TASK_APPROVAL_STATUSES.includes(status)) {
1309
- return {
1310
- content: [{
1311
- type: 'text',
1312
- text: JSON.stringify({ error: `status must be one of: ${TEAM_TASK_APPROVAL_STATUSES.join(', ')}` }),
1313
- }],
1314
- isError: true,
1315
- };
1316
- }
1317
- const rawRequired = args.required;
1318
- if (rawRequired !== undefined && typeof rawRequired !== 'boolean') {
1319
- return { content: [{ type: 'text', text: JSON.stringify({ error: 'required must be a boolean when provided' }) }], isError: true };
1320
- }
1321
- const required = rawRequired !== false;
1322
- await teamWriteTaskApproval(teamName, {
1323
- task_id: taskId,
1324
- required,
1325
- status: status,
1326
- reviewer,
1327
- decision_reason: decisionReason,
1328
- decided_at: new Date().toISOString(),
1329
- }, cwd);
1330
- return { content: [{ type: 'text', text: JSON.stringify({ ok: true, task_id: taskId, status }) }] };
1331
- }
1332
- default:
1333
- return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };
1334
435
  }
1335
436
  }
1336
437
  catch (error) {
1337
438
  return {
1338
- content: [{ type: 'text', text: JSON.stringify({ error: error.message }) }],
439
+ content: [
440
+ {
441
+ type: "text",
442
+ text: JSON.stringify({ error: error.message }),
443
+ },
444
+ ],
1339
445
  isError: true,
1340
446
  };
1341
447
  }
1342
448
  }
1343
449
  server.setRequestHandler(CallToolRequestSchema, handleStateToolCall);
1344
450
  // Start server
1345
- if (shouldAutoStartMcpServer('state')) {
451
+ if (shouldAutoStartMcpServer("state")) {
1346
452
  const transport = new StdioServerTransport();
1347
453
  server.connect(transport).catch(console.error);
1348
454
  }