triflux 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/README.md +2 -0
- package/adapters/codex/skills/tfx-harness/SKILL.md +30 -0
- package/adapters/codex/skills/tfx-harness/skill.json +5 -0
- package/bin/triflux.mjs +14 -0
- package/hooks/keyword-rules.json +139 -19
- package/hub/cli-adapter-base.mjs +11 -2
- package/hub/codex-adapter.mjs +3 -0
- package/hub/intent.mjs +24 -32
- package/hub/router.mjs +272 -0
- package/hub/schema.sql +61 -2
- package/hub/server.mjs +42 -0
- package/hub/store.mjs +577 -5
- package/hub/team/conductor.mjs +23 -2
- package/hub/team/execution-mode.mjs +12 -2
- package/hub/team/headless.mjs +16 -0
- package/hub/team/retry-state-machine.mjs +8 -3
- package/hub/team/swarm-hypervisor.mjs +1 -0
- package/hub/team/swarm-preflight.mjs +47 -0
- package/hub/team/synapse-http.mjs +149 -4
- package/hub/team/worktree-lifecycle.mjs +56 -59
- package/hub/tools.mjs +11 -0
- package/hub/workers/codex-mcp.mjs +20 -4
- package/hub/workers/delegator-mcp.mjs +3 -49
- package/package.json +2 -1
- package/scripts/__tests__/lint-skills.test.mjs +68 -0
- package/scripts/__tests__/tfx-route-node-entry.test.mjs +36 -0
- package/scripts/keyword-detector.mjs +55 -8
- package/scripts/lib/agent-route-policy.mjs +360 -0
- package/scripts/lib/cli-codex.mjs +22 -160
- package/scripts/lib/codex-profile-config.mjs +29 -3
- package/scripts/lib/keyword-rules.mjs +12 -0
- package/scripts/lint-skills.mjs +65 -0
- package/scripts/pack.mjs +13 -0
- package/scripts/release/check-packages-mirror.mjs +5 -0
- package/scripts/setup.mjs +94 -2
- package/scripts/tfx-route.mjs +14 -1
- package/scripts/tfx-route.sh +233 -70
- package/skills/tfx-harness/SKILL.md +19 -76
- package/skills/tfx-hub/SKILL.md +1 -1
- package/skills/tfx-interview/SKILL.md +3 -1
- package/skills/tfx-interview/skill.json +1 -13
- package/skills/tfx-profile/SKILL.md +25 -15
- package/skills/tfx-prune/SKILL.md +3 -1
- package/skills/tfx-prune/skill.json +1 -8
- package/skills/tfx-setup/SKILL.md +1 -1
- package/tui/codex-profile.mjs +11 -2
- package/tui/setup.mjs +2 -0
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 "../../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 "../account-broker.mjs";
|
|
23
25
|
import { runPreflight as runCodexPreflight } from "../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,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { existsSync, readFileSync } from "node:fs";
|
|
4
4
|
import { dirname, resolve as pathResolve } from "node:path";
|
|
5
|
-
|
|
5
|
+
import { resolveNestedCodexAgentProfile } from "../../scripts/lib/cli-codex.mjs";
|
|
6
6
|
import { codexProfileConfigOverrides } from "../../scripts/lib/codex-profile-config.mjs";
|
|
7
7
|
import { whichCommand } from "../platform.mjs";
|
|
8
8
|
|
|
@@ -160,7 +160,17 @@ export function buildSpawnSpecForMode(mode, opts = {}) {
|
|
|
160
160
|
// still contains an inline [profiles.X] table (which codex re-injects on
|
|
161
161
|
// config rewrite). These go straight to spawn argv (shell: false), so push
|
|
162
162
|
// the raw TOML-scalar overrides without shell-quoting.
|
|
163
|
-
|
|
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
|
+
})) {
|
|
164
174
|
args.push("-c", override);
|
|
165
175
|
}
|
|
166
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 "../bridge.mjs";
|
|
24
25
|
import { escapePwshSingleQuoted } from "../cli-adapter-base.mjs";
|
|
25
26
|
import { getMaxSpawnPerSec } from "../lib/spawn-trace.mjs";
|
|
@@ -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 =
|
|
@@ -994,6 +1007,7 @@ async function dispatchProgressive(sessionName, assignments, opts = {}) {
|
|
|
994
1007
|
mcp: assignment.mcp,
|
|
995
1008
|
role: assignment.role,
|
|
996
1009
|
model: assignment.model,
|
|
1010
|
+
profile: assignment.profile,
|
|
997
1011
|
cwd: assignment.cwd || assignment.workdir,
|
|
998
1012
|
},
|
|
999
1013
|
);
|
|
@@ -1076,6 +1090,7 @@ async function dispatchBatch(sessionName, assignments, opts = {}) {
|
|
|
1076
1090
|
mcp: assignment.mcp,
|
|
1077
1091
|
role: assignment.role,
|
|
1078
1092
|
model: assignment.model,
|
|
1093
|
+
profile: assignment.profile,
|
|
1079
1094
|
cwd: assignment.cwd || assignment.workdir,
|
|
1080
1095
|
},
|
|
1081
1096
|
);
|
|
@@ -1139,6 +1154,7 @@ async function dispatchDaemonBatch(sessionName, assignments, opts = {}) {
|
|
|
1139
1154
|
mcp: assignment.mcp,
|
|
1140
1155
|
role: assignment.role,
|
|
1141
1156
|
model: assignment.model,
|
|
1157
|
+
profile: assignment.profile,
|
|
1142
1158
|
cwd: assignment.cwd || assignment.workdir,
|
|
1143
1159
|
},
|
|
1144
1160
|
);
|
|
@@ -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
|
}
|
|
@@ -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, {
|
|
@@ -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 "../lib/hosts-compat.mjs";
|
|
6
7
|
import { whichCommand } from "../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),
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { homedir, hostname } from "node:os";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { normalizeRoleKey } from "../role-contract.mjs";
|
|
4
7
|
|
|
5
8
|
const HUB_DEFAULT_PORT = 27888;
|
|
6
9
|
// Same token file as hub/bridge.mjs (HUB_TOKEN_FILE). Kept inline rather than
|
|
@@ -21,6 +24,144 @@ function normalizeToken(raw) {
|
|
|
21
24
|
return token || null;
|
|
22
25
|
}
|
|
23
26
|
|
|
27
|
+
function opaqueId(prefix, value) {
|
|
28
|
+
return `${prefix}_${createHash("sha256")
|
|
29
|
+
.update(String(value || ""))
|
|
30
|
+
.digest("base64url")
|
|
31
|
+
.slice(0, 22)}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function normalizedProjectId(value) {
|
|
35
|
+
try {
|
|
36
|
+
return normalizeRoleKey({ project_id: value, role_kind: "cto" }).project_id;
|
|
37
|
+
} catch {
|
|
38
|
+
return "";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function resolveProjectId(cwd, meta, opts = {}) {
|
|
43
|
+
const explicit = meta?.project_id || process.env.TFX_PROJECT_ID;
|
|
44
|
+
if (explicit) return normalizedProjectId(explicit);
|
|
45
|
+
if (typeof opts.resolveProjectId === "function") {
|
|
46
|
+
return normalizedProjectId(opts.resolveProjectId(cwd));
|
|
47
|
+
}
|
|
48
|
+
let current = cwd;
|
|
49
|
+
while (current) {
|
|
50
|
+
const manifest = join(current, ".triflux", "project.json");
|
|
51
|
+
if (existsSync(manifest)) {
|
|
52
|
+
try {
|
|
53
|
+
const parsed = JSON.parse(readFileSync(manifest, "utf8"));
|
|
54
|
+
if (parsed?.schema_version !== "tfx.project.v1") return "";
|
|
55
|
+
return normalizedProjectId(parsed.project_id);
|
|
56
|
+
} catch {
|
|
57
|
+
return "";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const parent = dirname(current);
|
|
61
|
+
if (parent === current) break;
|
|
62
|
+
current = parent;
|
|
63
|
+
}
|
|
64
|
+
return "";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function inferSessionCli(meta, opts = {}) {
|
|
68
|
+
const explicit = String(meta?.cli || meta?.actor_cli || "")
|
|
69
|
+
.trim()
|
|
70
|
+
.toLowerCase();
|
|
71
|
+
if (explicit === "codex" || explicit === "claude") return explicit;
|
|
72
|
+
const entrypoint = String(opts.entrypoint ?? process.argv[1] ?? "");
|
|
73
|
+
if (entrypoint.includes("codex-session-hook")) return "codex";
|
|
74
|
+
if (entrypoint.includes("agy-session-hook")) return "other";
|
|
75
|
+
return "claude";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function resolveTmuxSession(meta, opts = {}) {
|
|
79
|
+
const explicit =
|
|
80
|
+
meta?.tmux_session || meta?.session_name || process.env.TFX_TMUX_SESSION;
|
|
81
|
+
if (explicit) return String(explicit).trim();
|
|
82
|
+
if (!process.env.TMUX) return "";
|
|
83
|
+
try {
|
|
84
|
+
const run =
|
|
85
|
+
opts.tmuxSessionName ||
|
|
86
|
+
(() =>
|
|
87
|
+
execFileSync("tmux", ["display-message", "-p", "#{session_name}"], {
|
|
88
|
+
encoding: "utf8",
|
|
89
|
+
timeout: 200,
|
|
90
|
+
windowsHide: true,
|
|
91
|
+
}));
|
|
92
|
+
return String(run() || "").trim();
|
|
93
|
+
} catch {
|
|
94
|
+
return "";
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function buildSynapseRegistrationMeta(meta, opts = {}) {
|
|
99
|
+
if (!meta || meta.sessionKind !== "interactive") return meta;
|
|
100
|
+
const sessionId = String(meta.sessionId || "").trim();
|
|
101
|
+
if (!sessionId) return meta;
|
|
102
|
+
const cwd = typeof meta.cwd === "string" ? meta.cwd : process.cwd();
|
|
103
|
+
const cli = inferSessionCli(meta, opts);
|
|
104
|
+
const hostId = String(
|
|
105
|
+
meta.host_id ||
|
|
106
|
+
process.env.TFX_HOST_ID ||
|
|
107
|
+
opaqueId("hst", opts.hostname?.() || hostname()),
|
|
108
|
+
).trim();
|
|
109
|
+
const expiresAtMs = Number(opts.nowMs?.() ?? Date.now()) + 299000;
|
|
110
|
+
const transportLocators = [];
|
|
111
|
+
if (cli === "claude") {
|
|
112
|
+
transportLocators.push({
|
|
113
|
+
schema_version: "tfx.transport-locator.v1",
|
|
114
|
+
transport_id: opaqueId("trn", `claude-uds:${hostId}:${sessionId}`),
|
|
115
|
+
kind: "claude-uds",
|
|
116
|
+
host_id: hostId,
|
|
117
|
+
capabilities: ["probe", "wake", "activate", "interrupt"],
|
|
118
|
+
priority: 100,
|
|
119
|
+
expires_at_ms: expiresAtMs,
|
|
120
|
+
locator: {
|
|
121
|
+
session_id: sessionId,
|
|
122
|
+
bridge_id: String(
|
|
123
|
+
meta.bridge_id || process.env.TFX_BRIDGE_ID || "local-hub",
|
|
124
|
+
),
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
const tmuxSession = resolveTmuxSession(meta, opts);
|
|
129
|
+
if (tmuxSession) {
|
|
130
|
+
const muxServerSource =
|
|
131
|
+
meta.mux_server_id ||
|
|
132
|
+
process.env.TFX_MUX_SERVER_ID ||
|
|
133
|
+
String(process.env.TMUX || "default").split(",", 1)[0];
|
|
134
|
+
transportLocators.push({
|
|
135
|
+
schema_version: "tfx.transport-locator.v1",
|
|
136
|
+
transport_id: opaqueId("trn", `tmux:${hostId}:${tmuxSession}`),
|
|
137
|
+
kind: "tmux",
|
|
138
|
+
host_id: hostId,
|
|
139
|
+
capabilities: ["probe", "wake", "activate", "interrupt"],
|
|
140
|
+
priority: cli === "codex" ? 100 : 50,
|
|
141
|
+
expires_at_ms: expiresAtMs,
|
|
142
|
+
locator: {
|
|
143
|
+
session_name: tmuxSession,
|
|
144
|
+
mux_server_id: opaqueId("mux", muxServerSource),
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
const directAgentId = `session:${sessionId}`;
|
|
149
|
+
return {
|
|
150
|
+
...meta,
|
|
151
|
+
agent_id:
|
|
152
|
+
meta.agent_id ||
|
|
153
|
+
(directAgentId.length <= 64 && /^[a-zA-Z0-9._:-]+$/u.test(directAgentId)
|
|
154
|
+
? directAgentId
|
|
155
|
+
: opaqueId("session", sessionId)),
|
|
156
|
+
cli,
|
|
157
|
+
project_id: resolveProjectId(cwd, meta, opts) || undefined,
|
|
158
|
+
session_id: meta.session_id || sessionId,
|
|
159
|
+
host_id: hostId,
|
|
160
|
+
transport_locators_json:
|
|
161
|
+
meta.transport_locators_json ?? JSON.stringify(transportLocators),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
24
165
|
// Mirrors hub/bridge.mjs:readHubToken — env override first, then the token file.
|
|
25
166
|
// Missing token → null (backward-compatible with token-less hubs).
|
|
26
167
|
function readHubToken() {
|
|
@@ -139,7 +280,11 @@ export function drainPendingSynapse(timeoutMs = 1000) {
|
|
|
139
280
|
}
|
|
140
281
|
|
|
141
282
|
export function registerSynapseSession(meta, opts = {}) {
|
|
142
|
-
return fireAndForgetSynapse(
|
|
283
|
+
return fireAndForgetSynapse(
|
|
284
|
+
"/synapse/register",
|
|
285
|
+
buildSynapseRegistrationMeta(meta, opts),
|
|
286
|
+
opts,
|
|
287
|
+
);
|
|
143
288
|
}
|
|
144
289
|
|
|
145
290
|
export function heartbeatSynapseSession(
|
|
@@ -4,8 +4,16 @@
|
|
|
4
4
|
// Remote support: host option → SSH-based git operations via remote-session.mjs.
|
|
5
5
|
|
|
6
6
|
import { execFile } from "node:child_process";
|
|
7
|
-
import {
|
|
8
|
-
|
|
7
|
+
import {
|
|
8
|
+
access,
|
|
9
|
+
mkdir,
|
|
10
|
+
mkdtemp,
|
|
11
|
+
readdir,
|
|
12
|
+
realpath,
|
|
13
|
+
rm,
|
|
14
|
+
} from "node:fs/promises";
|
|
15
|
+
import { tmpdir } from "node:os";
|
|
16
|
+
import { join, normalize, relative, resolve } from "node:path";
|
|
9
17
|
import { remoteGit, validateHost } from "./remote-session.mjs";
|
|
10
18
|
|
|
11
19
|
const SWARM_ROOT = ".codex-swarm";
|
|
@@ -327,24 +335,23 @@ export async function rebaseShardOntoIntegration({
|
|
|
327
335
|
}
|
|
328
336
|
}
|
|
329
337
|
|
|
330
|
-
// #127 BUG-E:
|
|
331
|
-
//
|
|
332
|
-
//
|
|
333
|
-
//
|
|
334
|
-
//
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
} catch {
|
|
340
|
-
/* detached HEAD or unknown — skip restore */
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
// Backup integration HEAD for rollback
|
|
338
|
+
// #127 BUG-E accident history: this function used to checkout the
|
|
339
|
+
// integration branch in rootDir, leak rootDir HEAD, and redirect later user
|
|
340
|
+
// edits/commits into swarm/.../merge. Integration now happens exclusively in
|
|
341
|
+
// a disposable worktree, so rootDir HEAD never moves and needs no restore.
|
|
342
|
+
//
|
|
343
|
+
// #129 BUG-J accident history: checkout failure was followed by a blind
|
|
344
|
+
// reset --hard (or branch -f rollback) against rootDir, which rewound the
|
|
345
|
+
// caller branch. Rollback now resets only the disposable worktree; there is
|
|
346
|
+
// no rootDir reset path and therefore no stash-create last-resort path.
|
|
344
347
|
const backupCommit = await git(["rev-parse", integrationBranch], rootDir);
|
|
345
348
|
|
|
346
349
|
let shaList = [];
|
|
347
350
|
let ffError = null;
|
|
351
|
+
let integrationWorktreeAdded = false;
|
|
352
|
+
const integrationWorktree = await mkdtemp(
|
|
353
|
+
join(tmpdir(), "tfx-swarm-integration-"),
|
|
354
|
+
);
|
|
348
355
|
|
|
349
356
|
try {
|
|
350
357
|
const log = await git(
|
|
@@ -361,11 +368,15 @@ export async function rebaseShardOntoIntegration({
|
|
|
361
368
|
.map((s) => s.trim())
|
|
362
369
|
.filter(Boolean);
|
|
363
370
|
|
|
364
|
-
await git(
|
|
371
|
+
await git(
|
|
372
|
+
["worktree", "add", "--quiet", integrationWorktree, integrationBranch],
|
|
373
|
+
rootDir,
|
|
374
|
+
);
|
|
375
|
+
integrationWorktreeAdded = true;
|
|
365
376
|
|
|
366
377
|
try {
|
|
367
|
-
await git(["merge", "--ff-only", shardBranch],
|
|
368
|
-
const headCommit = await git(["rev-parse", "HEAD"],
|
|
378
|
+
await git(["merge", "--ff-only", shardBranch], integrationWorktree);
|
|
379
|
+
const headCommit = await git(["rev-parse", "HEAD"], integrationWorktree);
|
|
369
380
|
return {
|
|
370
381
|
ok: true,
|
|
371
382
|
headCommit,
|
|
@@ -376,12 +387,15 @@ export async function rebaseShardOntoIntegration({
|
|
|
376
387
|
} catch (err) {
|
|
377
388
|
ffError = err;
|
|
378
389
|
try {
|
|
379
|
-
await git(["merge", "--abort"],
|
|
390
|
+
await git(["merge", "--abort"], integrationWorktree);
|
|
380
391
|
} catch {
|
|
381
392
|
/* ff-only usually leaves no merge state */
|
|
382
393
|
}
|
|
383
394
|
try {
|
|
384
|
-
|
|
395
|
+
// Safe destructive operation: this cwd is the disposable integration
|
|
396
|
+
// worktree, never rootDir. It restores the integration ref before the
|
|
397
|
+
// cherry-pick fallback without touching user files.
|
|
398
|
+
await git(["reset", "--hard", backupCommit], integrationWorktree);
|
|
385
399
|
} catch {
|
|
386
400
|
/* fallback below will report if cherry-pick also fails */
|
|
387
401
|
}
|
|
@@ -391,10 +405,10 @@ export async function rebaseShardOntoIntegration({
|
|
|
391
405
|
// fail when histories diverge or a shard branch is checked out by a swarm
|
|
392
406
|
// worktree. Cherry-pick reads commits without moving the shard branch ref.
|
|
393
407
|
for (const sha of shaList) {
|
|
394
|
-
await git(["cherry-pick", sha],
|
|
408
|
+
await git(["cherry-pick", sha], integrationWorktree);
|
|
395
409
|
}
|
|
396
410
|
|
|
397
|
-
const headCommit = await git(["rev-parse", "HEAD"],
|
|
411
|
+
const headCommit = await git(["rev-parse", "HEAD"], integrationWorktree);
|
|
398
412
|
return {
|
|
399
413
|
ok: true,
|
|
400
414
|
headCommit,
|
|
@@ -406,42 +420,17 @@ export async function rebaseShardOntoIntegration({
|
|
|
406
420
|
: "cherry-pick fallback",
|
|
407
421
|
};
|
|
408
422
|
} catch (err) {
|
|
409
|
-
|
|
410
|
-
await git(["cherry-pick", "--abort"], rootDir);
|
|
411
|
-
} catch {
|
|
412
|
-
/* already clean */
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
// #129 BUG-J: Roll back integrationBranch without mutating whatever
|
|
416
|
-
// branch HEAD currently points at. The previous implementation did a
|
|
417
|
-
// blind `reset --hard backupCommit` on current HEAD — if the earlier
|
|
418
|
-
// `git checkout integrationBranch` inside the try block failed (e.g.
|
|
419
|
-
// integrationBranch was already checked out by a swarm worktree) the
|
|
420
|
-
// caller's branch silently rewound to integrationBranch's backup commit.
|
|
421
|
-
// Observed 2026-04-20: fix branch tree lost to main HEAD during swarm
|
|
422
|
-
// probe, requiring `git merge --ff-only origin/<branch>` recovery.
|
|
423
|
-
let currentBranch = null;
|
|
424
|
-
try {
|
|
425
|
-
const head = await git(["rev-parse", "--abbrev-ref", "HEAD"], rootDir);
|
|
426
|
-
if (head && head !== "HEAD") currentBranch = head;
|
|
427
|
-
} catch {
|
|
428
|
-
/* detached — fall through to ref-only update */
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
if (currentBranch === integrationBranch) {
|
|
432
|
-
// HEAD is on integrationBranch — reset advances this branch only.
|
|
423
|
+
if (integrationWorktreeAdded) {
|
|
433
424
|
try {
|
|
434
|
-
await git(["
|
|
425
|
+
await git(["cherry-pick", "--abort"], integrationWorktree);
|
|
435
426
|
} catch {
|
|
436
|
-
/*
|
|
427
|
+
/* already clean */
|
|
437
428
|
}
|
|
438
|
-
} else {
|
|
439
|
-
// HEAD is elsewhere (originalBranch, detached, or another branch).
|
|
440
|
-
// Update the integrationBranch ref directly; never touch current HEAD.
|
|
441
429
|
try {
|
|
442
|
-
|
|
430
|
+
// #129 rollback is now local to the disposable integration worktree.
|
|
431
|
+
await git(["reset", "--hard", backupCommit], integrationWorktree);
|
|
443
432
|
} catch {
|
|
444
|
-
/* best-effort
|
|
433
|
+
/* best-effort */
|
|
445
434
|
}
|
|
446
435
|
}
|
|
447
436
|
|
|
@@ -453,14 +442,22 @@ export async function rebaseShardOntoIntegration({
|
|
|
453
442
|
fallbackReason: ffError?.message || null,
|
|
454
443
|
};
|
|
455
444
|
} finally {
|
|
456
|
-
|
|
457
|
-
// (detached HEAD case) or already on target.
|
|
458
|
-
if (originalBranch) {
|
|
445
|
+
if (integrationWorktreeAdded) {
|
|
459
446
|
try {
|
|
460
|
-
await git(
|
|
447
|
+
await git(
|
|
448
|
+
["worktree", "remove", "--force", integrationWorktree],
|
|
449
|
+
rootDir,
|
|
450
|
+
);
|
|
461
451
|
} catch {
|
|
462
|
-
|
|
452
|
+
await rm(integrationWorktree, { recursive: true, force: true });
|
|
453
|
+
try {
|
|
454
|
+
await git(["worktree", "prune"], rootDir);
|
|
455
|
+
} catch {
|
|
456
|
+
/* best-effort metadata cleanup */
|
|
457
|
+
}
|
|
463
458
|
}
|
|
459
|
+
} else {
|
|
460
|
+
await rm(integrationWorktree, { recursive: true, force: true });
|
|
464
461
|
}
|
|
465
462
|
}
|
|
466
463
|
}
|
package/hub/tools.mjs
CHANGED
|
@@ -153,6 +153,17 @@ export function createTools(store, router, hitl, pipe = null) {
|
|
|
153
153
|
},
|
|
154
154
|
topics: { type: "array", items: { type: "string" }, maxItems: 64 },
|
|
155
155
|
metadata: { type: "object" },
|
|
156
|
+
project_id: { type: "string", pattern: "^prj_[A-Za-z0-9_-]{22}$" },
|
|
157
|
+
session_id: { type: "string", minLength: 1, maxLength: 256 },
|
|
158
|
+
host_id: { type: "string", pattern: "^hst_[A-Za-z0-9_-]+$" },
|
|
159
|
+
transport_locators_json: {
|
|
160
|
+
description:
|
|
161
|
+
"tfx.transport-locator.v1 객체 배열 또는 그 JSON 문자열",
|
|
162
|
+
oneOf: [
|
|
163
|
+
{ type: "array", items: { type: "object" } },
|
|
164
|
+
{ type: "string" },
|
|
165
|
+
],
|
|
166
|
+
},
|
|
156
167
|
heartbeat_ttl_ms: {
|
|
157
168
|
type: "integer",
|
|
158
169
|
minimum: 10000,
|
|
@@ -146,9 +146,11 @@ export function resolveCodexProfileConfig(profileName) {
|
|
|
146
146
|
} catch {
|
|
147
147
|
// Profile file absent (non-file alias). Derive effort from the naming
|
|
148
148
|
// convention; leave model unset so codex uses its config.toml default.
|
|
149
|
-
const suffix = /_(xhigh|high|med|medium|low)$/.exec(profileName);
|
|
149
|
+
const suffix = /_(ultra|max|xhigh|high|med|medium|low)$/.exec(profileName);
|
|
150
150
|
if (suffix) {
|
|
151
151
|
const map = {
|
|
152
|
+
ultra: "ultra",
|
|
153
|
+
max: "max",
|
|
152
154
|
xhigh: "xhigh",
|
|
153
155
|
high: "high",
|
|
154
156
|
med: "medium",
|
|
@@ -193,16 +195,27 @@ export function buildCodexArguments(prompt, opts = {}) {
|
|
|
193
195
|
// profile effort merges on top of any opts.config.
|
|
194
196
|
if (typeof opts.profile === "string" && opts.profile) {
|
|
195
197
|
const resolved = resolveCodexProfileConfig(opts.profile);
|
|
196
|
-
|
|
197
|
-
|
|
198
|
+
const profileDerivedUltra =
|
|
199
|
+
resolved.reasoningEffort?.toLowerCase() === "ultra";
|
|
200
|
+
if (typeof args.model !== "string") {
|
|
201
|
+
if (profileDerivedUltra) args.model = "gpt-5.6-sol";
|
|
202
|
+
else if (resolved.model) args.model = resolved.model;
|
|
198
203
|
}
|
|
199
204
|
if (resolved.reasoningEffort) {
|
|
200
205
|
args.config = {
|
|
201
206
|
...(args.config || {}),
|
|
202
|
-
model_reasoning_effort:
|
|
207
|
+
model_reasoning_effort: profileDerivedUltra
|
|
208
|
+
? "max"
|
|
209
|
+
: resolved.reasoningEffort,
|
|
203
210
|
};
|
|
204
211
|
}
|
|
205
212
|
}
|
|
213
|
+
if (typeof opts.reasoningEffort === "string" && opts.reasoningEffort) {
|
|
214
|
+
args.config = {
|
|
215
|
+
...(args.config || {}),
|
|
216
|
+
model_reasoning_effort: opts.reasoningEffort,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
206
219
|
|
|
207
220
|
return args;
|
|
208
221
|
}
|
|
@@ -694,6 +707,9 @@ function parseCliArgs(argv) {
|
|
|
694
707
|
case "--model":
|
|
695
708
|
options.model = next();
|
|
696
709
|
break;
|
|
710
|
+
case "--reasoning-effort":
|
|
711
|
+
options.reasoningEffort = next();
|
|
712
|
+
break;
|
|
697
713
|
case "--approval-policy":
|
|
698
714
|
options.approvalPolicy = next();
|
|
699
715
|
break;
|