triflux 3.2.0-dev.1 → 3.2.0-dev.10
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.ko.md +26 -18
- package/README.md +26 -18
- package/bin/triflux.mjs +1614 -1084
- package/hooks/hooks.json +12 -0
- package/hooks/keyword-rules.json +354 -0
- package/hub/bridge.mjs +371 -193
- package/hub/hitl.mjs +45 -31
- package/hub/pipe.mjs +457 -0
- package/hub/router.mjs +422 -161
- package/hub/server.mjs +429 -344
- package/hub/store.mjs +388 -314
- package/hub/team/cli-team-common.mjs +348 -0
- package/hub/team/cli-team-control.mjs +393 -0
- package/hub/team/cli-team-start.mjs +516 -0
- package/hub/team/cli-team-status.mjs +269 -0
- package/hub/team/cli.mjs +99 -368
- package/hub/team/dashboard.mjs +165 -64
- package/hub/team/native-supervisor.mjs +300 -0
- package/hub/team/native.mjs +62 -0
- package/hub/team/nativeProxy.mjs +534 -0
- package/hub/team/orchestrator.mjs +99 -35
- package/hub/team/pane.mjs +138 -101
- package/hub/team/psmux.mjs +297 -0
- package/hub/team/session.mjs +608 -186
- package/hub/team/shared.mjs +13 -0
- package/hub/team/staleState.mjs +299 -0
- package/hub/tools.mjs +140 -53
- package/hub/workers/claude-worker.mjs +446 -0
- package/hub/workers/codex-mcp.mjs +414 -0
- package/hub/workers/factory.mjs +18 -0
- package/hub/workers/gemini-worker.mjs +349 -0
- package/hub/workers/interface.mjs +41 -0
- package/hud/hud-qos-status.mjs +1789 -1732
- package/package.json +6 -2
- package/scripts/__tests__/keyword-detector.test.mjs +234 -0
- package/scripts/hub-ensure.mjs +83 -0
- package/scripts/keyword-detector.mjs +272 -0
- package/scripts/keyword-rules-expander.mjs +521 -0
- package/scripts/lib/keyword-rules.mjs +168 -0
- package/scripts/psmux-steering-prototype.sh +368 -0
- package/scripts/run.cjs +62 -0
- package/scripts/setup.mjs +189 -7
- package/scripts/test-tfx-route-no-claude-native.mjs +49 -0
- package/scripts/tfx-route-worker.mjs +161 -0
- package/scripts/tfx-route.sh +943 -508
- package/skills/tfx-auto/SKILL.md +90 -564
- package/skills/tfx-auto-codex/SKILL.md +77 -0
- package/skills/tfx-codex/SKILL.md +1 -4
- package/skills/tfx-doctor/SKILL.md +1 -0
- package/skills/tfx-gemini/SKILL.md +1 -4
- package/skills/tfx-multi/SKILL.md +296 -0
- package/skills/tfx-setup/SKILL.md +1 -4
- package/skills/tfx-team/SKILL.md +0 -172
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// hub/team/shared.mjs — 팀 모듈 공유 유틸리티
|
|
2
|
+
// cli.mjs, dashboard.mjs 등에서 중복되던 상수/함수를 통합
|
|
3
|
+
|
|
4
|
+
// ── ANSI 색상 상수 ──
|
|
5
|
+
export const AMBER = "\x1b[38;5;214m";
|
|
6
|
+
export const GREEN = "\x1b[38;5;82m";
|
|
7
|
+
export const RED = "\x1b[38;5;196m";
|
|
8
|
+
export const GRAY = "\x1b[38;5;245m";
|
|
9
|
+
export const DIM = "\x1b[2m";
|
|
10
|
+
export const BOLD = "\x1b[1m";
|
|
11
|
+
export const RESET = "\x1b[0m";
|
|
12
|
+
export const WHITE = "\x1b[97m";
|
|
13
|
+
export const YELLOW = "\x1b[33m";
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
// hub/team/staleState.mjs
|
|
2
|
+
// .omc/state 아래에 남은 stale team 상태를 탐지/정리한다.
|
|
3
|
+
|
|
4
|
+
import { existsSync, readFileSync, readdirSync, rmSync, statSync, unlinkSync } from "node:fs";
|
|
5
|
+
import { execFileSync } from "node:child_process";
|
|
6
|
+
import { dirname, join, resolve } from "node:path";
|
|
7
|
+
|
|
8
|
+
export const TEAM_STATE_FILE_NAME = "team-state.json";
|
|
9
|
+
export const STALE_TEAM_MAX_AGE_MS = 60 * 60 * 1000;
|
|
10
|
+
|
|
11
|
+
function safeStat(path) {
|
|
12
|
+
try {
|
|
13
|
+
return statSync(path);
|
|
14
|
+
} catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function parseStartedAtMs(value) {
|
|
20
|
+
if (typeof value !== "string" || !value.trim()) return null;
|
|
21
|
+
const parsed = Date.parse(value);
|
|
22
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function findPidCandidates(state) {
|
|
26
|
+
const pidSet = new Set();
|
|
27
|
+
const pushPid = (value) => {
|
|
28
|
+
const pid = Number(value);
|
|
29
|
+
if (Number.isInteger(pid) && pid > 0) pidSet.add(pid);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
pushPid(state?.pid);
|
|
33
|
+
pushPid(state?.processId);
|
|
34
|
+
pushPid(state?.process_id);
|
|
35
|
+
pushPid(state?.leadPid);
|
|
36
|
+
pushPid(state?.lead_pid);
|
|
37
|
+
pushPid(state?.native?.supervisorPid);
|
|
38
|
+
pushPid(state?.native?.supervisor_pid);
|
|
39
|
+
|
|
40
|
+
return Array.from(pidSet);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function findSessionNames(state) {
|
|
44
|
+
const sessionNameSet = new Set();
|
|
45
|
+
const pushName = (value) => {
|
|
46
|
+
if (typeof value !== "string") return;
|
|
47
|
+
const trimmed = value.trim();
|
|
48
|
+
if (trimmed) sessionNameSet.add(trimmed);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
pushName(state?.sessionName);
|
|
52
|
+
pushName(state?.session_name);
|
|
53
|
+
pushName(state?.native?.teamName);
|
|
54
|
+
pushName(state?.native?.team_name);
|
|
55
|
+
|
|
56
|
+
return Array.from(sessionNameSet);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function findProcessTokens(state, sessionId) {
|
|
60
|
+
const tokenSet = new Set();
|
|
61
|
+
const pushToken = (value) => {
|
|
62
|
+
if (typeof value !== "string") return;
|
|
63
|
+
const trimmed = value.trim();
|
|
64
|
+
if (trimmed.length >= 6) tokenSet.add(trimmed.toLowerCase());
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
pushToken(sessionId);
|
|
68
|
+
pushToken(state?.session_id);
|
|
69
|
+
pushToken(state?.sessionId);
|
|
70
|
+
pushToken(state?.teamName);
|
|
71
|
+
pushToken(state?.team_name);
|
|
72
|
+
pushToken(state?.name);
|
|
73
|
+
pushToken(state?.native?.teamName);
|
|
74
|
+
pushToken(state?.native?.team_name);
|
|
75
|
+
|
|
76
|
+
return Array.from(tokenSet);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function isPidAlive(pid) {
|
|
80
|
+
try {
|
|
81
|
+
process.kill(pid, 0);
|
|
82
|
+
return true;
|
|
83
|
+
} catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function normalizeProcessEntries(processEntries = []) {
|
|
89
|
+
if (!Array.isArray(processEntries)) return [];
|
|
90
|
+
|
|
91
|
+
return processEntries.map((entry) => ({
|
|
92
|
+
pid: Number(entry?.pid ?? entry?.ProcessId ?? 0),
|
|
93
|
+
command: String(entry?.command ?? entry?.CommandLine ?? entry?.Name ?? "").toLowerCase(),
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function readProcessEntries() {
|
|
98
|
+
try {
|
|
99
|
+
if (process.platform === "win32") {
|
|
100
|
+
const raw = execFileSync(
|
|
101
|
+
"powershell",
|
|
102
|
+
[
|
|
103
|
+
"-NoProfile",
|
|
104
|
+
"-Command",
|
|
105
|
+
"$ErrorActionPreference='SilentlyContinue'; Get-CimInstance Win32_Process | Select-Object ProcessId,Name,CommandLine | ConvertTo-Json -Compress",
|
|
106
|
+
],
|
|
107
|
+
{
|
|
108
|
+
encoding: "utf8",
|
|
109
|
+
timeout: 10000,
|
|
110
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
111
|
+
windowsHide: true,
|
|
112
|
+
},
|
|
113
|
+
).trim();
|
|
114
|
+
|
|
115
|
+
if (!raw) return [];
|
|
116
|
+
const parsed = JSON.parse(raw);
|
|
117
|
+
return normalizeProcessEntries(Array.isArray(parsed) ? parsed : [parsed]);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const raw = execFileSync("ps", ["-ax", "-o", "pid=,command="], {
|
|
121
|
+
encoding: "utf8",
|
|
122
|
+
timeout: 10000,
|
|
123
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
124
|
+
}).trim();
|
|
125
|
+
|
|
126
|
+
if (!raw) return [];
|
|
127
|
+
return raw
|
|
128
|
+
.split(/\r?\n/)
|
|
129
|
+
.map((line) => line.trim())
|
|
130
|
+
.filter(Boolean)
|
|
131
|
+
.map((line) => {
|
|
132
|
+
const match = /^(\d+)\s+(.*)$/.exec(line);
|
|
133
|
+
return {
|
|
134
|
+
pid: Number(match?.[1] || 0),
|
|
135
|
+
command: String(match?.[2] || "").toLowerCase(),
|
|
136
|
+
};
|
|
137
|
+
});
|
|
138
|
+
} catch {
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function resolveLiveness(state, sessionId, liveSessionNames, processEntries) {
|
|
144
|
+
const pidCandidates = findPidCandidates(state);
|
|
145
|
+
for (const pid of pidCandidates) {
|
|
146
|
+
if (isPidAlive(pid)) {
|
|
147
|
+
return { active: true, reason: `pid:${pid}` };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const sessionNames = findSessionNames(state);
|
|
152
|
+
for (const sessionName of sessionNames) {
|
|
153
|
+
if (liveSessionNames.has(sessionName)) {
|
|
154
|
+
return { active: true, reason: `session:${sessionName}` };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const processTokens = findProcessTokens(state, sessionId);
|
|
159
|
+
if (processTokens.length > 0) {
|
|
160
|
+
const matched = processEntries.find((entry) => (
|
|
161
|
+
entry.pid > 0 && processTokens.some((token) => entry.command.includes(token))
|
|
162
|
+
));
|
|
163
|
+
if (matched) {
|
|
164
|
+
return { active: true, reason: `command:${matched.pid}` };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return { active: false, reason: "process_missing" };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function collectTeamStateTargets(stateRoot) {
|
|
172
|
+
const targets = [];
|
|
173
|
+
const rootStateFile = join(stateRoot, TEAM_STATE_FILE_NAME);
|
|
174
|
+
if (existsSync(rootStateFile)) {
|
|
175
|
+
targets.push({
|
|
176
|
+
scope: "root",
|
|
177
|
+
sessionId: "root",
|
|
178
|
+
stateFile: rootStateFile,
|
|
179
|
+
cleanupPath: rootStateFile,
|
|
180
|
+
cleanupType: "file",
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const sessionsDir = join(stateRoot, "sessions");
|
|
185
|
+
const sessionsStat = safeStat(sessionsDir);
|
|
186
|
+
if (!sessionsStat?.isDirectory()) {
|
|
187
|
+
return targets;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
for (const entry of readdirSync(sessionsDir, { withFileTypes: true })) {
|
|
191
|
+
if (!entry.isDirectory()) continue;
|
|
192
|
+
const sessionDir = join(sessionsDir, entry.name);
|
|
193
|
+
const stateFile = join(sessionDir, TEAM_STATE_FILE_NAME);
|
|
194
|
+
if (!existsSync(stateFile)) continue;
|
|
195
|
+
|
|
196
|
+
targets.push({
|
|
197
|
+
scope: "session",
|
|
198
|
+
sessionId: entry.name,
|
|
199
|
+
stateFile,
|
|
200
|
+
cleanupPath: sessionDir,
|
|
201
|
+
cleanupType: "dir",
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return targets;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function findNearestOmcStateDir(startDir = process.cwd()) {
|
|
209
|
+
let currentDir = resolve(startDir);
|
|
210
|
+
|
|
211
|
+
while (true) {
|
|
212
|
+
const candidate = join(currentDir, ".omc", "state");
|
|
213
|
+
const candidateStat = safeStat(candidate);
|
|
214
|
+
if (candidateStat?.isDirectory()) {
|
|
215
|
+
return candidate;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const parentDir = dirname(currentDir);
|
|
219
|
+
if (parentDir === currentDir) {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
currentDir = parentDir;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function inspectStaleOmcTeams(options = {}) {
|
|
227
|
+
const stateRoot = options.stateRoot || findNearestOmcStateDir(options.startDir || process.cwd());
|
|
228
|
+
if (!stateRoot) {
|
|
229
|
+
return { stateRoot: null, entries: [] };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const liveSessionNames = new Set(options.liveSessionNames || []);
|
|
233
|
+
const processEntries = normalizeProcessEntries(options.processEntries || readProcessEntries());
|
|
234
|
+
const nowMs = Number.isFinite(options.nowMs) ? options.nowMs : Date.now();
|
|
235
|
+
const maxAgeMs = Number.isFinite(options.maxAgeMs) ? options.maxAgeMs : STALE_TEAM_MAX_AGE_MS;
|
|
236
|
+
const targets = collectTeamStateTargets(stateRoot);
|
|
237
|
+
const entries = [];
|
|
238
|
+
|
|
239
|
+
for (const target of targets) {
|
|
240
|
+
let state = null;
|
|
241
|
+
try {
|
|
242
|
+
state = JSON.parse(readFileSync(target.stateFile, "utf8"));
|
|
243
|
+
} catch {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const fileStat = safeStat(target.stateFile);
|
|
248
|
+
const startedAtMs = parseStartedAtMs(state?.started_at)
|
|
249
|
+
?? parseStartedAtMs(state?.startedAt)
|
|
250
|
+
?? fileStat?.mtimeMs
|
|
251
|
+
?? null;
|
|
252
|
+
const ageMs = startedAtMs == null ? null : Math.max(0, nowMs - startedAtMs);
|
|
253
|
+
const liveness = resolveLiveness(state, target.sessionId, liveSessionNames, processEntries);
|
|
254
|
+
const stale = ageMs != null && ageMs >= maxAgeMs && !liveness.active;
|
|
255
|
+
|
|
256
|
+
entries.push({
|
|
257
|
+
...target,
|
|
258
|
+
teamName: state?.teamName || state?.team_name || state?.native?.teamName || state?.name || null,
|
|
259
|
+
state,
|
|
260
|
+
startedAtMs,
|
|
261
|
+
ageMs,
|
|
262
|
+
ageSec: ageMs == null ? null : Math.floor(ageMs / 1000),
|
|
263
|
+
active: liveness.active,
|
|
264
|
+
activeReason: liveness.reason,
|
|
265
|
+
stale,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
stateRoot,
|
|
271
|
+
entries: entries
|
|
272
|
+
.filter((entry) => entry.stale)
|
|
273
|
+
.sort((left, right) => (right.ageMs || 0) - (left.ageMs || 0)),
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function cleanupStaleOmcTeams(entries = []) {
|
|
278
|
+
let cleaned = 0;
|
|
279
|
+
let failed = 0;
|
|
280
|
+
const results = [];
|
|
281
|
+
|
|
282
|
+
for (const entry of entries) {
|
|
283
|
+
try {
|
|
284
|
+
if (entry.cleanupType === "dir") {
|
|
285
|
+
rmSync(entry.cleanupPath, { recursive: true, force: true });
|
|
286
|
+
} else {
|
|
287
|
+
unlinkSync(entry.cleanupPath);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
cleaned += 1;
|
|
291
|
+
results.push({ ok: true, entry });
|
|
292
|
+
} catch (error) {
|
|
293
|
+
failed += 1;
|
|
294
|
+
results.push({ ok: false, entry, error });
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return { cleaned, failed, results };
|
|
299
|
+
}
|
package/hub/tools.mjs
CHANGED
|
@@ -1,15 +1,23 @@
|
|
|
1
|
-
// hub/tools.mjs — MCP 도구
|
|
2
|
-
// register
|
|
1
|
+
// hub/tools.mjs — MCP 도구 정의
|
|
2
|
+
// register/status/publish/ask/poll/handoff/HITL + team proxy
|
|
3
3
|
// 모든 도구 응답: { ok: boolean, error?: { code, message }, data?: ... }
|
|
4
4
|
|
|
5
|
+
import {
|
|
6
|
+
teamInfo,
|
|
7
|
+
teamTaskList,
|
|
8
|
+
teamTaskUpdate,
|
|
9
|
+
teamSendMessage,
|
|
10
|
+
} from './team/nativeProxy.mjs';
|
|
11
|
+
|
|
5
12
|
/**
|
|
6
13
|
* MCP 도구 목록 생성
|
|
7
14
|
* @param {object} store — createStore() 반환
|
|
8
15
|
* @param {object} router — createRouter() 반환
|
|
9
|
-
* @param {object} hitl — createHitlManager() 반환
|
|
10
|
-
* @
|
|
11
|
-
|
|
12
|
-
|
|
16
|
+
* @param {object} hitl — createHitlManager() 반환
|
|
17
|
+
* @param {object} pipe — createPipeServer() 반환
|
|
18
|
+
* @returns {Array<{name, description, inputSchema, handler}>}
|
|
19
|
+
*/
|
|
20
|
+
export function createTools(store, router, hitl, pipe = null) {
|
|
13
21
|
/** 도구 핸들러 래퍼 — 에러 처리 + MCP content 형식 변환 */
|
|
14
22
|
function wrap(code, fn) {
|
|
15
23
|
return async (args) => {
|
|
@@ -41,10 +49,10 @@ export function createTools(store, router, hitl) {
|
|
|
41
49
|
heartbeat_ttl_ms: { type: 'integer', minimum: 10000, maximum: 7200000 },
|
|
42
50
|
},
|
|
43
51
|
},
|
|
44
|
-
handler: wrap('REGISTER_FAILED', (args) => {
|
|
45
|
-
const data =
|
|
46
|
-
return { ok: true, data };
|
|
47
|
-
}),
|
|
52
|
+
handler: wrap('REGISTER_FAILED', (args) => {
|
|
53
|
+
const data = router.registerAgent(args);
|
|
54
|
+
return { ok: true, data };
|
|
55
|
+
}),
|
|
48
56
|
},
|
|
49
57
|
|
|
50
58
|
// ── 2. status ──
|
|
@@ -114,10 +122,10 @@ export function createTools(store, router, hitl) {
|
|
|
114
122
|
}),
|
|
115
123
|
},
|
|
116
124
|
|
|
117
|
-
// ── 5. poll_messages ──
|
|
118
|
-
{
|
|
119
|
-
name: 'poll_messages',
|
|
120
|
-
description: '
|
|
125
|
+
// ── 5. poll_messages ──
|
|
126
|
+
{
|
|
127
|
+
name: 'poll_messages',
|
|
128
|
+
description: 'Deprecated. poll_messages 대신 Named Pipe subscribe/publish 채널을 사용합니다',
|
|
121
129
|
inputSchema: {
|
|
122
130
|
type: 'object',
|
|
123
131
|
required: ['agent_id'],
|
|
@@ -129,46 +137,34 @@ export function createTools(store, router, hitl) {
|
|
|
129
137
|
ack_ids: { type: 'array', items: { type: 'string' }, maxItems: 100 },
|
|
130
138
|
auto_ack: { type: 'boolean', default: false },
|
|
131
139
|
},
|
|
132
|
-
},
|
|
133
|
-
handler: wrap('
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
// 1차 폴링
|
|
142
|
-
let messages = store.pollForAgent(args.agent_id, {
|
|
143
|
-
max_messages: args.max_messages,
|
|
144
|
-
include_topics: args.include_topics,
|
|
145
|
-
auto_ack: args.auto_ack,
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
// wait_ms > 0 이고 메시지 없으면 짧은 간격으로 반복 재시도
|
|
149
|
-
if (!messages.length && args.wait_ms > 0) {
|
|
150
|
-
const interval = Math.min(args.wait_ms, 500);
|
|
151
|
-
const deadline = Date.now() + Math.min(args.wait_ms, 30000);
|
|
152
|
-
while (!messages.length && Date.now() < deadline) {
|
|
153
|
-
await new Promise(r => setTimeout(r, interval));
|
|
154
|
-
messages = store.pollForAgent(args.agent_id, {
|
|
155
|
-
max_messages: args.max_messages,
|
|
156
|
-
include_topics: args.include_topics,
|
|
157
|
-
auto_ack: args.auto_ack,
|
|
158
|
-
});
|
|
159
|
-
}
|
|
140
|
+
},
|
|
141
|
+
handler: wrap('POLL_DEPRECATED', async (args) => {
|
|
142
|
+
const replay = router.drainAgent(args.agent_id, {
|
|
143
|
+
max_messages: args.max_messages,
|
|
144
|
+
include_topics: args.include_topics,
|
|
145
|
+
auto_ack: args.auto_ack,
|
|
146
|
+
});
|
|
147
|
+
if (args.ack_ids?.length) {
|
|
148
|
+
router.ackMessages(args.ack_ids, args.agent_id);
|
|
160
149
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
150
|
+
return {
|
|
151
|
+
ok: false,
|
|
152
|
+
error: {
|
|
153
|
+
code: 'POLL_DEPRECATED',
|
|
154
|
+
message: 'poll_messages는 deprecated 되었습니다. pipe subscribe/publish 채널을 사용하세요.',
|
|
155
|
+
},
|
|
156
|
+
data: {
|
|
157
|
+
pipe_path: pipe?.path || null,
|
|
158
|
+
delivery_mode: 'pipe_push',
|
|
159
|
+
protocol: 'ndjson',
|
|
160
|
+
replay: {
|
|
161
|
+
messages: replay,
|
|
162
|
+
count: replay.length,
|
|
163
|
+
},
|
|
164
|
+
server_time_ms: Date.now(),
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
}),
|
|
172
168
|
},
|
|
173
169
|
|
|
174
170
|
// ── 6. handoff ──
|
|
@@ -238,5 +234,96 @@ export function createTools(store, router, hitl) {
|
|
|
238
234
|
return hitl.submitHumanInput(args);
|
|
239
235
|
}),
|
|
240
236
|
},
|
|
237
|
+
|
|
238
|
+
// ── 9. team_info ──
|
|
239
|
+
{
|
|
240
|
+
name: 'team_info',
|
|
241
|
+
description: 'Claude Native Teams 메타/멤버/경로 정보를 조회합니다',
|
|
242
|
+
inputSchema: {
|
|
243
|
+
type: 'object',
|
|
244
|
+
required: ['team_name'],
|
|
245
|
+
properties: {
|
|
246
|
+
team_name: { type: 'string', minLength: 1, maxLength: 128, pattern: '^[a-z0-9][a-z0-9-]*$' },
|
|
247
|
+
include_members: { type: 'boolean', default: true },
|
|
248
|
+
include_paths: { type: 'boolean', default: true },
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
handler: wrap('TEAM_INFO_FAILED', (args) => {
|
|
252
|
+
return teamInfo(args);
|
|
253
|
+
}),
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
// ── 10. team_task_list ──
|
|
257
|
+
{
|
|
258
|
+
name: 'team_task_list',
|
|
259
|
+
description: 'Claude Native Teams task 목록을 owner/status 조건으로 조회합니다',
|
|
260
|
+
inputSchema: {
|
|
261
|
+
type: 'object',
|
|
262
|
+
required: ['team_name'],
|
|
263
|
+
properties: {
|
|
264
|
+
team_name: { type: 'string', pattern: '^[a-z0-9][a-z0-9-]*$' },
|
|
265
|
+
owner: { type: 'string' },
|
|
266
|
+
statuses: {
|
|
267
|
+
type: 'array',
|
|
268
|
+
items: { type: 'string', enum: ['pending', 'in_progress', 'completed', 'failed', 'deleted'] },
|
|
269
|
+
maxItems: 8,
|
|
270
|
+
},
|
|
271
|
+
include_internal: { type: 'boolean', default: false },
|
|
272
|
+
limit: { type: 'integer', minimum: 1, maximum: 1000, default: 200 },
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
handler: wrap('TEAM_TASK_LIST_FAILED', (args) => {
|
|
276
|
+
return teamTaskList(args);
|
|
277
|
+
}),
|
|
278
|
+
},
|
|
279
|
+
|
|
280
|
+
// ── 11. team_task_update ──
|
|
281
|
+
{
|
|
282
|
+
name: 'team_task_update',
|
|
283
|
+
description: 'Claude Native Teams task를 claim/update 합니다',
|
|
284
|
+
inputSchema: {
|
|
285
|
+
type: 'object',
|
|
286
|
+
required: ['team_name', 'task_id'],
|
|
287
|
+
properties: {
|
|
288
|
+
team_name: { type: 'string', pattern: '^[a-z0-9][a-z0-9-]*$' },
|
|
289
|
+
task_id: { type: 'string', minLength: 1, maxLength: 64 },
|
|
290
|
+
claim: { type: 'boolean', default: false },
|
|
291
|
+
owner: { type: 'string' },
|
|
292
|
+
status: { type: 'string', enum: ['pending', 'in_progress', 'completed', 'failed', 'deleted'] },
|
|
293
|
+
subject: { type: 'string' },
|
|
294
|
+
description: { type: 'string' },
|
|
295
|
+
activeForm: { type: 'string' },
|
|
296
|
+
add_blocks: { type: 'array', items: { type: 'string' } },
|
|
297
|
+
add_blocked_by: { type: 'array', items: { type: 'string' } },
|
|
298
|
+
metadata_patch: { type: 'object' },
|
|
299
|
+
if_match_mtime_ms: { type: 'number' },
|
|
300
|
+
actor: { type: 'string' },
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
handler: wrap('TEAM_TASK_UPDATE_FAILED', (args) => {
|
|
304
|
+
return teamTaskUpdate(args);
|
|
305
|
+
}),
|
|
306
|
+
},
|
|
307
|
+
|
|
308
|
+
// ── 12. team_send_message ──
|
|
309
|
+
{
|
|
310
|
+
name: 'team_send_message',
|
|
311
|
+
description: 'Claude Native Teams inbox에 메시지를 append 합니다',
|
|
312
|
+
inputSchema: {
|
|
313
|
+
type: 'object',
|
|
314
|
+
required: ['team_name', 'from', 'text'],
|
|
315
|
+
properties: {
|
|
316
|
+
team_name: { type: 'string', pattern: '^[a-z0-9][a-z0-9-]*$' },
|
|
317
|
+
from: { type: 'string', minLength: 1, maxLength: 128 },
|
|
318
|
+
to: { type: 'string', default: 'team-lead' },
|
|
319
|
+
text: { type: 'string', minLength: 1, maxLength: 200000 },
|
|
320
|
+
summary: { type: 'string', maxLength: 1000 },
|
|
321
|
+
color: { type: 'string', default: 'blue' },
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
handler: wrap('TEAM_SEND_MESSAGE_FAILED', (args) => {
|
|
325
|
+
return teamSendMessage(args);
|
|
326
|
+
}),
|
|
327
|
+
},
|
|
241
328
|
];
|
|
242
329
|
}
|