triflux 10.2.0 → 10.3.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/README.md +236 -156
- package/hub/bridge.mjs +638 -290
- package/hub/codex-compat.mjs +1 -1
- package/hub/fullcycle.mjs +1 -1
- package/hub/intent.mjs +1 -0
- package/hub/lib/mcp-response-cache.mjs +205 -0
- package/hub/pipe.mjs +228 -119
- package/hub/reflexion.mjs +87 -13
- package/hub/research.mjs +1 -0
- package/hub/server.mjs +997 -611
- package/hub/team/ansi.mjs +1 -1
- package/hub/team/conductor-registry.mjs +121 -0
- package/hub/team/conductor.mjs +256 -125
- package/hub/team/execution-mode.mjs +105 -0
- package/hub/team/headless.mjs +686 -252
- package/hub/team/lead-control.mjs +91 -4
- package/hub/team/mcp-selector.mjs +145 -0
- package/hub/team/session-sync.mjs +153 -6
- package/hub/team/swarm-hypervisor.mjs +208 -86
- package/hub/team/tui-lite.mjs +18 -2
- package/hub/token-mode.mjs +1 -0
- package/hub/tools.mjs +474 -252
- package/package.json +5 -5
- package/scripts/codex-gateway-preflight.mjs +133 -0
- package/scripts/codex-mcp-gateway-sync.mjs +199 -0
- package/skills/star-prompt/SKILL.md +169 -69
- package/skills/tfx-setup/SKILL.md +124 -0
- package/skills/tfx-swarm/SKILL.md +124 -72
|
@@ -10,45 +10,92 @@
|
|
|
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 { EventEmitter } from
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
import { createConductor, STATES } from
|
|
18
|
-
import {
|
|
19
|
-
import { createEventLog } from
|
|
20
|
-
import { probeRemoteEnv, resolveRemoteDir } from
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
13
|
+
import { EventEmitter } from "node:events";
|
|
14
|
+
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { getHostConfig } from "../lib/ssh-command.mjs";
|
|
17
|
+
import { createConductor, STATES } from "./conductor.mjs";
|
|
18
|
+
import { ensureConductorRegistry } from "./conductor-registry.mjs";
|
|
19
|
+
import { createEventLog } from "./event-log.mjs";
|
|
20
|
+
import { probeRemoteEnv, resolveRemoteDir } from "./remote-session.mjs";
|
|
21
|
+
import { createSwarmLocks } from "./swarm-locks.mjs";
|
|
22
|
+
import { fetchRemoteShard } from "./worktree-lifecycle.mjs";
|
|
23
|
+
|
|
24
|
+
let importedCreateRegistry = null;
|
|
25
|
+
let meshRegistryImportError = null;
|
|
26
|
+
try {
|
|
27
|
+
({ createRegistry: importedCreateRegistry } = await import(
|
|
28
|
+
"../../mesh/mesh-registry.mjs"
|
|
29
|
+
));
|
|
30
|
+
} catch (err) {
|
|
31
|
+
meshRegistryImportError = err;
|
|
32
|
+
}
|
|
23
33
|
|
|
24
34
|
// ── Swarm states ──────────────────────────────────────────────
|
|
25
35
|
|
|
26
36
|
export const SWARM_STATES = Object.freeze({
|
|
27
|
-
PLANNING:
|
|
28
|
-
LAUNCHING:
|
|
29
|
-
RUNNING:
|
|
30
|
-
INTEGRATING:
|
|
31
|
-
VALIDATING:
|
|
32
|
-
COMPLETED:
|
|
33
|
-
FAILED:
|
|
37
|
+
PLANNING: "planning",
|
|
38
|
+
LAUNCHING: "launching",
|
|
39
|
+
RUNNING: "running",
|
|
40
|
+
INTEGRATING: "integrating",
|
|
41
|
+
VALIDATING: "validating",
|
|
42
|
+
COMPLETED: "completed",
|
|
43
|
+
FAILED: "failed",
|
|
34
44
|
});
|
|
35
45
|
|
|
36
46
|
// ── Failure mode classification ───────────────────────────────
|
|
37
47
|
|
|
38
48
|
const FAILURE_MODES = Object.freeze({
|
|
39
|
-
F1_CRASH:
|
|
40
|
-
F2_RATE_LIMIT:
|
|
41
|
-
F3_STALL:
|
|
42
|
-
F4_LEASE_VIOLATION:
|
|
43
|
-
F5_MERGE_CONFLICT:
|
|
49
|
+
F1_CRASH: "F1_crash",
|
|
50
|
+
F2_RATE_LIMIT: "F2_rate_limit",
|
|
51
|
+
F3_STALL: "F3_stall",
|
|
52
|
+
F4_LEASE_VIOLATION: "F4_lease_violation",
|
|
53
|
+
F5_MERGE_CONFLICT: "F5_merge_conflict",
|
|
44
54
|
});
|
|
45
55
|
|
|
46
56
|
const FALLBACK_AGENTS = Object.freeze({
|
|
47
|
-
codex:
|
|
48
|
-
gemini:
|
|
49
|
-
claude:
|
|
57
|
+
codex: "gemini",
|
|
58
|
+
gemini: "codex",
|
|
59
|
+
claude: "codex",
|
|
50
60
|
});
|
|
51
61
|
|
|
62
|
+
function createNoopRegistry() {
|
|
63
|
+
return Object.freeze({
|
|
64
|
+
register() {},
|
|
65
|
+
unregister() {},
|
|
66
|
+
discover() {
|
|
67
|
+
return [];
|
|
68
|
+
},
|
|
69
|
+
getAgent() {
|
|
70
|
+
return null;
|
|
71
|
+
},
|
|
72
|
+
listAll() {
|
|
73
|
+
return [];
|
|
74
|
+
},
|
|
75
|
+
clear() {},
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function createSharedRegistry(factory) {
|
|
80
|
+
if (typeof factory !== "function") {
|
|
81
|
+
return {
|
|
82
|
+
registry: createNoopRegistry(),
|
|
83
|
+
fallbackReason: meshRegistryImportError
|
|
84
|
+
? `mesh_import_failed:${meshRegistryImportError.message}`
|
|
85
|
+
: "mesh_registry_unavailable",
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
return { registry: factory(), fallbackReason: null };
|
|
91
|
+
} catch (err) {
|
|
92
|
+
return {
|
|
93
|
+
registry: createNoopRegistry(),
|
|
94
|
+
fallbackReason: `mesh_registry_init_failed:${err.message}`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
52
99
|
/**
|
|
53
100
|
* Create a swarm hypervisor.
|
|
54
101
|
* @param {object} opts
|
|
@@ -72,13 +119,18 @@ export function createSwarmHypervisor(opts) {
|
|
|
72
119
|
_deps = {},
|
|
73
120
|
} = opts;
|
|
74
121
|
|
|
75
|
-
if (!workdir) throw new Error(
|
|
76
|
-
if (!logsDir) throw new Error(
|
|
122
|
+
if (!workdir) throw new Error("workdir is required");
|
|
123
|
+
if (!logsDir) throw new Error("logsDir is required");
|
|
77
124
|
|
|
78
125
|
mkdirSync(logsDir, { recursive: true });
|
|
126
|
+
ensureConductorRegistry();
|
|
79
127
|
|
|
128
|
+
const createConductorImpl = _deps.createConductor || createConductor;
|
|
129
|
+
const createRegistryImpl = _deps.createRegistry || importedCreateRegistry;
|
|
80
130
|
const emitter = new EventEmitter();
|
|
81
|
-
const eventLog = createEventLog(join(logsDir,
|
|
131
|
+
const eventLog = createEventLog(join(logsDir, "swarm-events.jsonl"));
|
|
132
|
+
const { registry: sharedRegistry, fallbackReason: meshRegistryFallback } =
|
|
133
|
+
createSharedRegistry(createRegistryImpl);
|
|
82
134
|
|
|
83
135
|
let state = SWARM_STATES.PLANNING;
|
|
84
136
|
let plan = null;
|
|
@@ -90,16 +142,20 @@ export function createSwarmHypervisor(opts) {
|
|
|
90
142
|
/** @type {Map<string, { conductor, shardConfig }>} redundant workers for critical shards */
|
|
91
143
|
const redundantWorkers = new Map();
|
|
92
144
|
|
|
93
|
-
const results = new Map();
|
|
94
|
-
const failures = new Map();
|
|
145
|
+
const results = new Map(); // shardName → validated result
|
|
146
|
+
const failures = new Map(); // shardName → failure info
|
|
147
|
+
|
|
148
|
+
if (meshRegistryFallback) {
|
|
149
|
+
eventLog.append("mesh_registry_fallback", { reason: meshRegistryFallback });
|
|
150
|
+
}
|
|
95
151
|
|
|
96
152
|
// ── State machine ───────────────────────────────────────────
|
|
97
153
|
|
|
98
|
-
function setState(next, reason =
|
|
154
|
+
function setState(next, reason = "") {
|
|
99
155
|
const prev = state;
|
|
100
156
|
state = next;
|
|
101
|
-
eventLog.append(
|
|
102
|
-
emitter.emit(
|
|
157
|
+
eventLog.append("swarm_state", { from: prev, to: next, reason });
|
|
158
|
+
emitter.emit("stateChange", { from: prev, to: next, reason });
|
|
103
159
|
}
|
|
104
160
|
|
|
105
161
|
// ── Worker lifecycle ────────────────────────────────────────
|
|
@@ -130,7 +186,10 @@ export function createSwarmHypervisor(opts) {
|
|
|
130
186
|
}
|
|
131
187
|
|
|
132
188
|
function launchShard(shard, isRedundant = false) {
|
|
133
|
-
const shardLogsDir = join(
|
|
189
|
+
const shardLogsDir = join(
|
|
190
|
+
logsDir,
|
|
191
|
+
isRedundant ? `${shard.name}-redundant` : shard.name,
|
|
192
|
+
);
|
|
134
193
|
mkdirSync(shardLogsDir, { recursive: true });
|
|
135
194
|
|
|
136
195
|
// Remote shard: probe environment before conductor creation
|
|
@@ -138,24 +197,44 @@ export function createSwarmHypervisor(opts) {
|
|
|
138
197
|
try {
|
|
139
198
|
shard._remoteEnv = probeRemoteEnv(shard.host);
|
|
140
199
|
if (!shard._remoteEnv.claudePath) {
|
|
141
|
-
eventLog.append(
|
|
142
|
-
|
|
200
|
+
eventLog.append("remote_probe_no_claude", {
|
|
201
|
+
shard: shard.name,
|
|
202
|
+
host: shard.host,
|
|
203
|
+
});
|
|
204
|
+
failures.set(shard.name, {
|
|
205
|
+
mode: FAILURE_MODES.F1_CRASH,
|
|
206
|
+
reason: `claude not found on ${shard.host}`,
|
|
207
|
+
});
|
|
143
208
|
return null;
|
|
144
209
|
}
|
|
145
|
-
eventLog.append(
|
|
210
|
+
eventLog.append("remote_probe_ok", {
|
|
211
|
+
shard: shard.name,
|
|
212
|
+
host: shard.host,
|
|
213
|
+
env: shard._remoteEnv,
|
|
214
|
+
});
|
|
146
215
|
} catch (err) {
|
|
147
|
-
eventLog.append(
|
|
148
|
-
|
|
216
|
+
eventLog.append("remote_probe_failed", {
|
|
217
|
+
shard: shard.name,
|
|
218
|
+
host: shard.host,
|
|
219
|
+
error: err.message,
|
|
220
|
+
});
|
|
221
|
+
failures.set(shard.name, {
|
|
222
|
+
mode: FAILURE_MODES.F1_CRASH,
|
|
223
|
+
reason: `remote probe failed: ${err.message}`,
|
|
224
|
+
});
|
|
149
225
|
return null;
|
|
150
226
|
}
|
|
151
227
|
}
|
|
152
228
|
|
|
153
|
-
const conductor =
|
|
229
|
+
const conductor = createConductorImpl({
|
|
154
230
|
logsDir: shardLogsDir,
|
|
155
231
|
maxRestarts,
|
|
156
232
|
graceMs,
|
|
157
233
|
probeOpts,
|
|
158
|
-
|
|
234
|
+
meshRegistry: sharedRegistry,
|
|
235
|
+
enableMesh: true,
|
|
236
|
+
onCompleted: (sessionId) =>
|
|
237
|
+
handleShardCompleted(shard.name, sessionId, isRedundant),
|
|
159
238
|
});
|
|
160
239
|
|
|
161
240
|
const sessionConfig = buildSessionConfig(shard);
|
|
@@ -164,7 +243,7 @@ export function createSwarmHypervisor(opts) {
|
|
|
164
243
|
if (!isRedundant) {
|
|
165
244
|
const leaseResult = lockManager.acquire(shard.name, shard.files);
|
|
166
245
|
if (!leaseResult.ok) {
|
|
167
|
-
eventLog.append(
|
|
246
|
+
eventLog.append("lease_denied", {
|
|
168
247
|
shard: shard.name,
|
|
169
248
|
conflicts: leaseResult.conflicts,
|
|
170
249
|
});
|
|
@@ -178,7 +257,7 @@ export function createSwarmHypervisor(opts) {
|
|
|
178
257
|
|
|
179
258
|
conductor.spawnSession(sessionConfig);
|
|
180
259
|
|
|
181
|
-
eventLog.append(
|
|
260
|
+
eventLog.append("shard_launched", {
|
|
182
261
|
shard: shard.name,
|
|
183
262
|
agent: shard.agent,
|
|
184
263
|
sessionId: sessionConfig.id,
|
|
@@ -188,7 +267,12 @@ export function createSwarmHypervisor(opts) {
|
|
|
188
267
|
host: shard.host || null,
|
|
189
268
|
});
|
|
190
269
|
|
|
191
|
-
const entry = {
|
|
270
|
+
const entry = {
|
|
271
|
+
conductor,
|
|
272
|
+
shardConfig: shard,
|
|
273
|
+
sessionConfig,
|
|
274
|
+
startedAt: Date.now(),
|
|
275
|
+
};
|
|
192
276
|
|
|
193
277
|
if (isRedundant) {
|
|
194
278
|
redundantWorkers.set(shard.name, entry);
|
|
@@ -197,7 +281,7 @@ export function createSwarmHypervisor(opts) {
|
|
|
197
281
|
}
|
|
198
282
|
|
|
199
283
|
// Listen for dead events (F1/F2/F3)
|
|
200
|
-
conductor.on(
|
|
284
|
+
conductor.on("dead", ({ sessionId, reason }) => {
|
|
201
285
|
handleShardFailed(shard.name, sessionId, reason, isRedundant);
|
|
202
286
|
});
|
|
203
287
|
|
|
@@ -207,31 +291,35 @@ export function createSwarmHypervisor(opts) {
|
|
|
207
291
|
// ── Completion handling ─────────────────────────────────────
|
|
208
292
|
|
|
209
293
|
function handleShardCompleted(shardName, sessionId, isRedundant) {
|
|
210
|
-
eventLog.append(
|
|
294
|
+
eventLog.append("shard_completed", {
|
|
295
|
+
shard: shardName,
|
|
296
|
+
sessionId,
|
|
297
|
+
isRedundant,
|
|
298
|
+
});
|
|
211
299
|
|
|
212
300
|
if (isRedundant) {
|
|
213
301
|
// Redundant worker completed first — kill primary if still running
|
|
214
302
|
const primary = workers.get(shardName);
|
|
215
303
|
if (primary && !isTerminal(primary)) {
|
|
216
|
-
eventLog.append(
|
|
217
|
-
void primary.conductor.shutdown(
|
|
304
|
+
eventLog.append("redundant_wins", { shard: shardName });
|
|
305
|
+
void primary.conductor.shutdown("redundant_completed_first");
|
|
218
306
|
}
|
|
219
307
|
} else {
|
|
220
308
|
// Primary completed — kill redundant if exists
|
|
221
309
|
const redundant = redundantWorkers.get(shardName);
|
|
222
310
|
if (redundant) {
|
|
223
|
-
void redundant.conductor.shutdown(
|
|
311
|
+
void redundant.conductor.shutdown("primary_completed_first");
|
|
224
312
|
}
|
|
225
313
|
}
|
|
226
314
|
|
|
227
|
-
emitter.emit(
|
|
315
|
+
emitter.emit("shardCompleted", { shardName, sessionId, isRedundant });
|
|
228
316
|
checkAllShardsCompleted();
|
|
229
317
|
}
|
|
230
318
|
|
|
231
319
|
function handleShardFailed(shardName, sessionId, reason, isRedundant) {
|
|
232
320
|
const failureMode = classifyFailure(reason);
|
|
233
321
|
|
|
234
|
-
eventLog.append(
|
|
322
|
+
eventLog.append("shard_failed", {
|
|
235
323
|
shard: shardName,
|
|
236
324
|
sessionId,
|
|
237
325
|
reason,
|
|
@@ -247,7 +335,7 @@ export function createSwarmHypervisor(opts) {
|
|
|
247
335
|
if (shard) {
|
|
248
336
|
const fallbackAgent = FALLBACK_AGENTS[shard.agent];
|
|
249
337
|
if (fallbackAgent) {
|
|
250
|
-
eventLog.append(
|
|
338
|
+
eventLog.append("fallback_agent", {
|
|
251
339
|
shard: shardName,
|
|
252
340
|
from: shard.agent,
|
|
253
341
|
to: fallbackAgent,
|
|
@@ -263,7 +351,7 @@ export function createSwarmHypervisor(opts) {
|
|
|
263
351
|
failures.set(shardName, { mode: failureMode, reason, sessionId });
|
|
264
352
|
lockManager.release(shardName);
|
|
265
353
|
|
|
266
|
-
emitter.emit(
|
|
354
|
+
emitter.emit("shardFailed", { shardName, failureMode, reason });
|
|
267
355
|
checkAllShardsCompleted();
|
|
268
356
|
}
|
|
269
357
|
|
|
@@ -279,7 +367,9 @@ export function createSwarmHypervisor(opts) {
|
|
|
279
367
|
|
|
280
368
|
function isTerminal(entry) {
|
|
281
369
|
const snap = entry.conductor.getSnapshot();
|
|
282
|
-
return snap.every(
|
|
370
|
+
return snap.every(
|
|
371
|
+
(s) => s.state === STATES.COMPLETED || s.state === STATES.DEAD,
|
|
372
|
+
);
|
|
283
373
|
}
|
|
284
374
|
|
|
285
375
|
// ── Integration ─────────────────────────────────────────────
|
|
@@ -306,7 +396,7 @@ export function createSwarmHypervisor(opts) {
|
|
|
306
396
|
function validateResult(shardName, changedFiles) {
|
|
307
397
|
const violations = lockManager.validateChanges(shardName, changedFiles);
|
|
308
398
|
|
|
309
|
-
eventLog.append(
|
|
399
|
+
eventLog.append("validate_result", {
|
|
310
400
|
shard: shardName,
|
|
311
401
|
changedFiles,
|
|
312
402
|
violations,
|
|
@@ -324,14 +414,14 @@ export function createSwarmHypervisor(opts) {
|
|
|
324
414
|
* Uses git operations for conflict detection.
|
|
325
415
|
*/
|
|
326
416
|
async function integrateResults() {
|
|
327
|
-
setState(SWARM_STATES.INTEGRATING,
|
|
417
|
+
setState(SWARM_STATES.INTEGRATING, "all_shards_done");
|
|
328
418
|
|
|
329
419
|
const integrated = [];
|
|
330
420
|
const integrationFailures = [];
|
|
331
421
|
|
|
332
422
|
for (const shardName of plan.mergeOrder) {
|
|
333
423
|
if (failures.has(shardName)) {
|
|
334
|
-
eventLog.append(
|
|
424
|
+
eventLog.append("skip_failed_shard", { shard: shardName });
|
|
335
425
|
continue;
|
|
336
426
|
}
|
|
337
427
|
|
|
@@ -343,7 +433,10 @@ export function createSwarmHypervisor(opts) {
|
|
|
343
433
|
if (shard?.host && shard._remoteEnv) {
|
|
344
434
|
const hostConfig = getHostConfig(shard.host, config.rootDir);
|
|
345
435
|
const sshUser = hostConfig?.ssh_user || shard.host;
|
|
346
|
-
const remoteRepoPath = resolveRemoteDir(
|
|
436
|
+
const remoteRepoPath = resolveRemoteDir(
|
|
437
|
+
config.rootDir || process.cwd(),
|
|
438
|
+
shard._remoteEnv,
|
|
439
|
+
);
|
|
347
440
|
const fetchResult = await fetchRemoteShard({
|
|
348
441
|
host: shard.host,
|
|
349
442
|
sshUser,
|
|
@@ -353,11 +446,17 @@ export function createSwarmHypervisor(opts) {
|
|
|
353
446
|
});
|
|
354
447
|
|
|
355
448
|
if (!fetchResult.ok) {
|
|
356
|
-
eventLog.append(
|
|
449
|
+
eventLog.append("remote_fetch_failed", {
|
|
450
|
+
shard: shardName,
|
|
451
|
+
error: fetchResult.error,
|
|
452
|
+
});
|
|
357
453
|
integrationFailures.push(shardName);
|
|
358
454
|
continue;
|
|
359
455
|
}
|
|
360
|
-
eventLog.append(
|
|
456
|
+
eventLog.append("remote_fetch_ok", {
|
|
457
|
+
shard: shardName,
|
|
458
|
+
headCommit: fetchResult.headCommit,
|
|
459
|
+
});
|
|
361
460
|
}
|
|
362
461
|
|
|
363
462
|
// Read shard output log for changed files
|
|
@@ -370,7 +469,7 @@ export function createSwarmHypervisor(opts) {
|
|
|
370
469
|
mode: FAILURE_MODES.F4_LEASE_VIOLATION,
|
|
371
470
|
violations: validation.violations,
|
|
372
471
|
});
|
|
373
|
-
eventLog.append(
|
|
472
|
+
eventLog.append("lease_violation_revert", {
|
|
374
473
|
shard: shardName,
|
|
375
474
|
violations: validation.violations,
|
|
376
475
|
});
|
|
@@ -386,19 +485,24 @@ export function createSwarmHypervisor(opts) {
|
|
|
386
485
|
integrated.push(shardName);
|
|
387
486
|
}
|
|
388
487
|
|
|
389
|
-
eventLog.append(
|
|
488
|
+
eventLog.append("integration_complete", {
|
|
390
489
|
integrated,
|
|
391
490
|
failed: integrationFailures,
|
|
392
|
-
skipped: [...failures.keys()].filter(
|
|
491
|
+
skipped: [...failures.keys()].filter(
|
|
492
|
+
(n) => !integrationFailures.includes(n),
|
|
493
|
+
),
|
|
393
494
|
});
|
|
394
495
|
|
|
395
496
|
if (integrationFailures.length > 0 && integrated.length === 0) {
|
|
396
|
-
setState(SWARM_STATES.FAILED,
|
|
497
|
+
setState(SWARM_STATES.FAILED, "all_shards_failed_integration");
|
|
397
498
|
} else {
|
|
398
|
-
setState(
|
|
499
|
+
setState(
|
|
500
|
+
SWARM_STATES.COMPLETED,
|
|
501
|
+
`${integrated.length}/${plan.shards.length} integrated`,
|
|
502
|
+
);
|
|
399
503
|
}
|
|
400
504
|
|
|
401
|
-
emitter.emit(
|
|
505
|
+
emitter.emit("integrationComplete", {
|
|
402
506
|
integrated,
|
|
403
507
|
failed: integrationFailures,
|
|
404
508
|
results: [...results.values()],
|
|
@@ -419,11 +523,16 @@ export function createSwarmHypervisor(opts) {
|
|
|
419
523
|
const snap = worker.conductor.getSnapshot();
|
|
420
524
|
for (const session of snap) {
|
|
421
525
|
if (session.outPath && existsSync(session.outPath)) {
|
|
422
|
-
const output = readFileSync(session.outPath,
|
|
423
|
-
return extractFilePathsFromOutput(
|
|
526
|
+
const output = readFileSync(session.outPath, "utf8");
|
|
527
|
+
return extractFilePathsFromOutput(
|
|
528
|
+
output,
|
|
529
|
+
plan.leaseMap.get(shardName) || [],
|
|
530
|
+
);
|
|
424
531
|
}
|
|
425
532
|
}
|
|
426
|
-
} catch {
|
|
533
|
+
} catch {
|
|
534
|
+
/* best-effort */
|
|
535
|
+
}
|
|
427
536
|
|
|
428
537
|
// Fallback: trust the lease map (shard was allowed these files)
|
|
429
538
|
return plan.leaseMap.get(shardName) || [];
|
|
@@ -446,8 +555,8 @@ export function createSwarmHypervisor(opts) {
|
|
|
446
555
|
// Match common patterns
|
|
447
556
|
const patterns = [
|
|
448
557
|
/(?:wrote|created|modified|updated|edited)\s+['"]?([^\s'"]+\.\w+)/i,
|
|
449
|
-
/^[+-]{3}\s+[ab]\/(.+)/,
|
|
450
|
-
/^diff --git a\/(.+)\s+b\//,
|
|
558
|
+
/^[+-]{3}\s+[ab]\/(.+)/, // diff headers
|
|
559
|
+
/^diff --git a\/(.+)\s+b\//, // git diff headers
|
|
451
560
|
];
|
|
452
561
|
|
|
453
562
|
for (const re of patterns) {
|
|
@@ -458,9 +567,9 @@ export function createSwarmHypervisor(opts) {
|
|
|
458
567
|
|
|
459
568
|
// Intersect with allowed files if we found anything
|
|
460
569
|
if (found.size > 0) {
|
|
461
|
-
return [...found].filter((f) =>
|
|
462
|
-
(a) => f.endsWith(a) || a.endsWith(f) || f === a,
|
|
463
|
-
)
|
|
570
|
+
return [...found].filter((f) =>
|
|
571
|
+
allowedFiles.some((a) => f.endsWith(a) || a.endsWith(f) || f === a),
|
|
572
|
+
);
|
|
464
573
|
}
|
|
465
574
|
|
|
466
575
|
return allowedFiles;
|
|
@@ -515,9 +624,9 @@ export function createSwarmHypervisor(opts) {
|
|
|
515
624
|
|
|
516
625
|
// Warn about file conflicts but don't block
|
|
517
626
|
if (plan.conflicts.length > 0) {
|
|
518
|
-
eventLog.append(
|
|
519
|
-
emitter.emit(
|
|
520
|
-
type:
|
|
627
|
+
eventLog.append("file_conflicts_warning", { conflicts: plan.conflicts });
|
|
628
|
+
emitter.emit("warning", {
|
|
629
|
+
type: "file_conflicts",
|
|
521
630
|
conflicts: plan.conflicts,
|
|
522
631
|
});
|
|
523
632
|
}
|
|
@@ -525,7 +634,7 @@ export function createSwarmHypervisor(opts) {
|
|
|
525
634
|
// Initialize lock manager
|
|
526
635
|
lockManager = createSwarmLocks({
|
|
527
636
|
repoRoot: workdir,
|
|
528
|
-
persistPath: join(workdir,
|
|
637
|
+
persistPath: join(workdir, ".triflux", "swarm-locks.json"),
|
|
529
638
|
});
|
|
530
639
|
|
|
531
640
|
setState(SWARM_STATES.LAUNCHING, `${plan.shards.length} shards`);
|
|
@@ -561,11 +670,14 @@ export function createSwarmHypervisor(opts) {
|
|
|
561
670
|
launchReady();
|
|
562
671
|
|
|
563
672
|
// Re-check pending on each shard completion (dependency chains)
|
|
564
|
-
emitter.on(
|
|
673
|
+
emitter.on("shardCompleted", () => {
|
|
565
674
|
if (pending.size > 0) launchReady();
|
|
566
675
|
});
|
|
567
676
|
|
|
568
|
-
setState(
|
|
677
|
+
setState(
|
|
678
|
+
SWARM_STATES.RUNNING,
|
|
679
|
+
`${launched.size} launched, ${pending.size} pending deps`,
|
|
680
|
+
);
|
|
569
681
|
|
|
570
682
|
return getStatus();
|
|
571
683
|
}
|
|
@@ -574,8 +686,8 @@ export function createSwarmHypervisor(opts) {
|
|
|
574
686
|
* Graceful shutdown — kill all workers and release locks.
|
|
575
687
|
* @param {string} [reason]
|
|
576
688
|
*/
|
|
577
|
-
async function shutdown(reason =
|
|
578
|
-
eventLog.append(
|
|
689
|
+
async function shutdown(reason = "shutdown") {
|
|
690
|
+
eventLog.append("swarm_shutdown", { reason, state });
|
|
579
691
|
|
|
580
692
|
const shutdowns = [];
|
|
581
693
|
for (const [, w] of workers) {
|
|
@@ -587,6 +699,7 @@ export function createSwarmHypervisor(opts) {
|
|
|
587
699
|
|
|
588
700
|
await Promise.allSettled(shutdowns);
|
|
589
701
|
|
|
702
|
+
sharedRegistry.clear();
|
|
590
703
|
lockManager?.releaseAll();
|
|
591
704
|
await eventLog.flush();
|
|
592
705
|
await eventLog.close();
|
|
@@ -595,18 +708,27 @@ export function createSwarmHypervisor(opts) {
|
|
|
595
708
|
setState(SWARM_STATES.FAILED, reason);
|
|
596
709
|
}
|
|
597
710
|
|
|
598
|
-
emitter.emit(
|
|
711
|
+
emitter.emit("shutdown", { reason });
|
|
599
712
|
}
|
|
600
713
|
|
|
601
714
|
return Object.freeze({
|
|
602
715
|
launch,
|
|
603
716
|
shutdown,
|
|
604
717
|
getStatus,
|
|
718
|
+
getMeshRegistry() {
|
|
719
|
+
return sharedRegistry;
|
|
720
|
+
},
|
|
605
721
|
validateResult,
|
|
606
722
|
on: emitter.on.bind(emitter),
|
|
607
723
|
off: emitter.off.bind(emitter),
|
|
608
|
-
get state() {
|
|
609
|
-
|
|
610
|
-
|
|
724
|
+
get state() {
|
|
725
|
+
return state;
|
|
726
|
+
},
|
|
727
|
+
get plan() {
|
|
728
|
+
return plan;
|
|
729
|
+
},
|
|
730
|
+
get eventLogPath() {
|
|
731
|
+
return eventLog.filePath;
|
|
732
|
+
},
|
|
611
733
|
});
|
|
612
734
|
}
|
package/hub/team/tui-lite.mjs
CHANGED
|
@@ -128,7 +128,7 @@ function buildHeader(width, names, workers, pipeline, startedAt) {
|
|
|
128
128
|
else if (status === "running" || status === "in_progress") counts.running++;
|
|
129
129
|
}
|
|
130
130
|
const elapsed = Math.max(0, Math.round((Date.now() - startedAt) / 1000));
|
|
131
|
-
const line1 = color(` triflux ${VERSION} `, FG.
|
|
131
|
+
const line1 = color(` triflux ${VERSION} `, FG.white, BG.header)
|
|
132
132
|
+ ` ${bold(`phase ${pipeline.phase || "exec"}`)}`
|
|
133
133
|
+ ` ${dim(`+${elapsed}s`)} ${names.length} workers`;
|
|
134
134
|
const line2 = `${color(`ok ${counts.ok}`, MOCHA.ok)} ${color(`partial ${counts.partial}`, MOCHA.partial)} ${color(`failed ${counts.failed}`, MOCHA.fail)} ${color(`running ${counts.running}`, MOCHA.executing)}`;
|
|
@@ -210,6 +210,7 @@ export function createLiteDashboard(opts = {}) {
|
|
|
210
210
|
let focusTab = "log";
|
|
211
211
|
let helpVisible = false;
|
|
212
212
|
let prevFrame = [];
|
|
213
|
+
let prevWidth = 0;
|
|
213
214
|
let inputAttached = false;
|
|
214
215
|
let rawModeEnabled = false;
|
|
215
216
|
|
|
@@ -264,7 +265,14 @@ export function createLiteDashboard(opts = {}) {
|
|
|
264
265
|
return;
|
|
265
266
|
}
|
|
266
267
|
if (key === "\r" || key === "\n") {
|
|
267
|
-
|
|
268
|
+
if (typeof onOpenSelectedWorker !== "function") {
|
|
269
|
+
// 콜백 없으면 탭 순환 (기본 동작)
|
|
270
|
+
const tabs = ["log", "detail", "files"];
|
|
271
|
+
focusTab = tabs[(tabs.indexOf(focusTab) + 1) % tabs.length];
|
|
272
|
+
} else {
|
|
273
|
+
triggerOpenSelected();
|
|
274
|
+
}
|
|
275
|
+
render();
|
|
268
276
|
return;
|
|
269
277
|
}
|
|
270
278
|
if (key === "\x1b[13;2u" || key === "\x1b[27;13;2~" || key === "\x1b\r" || key === "\x1b\n") {
|
|
@@ -339,6 +347,13 @@ export function createLiteDashboard(opts = {}) {
|
|
|
339
347
|
if (isTTY) {
|
|
340
348
|
const width = viewportColumns();
|
|
341
349
|
const padded = rowsOut.map((line) => padRight(String(line ?? ""), width));
|
|
350
|
+
// Full redraw on first frame or terminal resize to avoid artifacts
|
|
351
|
+
if (prevFrame.length === 0 || width !== prevWidth) {
|
|
352
|
+
prevWidth = width;
|
|
353
|
+
write(cursorHome + padded.map((l) => l + clearToEnd).join("\n") + eraseBelow);
|
|
354
|
+
prevFrame = padded;
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
342
357
|
// Diff-based rendering: only rewrite lines that actually changed
|
|
343
358
|
let buf = "";
|
|
344
359
|
for (let i = 0; i < padded.length; i++) {
|
|
@@ -361,6 +376,7 @@ export function createLiteDashboard(opts = {}) {
|
|
|
361
376
|
if (rawModeEnabled && typeof input?.setRawMode === "function") input.setRawMode(false);
|
|
362
377
|
if (inputAttached && typeof input?.pause === "function") input.pause();
|
|
363
378
|
if (isTTY) write(cursorShow + altScreenOff);
|
|
379
|
+
prevFrame = [];
|
|
364
380
|
closed = true;
|
|
365
381
|
}
|
|
366
382
|
|