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.
- package/README.de.md +7 -2
- package/README.es.md +7 -2
- package/README.fr.md +7 -2
- package/README.it.md +7 -2
- package/README.ja.md +7 -2
- package/README.ko.md +7 -2
- package/README.md +61 -11
- package/README.pt.md +7 -2
- package/README.ru.md +7 -2
- package/README.tr.md +7 -2
- package/README.vi.md +7 -2
- package/README.zh-TW.md +366 -0
- package/README.zh.md +7 -2
- package/dist/cli/__tests__/index.test.js +70 -4
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/setup-skills-overwrite.test.js +100 -1
- package/dist/cli/__tests__/setup-skills-overwrite.test.js.map +1 -1
- package/dist/cli/__tests__/team.test.js +219 -1
- package/dist/cli/__tests__/team.test.js.map +1 -1
- package/dist/cli/catalog-contract.d.ts.map +1 -1
- package/dist/cli/catalog-contract.js +8 -2
- package/dist/cli/catalog-contract.js.map +1 -1
- package/dist/cli/index.d.ts +7 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +58 -12
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +50 -17
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/team.d.ts.map +1 -1
- package/dist/cli/team.js +257 -0
- package/dist/cli/team.js.map +1 -1
- package/dist/config/__tests__/models.test.js +11 -11
- package/dist/config/__tests__/models.test.js.map +1 -1
- package/dist/config/models.d.ts +4 -3
- package/dist/config/models.d.ts.map +1 -1
- package/dist/config/models.js +6 -5
- package/dist/config/models.js.map +1 -1
- package/dist/hooks/__tests__/keyword-detector.test.js +46 -3
- package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js +23 -7
- package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +176 -1
- package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +61 -1
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-worker-idle.test.js +17 -7
- package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
- package/dist/hooks/__tests__/openclaw-setup-contract.test.js +26 -16
- package/dist/hooks/__tests__/openclaw-setup-contract.test.js.map +1 -1
- package/dist/hooks/keyword-detector.d.ts +2 -1
- package/dist/hooks/keyword-detector.d.ts.map +1 -1
- package/dist/hooks/keyword-detector.js +41 -4
- package/dist/hooks/keyword-detector.js.map +1 -1
- package/dist/hooks/keyword-registry.d.ts.map +1 -1
- package/dist/hooks/keyword-registry.js +5 -0
- package/dist/hooks/keyword-registry.js.map +1 -1
- package/dist/mcp/__tests__/path-traversal.test.js +9 -227
- package/dist/mcp/__tests__/path-traversal.test.js.map +1 -1
- package/dist/mcp/__tests__/state-server-schema.test.js +16 -20
- package/dist/mcp/__tests__/state-server-schema.test.js.map +1 -1
- package/dist/mcp/__tests__/state-server-team-tools.test.js +30 -487
- package/dist/mcp/__tests__/state-server-team-tools.test.js.map +1 -1
- package/dist/mcp/state-server.d.ts +179 -0
- package/dist/mcp/state-server.d.ts.map +1 -1
- package/dist/mcp/state-server.js +217 -1111
- package/dist/mcp/state-server.js.map +1 -1
- package/dist/mcp/team-server.d.ts.map +1 -1
- package/dist/mcp/team-server.js +28 -7
- package/dist/mcp/team-server.js.map +1 -1
- package/dist/notifications/__tests__/dispatch-cooldown.test.d.ts +5 -0
- package/dist/notifications/__tests__/dispatch-cooldown.test.d.ts.map +1 -0
- package/dist/notifications/__tests__/dispatch-cooldown.test.js +100 -0
- package/dist/notifications/__tests__/dispatch-cooldown.test.js.map +1 -0
- package/dist/notifications/__tests__/temp-mode.test.d.ts +2 -0
- package/dist/notifications/__tests__/temp-mode.test.d.ts.map +1 -0
- package/dist/notifications/__tests__/temp-mode.test.js +172 -0
- package/dist/notifications/__tests__/temp-mode.test.js.map +1 -0
- package/dist/notifications/config.d.ts.map +1 -1
- package/dist/notifications/config.js +59 -6
- package/dist/notifications/config.js.map +1 -1
- package/dist/notifications/dispatch-cooldown.d.ts +36 -0
- package/dist/notifications/dispatch-cooldown.d.ts.map +1 -0
- package/dist/notifications/dispatch-cooldown.js +109 -0
- package/dist/notifications/dispatch-cooldown.js.map +1 -0
- package/dist/notifications/index.d.ts +5 -0
- package/dist/notifications/index.d.ts.map +1 -1
- package/dist/notifications/index.js +39 -8
- package/dist/notifications/index.js.map +1 -1
- package/dist/notifications/temp-contract.d.ts +22 -0
- package/dist/notifications/temp-contract.d.ts.map +1 -0
- package/dist/notifications/temp-contract.js +147 -0
- package/dist/notifications/temp-contract.js.map +1 -0
- package/dist/notifications/types.d.ts +18 -0
- package/dist/notifications/types.d.ts.map +1 -1
- package/dist/openclaw/__tests__/config.test.js +81 -0
- package/dist/openclaw/__tests__/config.test.js.map +1 -1
- package/dist/openclaw/__tests__/dispatcher.test.js +50 -7
- package/dist/openclaw/__tests__/dispatcher.test.js.map +1 -1
- package/dist/openclaw/config.d.ts +4 -0
- package/dist/openclaw/config.d.ts.map +1 -1
- package/dist/openclaw/config.js +110 -16
- package/dist/openclaw/config.js.map +1 -1
- package/dist/openclaw/dispatcher.d.ts +10 -4
- package/dist/openclaw/dispatcher.d.ts.map +1 -1
- package/dist/openclaw/dispatcher.js +40 -10
- package/dist/openclaw/dispatcher.js.map +1 -1
- package/dist/openclaw/types.d.ts +5 -1
- package/dist/openclaw/types.d.ts.map +1 -1
- package/dist/team/__tests__/api-interop.test.d.ts +2 -0
- package/dist/team/__tests__/api-interop.test.d.ts.map +1 -0
- package/dist/team/__tests__/api-interop.test.js +1052 -0
- package/dist/team/__tests__/api-interop.test.js.map +1 -0
- package/dist/team/__tests__/mcp-comm.test.js +30 -0
- package/dist/team/__tests__/mcp-comm.test.js.map +1 -1
- package/dist/team/__tests__/runtime-cli.test.js +6 -0
- package/dist/team/__tests__/runtime-cli.test.js.map +1 -1
- package/dist/team/__tests__/runtime.test.js +52 -22
- package/dist/team/__tests__/runtime.test.js.map +1 -1
- package/dist/team/__tests__/tmux-claude-workers-demo.test.d.ts +2 -0
- package/dist/team/__tests__/tmux-claude-workers-demo.test.d.ts.map +1 -0
- package/dist/team/__tests__/tmux-claude-workers-demo.test.js +190 -0
- package/dist/team/__tests__/tmux-claude-workers-demo.test.js.map +1 -0
- package/dist/team/__tests__/tmux-session.test.js +45 -2
- package/dist/team/__tests__/tmux-session.test.js.map +1 -1
- package/dist/team/__tests__/worker-bootstrap.test.js +20 -12
- package/dist/team/__tests__/worker-bootstrap.test.js.map +1 -1
- package/dist/team/api-interop.d.ts +19 -0
- package/dist/team/api-interop.d.ts.map +1 -0
- package/dist/team/api-interop.js +578 -0
- package/dist/team/api-interop.js.map +1 -0
- package/dist/team/mcp-comm.d.ts.map +1 -1
- package/dist/team/mcp-comm.js +26 -0
- package/dist/team/mcp-comm.js.map +1 -1
- package/dist/team/runtime-cli.d.ts +3 -0
- package/dist/team/runtime-cli.d.ts.map +1 -1
- package/dist/team/runtime-cli.js +24 -2
- package/dist/team/runtime-cli.js.map +1 -1
- package/dist/team/runtime.d.ts.map +1 -1
- package/dist/team/runtime.js +67 -11
- package/dist/team/runtime.js.map +1 -1
- package/dist/team/scaling.js.map +1 -1
- package/dist/team/state/types.d.ts +1 -1
- package/dist/team/state/types.d.ts.map +1 -1
- package/dist/team/state.d.ts +1 -1
- package/dist/team/state.d.ts.map +1 -1
- package/dist/team/tmux-session.d.ts +1 -1
- package/dist/team/tmux-session.d.ts.map +1 -1
- package/dist/team/tmux-session.js +17 -5
- package/dist/team/tmux-session.js.map +1 -1
- package/dist/team/worker-bootstrap.d.ts.map +1 -1
- package/dist/team/worker-bootstrap.js +48 -19
- package/dist/team/worker-bootstrap.js.map +1 -1
- package/package.json +1 -1
- package/scripts/demo-claude-workers.sh +241 -0
- package/scripts/demo-team-e2e.sh +179 -0
- package/scripts/notify-hook/team-dispatch.js +186 -12
- package/scripts/notify-hook/team-leader-nudge.js +42 -2
- package/scripts/notify-hook/team-worker.js +63 -4
- package/skills/configure-notifications/SKILL.md +193 -185
- package/skills/omx-setup/SKILL.md +1 -1
- package/skills/team/SKILL.md +47 -5
- package/skills/worker/SKILL.md +40 -10
- package/templates/AGENTS.md +7 -3
- package/templates/catalog-manifest.json +26 -3
- package/skills/configure-discord/SKILL.md +0 -256
- package/skills/configure-openclaw/SKILL.md +0 -264
- package/skills/configure-slack/SKILL.md +0 -226
- package/skills/configure-telegram/SKILL.md +0 -232
package/dist/mcp/state-server.js
CHANGED
|
@@ -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
|
|
7
|
-
import { StdioServerTransport } from
|
|
8
|
-
import { CallToolRequestSchema, ListToolsRequestSchema, } from
|
|
9
|
-
import { readFile, writeFile, readdir, mkdir, unlink, rename } from
|
|
10
|
-
import { existsSync
|
|
11
|
-
import {
|
|
12
|
-
import { getAllScopedStatePaths, getReadScopedStateDirs, getReadScopedStatePaths, resolveStateScope, getStateDir, getStatePath, resolveWorkingDirectoryForState, validateSessionId, } from
|
|
13
|
-
import { withModeRuntimeContext } from
|
|
14
|
-
import { RALPH_PHASES, validateAndNormalizeRalphState } from
|
|
15
|
-
import { ensureCanonicalRalphArtifacts } from
|
|
16
|
-
import { shouldAutoStartMcpServer } from
|
|
17
|
-
import {
|
|
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
|
-
|
|
21
|
-
|
|
19
|
+
"autopilot",
|
|
20
|
+
"team",
|
|
21
|
+
"ralph",
|
|
22
|
+
"ultrawork",
|
|
23
|
+
"ultraqa",
|
|
24
|
+
"ralplan",
|
|
22
25
|
];
|
|
23
26
|
const STATE_TOOL_NAMES = new Set([
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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,
|
|
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
|
-
|
|
94
|
-
|
|
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:
|
|
496
|
-
description:
|
|
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:
|
|
72
|
+
type: "object",
|
|
499
73
|
properties: {
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
workingDirectory: {
|
|
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: [
|
|
88
|
+
required: ["mode"],
|
|
508
89
|
},
|
|
509
90
|
},
|
|
510
91
|
{
|
|
511
|
-
name:
|
|
512
|
-
description:
|
|
92
|
+
name: "state_write",
|
|
93
|
+
description: "Write/update state for a specific mode. Creates directories if needed.",
|
|
513
94
|
inputSchema: {
|
|
514
|
-
type:
|
|
95
|
+
type: "object",
|
|
515
96
|
properties: {
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
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: [
|
|
113
|
+
required: ["mode"],
|
|
522
114
|
},
|
|
523
115
|
},
|
|
524
116
|
{
|
|
525
|
-
name:
|
|
526
|
-
description:
|
|
117
|
+
name: "state_clear",
|
|
118
|
+
description: "Clear/delete state for a specific mode.",
|
|
527
119
|
inputSchema: {
|
|
528
|
-
type:
|
|
120
|
+
type: "object",
|
|
529
121
|
properties: {
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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: [
|
|
133
|
+
required: ["mode"],
|
|
545
134
|
},
|
|
546
135
|
},
|
|
547
136
|
{
|
|
548
|
-
name:
|
|
549
|
-
description:
|
|
137
|
+
name: "state_list_active",
|
|
138
|
+
description: "List all currently active modes.",
|
|
550
139
|
inputSchema: {
|
|
551
|
-
type:
|
|
140
|
+
type: "object",
|
|
552
141
|
properties: {
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
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:
|
|
566
|
-
description:
|
|
151
|
+
name: "state_get_status",
|
|
152
|
+
description: "Get detailed status for a specific mode or all modes.",
|
|
567
153
|
inputSchema: {
|
|
568
|
-
type:
|
|
154
|
+
type: "object",
|
|
569
155
|
properties: {
|
|
570
|
-
|
|
571
|
-
workingDirectory: { type:
|
|
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
|
-
|
|
578
|
-
|
|
579
|
-
|
|
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: [
|
|
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: [
|
|
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(
|
|
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
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
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
|
|
234
|
+
case "state_read": {
|
|
741
235
|
const mode = args.mode;
|
|
742
236
|
if (!SUPPORTED_MODES.includes(mode)) {
|
|
743
237
|
return {
|
|
744
|
-
content: [
|
|
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 {
|
|
252
|
+
return {
|
|
253
|
+
content: [
|
|
254
|
+
{ type: "text", text: JSON.stringify({ exists: false, mode }) },
|
|
255
|
+
],
|
|
256
|
+
};
|
|
752
257
|
}
|
|
753
|
-
const data = await readFile(path,
|
|
754
|
-
return { content: [{ type:
|
|
258
|
+
const data = await readFile(path, "utf-8");
|
|
259
|
+
return { content: [{ type: "text", text: data }] };
|
|
755
260
|
}
|
|
756
|
-
case
|
|
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,
|
|
270
|
+
existing = JSON.parse(await readFile(path, "utf-8"));
|
|
766
271
|
}
|
|
767
272
|
catch (e) {
|
|
768
|
-
process.stderr.write(
|
|
273
|
+
process.stderr.write("[state-server] Failed to parse state file: " + e + "\n");
|
|
769
274
|
}
|
|
770
275
|
}
|
|
771
|
-
const mergedRaw = {
|
|
772
|
-
|
|
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 =
|
|
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 ===
|
|
780
|
-
|
|
781
|
-
|
|
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
|
-
|
|
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 {
|
|
312
|
+
return {
|
|
313
|
+
content: [
|
|
314
|
+
{
|
|
315
|
+
type: "text",
|
|
316
|
+
text: JSON.stringify({ success: true, mode, path }),
|
|
317
|
+
},
|
|
318
|
+
],
|
|
319
|
+
};
|
|
800
320
|
}
|
|
801
|
-
case
|
|
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 {
|
|
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
|
-
|
|
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:
|
|
356
|
+
warning: "all_sessions clears global and session-scoped state files",
|
|
829
357
|
}),
|
|
830
|
-
}
|
|
358
|
+
},
|
|
359
|
+
],
|
|
831
360
|
};
|
|
832
361
|
}
|
|
833
|
-
case
|
|
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(
|
|
371
|
+
if (!f.endsWith("-state.json"))
|
|
843
372
|
continue;
|
|
844
|
-
const mode = f.replace(
|
|
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),
|
|
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(
|
|
384
|
+
process.stderr.write("[state-server] Failed to parse state file: " + e + "\n");
|
|
856
385
|
}
|
|
857
386
|
}
|
|
858
387
|
}
|
|
859
|
-
return {
|
|
388
|
+
return {
|
|
389
|
+
content: [
|
|
390
|
+
{ type: "text", text: JSON.stringify({ active_modes: active }) },
|
|
391
|
+
],
|
|
392
|
+
};
|
|
860
393
|
}
|
|
861
|
-
case
|
|
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(
|
|
404
|
+
if (!f.endsWith("-state.json"))
|
|
872
405
|
continue;
|
|
873
|
-
const m = f.replace(
|
|
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),
|
|
881
|
-
statuses[m] = {
|
|
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:
|
|
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:
|
|
427
|
+
content: [{ type: "text", text: JSON.stringify({ statuses }) }],
|
|
928
428
|
};
|
|
929
429
|
}
|
|
930
|
-
|
|
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:
|
|
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: [
|
|
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(
|
|
451
|
+
if (shouldAutoStartMcpServer("state")) {
|
|
1346
452
|
const transport = new StdioServerTransport();
|
|
1347
453
|
server.connect(transport).catch(console.error);
|
|
1348
454
|
}
|