triflux 10.9.19 → 10.9.21
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/CLAUDE.md +212 -0
- package/hub/lib/bash-path.mjs +73 -0
- package/hub/team/dashboard-open.mjs +1 -68
- package/hub/team/native-supervisor.mjs +9 -2
- package/hub/team/psmux.mjs +5 -13
- package/hub/team/session.mjs +6 -26
- package/hub/team/swarm-hypervisor.mjs +205 -27
- package/hub/team/synapse-http.mjs +1 -0
- package/hub/team/tui-core.mjs +292 -0
- package/hub/team/tui-lite.mjs +20 -154
- package/hub/team/tui-synapse.mjs +213 -0
- package/hub/team/tui-widgets.mjs +262 -0
- package/hub/team/tui.mjs +159 -255
- package/hub/workers/delegator-mcp.mjs +2 -2
- package/package.json +21 -62
- package/references/hosts.json +46 -0
- package/scripts/__tests__/keyword-detector.test.mjs +4 -4
- package/scripts/cross-review-gate.mjs +13 -0
- package/scripts/remote-spawn.mjs +11 -46
- package/scripts/session-spawn-helper.mjs +8 -21
- package/scripts/test-tfx-route-no-claude-native.mjs +4 -2
- package/scripts/tfx-route.sh +13 -0
- package/skills/tfx-deep-interview/SKILL.md +6 -6
- package/skills/tfx-deep-interview/SKILL.md.tmpl +6 -6
- package/skills/tfx-index/SKILL.md +1 -1
- package/skills/tfx-index/SKILL.md.tmpl +1 -1
- package/skills/tfx-interview/SKILL.md +9 -9
- package/skills/tfx-interview/SKILL.md.tmpl +9 -9
- package/skills/tfx-plan/SKILL.md +1 -1
- package/skills/tfx-plan/SKILL.md.tmpl +1 -1
- package/skills/tfx-research/SKILL.md +1 -1
- package/skills/tfx-research/SKILL.md.tmpl +1 -1
- package/skills/tfx-workspace/async-tests/run-tests.sh +203 -0
- package/skills/tfx-workspace/evals/evals.json +79 -0
- package/skills/tfx-workspace/iteration-1/benchmark.json +524 -0
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/eval_metadata.json +11 -0
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/grading.json +25 -0
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/outputs/analysis.md +154 -0
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/grading.json +25 -0
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/outputs/analysis.md +126 -0
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/eval_metadata.json +11 -0
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/grading.json +25 -0
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/outputs/analysis.md +119 -0
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/grading.json +25 -0
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/outputs/analysis.md +115 -0
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/eval_metadata.json +10 -0
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/grading.json +20 -0
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/outputs/analysis.md +86 -0
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/grading.json +20 -0
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/outputs/analysis.md +81 -0
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-1/multi-team-creation/eval_metadata.json +12 -0
- package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/grading.json +30 -0
- package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/outputs/analysis.md +316 -0
- package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/grading.json +30 -0
- package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/outputs/analysis.md +352 -0
- package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-1/review.html +1325 -0
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/eval_metadata.json +12 -0
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/grading.json +30 -0
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/outputs/analysis.md +97 -0
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/grading.json +30 -0
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/outputs/analysis.md +94 -0
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/eval_metadata.json +12 -0
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/grading.json +30 -0
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/outputs/analysis.md +209 -0
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/grading.json +30 -0
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/outputs/analysis.md +193 -0
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-2/benchmark.json +144 -0
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/eval_metadata.json +13 -0
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/grading.json +35 -0
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/outputs/analysis.md +382 -0
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/grading.json +35 -0
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/outputs/analysis.md +333 -0
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-2/review.html +1325 -0
- package/skills/tfx-workspace/skill-snapshot/tfx-auto/SKILL.md +217 -0
- package/skills/tfx-workspace/skill-snapshot/tfx-auto-codex/SKILL.md +77 -0
- package/skills/tfx-workspace/skill-snapshot/tfx-codex/SKILL.md +65 -0
- package/skills/tfx-workspace/skill-snapshot/tfx-doctor/SKILL.md +94 -0
- package/skills/tfx-workspace/skill-snapshot/tfx-gemini/SKILL.md +82 -0
- package/skills/tfx-workspace/skill-snapshot/tfx-hub/SKILL.md +133 -0
- package/skills/tfx-workspace/skill-snapshot/tfx-multi/SKILL.md +426 -0
- package/skills/tfx-workspace/skill-snapshot/tfx-setup/SKILL.md +101 -0
- package/.claude-plugin/marketplace.json +0 -34
- package/.claude-plugin/plugin.json +0 -22
- package/config/mcp-registry.json +0 -29
- package/scripts/__tests__/release-governance.test.mjs +0 -148
- package/scripts/release/bump-version.mjs +0 -77
- package/scripts/release/check-sync.mjs +0 -51
- package/scripts/release/lib.mjs +0 -303
- package/scripts/release/prepare.mjs +0 -85
- package/scripts/release/publish.mjs +0 -87
- package/scripts/release/verify.mjs +0 -81
- package/scripts/release/version-manifest.json +0 -26
- package/tui/codex-profile.mjs +0 -457
- package/tui/core.mjs +0 -266
- package/tui/doctor.mjs +0 -375
- package/tui/gemini-profile.mjs +0 -299
- package/tui/monitor-data.mjs +0 -152
- package/tui/monitor.mjs +0 -339
- package/tui/setup.mjs +0 -598
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// F4: File lease violation → revert worker changes, flag shard as failed
|
|
11
11
|
// F5: Merge conflict → retry integration with conflict resolution
|
|
12
12
|
|
|
13
|
+
import { execFile } from "node:child_process";
|
|
13
14
|
import { EventEmitter } from "node:events";
|
|
14
15
|
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
15
16
|
import { join } from "node:path";
|
|
@@ -66,6 +67,7 @@ const FAILURE_MODES = Object.freeze({
|
|
|
66
67
|
F3_STALL: "F3_stall",
|
|
67
68
|
F4_LEASE_VIOLATION: "F4_lease_violation",
|
|
68
69
|
F5_MERGE_CONFLICT: "F5_merge_conflict",
|
|
70
|
+
F6_NO_COMMIT: "F6_no_commit",
|
|
69
71
|
});
|
|
70
72
|
|
|
71
73
|
const FALLBACK_AGENTS = Object.freeze({
|
|
@@ -249,6 +251,138 @@ export function createSwarmHypervisor(opts) {
|
|
|
249
251
|
return integrationResult;
|
|
250
252
|
}
|
|
251
253
|
|
|
254
|
+
function git(args, cwd = workdir) {
|
|
255
|
+
return new Promise((resolve, reject) => {
|
|
256
|
+
execFile(
|
|
257
|
+
"git",
|
|
258
|
+
args,
|
|
259
|
+
{ cwd, windowsHide: true, timeout: 30_000 },
|
|
260
|
+
(err, stdout, stderr) => {
|
|
261
|
+
if (err) {
|
|
262
|
+
reject(
|
|
263
|
+
new Error(
|
|
264
|
+
`git ${args[0]} failed: ${stderr?.trim() || err.message}`,
|
|
265
|
+
),
|
|
266
|
+
);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
resolve(stdout.trim());
|
|
270
|
+
},
|
|
271
|
+
);
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function computeAuthoritativeStatus(shardName, workerEntry, sessions) {
|
|
276
|
+
const failureInfo = failures.get(shardName) || null;
|
|
277
|
+
if (failureInfo) {
|
|
278
|
+
return {
|
|
279
|
+
status: "failed",
|
|
280
|
+
reason: failureInfo.mode || failureInfo.reason || "failed",
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (results.has(shardName)) {
|
|
285
|
+
return { status: "done", reason: "integrated" };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (!sessions.length) {
|
|
289
|
+
if (completedShards.has(shardName)) {
|
|
290
|
+
return { status: "done", reason: "awaiting_integration" };
|
|
291
|
+
}
|
|
292
|
+
return { status: "running", reason: "no_sessions" };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const states = sessions.map((session) => session.state);
|
|
296
|
+
if (states.every((stateValue) => stateValue === STATES.COMPLETED)) {
|
|
297
|
+
return { status: "done", reason: "awaiting_integration" };
|
|
298
|
+
}
|
|
299
|
+
if (states.some((stateValue) => stateValue === STATES.INPUT_WAIT)) {
|
|
300
|
+
return { status: "blocked", reason: "user_input" };
|
|
301
|
+
}
|
|
302
|
+
if (states.some((stateValue) => stateValue === STATES.STALLED)) {
|
|
303
|
+
return { status: "stalled", reason: "health_probe_stall" };
|
|
304
|
+
}
|
|
305
|
+
if (
|
|
306
|
+
states.some(
|
|
307
|
+
(stateValue) =>
|
|
308
|
+
stateValue === STATES.FAILED || stateValue === STATES.DEAD,
|
|
309
|
+
)
|
|
310
|
+
) {
|
|
311
|
+
return { status: "failed", reason: "session_terminal" };
|
|
312
|
+
}
|
|
313
|
+
if (
|
|
314
|
+
states.some(
|
|
315
|
+
(stateValue) =>
|
|
316
|
+
stateValue === STATES.STARTING ||
|
|
317
|
+
stateValue === STATES.RESTARTING ||
|
|
318
|
+
stateValue === STATES.INIT,
|
|
319
|
+
)
|
|
320
|
+
) {
|
|
321
|
+
return { status: "running", reason: "starting" };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return { status: "running", reason: "healthy" };
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async function collectCommitEvidence(worker, integrationBranch) {
|
|
328
|
+
const branchName = worker?.branchName || null;
|
|
329
|
+
const evidence = {
|
|
330
|
+
branchName,
|
|
331
|
+
integrationBranch,
|
|
332
|
+
commitsAhead: 0,
|
|
333
|
+
dirty: false,
|
|
334
|
+
dirtyFiles: [],
|
|
335
|
+
headCommit: null,
|
|
336
|
+
ok: false,
|
|
337
|
+
error: null,
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
if (!branchName) {
|
|
341
|
+
evidence.error = "missing_branch_name";
|
|
342
|
+
return evidence;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
try {
|
|
346
|
+
evidence.commitsAhead =
|
|
347
|
+
Number.parseInt(
|
|
348
|
+
await git([
|
|
349
|
+
"rev-list",
|
|
350
|
+
"--count",
|
|
351
|
+
`${integrationBranch}..${branchName}`,
|
|
352
|
+
]),
|
|
353
|
+
10,
|
|
354
|
+
) || 0;
|
|
355
|
+
} catch (err) {
|
|
356
|
+
evidence.error = err.message;
|
|
357
|
+
return evidence;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
try {
|
|
361
|
+
evidence.headCommit = await git(["rev-parse", branchName]);
|
|
362
|
+
} catch {
|
|
363
|
+
/* best-effort */
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (worker?.worktreePath && !worker?.shardConfig?.host) {
|
|
367
|
+
try {
|
|
368
|
+
const rawStatus = await git(["status", "--short"], worker.worktreePath);
|
|
369
|
+
evidence.dirtyFiles = rawStatus
|
|
370
|
+
.split(/\r?\n/)
|
|
371
|
+
.map((line) => line.trim())
|
|
372
|
+
.filter(Boolean)
|
|
373
|
+
.map((line) => line.slice(2).trim())
|
|
374
|
+
.filter(Boolean);
|
|
375
|
+
evidence.dirty = evidence.dirtyFiles.length > 0;
|
|
376
|
+
} catch (err) {
|
|
377
|
+
evidence.error = evidence.error || err.message;
|
|
378
|
+
return evidence;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
evidence.ok = evidence.commitsAhead > 0 && evidence.dirty === false;
|
|
383
|
+
return evidence;
|
|
384
|
+
}
|
|
385
|
+
|
|
252
386
|
// ── Worker lifecycle ────────────────────────────────────────
|
|
253
387
|
|
|
254
388
|
function buildSessionConfig(shard) {
|
|
@@ -523,6 +657,7 @@ export function createSwarmHypervisor(opts) {
|
|
|
523
657
|
if (/stall|l1_stall|timeout/u.test(r)) return FAILURE_MODES.F3_STALL;
|
|
524
658
|
if (/lease|violation/u.test(r)) return FAILURE_MODES.F4_LEASE_VIOLATION;
|
|
525
659
|
if (/merge|conflict/u.test(r)) return FAILURE_MODES.F5_MERGE_CONFLICT;
|
|
660
|
+
if (/no.?commit|dirty_worktree/u.test(r)) return FAILURE_MODES.F6_NO_COMMIT;
|
|
526
661
|
return FAILURE_MODES.F1_CRASH;
|
|
527
662
|
}
|
|
528
663
|
|
|
@@ -603,10 +738,7 @@ export function createSwarmHypervisor(opts) {
|
|
|
603
738
|
if (shard?.host && shard._remoteEnv) {
|
|
604
739
|
const hostConfig = getHostConfig(shard.host, workdir);
|
|
605
740
|
const sshUser = hostConfig?.ssh_user || shard.host;
|
|
606
|
-
const remoteRepoPath = resolveRemoteDir(
|
|
607
|
-
workdir,
|
|
608
|
-
shard._remoteEnv,
|
|
609
|
-
);
|
|
741
|
+
const remoteRepoPath = resolveRemoteDir(workdir, shard._remoteEnv);
|
|
610
742
|
const fetchResult = await fetchRemoteShard({
|
|
611
743
|
host: shard.host,
|
|
612
744
|
sshUser,
|
|
@@ -630,6 +762,38 @@ export function createSwarmHypervisor(opts) {
|
|
|
630
762
|
});
|
|
631
763
|
}
|
|
632
764
|
|
|
765
|
+
// Read shard output log for changed files
|
|
766
|
+
const commitEvidence = await collectCommitEvidence(
|
|
767
|
+
worker,
|
|
768
|
+
integrationBranch,
|
|
769
|
+
);
|
|
770
|
+
worker.commitEvidence = commitEvidence;
|
|
771
|
+
eventLog.append("commit_evidence", {
|
|
772
|
+
shard: shardName,
|
|
773
|
+
...commitEvidence,
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
const expectsCommitEvidence =
|
|
777
|
+
Array.isArray(shard?.files) && shard.files.length > 0;
|
|
778
|
+
if (expectsCommitEvidence && !commitEvidence.ok) {
|
|
779
|
+
failures.set(shardName, {
|
|
780
|
+
mode: FAILURE_MODES.F6_NO_COMMIT,
|
|
781
|
+
reason: commitEvidence.error
|
|
782
|
+
? `no_commit_evidence:${commitEvidence.error}`
|
|
783
|
+
: commitEvidence.dirty
|
|
784
|
+
? "dirty_worktree_without_commit"
|
|
785
|
+
: "no_commit_evidence",
|
|
786
|
+
commitEvidence,
|
|
787
|
+
});
|
|
788
|
+
eventLog.append("no_commit_guard_failed", {
|
|
789
|
+
shard: shardName,
|
|
790
|
+
...commitEvidence,
|
|
791
|
+
});
|
|
792
|
+
await maybeCleanupWorktree(shardName, worker, shard);
|
|
793
|
+
integrationFailures.push(shardName);
|
|
794
|
+
continue;
|
|
795
|
+
}
|
|
796
|
+
|
|
633
797
|
// Read shard output log for changed files
|
|
634
798
|
const changedFiles = detectChangedFiles(shardName, worker);
|
|
635
799
|
|
|
@@ -856,6 +1020,7 @@ export function createSwarmHypervisor(opts) {
|
|
|
856
1020
|
|
|
857
1021
|
for (const [name, w] of workers) {
|
|
858
1022
|
const snap = w.conductor.getSnapshot();
|
|
1023
|
+
const authoritative = computeAuthoritativeStatus(name, w, snap);
|
|
859
1024
|
workerStatuses.push({
|
|
860
1025
|
shard: name,
|
|
861
1026
|
agent: w.shardConfig.agent,
|
|
@@ -863,6 +1028,9 @@ export function createSwarmHypervisor(opts) {
|
|
|
863
1028
|
failed: failures.has(name),
|
|
864
1029
|
failureInfo: failures.get(name) || null,
|
|
865
1030
|
integrated: results.has(name),
|
|
1031
|
+
authoritativeStatus: authoritative.status,
|
|
1032
|
+
authoritativeReason: authoritative.reason,
|
|
1033
|
+
commitEvidence: w.commitEvidence || null,
|
|
866
1034
|
});
|
|
867
1035
|
}
|
|
868
1036
|
|
|
@@ -901,25 +1069,30 @@ export function createSwarmHypervisor(opts) {
|
|
|
901
1069
|
let hubKeepaliveTimer = null;
|
|
902
1070
|
function startHubKeepalive() {
|
|
903
1071
|
// 5분마다 Hub /status 핑 (idle timeout 기본 10분)
|
|
904
|
-
hubKeepaliveTimer = setInterval(
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
}
|
|
911
|
-
} catch {
|
|
912
|
-
// Hub 다운 — 재시작 시도
|
|
913
|
-
if (ensureHubAliveFn) {
|
|
914
|
-
eventLog.append("hub_keepalive_restart", { reason: "fetch_failed" });
|
|
915
|
-
try {
|
|
1072
|
+
hubKeepaliveTimer = setInterval(
|
|
1073
|
+
async () => {
|
|
1074
|
+
try {
|
|
1075
|
+
const resp = await fetch("http://127.0.0.1:27888/status");
|
|
1076
|
+
if (!resp.ok && ensureHubAliveFn) {
|
|
1077
|
+
eventLog.append("hub_keepalive_restart", {});
|
|
916
1078
|
await ensureHubAliveFn();
|
|
917
|
-
}
|
|
918
|
-
|
|
1079
|
+
}
|
|
1080
|
+
} catch {
|
|
1081
|
+
// Hub 다운 — 재시작 시도
|
|
1082
|
+
if (ensureHubAliveFn) {
|
|
1083
|
+
eventLog.append("hub_keepalive_restart", {
|
|
1084
|
+
reason: "fetch_failed",
|
|
1085
|
+
});
|
|
1086
|
+
try {
|
|
1087
|
+
await ensureHubAliveFn();
|
|
1088
|
+
} catch {
|
|
1089
|
+
eventLog.append("hub_restart_failed", {});
|
|
1090
|
+
}
|
|
919
1091
|
}
|
|
920
1092
|
}
|
|
921
|
-
}
|
|
922
|
-
|
|
1093
|
+
},
|
|
1094
|
+
5 * 60 * 1000,
|
|
1095
|
+
);
|
|
923
1096
|
}
|
|
924
1097
|
|
|
925
1098
|
function stopHubKeepalive() {
|
|
@@ -939,12 +1112,17 @@ export function createSwarmHypervisor(opts) {
|
|
|
939
1112
|
|
|
940
1113
|
// Hub alive 확인 — 죽어있으면 재시작
|
|
941
1114
|
if (ensureHubAliveFn) {
|
|
942
|
-
ensureHubAliveFn()
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
1115
|
+
ensureHubAliveFn()
|
|
1116
|
+
.then((hub) => {
|
|
1117
|
+
eventLog.append("hub_ensured", { port: hub?.port });
|
|
1118
|
+
})
|
|
1119
|
+
.catch((err) => {
|
|
1120
|
+
eventLog.append("hub_ensure_failed", { error: err.message });
|
|
1121
|
+
emitter.emit("warning", {
|
|
1122
|
+
type: "hub_unavailable",
|
|
1123
|
+
error: err.message,
|
|
1124
|
+
});
|
|
1125
|
+
});
|
|
948
1126
|
}
|
|
949
1127
|
|
|
950
1128
|
// Warn about file conflicts but don't block
|
|
@@ -1007,7 +1185,7 @@ export function createSwarmHypervisor(opts) {
|
|
|
1007
1185
|
`${launched.size} launched, ${pending.size} pending deps`,
|
|
1008
1186
|
);
|
|
1009
1187
|
|
|
1010
|
-
return getStatus();
|
|
1188
|
+
return { ...getStatus(), done: integrationPromise };
|
|
1011
1189
|
}
|
|
1012
1190
|
|
|
1013
1191
|
/**
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const DEFAULT_SYNAPSE_BASE_URL = "http://127.0.0.1:27888";
|
|
2
2
|
|
|
3
3
|
function resolveSynapseFetch(fetchImpl) {
|
|
4
|
+
if (fetchImpl === null) return null;
|
|
4
5
|
if (typeof fetchImpl === "function") return fetchImpl;
|
|
5
6
|
if (typeof globalThis.fetch === "function") {
|
|
6
7
|
return globalThis.fetch.bind(globalThis);
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
// hub/team/tui-core.mjs — 대시보드 공통 유틸리티 (ISSUE-11)
|
|
2
|
+
// tui.mjs / tui-lite.mjs 간 중복 로직을 단일 모듈로 통합.
|
|
3
|
+
|
|
4
|
+
import { FG, MOCHA, wcswidth } from "./ansi.mjs";
|
|
5
|
+
|
|
6
|
+
// ── 상수 ──────────────────────────────────────────────────────────────────
|
|
7
|
+
export const FALLBACK_COLUMNS = 100;
|
|
8
|
+
export const FALLBACK_ROWS = 30;
|
|
9
|
+
export const VALID_TABS = Object.freeze(["log", "detail", "files"]);
|
|
10
|
+
|
|
11
|
+
// ── 버전 로드 ─────────────────────────────────────────────────────────────
|
|
12
|
+
let _cachedVersion = null;
|
|
13
|
+
export async function loadVersion(fallback = "7.x") {
|
|
14
|
+
if (_cachedVersion) return _cachedVersion;
|
|
15
|
+
try {
|
|
16
|
+
const { createRequire } = await import("node:module");
|
|
17
|
+
const require = createRequire(import.meta.url);
|
|
18
|
+
_cachedVersion = require("../../package.json").version;
|
|
19
|
+
} catch {
|
|
20
|
+
_cachedVersion = fallback;
|
|
21
|
+
}
|
|
22
|
+
return _cachedVersion;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ── 수학 유틸 ─────────────────────────────────────────────────────────────
|
|
26
|
+
export function clamp(value, min, max) {
|
|
27
|
+
return Math.min(max, Math.max(min, value));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ── 텍스트 정규화 ─────────────────────────────────────────────────────────
|
|
31
|
+
export function stripCodeBlocks(text) {
|
|
32
|
+
return (
|
|
33
|
+
String(text || "")
|
|
34
|
+
.replace(/\r/g, "")
|
|
35
|
+
.replace(/```[\s\S]*?(?:```|$)/g, "\n")
|
|
36
|
+
.replace(/^\s*```.*$/gm, "")
|
|
37
|
+
.replace(/^(?: {4}|\t).+$/gm, "")
|
|
38
|
+
.replace(/^(?:PS\s+\S[^\n]*?>|>\s+|\$\s+)[^\n]*/gm, "")
|
|
39
|
+
.trim()
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function sanitizeTextBlock(text, rawMode = false) {
|
|
44
|
+
const normalized = rawMode
|
|
45
|
+
? String(text || "").replace(/\r/g, "")
|
|
46
|
+
: stripCodeBlocks(text);
|
|
47
|
+
return normalized
|
|
48
|
+
.split("\n")
|
|
49
|
+
.map((line) => line.trim())
|
|
50
|
+
.filter(Boolean)
|
|
51
|
+
.filter((line) => line !== "--- HANDOFF ---")
|
|
52
|
+
.join("\n")
|
|
53
|
+
.trim();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function sanitizeOneLine(text, fallback = "") {
|
|
57
|
+
return sanitizeTextBlock(text).replace(/\s+/g, " ").trim() || fallback;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function sanitizeFiles(files) {
|
|
61
|
+
if (!files) return [];
|
|
62
|
+
const raw = Array.isArray(files) ? files : String(files).split(",");
|
|
63
|
+
return raw.map((e) => sanitizeOneLine(e)).filter(Boolean);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function sanitizeFindings(findings) {
|
|
67
|
+
if (!findings) return [];
|
|
68
|
+
const raw = Array.isArray(findings)
|
|
69
|
+
? findings
|
|
70
|
+
: sanitizeTextBlock(findings).split("\n");
|
|
71
|
+
return raw.map((e) => sanitizeOneLine(e)).filter(Boolean);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function normalizeTokens(tokens) {
|
|
75
|
+
if (tokens === null || tokens === undefined || tokens === "") return "";
|
|
76
|
+
if (typeof tokens === "number" && Number.isFinite(tokens)) return tokens;
|
|
77
|
+
const raw = sanitizeOneLine(tokens);
|
|
78
|
+
if (!raw) return "";
|
|
79
|
+
const match = raw.match(/(\d+(?:[.,]\d+)?\s*[kKmM]?)/);
|
|
80
|
+
return match ? match[1].replace(/\s+/g, "").toLowerCase() : raw;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function formatTokens(tokens) {
|
|
84
|
+
if (tokens === null || tokens === undefined || tokens === "") return "n/a";
|
|
85
|
+
if (typeof tokens === "number" && Number.isFinite(tokens)) {
|
|
86
|
+
if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(1)}m`;
|
|
87
|
+
if (tokens >= 1_000) return `${(tokens / 1_000).toFixed(1)}k`;
|
|
88
|
+
return `${tokens}`;
|
|
89
|
+
}
|
|
90
|
+
return String(tokens);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── 워커 상태 ─────────────────────────────────────────────────────────────
|
|
94
|
+
export function runtimeStatus(worker) {
|
|
95
|
+
return worker?.handoff?.status || worker?.status || "pending";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 워커 상태 정규화 (tui.mjs / tui-lite.mjs 공통)
|
|
100
|
+
* @param {object} existing - 기존 워커 상태
|
|
101
|
+
* @param {object} state - 새 상태 패치
|
|
102
|
+
* @param {object} [opts]
|
|
103
|
+
* @param {boolean} [opts.trackChanges=false] - _prevStatus/_statusChangedAt 추적 (full TUI용)
|
|
104
|
+
* @param {function} [opts.now=Date.now] - 시간 함수
|
|
105
|
+
*/
|
|
106
|
+
export function normalizeWorkerState(existing = {}, state = {}, opts = {}) {
|
|
107
|
+
const { trackChanges = false, now = Date.now } = opts;
|
|
108
|
+
|
|
109
|
+
const nextHandoff =
|
|
110
|
+
state.handoff === undefined
|
|
111
|
+
? existing.handoff
|
|
112
|
+
: {
|
|
113
|
+
...(existing.handoff || {}),
|
|
114
|
+
...(state.handoff || {}),
|
|
115
|
+
verdict:
|
|
116
|
+
state.handoff?.verdict !== undefined
|
|
117
|
+
? sanitizeOneLine(state.handoff.verdict)
|
|
118
|
+
: existing.handoff?.verdict,
|
|
119
|
+
confidence:
|
|
120
|
+
state.handoff?.confidence !== undefined
|
|
121
|
+
? sanitizeOneLine(state.handoff.confidence)
|
|
122
|
+
: existing.handoff?.confidence,
|
|
123
|
+
status:
|
|
124
|
+
state.handoff?.status !== undefined
|
|
125
|
+
? sanitizeOneLine(state.handoff.status)
|
|
126
|
+
: existing.handoff?.status,
|
|
127
|
+
files_changed:
|
|
128
|
+
state.handoff?.files_changed !== undefined
|
|
129
|
+
? sanitizeFiles(state.handoff.files_changed)
|
|
130
|
+
: existing.handoff?.files_changed,
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const merged = {
|
|
134
|
+
...existing,
|
|
135
|
+
...state,
|
|
136
|
+
cli:
|
|
137
|
+
state.cli !== undefined
|
|
138
|
+
? sanitizeOneLine(state.cli, existing.cli || "codex")
|
|
139
|
+
: existing.cli || "codex",
|
|
140
|
+
role:
|
|
141
|
+
state.role !== undefined ? sanitizeOneLine(state.role) : existing.role,
|
|
142
|
+
status:
|
|
143
|
+
state.status !== undefined
|
|
144
|
+
? sanitizeOneLine(state.status, existing.status || "pending")
|
|
145
|
+
: existing.status || "pending",
|
|
146
|
+
snapshot:
|
|
147
|
+
state.snapshot !== undefined
|
|
148
|
+
? sanitizeTextBlock(state.snapshot)
|
|
149
|
+
: existing.snapshot,
|
|
150
|
+
summary:
|
|
151
|
+
state.summary !== undefined
|
|
152
|
+
? sanitizeTextBlock(state.summary)
|
|
153
|
+
: existing.summary,
|
|
154
|
+
detail:
|
|
155
|
+
state.detail !== undefined
|
|
156
|
+
? sanitizeTextBlock(state.detail)
|
|
157
|
+
: existing.detail,
|
|
158
|
+
findings:
|
|
159
|
+
state.findings !== undefined
|
|
160
|
+
? sanitizeFindings(state.findings)
|
|
161
|
+
: existing.findings,
|
|
162
|
+
files_changed:
|
|
163
|
+
state.files_changed !== undefined
|
|
164
|
+
? sanitizeFiles(state.files_changed)
|
|
165
|
+
: existing.files_changed,
|
|
166
|
+
confidence:
|
|
167
|
+
state.confidence !== undefined
|
|
168
|
+
? sanitizeOneLine(state.confidence)
|
|
169
|
+
: existing.confidence,
|
|
170
|
+
tokens:
|
|
171
|
+
state.tokens !== undefined
|
|
172
|
+
? normalizeTokens(state.tokens)
|
|
173
|
+
: existing.tokens,
|
|
174
|
+
progress:
|
|
175
|
+
state.progress !== undefined
|
|
176
|
+
? clamp(Number(state.progress) || 0, 0, 1)
|
|
177
|
+
: existing.progress,
|
|
178
|
+
handoff: nextHandoff,
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
if (trackChanges) {
|
|
182
|
+
const statusChanged =
|
|
183
|
+
state.status !== undefined &&
|
|
184
|
+
sanitizeOneLine(state.status) !== existing.status;
|
|
185
|
+
merged._prevStatus = statusChanged
|
|
186
|
+
? existing.status
|
|
187
|
+
: existing._prevStatus;
|
|
188
|
+
merged._statusChangedAt = statusChanged
|
|
189
|
+
? now()
|
|
190
|
+
: existing._statusChangedAt || 0;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return merged;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ── 색상 헬퍼 ─────────────────────────────────────────────────────────────
|
|
197
|
+
export function cliColor(cli) {
|
|
198
|
+
if (cli === "gemini") return FG.gemini;
|
|
199
|
+
if (cli === "claude") return FG.claude;
|
|
200
|
+
if (cli === "codex") return FG.codex;
|
|
201
|
+
return FG.white;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function statusColor(status) {
|
|
205
|
+
if (status === "ok" || status === "completed") return MOCHA.ok;
|
|
206
|
+
if (status === "partial") return MOCHA.partial;
|
|
207
|
+
if (status === "failed") return MOCHA.fail;
|
|
208
|
+
if (status === "running" || status === "in_progress") return MOCHA.executing;
|
|
209
|
+
return FG.muted;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function countStatuses(names, workers) {
|
|
213
|
+
let ok = 0,
|
|
214
|
+
partial = 0,
|
|
215
|
+
failed = 0,
|
|
216
|
+
running = 0;
|
|
217
|
+
for (const name of names) {
|
|
218
|
+
const st = workers.get(name);
|
|
219
|
+
const s = runtimeStatus(st);
|
|
220
|
+
if (s === "ok" || s === "completed") ok++;
|
|
221
|
+
else if (s === "partial") partial++;
|
|
222
|
+
else if (s === "failed") failed++;
|
|
223
|
+
else if (s === "running" || s === "in_progress") running++;
|
|
224
|
+
}
|
|
225
|
+
return { ok, partial, failed, running };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ── 뷰포트 해상도 ────────────────────────────────────────────────────────
|
|
229
|
+
export function resolveViewportColumns(opts = {}) {
|
|
230
|
+
const { columns, stream } = opts;
|
|
231
|
+
const v = Number.isFinite(columns)
|
|
232
|
+
? columns
|
|
233
|
+
: Number.isFinite(stream?.columns)
|
|
234
|
+
? stream.columns
|
|
235
|
+
: Number.isFinite(process.stdout?.columns)
|
|
236
|
+
? process.stdout.columns
|
|
237
|
+
: FALLBACK_COLUMNS;
|
|
238
|
+
return Math.max(48, v || FALLBACK_COLUMNS);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function resolveViewportRows(opts = {}) {
|
|
242
|
+
const { rows, stream } = opts;
|
|
243
|
+
const v = Number.isFinite(rows)
|
|
244
|
+
? rows
|
|
245
|
+
: Number.isFinite(stream?.rows)
|
|
246
|
+
? stream.rows
|
|
247
|
+
: Number.isFinite(process.stdout?.rows)
|
|
248
|
+
? process.stdout.rows
|
|
249
|
+
: FALLBACK_ROWS;
|
|
250
|
+
return Math.max(10, v || FALLBACK_ROWS);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ── 텍스트 래핑 ───────────────────────────────────────────────────────────
|
|
254
|
+
export function wrapLine(text, width) {
|
|
255
|
+
const limit = Math.max(8, width);
|
|
256
|
+
const source = String(text || "").trim();
|
|
257
|
+
if (!source) return [""];
|
|
258
|
+
const words = source.split(/\s+/);
|
|
259
|
+
const lines = [];
|
|
260
|
+
let current = "";
|
|
261
|
+
for (const word of words) {
|
|
262
|
+
const candidate = current ? `${current} ${word}` : word;
|
|
263
|
+
if (wcswidth(candidate) <= limit) {
|
|
264
|
+
current = candidate;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (current) {
|
|
268
|
+
lines.push(current);
|
|
269
|
+
current = "";
|
|
270
|
+
}
|
|
271
|
+
if (wcswidth(word) <= limit) {
|
|
272
|
+
current = word;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
let offset = 0;
|
|
276
|
+
while (offset < word.length) {
|
|
277
|
+
lines.push(word.slice(offset, offset + limit));
|
|
278
|
+
offset += limit;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (current) lines.push(current);
|
|
282
|
+
return lines.length > 0 ? lines : [source.slice(0, limit)];
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function wrapText(text, width, rawMode = false) {
|
|
286
|
+
const input = sanitizeTextBlock(text, rawMode);
|
|
287
|
+
if (!input) return [];
|
|
288
|
+
return input
|
|
289
|
+
.split("\n")
|
|
290
|
+
.flatMap((line) => wrapLine(line, width))
|
|
291
|
+
.filter(Boolean);
|
|
292
|
+
}
|