@triflux/remote 10.43.0 → 10.44.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/hygiene.mjs +1 -1
- package/hub/lib/cto-env.mjs +1 -0
- package/hub/pipe.mjs +7 -1
- package/hub/schema.sql +61 -2
- package/hub/server.mjs +104 -39
- package/hub/store.mjs +601 -9
- package/hub/team/conductor.mjs +23 -2
- package/hub/team/execution-mode.mjs +12 -7
- package/hub/team/headless.mjs +51 -15
- package/hub/team/intervention.mjs +1 -1
- package/hub/team/notify.mjs +31 -11
- package/hub/team/retry-state-machine.mjs +8 -3
- package/hub/team/swarm-hypervisor.mjs +4 -2
- package/hub/team/swarm-preflight.mjs +47 -0
- package/hub/team/worktree-lifecycle.mjs +56 -59
- package/hub/tools.mjs +13 -1
- package/hub/workers/codex-mcp.mjs +20 -4
- package/hub/workers/delegator-mcp.mjs +3 -49
- package/package.json +1 -1
- package/scripts/lib/agent-route-policy.mjs +307 -0
- package/scripts/lib/cli-codex.mjs +20 -160
- package/scripts/lib/codex-profile-config.mjs +29 -3
- package/scripts/lib/keyword-rules.mjs +12 -0
- package/cto/steward.mjs +0 -362
- package/hub/log-retention.mjs +0 -726
package/hub/team/conductor.mjs
CHANGED
|
@@ -19,6 +19,8 @@ import {
|
|
|
19
19
|
import { homedir } from "node:os";
|
|
20
20
|
import { dirname, join } from "node:path";
|
|
21
21
|
import { createRegistry } from "@triflux/core/mesh/mesh-registry.mjs";
|
|
22
|
+
import { resolveNestedCodexAgentProfile } from "../../scripts/lib/cli-codex.mjs";
|
|
23
|
+
import { codexProfileConfigOverrides } from "../../scripts/lib/codex-profile-config.mjs";
|
|
22
24
|
import { broker } from "@triflux/core/hub/account-broker.mjs";
|
|
23
25
|
import { runPreflight as runCodexPreflight } from "@triflux/core/hub/codex-preflight.mjs";
|
|
24
26
|
import {
|
|
@@ -608,7 +610,9 @@ export function createConductor(opts = {}) {
|
|
|
608
610
|
cli: session.config.agent,
|
|
609
611
|
prompt: session.config.prompt,
|
|
610
612
|
profile: session.config.profile,
|
|
613
|
+
role: session.config.role,
|
|
611
614
|
model: session.config.model,
|
|
615
|
+
env: session.config.env,
|
|
612
616
|
mcpServers: session.config.mcpServers,
|
|
613
617
|
excludeMcpServers,
|
|
614
618
|
resolveCommand: opts.deps?.resolveCliExecutable,
|
|
@@ -902,8 +906,25 @@ export function createConductor(opts = {}) {
|
|
|
902
906
|
} else if (agent === "gemini") {
|
|
903
907
|
remoteBin = "gemini -y";
|
|
904
908
|
} else {
|
|
905
|
-
|
|
906
|
-
|
|
909
|
+
const codexHome = session.config.env?.CODEX_HOME;
|
|
910
|
+
const profile = resolveNestedCodexAgentProfile(
|
|
911
|
+
session.config.role || "executor",
|
|
912
|
+
{
|
|
913
|
+
profileOverride: session.config.profile ?? "auto",
|
|
914
|
+
globalProfile:
|
|
915
|
+
session.config.env?.TFX_CODEX_PROFILE ??
|
|
916
|
+
process.env.TFX_CODEX_PROFILE,
|
|
917
|
+
codexHome,
|
|
918
|
+
},
|
|
919
|
+
);
|
|
920
|
+
const configArgs = codexProfileConfigOverrides(profile, {
|
|
921
|
+
codexHome,
|
|
922
|
+
disallowUltra: true,
|
|
923
|
+
enforceCanonicalProfile: true,
|
|
924
|
+
})
|
|
925
|
+
.map((override) => `-c '${override.replace(/'/g, `'\\''`)}'`)
|
|
926
|
+
.join(" ");
|
|
927
|
+
remoteBin = `codex exec${configArgs ? ` ${configArgs}` : ""} -s danger-full-access --dangerously-bypass-approvals-and-sandbox`;
|
|
907
928
|
}
|
|
908
929
|
|
|
909
930
|
// prompt는 stdin으로 전달 — 셸 이스케이프 문제 완전 회피
|
|
@@ -2,12 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { existsSync, readFileSync } from "node:fs";
|
|
4
4
|
import { dirname, resolve as pathResolve } from "node:path";
|
|
5
|
-
|
|
6
|
-
// scripts/lib is bundled into @triflux/remote itself (package.json files), so
|
|
7
|
-
// import the local mirror relatively rather than via @triflux/core. The core
|
|
8
|
-
// peer range (^10.0.0-alpha.1) could resolve to a build that predates this
|
|
9
|
-
// export, which would fail at import time; the bundled copy is version-skew
|
|
10
|
-
// safe and always carries the matching codexProfileConfigOverrides.
|
|
5
|
+
import { resolveNestedCodexAgentProfile } from "../../scripts/lib/cli-codex.mjs";
|
|
11
6
|
import { codexProfileConfigOverrides } from "../../scripts/lib/codex-profile-config.mjs";
|
|
12
7
|
import { whichCommand } from "@triflux/core/hub/platform.mjs";
|
|
13
8
|
|
|
@@ -165,7 +160,17 @@ export function buildSpawnSpecForMode(mode, opts = {}) {
|
|
|
165
160
|
// still contains an inline [profiles.X] table (which codex re-injects on
|
|
166
161
|
// config rewrite). These go straight to spawn argv (shell: false), so push
|
|
167
162
|
// the raw TOML-scalar overrides without shell-quoting.
|
|
168
|
-
|
|
163
|
+
const codexHome = opts.env?.CODEX_HOME;
|
|
164
|
+
const codexProfile = resolveNestedCodexAgentProfile(opts.role || "executor", {
|
|
165
|
+
profileOverride: opts.profile ?? "auto",
|
|
166
|
+
globalProfile: opts.env?.TFX_CODEX_PROFILE ?? process.env.TFX_CODEX_PROFILE,
|
|
167
|
+
codexHome,
|
|
168
|
+
});
|
|
169
|
+
for (const override of codexProfileConfigOverrides(codexProfile, {
|
|
170
|
+
codexHome,
|
|
171
|
+
disallowUltra: true,
|
|
172
|
+
enforceCanonicalProfile: true,
|
|
173
|
+
})) {
|
|
169
174
|
args.push("-c", override);
|
|
170
175
|
}
|
|
171
176
|
if (Array.isArray(opts.mcpServers)) {
|
package/hub/team/headless.mjs
CHANGED
|
@@ -20,6 +20,7 @@ import net from "node:net";
|
|
|
20
20
|
import { tmpdir } from "node:os";
|
|
21
21
|
import { dirname, join, resolve } from "node:path";
|
|
22
22
|
import { fileURLToPath } from "node:url";
|
|
23
|
+
import { resolveNestedCodexAgentProfile } from "../../scripts/lib/cli-codex.mjs";
|
|
23
24
|
import { requestJson } from "@triflux/core/hub/bridge.mjs";
|
|
24
25
|
import { escapePwshSingleQuoted } from "@triflux/core/hub/cli-adapter-base.mjs";
|
|
25
26
|
import { getMaxSpawnPerSec } from "@triflux/core/hub/lib/spawn-trace.mjs";
|
|
@@ -42,15 +43,15 @@ import {
|
|
|
42
43
|
import { startClaudeNativeBridge } from "./claude-native-bridge.mjs";
|
|
43
44
|
import { removeClaudeSessionProjection } from "./claude-session-projection.mjs";
|
|
44
45
|
import { resolveDashboardLayout } from "./dashboard-layout.mjs";
|
|
45
|
-
import {
|
|
46
|
-
createFileActivitySource,
|
|
47
|
-
createInterventionLadder,
|
|
48
|
-
} from "./intervention.mjs";
|
|
49
46
|
import {
|
|
50
47
|
formatHandoffForLead,
|
|
51
48
|
HANDOFF_INSTRUCTION_SHORT,
|
|
52
49
|
processHandoff,
|
|
53
50
|
} from "./handoff.mjs";
|
|
51
|
+
import {
|
|
52
|
+
createFileActivitySource,
|
|
53
|
+
createInterventionLadder,
|
|
54
|
+
} from "./intervention.mjs";
|
|
54
55
|
import {
|
|
55
56
|
capturePsmuxPane,
|
|
56
57
|
createPsmuxSession,
|
|
@@ -366,6 +367,14 @@ export function buildHeadlessCommand(cli, prompt, resultFile, opts = {}) {
|
|
|
366
367
|
writeFileSync(promptFile, fullPrompt, "utf8");
|
|
367
368
|
void IS_WINDOWS; // referenced for diagnostic guard chain below
|
|
368
369
|
|
|
370
|
+
const codexProfile =
|
|
371
|
+
resolvedCli === "codex"
|
|
372
|
+
? resolveNestedCodexAgentProfile(opts.role || cli, {
|
|
373
|
+
profileOverride: opts.profile ?? "auto",
|
|
374
|
+
globalProfile: process.env.TFX_CODEX_PROFILE,
|
|
375
|
+
codexHome: process.env.CODEX_HOME,
|
|
376
|
+
})
|
|
377
|
+
: undefined;
|
|
369
378
|
const backendCommand =
|
|
370
379
|
resolvedCli === "antigravity"
|
|
371
380
|
? buildRouteBackedHeadlessCommand(resolvedCli, promptFile, resultFile, {
|
|
@@ -375,6 +384,10 @@ export function buildHeadlessCommand(cli, prompt, resultFile, opts = {}) {
|
|
|
375
384
|
: getBackend(resolvedCli).buildArgs(fullPrompt, resultFile, {
|
|
376
385
|
...opts,
|
|
377
386
|
model,
|
|
387
|
+
profile: codexProfile,
|
|
388
|
+
codexHome: process.env.CODEX_HOME,
|
|
389
|
+
disallowUltra: true,
|
|
390
|
+
enforceCanonicalProfile: true,
|
|
378
391
|
promptFile,
|
|
379
392
|
});
|
|
380
393
|
const safeCwd =
|
|
@@ -571,7 +584,8 @@ export function createStallMonitor(paneId, resultFile, config, deps = {}) {
|
|
|
571
584
|
|
|
572
585
|
function createHeadlessIntervention(dispatch, fallback) {
|
|
573
586
|
return async (context) => {
|
|
574
|
-
if (typeof fallback === "function")
|
|
587
|
+
if (typeof fallback === "function")
|
|
588
|
+
return (await fallback(context)) === true;
|
|
575
589
|
|
|
576
590
|
const isDaemon = context.channel === "daemon";
|
|
577
591
|
const readActivitySignature = isDaemon
|
|
@@ -606,6 +620,8 @@ function createHeadlessIntervention(dispatch, fallback) {
|
|
|
606
620
|
},
|
|
607
621
|
readActivitySignature,
|
|
608
622
|
deps: {
|
|
623
|
+
// 이 트랙이 재실행 소유자다. pane 채널은 기존 completion wrapper를 다시
|
|
624
|
+
// dispatch하여 token/result 계약을 보존한다.
|
|
609
625
|
resumeHandler: async () => {
|
|
610
626
|
if (isDaemon || !dispatch.command) return { ok: false };
|
|
611
627
|
const restarted = dispatchCommand(
|
|
@@ -631,6 +647,7 @@ export function createActivityPollGuard({
|
|
|
631
647
|
const controller = new AbortController();
|
|
632
648
|
let lastMtime = 0;
|
|
633
649
|
let checking = false;
|
|
650
|
+
|
|
634
651
|
const finishIfTimedOut = async () => {
|
|
635
652
|
if (checking || controller.signal.aborted) return;
|
|
636
653
|
checking = true;
|
|
@@ -640,6 +657,7 @@ export function createActivityPollGuard({
|
|
|
640
657
|
checking = false;
|
|
641
658
|
}
|
|
642
659
|
};
|
|
660
|
+
|
|
643
661
|
return Object.freeze({
|
|
644
662
|
signal: controller.signal,
|
|
645
663
|
observe(content) {
|
|
@@ -989,6 +1007,7 @@ async function dispatchProgressive(sessionName, assignments, opts = {}) {
|
|
|
989
1007
|
mcp: assignment.mcp,
|
|
990
1008
|
role: assignment.role,
|
|
991
1009
|
model: assignment.model,
|
|
1010
|
+
profile: assignment.profile,
|
|
992
1011
|
cwd: assignment.cwd || assignment.workdir,
|
|
993
1012
|
},
|
|
994
1013
|
);
|
|
@@ -1071,6 +1090,7 @@ async function dispatchBatch(sessionName, assignments, opts = {}) {
|
|
|
1071
1090
|
mcp: assignment.mcp,
|
|
1072
1091
|
role: assignment.role,
|
|
1073
1092
|
model: assignment.model,
|
|
1093
|
+
profile: assignment.profile,
|
|
1074
1094
|
cwd: assignment.cwd || assignment.workdir,
|
|
1075
1095
|
},
|
|
1076
1096
|
);
|
|
@@ -1134,6 +1154,7 @@ async function dispatchDaemonBatch(sessionName, assignments, opts = {}) {
|
|
|
1134
1154
|
mcp: assignment.mcp,
|
|
1135
1155
|
role: assignment.role,
|
|
1136
1156
|
model: assignment.model,
|
|
1157
|
+
profile: assignment.profile,
|
|
1137
1158
|
cwd: assignment.cwd || assignment.workdir,
|
|
1138
1159
|
},
|
|
1139
1160
|
);
|
|
@@ -1306,12 +1327,11 @@ async function awaitAll(
|
|
|
1306
1327
|
{
|
|
1307
1328
|
pollInterval: stallOpts.pollInterval,
|
|
1308
1329
|
stallTimeout: stallOpts.stallTimeout,
|
|
1309
|
-
hardCeiling:
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
: (stallOpts.completionTimeout ?? timeoutSec * 1000),
|
|
1330
|
+
hardCeiling: lifecycle?.enabled
|
|
1331
|
+
? (stallOpts.hardCeiling ??
|
|
1332
|
+
stallOpts.completionTimeout ??
|
|
1333
|
+
lifecycle.hardCeilingMs)
|
|
1334
|
+
: (stallOpts.completionTimeout ?? timeoutSec * 1000),
|
|
1315
1335
|
maxRestarts: stallOpts.maxRestarts,
|
|
1316
1336
|
maxInterventions: stallOpts.maxInterventions,
|
|
1317
1337
|
command: d.command,
|
|
@@ -1443,7 +1463,10 @@ export async function waitForDaemonCompletion(
|
|
|
1443
1463
|
? createActivityLifecycle({
|
|
1444
1464
|
interventionMs: lifecycle.interventionMs,
|
|
1445
1465
|
hardCeilingMs: lifecycle.hardCeilingMs,
|
|
1446
|
-
onIntervene: createHeadlessIntervention(
|
|
1466
|
+
onIntervene: createHeadlessIntervention(
|
|
1467
|
+
dispatch,
|
|
1468
|
+
lifecycle.onIntervene,
|
|
1469
|
+
),
|
|
1447
1470
|
})
|
|
1448
1471
|
: null;
|
|
1449
1472
|
|
|
@@ -1873,11 +1896,24 @@ export async function runHeadless(sessionName, assignments, opts = {}) {
|
|
|
1873
1896
|
const idx = parseInt(match[1], 10) - 1;
|
|
1874
1897
|
const agentId = getHeadlessWorkerAgentId(sessionName, idx);
|
|
1875
1898
|
const nowMs = Date.now();
|
|
1876
|
-
if (
|
|
1899
|
+
if (
|
|
1900
|
+
(lastHubBeatByWorker.get(agentId) || 0) + HUB_ACTIVITY_MIN_INTERVAL_MS >
|
|
1901
|
+
nowMs
|
|
1902
|
+
)
|
|
1903
|
+
return;
|
|
1877
1904
|
lastHubBeatByWorker.set(agentId, nowMs);
|
|
1878
|
-
requestJson("/bridge/heartbeat", {
|
|
1905
|
+
requestJson("/bridge/heartbeat", {
|
|
1906
|
+
method: "POST",
|
|
1907
|
+
body: { agent_id: agentId },
|
|
1908
|
+
timeoutMs: 500,
|
|
1909
|
+
}).catch(() => {});
|
|
1879
1910
|
const assignJobId = normalizedAssignments[idx]?.assignJobId;
|
|
1880
|
-
if (assignJobId)
|
|
1911
|
+
if (assignJobId)
|
|
1912
|
+
requestJson("/bridge/assign/result", {
|
|
1913
|
+
method: "POST",
|
|
1914
|
+
body: { job_id: assignJobId, worker_agent: agentId, status: "running" },
|
|
1915
|
+
timeoutMs: 500,
|
|
1916
|
+
}).catch(() => {});
|
|
1881
1917
|
};
|
|
1882
1918
|
|
|
1883
1919
|
// onProgress 예외를 삼켜 실행 흐름 보호 (onPoll과 동일 패턴)
|
|
@@ -5,11 +5,11 @@ import { statSync } from "node:fs";
|
|
|
5
5
|
import { readdir, readlink } from "node:fs/promises";
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
7
|
import { isPidAlive } from "@triflux/core/hub/lib/process-utils.mjs";
|
|
8
|
-
import { killProcess } from "@triflux/core/hub/platform.mjs";
|
|
9
8
|
import {
|
|
10
9
|
resolveHardCeilingMs,
|
|
11
10
|
resolveStallInterventionMs as resolveStallThresholdMs,
|
|
12
11
|
} from "@triflux/core/hub/lib/timeout-defaults.mjs";
|
|
12
|
+
import { killProcess } from "@triflux/core/hub/platform.mjs";
|
|
13
13
|
|
|
14
14
|
export { resolveHardCeilingMs, resolveStallThresholdMs };
|
|
15
15
|
|
package/hub/team/notify.mjs
CHANGED
|
@@ -54,12 +54,17 @@ function normalizeEvent(event, defaults = {}) {
|
|
|
54
54
|
});
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
function defaultChannelConfig(name, env) {
|
|
57
|
+
function defaultChannelConfig(name, env, platform = process.platform) {
|
|
58
58
|
switch (name) {
|
|
59
59
|
case "bell":
|
|
60
|
-
return { enabled: true };
|
|
60
|
+
return { enabled: envFlag(env?.TRIFLUX_NOTIFY_BELL, true) };
|
|
61
61
|
case "toast":
|
|
62
|
-
return {
|
|
62
|
+
return {
|
|
63
|
+
// macOS `osascript display notification` can surface Script Editor
|
|
64
|
+
// authorization/UI prompts. Keep it opt-in there; Windows keeps the
|
|
65
|
+
// historical toast default, and other platforms skip as unsupported.
|
|
66
|
+
enabled: envFlag(env?.TRIFLUX_NOTIFY_TOAST, platform !== "darwin"),
|
|
67
|
+
};
|
|
63
68
|
case "webhook": {
|
|
64
69
|
const url = String(env?.TRIFLUX_NOTIFY_WEBHOOK || "");
|
|
65
70
|
return { enabled: Boolean(url), url };
|
|
@@ -69,8 +74,13 @@ function defaultChannelConfig(name, env) {
|
|
|
69
74
|
}
|
|
70
75
|
}
|
|
71
76
|
|
|
72
|
-
function
|
|
73
|
-
|
|
77
|
+
function envFlag(value, fallback) {
|
|
78
|
+
if (value === undefined || value === null || value === "") return fallback;
|
|
79
|
+
return !["0", "false", "no", "off"].includes(String(value).toLowerCase());
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function normalizeChannelConfig(name, value, env, platform) {
|
|
83
|
+
const base = defaultChannelConfig(name, env, platform);
|
|
74
84
|
const patch =
|
|
75
85
|
typeof value === "boolean"
|
|
76
86
|
? { enabled: value }
|
|
@@ -103,16 +113,21 @@ function normalizeChannelConfig(name, value, env) {
|
|
|
103
113
|
return freezeRecord(next);
|
|
104
114
|
}
|
|
105
115
|
|
|
106
|
-
function normalizeChannels(channels, env) {
|
|
116
|
+
function normalizeChannels(channels, env, platform) {
|
|
107
117
|
const source = channels && typeof channels === "object" ? channels : {};
|
|
108
118
|
const normalized = {};
|
|
109
119
|
for (const name of NOTIFY_CHANNELS) {
|
|
110
|
-
normalized[name] = normalizeChannelConfig(
|
|
120
|
+
normalized[name] = normalizeChannelConfig(
|
|
121
|
+
name,
|
|
122
|
+
source[name],
|
|
123
|
+
env,
|
|
124
|
+
platform,
|
|
125
|
+
);
|
|
111
126
|
}
|
|
112
127
|
return Object.freeze(normalized);
|
|
113
128
|
}
|
|
114
129
|
|
|
115
|
-
function updateChannelConfig(channels, channel, config, env) {
|
|
130
|
+
function updateChannelConfig(channels, channel, config, env, platform) {
|
|
116
131
|
if (!NOTIFY_CHANNELS.includes(channel)) {
|
|
117
132
|
throw new TypeError(`Unknown notify channel: ${channel}`);
|
|
118
133
|
}
|
|
@@ -126,6 +141,7 @@ function updateChannelConfig(channels, channel, config, env) {
|
|
|
126
141
|
...(typeof config === "boolean" ? { enabled: config } : config || {}),
|
|
127
142
|
},
|
|
128
143
|
env,
|
|
144
|
+
platform,
|
|
129
145
|
),
|
|
130
146
|
});
|
|
131
147
|
}
|
|
@@ -322,7 +338,7 @@ function createNotifierInstance(channels, deps) {
|
|
|
322
338
|
|
|
323
339
|
function setChannel(channel, config) {
|
|
324
340
|
return createNotifierInstance(
|
|
325
|
-
updateChannelConfig(channels, channel, config, deps.env),
|
|
341
|
+
updateChannelConfig(channels, channel, config, deps.env, deps.platform),
|
|
326
342
|
deps,
|
|
327
343
|
);
|
|
328
344
|
}
|
|
@@ -349,17 +365,21 @@ function createNotifierInstance(channels, deps) {
|
|
|
349
365
|
*/
|
|
350
366
|
export function createNotifier(opts = {}) {
|
|
351
367
|
const env = opts.env || process.env;
|
|
368
|
+
const platform = opts.platform || process.platform;
|
|
352
369
|
const deps = Object.freeze({
|
|
353
370
|
env,
|
|
354
371
|
stdout: opts.stdout || process.stdout,
|
|
355
372
|
execFile: opts.deps?.execFile || execFile,
|
|
356
373
|
fetch: opts.deps?.fetch || globalThis.fetch?.bind(globalThis),
|
|
357
|
-
platform
|
|
374
|
+
platform,
|
|
358
375
|
hostname: opts.hostname || os.hostname(),
|
|
359
376
|
powerShellCandidates: Object.freeze(
|
|
360
377
|
opts.deps?.powerShellCandidates || ["pwsh", "powershell.exe"],
|
|
361
378
|
),
|
|
362
379
|
});
|
|
363
380
|
|
|
364
|
-
return createNotifierInstance(
|
|
381
|
+
return createNotifierInstance(
|
|
382
|
+
normalizeChannels(opts.channels, env, platform),
|
|
383
|
+
deps,
|
|
384
|
+
);
|
|
365
385
|
}
|
|
@@ -41,13 +41,13 @@ export const MODES = Object.freeze({
|
|
|
41
41
|
const ESCALATION_CHAIN_CONFIG_PATH = ".triflux/config/escalation-chain.json";
|
|
42
42
|
|
|
43
43
|
// Escalation chain (2026-05-27 정책):
|
|
44
|
-
// 1. codex gpt-5.6-sol —
|
|
44
|
+
// 1. codex gpt-5.6-sol max — 최난도 단일 작업용 Codex escalation 단계
|
|
45
45
|
// 2. claude opus-4-8 — 최종 수단
|
|
46
46
|
const DEFAULT_ESCALATION_CHAIN = Object.freeze([
|
|
47
47
|
Object.freeze({
|
|
48
48
|
cli: "codex",
|
|
49
49
|
model: "gpt-5.6-sol",
|
|
50
|
-
profile: "
|
|
50
|
+
profile: "gpt56_sol_max",
|
|
51
51
|
}),
|
|
52
52
|
Object.freeze({ cli: "claude", model: "opus-4-8" }),
|
|
53
53
|
]);
|
|
@@ -295,7 +295,12 @@ function normalizeEscalationChainEntry(entry, index, source) {
|
|
|
295
295
|
model: entry.model,
|
|
296
296
|
};
|
|
297
297
|
if (typeof entry.profile === "string" && entry.profile.trim() !== "") {
|
|
298
|
-
|
|
298
|
+
const profile = entry.profile.trim();
|
|
299
|
+
normalized.profile =
|
|
300
|
+
entry.cli === "codex" &&
|
|
301
|
+
(profile === "ultra" || profile === "gpt56_sol_ultra")
|
|
302
|
+
? "gpt56_sol_max"
|
|
303
|
+
: profile;
|
|
299
304
|
}
|
|
300
305
|
return normalized;
|
|
301
306
|
}
|
|
@@ -20,8 +20,8 @@ import {
|
|
|
20
20
|
isDynamicRoutingEnabled,
|
|
21
21
|
} from "@triflux/core/hub/dynamic-routing-engine.mjs";
|
|
22
22
|
import { cleanupShardProcesses } from "@triflux/core/hub/lib/process-utils.mjs";
|
|
23
|
-
import { DEFAULT_SWARM_LOCK_TTL_MS } from "@triflux/core/hub/lib/timeout-defaults.mjs";
|
|
24
23
|
import { getHostConfig } from "@triflux/core/hub/lib/ssh-command.mjs";
|
|
24
|
+
import { DEFAULT_SWARM_LOCK_TTL_MS } from "@triflux/core/hub/lib/timeout-defaults.mjs";
|
|
25
25
|
import { buildWorkerPrompt } from "./build-worker-prompt.mjs";
|
|
26
26
|
import { registerSwarmShard } from "./claude-native-bridge.mjs";
|
|
27
27
|
import { createConductor, STATES } from "./conductor.mjs";
|
|
@@ -678,6 +678,7 @@ export function createSwarmHypervisor(opts) {
|
|
|
678
678
|
const config = {
|
|
679
679
|
id: buildSwarmSessionId(shard),
|
|
680
680
|
agent: shard.agent,
|
|
681
|
+
role: shard.role,
|
|
681
682
|
// #125: append Completion Protocol appendix so workers emit a
|
|
682
683
|
// sentinel-framed JSON payload that conductor can reliably capture.
|
|
683
684
|
prompt: buildWorkerPrompt(shard.prompt, {
|
|
@@ -1964,7 +1965,8 @@ export function createSwarmHypervisor(opts) {
|
|
|
1964
1965
|
lockRenewTimer = setInterval(() => {
|
|
1965
1966
|
if (!lockManager) return;
|
|
1966
1967
|
for (const shardName of workers.keys()) {
|
|
1967
|
-
if (!completedShards.has(shardName) && !failures.has(shardName))
|
|
1968
|
+
if (!completedShards.has(shardName) && !failures.has(shardName))
|
|
1969
|
+
lockManager.renew(shardName);
|
|
1968
1970
|
}
|
|
1969
1971
|
}, DEFAULT_SWARM_LOCK_TTL_MS / 2);
|
|
1970
1972
|
lockRenewTimer.unref?.();
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// hub/team/swarm-preflight.mjs — PRD swarm preflight report/gate (#188)
|
|
2
2
|
// Predicts launch-time blockers before any worker session or worktree spawn.
|
|
3
3
|
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
4
5
|
import { resolve } from "node:path";
|
|
5
6
|
import { readHosts, resolveHost } from "@triflux/core/hub/lib/hosts-compat.mjs";
|
|
6
7
|
import { whichCommand } from "@triflux/core/hub/platform.mjs";
|
|
@@ -83,6 +84,44 @@ function checkSensitive(plan, sensitiveDeny) {
|
|
|
83
84
|
return makeCheck("sensitiveDeny", true, { matches });
|
|
84
85
|
}
|
|
85
86
|
|
|
87
|
+
function parseTrackedStatus(rawStatus) {
|
|
88
|
+
const files = [];
|
|
89
|
+
const records = String(rawStatus || "").split("\0");
|
|
90
|
+
for (let index = 0; index < records.length; index++) {
|
|
91
|
+
const record = records[index];
|
|
92
|
+
if (record.length < 4) continue;
|
|
93
|
+
const status = record.slice(0, 2);
|
|
94
|
+
files.push(normalizePath(record.slice(3)));
|
|
95
|
+
if (/[RC]/u.test(status) && records[index + 1]) {
|
|
96
|
+
files.push(normalizePath(records[++index]));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return unique(files);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function checkRootDirtyLease(plan, repoRoot, deps = {}) {
|
|
103
|
+
const gitStatus =
|
|
104
|
+
deps.gitStatus ||
|
|
105
|
+
((cwd) =>
|
|
106
|
+
execFileSync(
|
|
107
|
+
"git",
|
|
108
|
+
["status", "--porcelain=v1", "-z", "--untracked-files=no"],
|
|
109
|
+
{ cwd, encoding: "utf8", windowsHide: true },
|
|
110
|
+
));
|
|
111
|
+
const dirtyFiles = parseTrackedStatus(gitStatus(repoRoot));
|
|
112
|
+
const leasedFiles = unique(
|
|
113
|
+
[...plan.leaseMap.values()].flat().map(normalizePath),
|
|
114
|
+
);
|
|
115
|
+
const leaseSet = new Set(leasedFiles);
|
|
116
|
+
const overlappingFiles = dirtyFiles.filter((file) => leaseSet.has(file));
|
|
117
|
+
|
|
118
|
+
return makeCheck("rootDirtyLease", overlappingFiles.length === 0, {
|
|
119
|
+
dirtyFiles,
|
|
120
|
+
leasedFiles,
|
|
121
|
+
overlappingFiles,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
86
125
|
function checkHosts(plan, repoRoot) {
|
|
87
126
|
const registry = readHosts(repoRoot);
|
|
88
127
|
const missing = [];
|
|
@@ -216,6 +255,13 @@ function collectMessages(checks) {
|
|
|
216
255
|
);
|
|
217
256
|
}
|
|
218
257
|
|
|
258
|
+
const rootDirtyLease = checks.rootDirtyLease;
|
|
259
|
+
if (rootDirtyLease && !rootDirtyLease.ok) {
|
|
260
|
+
errors.push(
|
|
261
|
+
`rootDir tracked changes overlap shard leases: ${rootDirtyLease.overlappingFiles.join(", ")}`,
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
219
265
|
return { errors: unique(errors), warnings: unique(warnings) };
|
|
220
266
|
}
|
|
221
267
|
|
|
@@ -258,6 +304,7 @@ export function runSwarmPreflight(prdPath, opts = {}) {
|
|
|
258
304
|
leases: makeCheck("leases", plan.conflicts.length === 0, {
|
|
259
305
|
conflicts: [...plan.conflicts],
|
|
260
306
|
}),
|
|
307
|
+
rootDirtyLease: checkRootDirtyLease(plan, repoRoot, opts.deps),
|
|
261
308
|
sensitiveDeny: checkSensitive(plan, sensitiveDeny),
|
|
262
309
|
hosts: checkHosts(plan, repoRoot),
|
|
263
310
|
workerCli: checkLocalWorkerClis(plan, opts.deps),
|