@triflux/remote 10.50.2 → 10.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cto/brief.mjs +0 -20
- package/cto/collect.mjs +1 -200
- package/cto/current.schema.json +2 -23
- package/cto/hygiene.mjs +2 -61
- package/cto/lake-root.mjs +0 -56
- package/cto/status.mjs +5 -187
- package/hub/index.mjs +0 -17
- package/hub/team/agent-map.json +0 -1
- package/hub/team/agents-row-attach.py +120 -0
- package/hub/team/agents-row.mjs +200 -0
- package/hub/team/ansi.mjs +1 -35
- package/hub/team/backend.mjs +9 -145
- package/hub/team/claude-daemon-control.mjs +1 -2
- package/hub/team/claude-session-projection.mjs +5 -73
- package/hub/team/cli/commands/attach.mjs +1 -17
- package/hub/team/cli/commands/control.mjs +12 -38
- package/hub/team/cli/commands/debug.mjs +2 -39
- package/hub/team/cli/commands/focus.mjs +5 -31
- package/hub/team/cli/commands/interrupt.mjs +6 -25
- package/hub/team/cli/commands/kill.mjs +1 -30
- package/hub/team/cli/commands/list.mjs +0 -16
- package/hub/team/cli/commands/send.mjs +6 -26
- package/hub/team/cli/commands/start/index.mjs +42 -133
- package/hub/team/cli/commands/start/parse-args.mjs +15 -40
- package/hub/team/cli/commands/start/start-headless.mjs +138 -105
- package/hub/team/cli/commands/start/start-mux.mjs +2 -7
- package/hub/team/cli/commands/status.mjs +2 -57
- package/hub/team/cli/commands/stop.mjs +14 -19
- package/hub/team/cli/help.mjs +4 -5
- package/hub/team/cli/manifest.mjs +5 -1
- package/hub/team/cli/render.mjs +0 -12
- package/hub/team/cli/services/runtime-mode.mjs +27 -98
- package/hub/team/cli/services/state-store.mjs +5 -20
- package/hub/team/handoff.mjs +3 -34
- package/hub/team/headless.mjs +108 -1296
- package/hub/team/orchestrator.mjs +4 -30
- package/hub/team/pane.mjs +6 -28
- package/hub/team/psmux.mjs +3 -34
- package/hub/team/remote-session.mjs +1 -2
- package/hub/team/session-context.mjs +1 -1
- package/hub/team/session.mjs +7 -91
- package/hub/team/tui-core.mjs +1 -1
- package/hub/team/tui-viewer.mjs +11 -58
- package/hub/team/tui.mjs +8 -306
- package/hub/team/uds-orchestrator.mjs +2 -580
- package/hub/team/wt-manager.mjs +2 -2
- package/hub/workers/factory.mjs +6 -84
- package/hub/workers/interface.mjs +1 -7
- package/hub/workers/lib/jsonrpc-core.mjs +11 -0
- package/hub/workers/lib/jsonrpc-ws-uds.mjs +5 -8
- package/package.json +3 -8
- package/scripts/lib/agent-route-policy.mjs +143 -62
- package/scripts/lib/cli-claude.mjs +0 -14
- package/scripts/lib/env-probe.mjs +2 -131
- package/scripts/lib/gemini-profiles.mjs +2 -69
- package/scripts/lib/legacy-mcp-cleanup.mjs +283 -0
- package/scripts/lib/machine-profile.mjs +3 -17
- package/scripts/lib/mcp-filter.mjs +109 -53
- package/scripts/lib/mcp-guard-engine.mjs +40 -98
- package/scripts/lib/mcp-health.mjs +2 -2
- package/scripts/lib/mcp-server-catalog.mjs +2 -2
- package/scripts/lib/psmux-info.mjs +11 -59
- package/scripts/lib/skill-template.mjs +0 -245
- package/hub/hub-lifecycle.mjs +0 -248
- package/hub/pipe.mjs +0 -825
- package/hub/promote-penalties.mjs +0 -138
- package/hub/public/dashboard.html +0 -416
- package/hub/schema.sql +0 -209
- package/hub/server.mjs +0 -3048
- package/hub/store-adapter.mjs +0 -182
- package/hub/store.mjs +0 -1736
- package/hub/team/build-worker-prompt.mjs +0 -92
- package/hub/team/check-mcp-hub.mjs +0 -49
- package/hub/team/claude-native-bridge.mjs +0 -987
- package/hub/team/cli/commands/start/start-in-process.mjs +0 -50
- package/hub/team/cli/commands/start/start-wt.mjs +0 -86
- package/hub/team/cli/services/hub-client.mjs +0 -264
- package/hub/team/cli/services/native-control.mjs +0 -145
- package/hub/team/codex-review.mjs +0 -520
- package/hub/team/conductor-mesh-bridge.mjs +0 -122
- package/hub/team/conductor-registry.mjs +0 -121
- package/hub/team/conductor.mjs +0 -1529
- package/hub/team/consensus-meta.mjs +0 -176
- package/hub/team/cto-auto-collect.mjs +0 -115
- package/hub/team/cto-pull.mjs +0 -31
- package/hub/team/daemon-pty-tmux-bridge.mjs +0 -146
- package/hub/team/dashboard.mjs +0 -322
- package/hub/team/event-log.mjs +0 -84
- package/hub/team/execution-mode.mjs +0 -324
- package/hub/team/extract-completion-payload.mjs +0 -110
- package/hub/team/headless-bridge-session.mjs +0 -17
- package/hub/team/health-probe.mjs +0 -435
- package/hub/team/interactive-native-launcher.mjs +0 -119
- package/hub/team/interactive-native-worker.mjs +0 -80
- package/hub/team/interactive-tui-transport.mjs +0 -479
- package/hub/team/launcher-template.mjs +0 -144
- package/hub/team/lead-control.mjs +0 -191
- package/hub/team/mcp-selector.mjs +0 -145
- package/hub/team/native-supervisor.mjs +0 -460
- package/hub/team/native.mjs +0 -727
- package/hub/team/recovery-store.mjs +0 -149
- package/hub/team/remote-probe.mjs +0 -292
- package/hub/team/remote-watcher.mjs +0 -506
- package/hub/team/runtime-strategy.mjs +0 -200
- package/hub/team/schemas/worker-completion.json +0 -39
- package/hub/team/sentinel-capture.mjs +0 -127
- package/hub/team/session-sync.mjs +0 -316
- package/hub/team/swarm-cli.mjs +0 -390
- package/hub/team/swarm-hypervisor.mjs +0 -2206
- package/hub/team/swarm-intent.mjs +0 -216
- package/hub/team/swarm-locks.mjs +0 -335
- package/hub/team/swarm-planner.mjs +0 -371
- package/hub/team/swarm-preflight.mjs +0 -366
- package/hub/team/swarm-reconciler.mjs +0 -242
- package/hub/team/synapse-cli.mjs +0 -458
- package/hub/team/synapse-http.mjs +0 -311
- package/hub/team/synapse-registry.mjs +0 -618
- package/hub/team/tui-remote-adapter.mjs +0 -377
- package/hub/team/tui-synapse.mjs +0 -223
- package/hub/team/worker-completion-validator.mjs +0 -79
- package/hub/team/worker-sandbox.mjs +0 -125
- package/hub/team/worktree-lifecycle.mjs +0 -639
- package/hub/team/wt-templates.json +0 -43
- package/hub/tools.mjs +0 -900
- package/hub/workers/antigravity-route-worker.mjs +0 -179
- package/hub/workers/codex-app-server-worker.mjs +0 -1067
- package/hub/workers/delegator-mcp.mjs +0 -1098
- package/hub/workers/lib/jsonrpc-stdio.mjs +0 -204
- package/scripts/lib/claudemd-scanner.mjs +0 -223
- package/scripts/lib/context.mjs +0 -67
- package/scripts/lib/dynamic-route-cli.mjs +0 -107
- package/scripts/lib/handoff.mjs +0 -187
- package/scripts/lib/logger.mjs +0 -122
- package/scripts/lib/stealth-fetch.mjs +0 -176
package/cto/status.mjs
CHANGED
|
@@ -2,16 +2,9 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
|
|
4
4
|
import { compactHygieneCounts, projectCtoHygiene } from "./hygiene.mjs";
|
|
5
|
-
import {
|
|
6
|
-
normalizeLiveSession,
|
|
7
|
-
pathLabel,
|
|
8
|
-
readSynapseSnapshot,
|
|
9
|
-
resolveLakeRootDir,
|
|
10
|
-
shortHash,
|
|
11
|
-
} from "./lake-root.mjs";
|
|
5
|
+
import { resolveLakeRootDir } from "./lake-root.mjs";
|
|
12
6
|
|
|
13
7
|
const SCHEMA_VERSION = "cto-lake.v1";
|
|
14
|
-
const SYNAPSE_TIMEOUT_MS = 1500;
|
|
15
8
|
|
|
16
9
|
function hasFlag(args, flag) {
|
|
17
10
|
return Array.isArray(args) && args.includes(flag);
|
|
@@ -25,174 +18,7 @@ function readJson(filePath) {
|
|
|
25
18
|
return JSON.parse(readFileSync(filePath, "utf8"));
|
|
26
19
|
}
|
|
27
20
|
|
|
28
|
-
|
|
29
|
-
if (typeof cwd !== "string" || !cwd) return "";
|
|
30
|
-
if (cwd.includes("\\") || /^[A-Za-z]:/u.test(cwd)) return cwd;
|
|
31
|
-
|
|
32
|
-
const normalized = cwd.replace(/\/+$/u, "");
|
|
33
|
-
const gitWorktreeMarker = "/.worktrees/";
|
|
34
|
-
const gitWorktreeIndex = normalized.indexOf(gitWorktreeMarker);
|
|
35
|
-
if (gitWorktreeIndex > 0) {
|
|
36
|
-
const suffix = normalized.slice(
|
|
37
|
-
gitWorktreeIndex + gitWorktreeMarker.length,
|
|
38
|
-
);
|
|
39
|
-
if (/^[^/]+(?:\/|$)/u.test(suffix)) {
|
|
40
|
-
return normalized.slice(0, gitWorktreeIndex);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const claudeMarker = "/.claude/worktrees/";
|
|
45
|
-
const claudeIndex = normalized.indexOf(claudeMarker);
|
|
46
|
-
if (claudeIndex > 0) {
|
|
47
|
-
const suffix = normalized.slice(claudeIndex + claudeMarker.length);
|
|
48
|
-
if (/^[^/]+(?:\/|$)/u.test(suffix)) {
|
|
49
|
-
return normalized.slice(0, claudeIndex);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const codexMarker = "/.codex-swarm/";
|
|
54
|
-
const codexIndex = normalized.indexOf(codexMarker);
|
|
55
|
-
if (codexIndex > 0) {
|
|
56
|
-
const suffix = normalized.slice(codexIndex + codexMarker.length);
|
|
57
|
-
if (/^wt-[^/]+(?:\/|$)/u.test(suffix)) {
|
|
58
|
-
return normalized.slice(0, codexIndex);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return cwd;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function redactedCwdFields(cwd) {
|
|
66
|
-
if (typeof cwd !== "string" || !cwd) return {};
|
|
67
|
-
const repoRoot = deriveRepoRootFromCwd(cwd);
|
|
68
|
-
return {
|
|
69
|
-
cwdLabel: pathLabel(cwd),
|
|
70
|
-
cwdHash: shortHash(cwd),
|
|
71
|
-
repoRootLabel: pathLabel(repoRoot),
|
|
72
|
-
repoRootHash: shortHash(repoRoot),
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function normalizeActiveShard(shard) {
|
|
77
|
-
return {
|
|
78
|
-
shard_name:
|
|
79
|
-
typeof shard?.shard_name === "string"
|
|
80
|
-
? shard.shard_name
|
|
81
|
-
: typeof shard?.shardName === "string"
|
|
82
|
-
? shard.shardName
|
|
83
|
-
: typeof shard?.name === "string"
|
|
84
|
-
? shard.name
|
|
85
|
-
: typeof shard?.id === "string"
|
|
86
|
-
? shard.id
|
|
87
|
-
: null,
|
|
88
|
-
phase:
|
|
89
|
-
typeof shard?.phase === "string"
|
|
90
|
-
? shard.phase
|
|
91
|
-
: typeof shard?.status === "string"
|
|
92
|
-
? shard.status
|
|
93
|
-
: "active",
|
|
94
|
-
members: Array.isArray(shard?.members)
|
|
95
|
-
? shard.members.map((member) => String(member))
|
|
96
|
-
: [],
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function normalizeSynapseOverlay(value) {
|
|
101
|
-
const sessions = Array.isArray(value)
|
|
102
|
-
? value
|
|
103
|
-
: Array.isArray(value?.sessions)
|
|
104
|
-
? value.sessions
|
|
105
|
-
: Array.isArray(value?.live_sessions)
|
|
106
|
-
? value.live_sessions
|
|
107
|
-
: [];
|
|
108
|
-
const activeShards = Array.isArray(value?.active_shards)
|
|
109
|
-
? value.active_shards
|
|
110
|
-
: Array.isArray(value?.activeShards)
|
|
111
|
-
? value.activeShards
|
|
112
|
-
: [];
|
|
113
|
-
|
|
114
|
-
return {
|
|
115
|
-
live_sessions: sessions
|
|
116
|
-
.map((session) => ({
|
|
117
|
-
...normalizeLiveSession(session),
|
|
118
|
-
host: typeof session?.host === "string" ? session.host : "local",
|
|
119
|
-
...redactedCwdFields(session?.cwd),
|
|
120
|
-
}))
|
|
121
|
-
.filter((session) => session.sessionId),
|
|
122
|
-
active_shards: activeShards.map(normalizeActiveShard),
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function withTimeout(promise, timeoutMs = SYNAPSE_TIMEOUT_MS) {
|
|
127
|
-
let timer = null;
|
|
128
|
-
const timeout = new Promise((_, reject) => {
|
|
129
|
-
timer = setTimeout(
|
|
130
|
-
() => reject(new Error("synapse read timeout")),
|
|
131
|
-
timeoutMs,
|
|
132
|
-
);
|
|
133
|
-
if (typeof timer.unref === "function") timer.unref();
|
|
134
|
-
});
|
|
135
|
-
return Promise.race([Promise.resolve(promise), timeout]).finally(() => {
|
|
136
|
-
if (timer) clearTimeout(timer);
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
async function readSynapseOverlay(opts) {
|
|
141
|
-
try {
|
|
142
|
-
const reader = opts.synapseReader || readSynapseSnapshot;
|
|
143
|
-
const raw = await withTimeout(
|
|
144
|
-
reader({
|
|
145
|
-
rootDir: opts.rootDir,
|
|
146
|
-
lakeRoot: opts.lakeRoot,
|
|
147
|
-
synapsePersistPath: opts.synapsePersistPath,
|
|
148
|
-
}),
|
|
149
|
-
opts.synapseTimeoutMs || SYNAPSE_TIMEOUT_MS,
|
|
150
|
-
);
|
|
151
|
-
return normalizeSynapseOverlay(raw);
|
|
152
|
-
} catch {
|
|
153
|
-
return { live_sessions: [], active_shards: [] };
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// active_shards 의 source 는 live_sessions 와 다르다: live_sessions 는 synapse
|
|
158
|
-
// overlay(실시간 세션)에서, active_shards 는 collect 가 .triflux/swarm-locks.json 을
|
|
159
|
-
// snapshot 한 current.json(sources.tfx_swarm.detail.shards)에서 온다. synapse
|
|
160
|
-
// overlay 가 shard 를 채우는 경로가 생기면 그쪽을 우선하고, 없으면 snapshot 으로
|
|
161
|
-
// fallback 한다(현재 overlay 의 active_shards 는 항상 비어 snapshot 을 쓴다).
|
|
162
|
-
function deriveActiveShards(current, overlay) {
|
|
163
|
-
// overlay(synapse)가 채운 shard 중 식별 가능한 것만 authoritative 로 본다.
|
|
164
|
-
// normalizeSynapseOverlay 가 shard_name 을 null 로 정규화한 무효 행이 snapshot
|
|
165
|
-
// fallback 을 가로채지 않도록 먼저 거른다.
|
|
166
|
-
const overlayShards = Array.isArray(overlay?.active_shards)
|
|
167
|
-
? overlay.active_shards.filter((shard) => shard?.shard_name)
|
|
168
|
-
: [];
|
|
169
|
-
if (overlayShards.length > 0) return overlayShards;
|
|
170
|
-
const shards = current?.sources?.tfx_swarm?.detail?.shards;
|
|
171
|
-
if (!Array.isArray(shards)) return [];
|
|
172
|
-
return shards.map(normalizeActiveShard).filter((shard) => shard.shard_name);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function deriveLiveSessionGroups(liveSessions) {
|
|
176
|
-
const groups = new Map();
|
|
177
|
-
for (const session of liveSessions || []) {
|
|
178
|
-
if (!session?.repoRootHash) continue;
|
|
179
|
-
if (!groups.has(session.repoRootHash)) {
|
|
180
|
-
groups.set(session.repoRootHash, {
|
|
181
|
-
repoRootLabel: session.repoRootLabel || "",
|
|
182
|
-
repoRootHash: session.repoRootHash,
|
|
183
|
-
session_count: 0,
|
|
184
|
-
sessions: [],
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
const group = groups.get(session.repoRootHash);
|
|
188
|
-
group.session_count += 1;
|
|
189
|
-
group.sessions.push(session.sessionId);
|
|
190
|
-
}
|
|
191
|
-
return [...groups.values()];
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function projectStatus(current, overlay) {
|
|
195
|
-
const liveSessions = overlay.live_sessions;
|
|
21
|
+
function projectStatus(current) {
|
|
196
22
|
return {
|
|
197
23
|
schema_version: current?.schema_version || SCHEMA_VERSION,
|
|
198
24
|
generated_at: current?.generated_at || null,
|
|
@@ -200,10 +26,7 @@ function projectStatus(current, overlay) {
|
|
|
200
26
|
sources: current?.sources || {},
|
|
201
27
|
summary: current?.summary || {},
|
|
202
28
|
ledger_tail: Array.isArray(current?.ledger_tail) ? current.ledger_tail : [],
|
|
203
|
-
|
|
204
|
-
live_session_groups: deriveLiveSessionGroups(liveSessions),
|
|
205
|
-
active_shards: deriveActiveShards(current, overlay),
|
|
206
|
-
hygiene: compactHygieneCounts(projectCtoHygiene({ current, overlay })),
|
|
29
|
+
hygiene: compactHygieneCounts(projectCtoHygiene({ current })),
|
|
207
30
|
};
|
|
208
31
|
}
|
|
209
32
|
|
|
@@ -262,7 +85,7 @@ function renderHumanStatus(status, now) {
|
|
|
262
85
|
`generated_at: ${status.generated_at || "unknown"} (${formatSnapshotAge(status.generated_at, now)})`,
|
|
263
86
|
`repo: ${formatRepo(status.repo)}`,
|
|
264
87
|
`sources: ${available}/${total} available`,
|
|
265
|
-
`active: ${
|
|
88
|
+
`active: ${activeGoals} goals`,
|
|
266
89
|
];
|
|
267
90
|
if (recent.length) {
|
|
268
91
|
lines.push("ledger:");
|
|
@@ -292,12 +115,7 @@ export async function runStatus(args = [], opts = {}) {
|
|
|
292
115
|
}
|
|
293
116
|
|
|
294
117
|
const current = readJson(currentPath);
|
|
295
|
-
const
|
|
296
|
-
...opts,
|
|
297
|
-
rootDir,
|
|
298
|
-
lakeRoot,
|
|
299
|
-
});
|
|
300
|
-
const status = projectStatus(current, overlay);
|
|
118
|
+
const status = projectStatus(current);
|
|
301
119
|
|
|
302
120
|
if (jsonOut) writeJson(stdout, status);
|
|
303
121
|
else stdout.write(renderHumanStatus(status, opts.now ?? Date.now()));
|
package/hub/index.mjs
CHANGED
|
@@ -1,21 +1,4 @@
|
|
|
1
1
|
// @triflux/remote — barrel export (auto-generated by pack.mjs)
|
|
2
|
-
// Server
|
|
3
|
-
export { startHub, getHubInfo, getOrCreateServer } from './server.mjs';
|
|
4
|
-
|
|
5
|
-
// Store
|
|
6
|
-
export { createStoreAdapter } from './store-adapter.mjs';
|
|
7
|
-
|
|
8
|
-
// Pipe
|
|
9
|
-
export { createPipeServer, getPipePath } from './pipe.mjs';
|
|
10
|
-
|
|
11
2
|
// Team
|
|
12
3
|
export { orchestrate, decomposeTask, buildLeadPrompt, buildPrompt } from './team/orchestrator.mjs';
|
|
13
4
|
export { createPsmuxSession, startCapture } from './team/psmux.mjs';
|
|
14
|
-
export { createConductor } from './team/conductor.mjs';
|
|
15
|
-
|
|
16
|
-
// Swarm
|
|
17
|
-
export { createSwarmLocks } from './team/swarm-locks.mjs';
|
|
18
|
-
export { parseShards, buildFileLeaseMap, buildMcpManifest, computeMergeOrder, planSwarm } from './team/swarm-planner.mjs';
|
|
19
|
-
export { createSwarmHypervisor, SWARM_STATES } from './team/swarm-hypervisor.mjs';
|
|
20
|
-
export { reconcile, buildRedundantIds, shouldRunRedundant } from './team/swarm-reconciler.mjs';
|
|
21
|
-
export { ensureWorktree, prepareIntegrationBranch, rebaseShardOntoIntegration, pruneWorktree, fetchRemoteShard } from './team/worktree-lifecycle.mjs';
|
package/hub/team/agent-map.json
CHANGED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# claude agents 행의 백킹 PTY 안에서 워커 tmux pane 에 붙는다.
|
|
3
|
+
# bg-pty-host 가 출력 이력을 다시 재생하면 바깥 터미널이 tmux 의 오래된 질의에 또 답한다.
|
|
4
|
+
# tmux 는 늦게 온 응답을 키 입력으로 넘기므로 여기서 응답 시퀀스를 걸러 낸다.
|
|
5
|
+
import fcntl
|
|
6
|
+
import os
|
|
7
|
+
import pty
|
|
8
|
+
import re
|
|
9
|
+
import select
|
|
10
|
+
import signal
|
|
11
|
+
import sys
|
|
12
|
+
import termios
|
|
13
|
+
import time
|
|
14
|
+
import tty
|
|
15
|
+
|
|
16
|
+
# DA1/DA2/DA3, 창 크기 보고(CSI ... t), DECRQM, kitty 키보드 플래그 응답, DCS, OSC
|
|
17
|
+
REPLY = re.compile(
|
|
18
|
+
rb"\x1b\[[?>=][0-9;]*c"
|
|
19
|
+
rb"|\x1b\[[0-9]+(?:;[0-9]+)*t"
|
|
20
|
+
rb"|\x1b\[\?[0-9;]*\$y"
|
|
21
|
+
rb"|\x1b\[\?[0-9]*u"
|
|
22
|
+
rb"|\x1bP[^\x1b]*\x1b\\"
|
|
23
|
+
rb"|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)"
|
|
24
|
+
)
|
|
25
|
+
# 응답이 read 경계에서 잘렸을 때 잠깐 붙잡아 둘 접두
|
|
26
|
+
PARTIAL = re.compile(
|
|
27
|
+
rb"(?:\x1b\[[?>=]?[0-9;$]*|\x1bP[^\x1b]*\x1b?|\x1b\][^\x07\x1b]*\x1b?)\Z"
|
|
28
|
+
)
|
|
29
|
+
HOLD_SEC = 0.05
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def strip_replies(buf):
|
|
33
|
+
"""걸러 낸 바이트와, 다음 read 까지 보류할 꼬리를 돌려준다."""
|
|
34
|
+
cleaned = REPLY.sub(b"", buf)
|
|
35
|
+
m = PARTIAL.search(cleaned)
|
|
36
|
+
if m and cleaned[m.start():] != b"\x1b":
|
|
37
|
+
return cleaned[: m.start()], cleaned[m.start():]
|
|
38
|
+
return cleaned, b""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def copy_winsize(src, dst):
|
|
42
|
+
try:
|
|
43
|
+
size = fcntl.ioctl(src, termios.TIOCGWINSZ, b"\0" * 8)
|
|
44
|
+
fcntl.ioctl(dst, termios.TIOCSWINSZ, size)
|
|
45
|
+
except OSError:
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def relay(argv, env):
|
|
50
|
+
pid, fd = pty.fork()
|
|
51
|
+
if pid == 0:
|
|
52
|
+
os.execvpe(argv[0], argv, env)
|
|
53
|
+
stdin = sys.stdin.fileno()
|
|
54
|
+
stdout = sys.stdout.fileno()
|
|
55
|
+
copy_winsize(stdin, fd)
|
|
56
|
+
signal.signal(signal.SIGWINCH, lambda *_: copy_winsize(stdin, fd))
|
|
57
|
+
saved = termios.tcgetattr(stdin) if os.isatty(stdin) else None
|
|
58
|
+
if saved:
|
|
59
|
+
tty.setraw(stdin)
|
|
60
|
+
pending = b""
|
|
61
|
+
deadline = 0.0
|
|
62
|
+
try:
|
|
63
|
+
while True:
|
|
64
|
+
# 자식 출력이 계속 바빠도 보류한 바이트가 HOLD_SEC 를 넘겨 묶이지 않게 마감을 둔다.
|
|
65
|
+
timeout = max(0.0, deadline - time.monotonic()) if pending else None
|
|
66
|
+
try:
|
|
67
|
+
ready, _, _ = select.select([stdin, fd], [], [], timeout)
|
|
68
|
+
except InterruptedError:
|
|
69
|
+
continue
|
|
70
|
+
if pending and time.monotonic() >= deadline:
|
|
71
|
+
os.write(fd, pending)
|
|
72
|
+
pending = b""
|
|
73
|
+
if fd in ready:
|
|
74
|
+
try:
|
|
75
|
+
data = os.read(fd, 65536)
|
|
76
|
+
except OSError:
|
|
77
|
+
break
|
|
78
|
+
if not data:
|
|
79
|
+
break
|
|
80
|
+
os.write(stdout, data)
|
|
81
|
+
if stdin in ready:
|
|
82
|
+
# 행 PTY 가 닫히면 slave 읽기는 EIO 를 던진다.
|
|
83
|
+
try:
|
|
84
|
+
data = os.read(stdin, 65536)
|
|
85
|
+
except OSError:
|
|
86
|
+
break
|
|
87
|
+
if not data:
|
|
88
|
+
break
|
|
89
|
+
out, held = strip_replies(pending + data)
|
|
90
|
+
if held and not pending:
|
|
91
|
+
deadline = time.monotonic() + HOLD_SEC
|
|
92
|
+
pending = held
|
|
93
|
+
if out:
|
|
94
|
+
os.write(fd, out)
|
|
95
|
+
finally:
|
|
96
|
+
if saved:
|
|
97
|
+
try:
|
|
98
|
+
termios.tcsetattr(stdin, termios.TCSADRAIN, saved)
|
|
99
|
+
except termios.error:
|
|
100
|
+
pass
|
|
101
|
+
# 행 PTY 가 닫히면 attach client 도 끝낸다. 워커 방은 그대로 남는다.
|
|
102
|
+
os.close(fd)
|
|
103
|
+
_, status = os.waitpid(pid, 0)
|
|
104
|
+
return os.waitstatus_to_exitcode(status)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def main():
|
|
108
|
+
# 사용법: agents-row-attach.py -- <tmux attach 명령>
|
|
109
|
+
argv = sys.argv[1:]
|
|
110
|
+
if argv[:1] == ["--"]:
|
|
111
|
+
argv = argv[1:]
|
|
112
|
+
if not argv:
|
|
113
|
+
print("usage: agents-row-attach.py -- <command>", file=sys.stderr)
|
|
114
|
+
return 2
|
|
115
|
+
env = {k: v for k, v in os.environ.items() if k not in ("TMUX", "TMUX_PANE")}
|
|
116
|
+
return relay(argv, env)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
if __name__ == "__main__":
|
|
120
|
+
sys.exit(main())
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// hub/team/agents-row.mjs: 워커 tmux pane 을 `claude agents` 행으로 노출한다.
|
|
2
|
+
// 행의 백킹 job 은 공식 `claude --bg --exec` 로 띄운 tmux attach client 다.
|
|
3
|
+
// Enter 를 누르면 그 PTY, 곧 워커가 도는 tmux 방이 열린다(ADR-0026).
|
|
4
|
+
|
|
5
|
+
import { execFile } from "node:child_process";
|
|
6
|
+
import { accessSync, constants } from "node:fs";
|
|
7
|
+
import { delimiter, dirname, isAbsolute, join } from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { promisify } from "node:util";
|
|
10
|
+
import { getMultiplexerType, psmuxExec } from "./psmux.mjs";
|
|
11
|
+
|
|
12
|
+
const execFileAsync = promisify(execFile);
|
|
13
|
+
const ATTACH_HELPER = join(
|
|
14
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
15
|
+
"agents-row-attach.py",
|
|
16
|
+
);
|
|
17
|
+
const READ_ONLY_SUFFIX = " [read-only]";
|
|
18
|
+
|
|
19
|
+
function shellQuote(value) {
|
|
20
|
+
return `'${String(value).replace(/'/g, "'\\''")}'`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// daemon 이 띄운 셸의 PATH 는 호출자와 다를 수 있어 절대 경로로 넘긴다.
|
|
24
|
+
export function resolveExecutable(name, envPath = process.env.PATH || "") {
|
|
25
|
+
if (isAbsolute(name)) return name;
|
|
26
|
+
for (const dir of envPath.split(delimiter)) {
|
|
27
|
+
if (!dir) continue;
|
|
28
|
+
const candidate = join(dir, name);
|
|
29
|
+
try {
|
|
30
|
+
accessSync(candidate, constants.X_OK);
|
|
31
|
+
return candidate;
|
|
32
|
+
} catch {
|
|
33
|
+
/* 다음 후보 */
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return "";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** tmux target 을 서버 안에서 유일한 socket/session/pane ID 로 고정한다. */
|
|
40
|
+
export function resolveTmuxPane(target, { tmux = psmuxExec } = {}) {
|
|
41
|
+
const out = tmux([
|
|
42
|
+
"display-message",
|
|
43
|
+
"-p",
|
|
44
|
+
"-t",
|
|
45
|
+
String(target),
|
|
46
|
+
"#{socket_path}\t#{session_id}\t#{pane_id}\t#{session_name}",
|
|
47
|
+
]);
|
|
48
|
+
const [socketPath, sessionId, paneId, sessionName] = String(out)
|
|
49
|
+
.trim()
|
|
50
|
+
.split("\t");
|
|
51
|
+
if (!socketPath || !/^\$\d+$/u.test(sessionId) || !/^%\d+$/u.test(paneId)) {
|
|
52
|
+
throw new Error(`tmux pane을 확인할 수 없습니다: ${target}`);
|
|
53
|
+
}
|
|
54
|
+
return { socketPath, sessionId, paneId, sessionName };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 행의 백킹 명령. pane 이 등록한 세션에 그대로 있는 동안 attach 를 반복하고,
|
|
59
|
+
* 방이 사라지면 자기 행을 지운다. 대화형은 python3 중계로 터미널 응답 누수를 걸러 내고,
|
|
60
|
+
* 그 밖에는 읽기 전용 attach 로 입력 자체를 막는다.
|
|
61
|
+
*/
|
|
62
|
+
export function buildAgentsRowCommand({
|
|
63
|
+
tmuxBin,
|
|
64
|
+
claudeBin = "claude",
|
|
65
|
+
pane,
|
|
66
|
+
interactive = false,
|
|
67
|
+
python = "",
|
|
68
|
+
}) {
|
|
69
|
+
const q = shellQuote;
|
|
70
|
+
const tmux = `${q(tmuxBin)} -S ${q(pane.socketPath)}`;
|
|
71
|
+
const readOnly = !(interactive && python);
|
|
72
|
+
const attach = [
|
|
73
|
+
...(readOnly ? [] : [q(python), q(ATTACH_HELPER), "--"]),
|
|
74
|
+
"env -u TMUX -u TMUX_PANE",
|
|
75
|
+
tmux,
|
|
76
|
+
`attach-session -t ${q(pane.paneId)}`,
|
|
77
|
+
// -r 은 read-only,ignore-size 와 같고 tmux 3.2 이전에도 있다.
|
|
78
|
+
...(readOnly ? ["-r"] : []),
|
|
79
|
+
].join(" ");
|
|
80
|
+
// 같은 번호의 다른 방에 붙지 않도록 pane 의 세션 ID 를 매번 대조한다.
|
|
81
|
+
const alive = `[ "$(${tmux} display-message -p -t ${q(pane.paneId)} '#{session_id}' 2>/dev/null)" = ${q(pane.sessionId)} ]`;
|
|
82
|
+
// CLAUDE_JOB_DIR 의 끝이 이 job 의 short id 다. job 이 끝난 뒤 지우도록 nohup 으로 띄운다.
|
|
83
|
+
const removeSelf = `nohup sh -c 'sleep 1; exec "$0" rm "\${CLAUDE_JOB_DIR##*/}"' ${q(claudeBin)} >/dev/null 2>&1 &`;
|
|
84
|
+
// attach 가 바로 끝나기를 5번 연달아 반복하면 고칠 수 없는 실패로 보고 멈춘다.
|
|
85
|
+
const loop = `n=0; while ${alive}; do t=$(date +%s); ${attach}; if [ $(($(date +%s) - t)) -lt 2 ]; then n=$((n + 1)); else n=0; fi; [ "$n" -ge 5 ] && break; sleep 1; done`;
|
|
86
|
+
return { readOnly, command: `${loop}; ${removeSelf}` };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// macOS 에는 CLT 설치 안내만 띄우는 /usr/bin/python3 스텁이 있어 실제로 한 번 돌려 본다.
|
|
90
|
+
async function usablePython(resolve, run) {
|
|
91
|
+
const python = resolve("python3");
|
|
92
|
+
if (!python) return "";
|
|
93
|
+
try {
|
|
94
|
+
await run(python, ["-c", "import pty"], { timeout: 5_000 });
|
|
95
|
+
return python;
|
|
96
|
+
} catch {
|
|
97
|
+
return "";
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function parseBackgroundShort(stdout) {
|
|
102
|
+
const match = /backgrounded · ([0-9a-f]{6,}) ·/u.exec(String(stdout));
|
|
103
|
+
return match ? match[1] : "";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 워커 pane 하나를 `claude agents` 행으로 연다.
|
|
108
|
+
* @returns {Promise<{short:string, name:string, readOnly:boolean, pane:object}>}
|
|
109
|
+
*/
|
|
110
|
+
export async function openAgentsRow({
|
|
111
|
+
name,
|
|
112
|
+
target,
|
|
113
|
+
cwd = process.cwd(),
|
|
114
|
+
interactive = false,
|
|
115
|
+
_deps = {},
|
|
116
|
+
} = {}) {
|
|
117
|
+
if (!name) throw new Error("name is required");
|
|
118
|
+
const tmux = _deps.tmux || psmuxExec;
|
|
119
|
+
const run = _deps.execFile || execFileAsync;
|
|
120
|
+
const resolve = _deps.resolveExecutable || resolveExecutable;
|
|
121
|
+
const pane = resolveTmuxPane(target, { tmux });
|
|
122
|
+
const tmuxBin = resolve(getMultiplexerType()) || getMultiplexerType();
|
|
123
|
+
const claudeBin = _deps.claudeBin || resolve("claude") || "claude";
|
|
124
|
+
const python = interactive ? await usablePython(resolve, run) : "";
|
|
125
|
+
const { command, readOnly } = buildAgentsRowCommand({
|
|
126
|
+
tmuxBin,
|
|
127
|
+
claudeBin,
|
|
128
|
+
pane,
|
|
129
|
+
interactive,
|
|
130
|
+
python,
|
|
131
|
+
});
|
|
132
|
+
// 대화형인데 읽기 전용으로 떨어졌으면 행 이름에서 바로 보이게 한다.
|
|
133
|
+
const rowName = interactive && readOnly ? `${name}${READ_ONLY_SUFFIX}` : name;
|
|
134
|
+
const { stdout = "", stderr = "" } = await run(
|
|
135
|
+
claudeBin,
|
|
136
|
+
["--bg", "--name", rowName, "--exec", command],
|
|
137
|
+
{ cwd, timeout: 20_000, encoding: "utf8" },
|
|
138
|
+
);
|
|
139
|
+
const short = parseBackgroundShort(`${stdout}\n${stderr}`);
|
|
140
|
+
if (!short) {
|
|
141
|
+
throw new Error(`claude --bg 출력에서 id를 찾지 못했습니다: ${stdout}`);
|
|
142
|
+
}
|
|
143
|
+
return { short, name: rowName, readOnly, pane };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** 행을 지운다. rm 은 실행 중인 job 도 멈춘다. 워커 tmux 방은 건드리지 않는다. */
|
|
147
|
+
export async function closeAgentsRow(short, { _deps = {} } = {}) {
|
|
148
|
+
if (!short) return;
|
|
149
|
+
const run = _deps.execFile || execFileAsync;
|
|
150
|
+
await run(_deps.claudeBin || "claude", ["rm", short], {
|
|
151
|
+
timeout: 10_000,
|
|
152
|
+
}).catch(() => {});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** 실행이 연 행만 정확히 한 번 닫는다. */
|
|
156
|
+
export function createAgentsRowOwnership(close = closeAgentsRow) {
|
|
157
|
+
const shorts = new Set();
|
|
158
|
+
return {
|
|
159
|
+
get size() {
|
|
160
|
+
return shorts.size;
|
|
161
|
+
},
|
|
162
|
+
add(short) {
|
|
163
|
+
if (short) shorts.add(short);
|
|
164
|
+
},
|
|
165
|
+
async release() {
|
|
166
|
+
const owned = [...shorts];
|
|
167
|
+
shorts.clear();
|
|
168
|
+
await Promise.all(owned.map((short) => close(short)));
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* tfx-live start 가 띄운 Claude 외 CLI 세션을 대화형 행으로 노출한다.
|
|
175
|
+
* Claude 세션은 원래 목록에 뜨므로 건너뛴다. 실패해도 start 는 성공으로 두고 경고만 돌려준다.
|
|
176
|
+
*/
|
|
177
|
+
export async function exposeLiveSession(
|
|
178
|
+
{ cli, session, name, cwd, remote, ready },
|
|
179
|
+
_deps,
|
|
180
|
+
) {
|
|
181
|
+
if (process.env.TFX_AGENTS_ROW === "0") return {};
|
|
182
|
+
if (remote || cli === "claude" || !ready) return {};
|
|
183
|
+
try {
|
|
184
|
+
const row = await openAgentsRow({
|
|
185
|
+
name: name || session,
|
|
186
|
+
target: session,
|
|
187
|
+
cwd,
|
|
188
|
+
interactive: true,
|
|
189
|
+
_deps,
|
|
190
|
+
});
|
|
191
|
+
return {
|
|
192
|
+
agentsRow: row.short,
|
|
193
|
+
...(row.readOnly
|
|
194
|
+
? { agentsRowWarning: "python3 not found; row is read-only" }
|
|
195
|
+
: {}),
|
|
196
|
+
};
|
|
197
|
+
} catch (error) {
|
|
198
|
+
return { agentsRowWarning: error?.message || String(error) };
|
|
199
|
+
}
|
|
200
|
+
}
|
package/hub/team/ansi.mjs
CHANGED
|
@@ -21,9 +21,6 @@ export function moveTo(row, col) {
|
|
|
21
21
|
export function moveUp(n = 1) {
|
|
22
22
|
return `${ESC}[${n}A`;
|
|
23
23
|
}
|
|
24
|
-
export function moveDown(n = 1) {
|
|
25
|
-
return `${ESC}[${n}B`;
|
|
26
|
-
}
|
|
27
24
|
|
|
28
25
|
// ── 줄 제어 ──
|
|
29
26
|
export const clearLine = `${ESC}[2K`;
|
|
@@ -47,7 +44,7 @@ export const FG = {
|
|
|
47
44
|
gray: `${ESC}[90m`,
|
|
48
45
|
// triflux 브랜드
|
|
49
46
|
codex: `${ESC}[38;2;16;163;127m`, // #10a37f codex green
|
|
50
|
-
|
|
47
|
+
antigravity: `${ESC}[38;5;39m`, // blue
|
|
51
48
|
claude: `${ESC}[38;2;232;112;64m`, // orange
|
|
52
49
|
triflux: `${ESC}[38;5;214m`, // amber
|
|
53
50
|
accent: `${ESC}[38;5;75m`, // light blue (Catppuccin blue)
|
|
@@ -439,40 +436,9 @@ export function progressBar(percent, width = 20, time) {
|
|
|
439
436
|
return `${fillText}${emptyText}`;
|
|
440
437
|
}
|
|
441
438
|
|
|
442
|
-
// ── 애니메이션 진행률 바 (shimmer sweep) ──
|
|
443
|
-
export function animatedProgressBar(percent, width = 20, tick = 0) {
|
|
444
|
-
const ratio = Math.max(0, Math.min(100, percent)) / 100;
|
|
445
|
-
const filled = Math.round(ratio * width);
|
|
446
|
-
const empty = width - filled;
|
|
447
|
-
if (filled === 0 || percent >= 100) return progressBar(percent, width);
|
|
448
|
-
const baseClr = percent >= 50 ? MOCHA.partial : MOCHA.fail;
|
|
449
|
-
const pos = tick % (filled + 3);
|
|
450
|
-
let bar = "";
|
|
451
|
-
for (let i = 0; i < filled; i++) {
|
|
452
|
-
const d = Math.abs(i - pos);
|
|
453
|
-
if (d === 0) bar += `${ESC}[97m█`;
|
|
454
|
-
else if (d === 1) bar += `${baseClr}▓`;
|
|
455
|
-
else bar += `${baseClr}█`;
|
|
456
|
-
}
|
|
457
|
-
return `${bar}${MOCHA.border}${"░".repeat(empty)}${RESET}`;
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
// ── 상태 아이콘 ──
|
|
461
439
|
export const STATUS_ICON = {
|
|
462
440
|
running: `${MOCHA.partial}⏳${RESET}`,
|
|
463
441
|
completed: `${MOCHA.ok}✓${RESET}`,
|
|
464
442
|
failed: `${MOCHA.fail}✗${RESET}`,
|
|
465
443
|
pending: `${FG.gray}⏸${RESET}`,
|
|
466
444
|
};
|
|
467
|
-
|
|
468
|
-
export const CLI_ICON = {
|
|
469
|
-
codex: `${FG.codex}⚪${RESET}`,
|
|
470
|
-
gemini: `${FG.gemini}🔵${RESET}`,
|
|
471
|
-
claude: `${FG.claude}🟠${RESET}`,
|
|
472
|
-
};
|
|
473
|
-
|
|
474
|
-
// ── 로딩 도트 (braille spinner) ──
|
|
475
|
-
const BRAILLE_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
476
|
-
export function loadingDots(tick = 0, clr = MOCHA.thinking) {
|
|
477
|
-
return `${clr}${BRAILLE_FRAMES[tick % BRAILLE_FRAMES.length]}${RESET}`;
|
|
478
|
-
}
|