triflux 10.45.0 → 10.47.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/bin/tfx-live.mjs +576 -53
- package/config/routing-policy.json +1 -1
- package/hooks/session-start-fast.mjs +6 -0
- package/hub/dynamic-routing-engine.mjs +1 -1
- package/hub/lib/cto-env.mjs +2 -3
- package/hub/server.mjs +2 -51
- package/hub/team/recovery-store.mjs +1 -1
- package/hub/team/retry-state-machine.mjs +9 -1
- package/hub/team/synapse-registry.mjs +14 -11
- package/hub/team/uds-orchestrator.mjs +384 -83
- package/hub/team/worktree-lifecycle.mjs +8 -48
- package/hub/tray-lifecycle.mjs +1 -1
- package/hub/workers/codex-mcp.mjs +1 -1
- package/hud/providers/codex.mjs +91 -8
- package/hud/providers/cto.mjs +3 -0
- package/package.json +1 -1
- package/scripts/__tests__/release-governance.test.mjs +235 -11
- package/scripts/__tests__/release-publish-ancestor.test.mjs +67 -0
- package/scripts/__tests__/release-verify-wait.test.mjs +169 -0
- package/scripts/__tests__/resolve-auto-release.test.mjs +216 -0
- package/scripts/__tests__/tfx-route-bash-node-parity.test.mjs +1 -1
- package/scripts/__tests__/tfx-route-node-entry.test.mjs +4 -4
- package/scripts/lib/agent-route-policy.mjs +18 -17
- package/scripts/lib/codex-profile-config.mjs +19 -5
- package/scripts/release/publish.mjs +16 -1
- package/scripts/release/resolve-auto-release.mjs +136 -0
- package/scripts/release/verify.mjs +74 -25
- package/scripts/setup.mjs +20 -7
- package/scripts/tfx-route.sh +27 -24
- package/skills/tfx-auto/SKILL.md +5 -4
- package/skills/tfx-live/SKILL.md +112 -7
- package/skills/tfx-profile/SKILL.md +9 -7
- package/skills/tfx-setup/SKILL.md +1 -1
- package/skills/tfx-ship/SKILL.md +43 -36
- package/tui/codex-profile.mjs +2 -3
- package/tui/setup.mjs +4 -3
- package/hub/team/git-preflight.mjs +0 -286
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { execFile, execFileSync } from "node:child_process";
|
|
14
14
|
import { dirname, join } from "node:path";
|
|
15
15
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
16
|
+
import { resolveRoleControlSnapshot } from "../hub/lib/cto-env.mjs";
|
|
16
17
|
import {
|
|
17
18
|
buildSynapseTaskSummary,
|
|
18
19
|
drainPendingSynapse,
|
|
@@ -95,6 +96,11 @@ async function defaultResolveParticipantLakeRoot(cwd) {
|
|
|
95
96
|
*/
|
|
96
97
|
export async function emitParticipantSessionStarted(stdinData, seams = {}) {
|
|
97
98
|
try {
|
|
99
|
+
if (
|
|
100
|
+
!resolveRoleControlSnapshot(seams.env || process.env).auto_collect_enabled
|
|
101
|
+
) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
98
104
|
const payload = parseStartPayload(stdinData);
|
|
99
105
|
const sessionId = String(payload?.session_id || "").trim();
|
|
100
106
|
if (!sessionId) return null;
|
package/hub/lib/cto-env.mjs
CHANGED
|
@@ -38,10 +38,9 @@ export function resolveRoleControlSnapshot(env = process.env, options = {}) {
|
|
|
38
38
|
const ctoManagerEnabled = masterEnabled && isEnvOn(env, "TFX_CTO_MANAGER");
|
|
39
39
|
const leadManagerEnabled =
|
|
40
40
|
ctoManagerEnabled && isEnvOn(env, "TFX_LEAD_MANAGER");
|
|
41
|
-
const northStarEnabled =
|
|
42
|
-
masterEnabled && !isEnvOff(env, "TFX_CTO_NORTH_STAR");
|
|
41
|
+
const northStarEnabled = masterEnabled && isEnvOn(env, "TFX_CTO_NORTH_STAR");
|
|
43
42
|
const autoCollectEnabled =
|
|
44
|
-
masterEnabled &&
|
|
43
|
+
masterEnabled && isEnvOn(env, "TFX_CTO_AUTO_COLLECT");
|
|
45
44
|
const values = {
|
|
46
45
|
master_enabled: masterEnabled,
|
|
47
46
|
cto_manager_enabled: ctoManagerEnabled,
|
package/hub/server.mjs
CHANGED
|
@@ -72,9 +72,7 @@ import {
|
|
|
72
72
|
} from "./state.mjs";
|
|
73
73
|
import { createStoreAdapter } from "./store-adapter.mjs";
|
|
74
74
|
import { createCtoAutoCollector } from "./team/cto-auto-collect.mjs";
|
|
75
|
-
import { createGitPreflight } from "./team/git-preflight.mjs";
|
|
76
75
|
import { nativeProxy } from "./team/nativeProxy.mjs";
|
|
77
|
-
import { createSwarmLocks } from "./team/swarm-locks.mjs";
|
|
78
76
|
import {
|
|
79
77
|
createSynapseRegistry,
|
|
80
78
|
projectPeer,
|
|
@@ -119,14 +117,6 @@ const AIMD_WINDOW_MS = 30 * 60 * 1000;
|
|
|
119
117
|
const AIMD_INITIAL_BATCH_SIZE = 3;
|
|
120
118
|
const AIMD_MIN_BATCH_SIZE = 1;
|
|
121
119
|
const AIMD_MAX_BATCH_SIZE = 10;
|
|
122
|
-
const SYNAPSE_VALID_OPS = new Set([
|
|
123
|
-
"checkout",
|
|
124
|
-
"rebase",
|
|
125
|
-
"cherry-pick",
|
|
126
|
-
"reset",
|
|
127
|
-
"stash-pop",
|
|
128
|
-
"worktree-remove",
|
|
129
|
-
]);
|
|
130
120
|
const HUB_IDLE_TIMEOUT_DEFAULT_MS = 30 * 60 * 1000;
|
|
131
121
|
const HUB_IDLE_SWEEP_DEFAULT_MS = 60 * 1000;
|
|
132
122
|
const STATIC_CONTENT_TYPES = Object.freeze({
|
|
@@ -1124,21 +1114,13 @@ export async function startHub({
|
|
|
1124
1114
|
}
|
|
1125
1115
|
const delegatorService = new DelegatorService({ worker: delegatorWorker });
|
|
1126
1116
|
|
|
1127
|
-
// Synapse Layer 4: session registry
|
|
1117
|
+
// Synapse Layer 4: session registry
|
|
1128
1118
|
const synapseEmitter = new EventEmitter();
|
|
1129
1119
|
synapseEmitter.setMaxListeners(50);
|
|
1130
1120
|
const synapseRegistry = createSynapseRegistry({
|
|
1131
1121
|
persistPath: join(CACHE_DIR, "tfx-hub", "synapse-sessions.json"),
|
|
1132
1122
|
emitter: synapseEmitter,
|
|
1133
1123
|
});
|
|
1134
|
-
const swarmLocks = createSwarmLocks({
|
|
1135
|
-
repoRoot: PROJECT_ROOT,
|
|
1136
|
-
persistPath: join(CACHE_DIR, "tfx-hub", "swarm-locks.json"),
|
|
1137
|
-
});
|
|
1138
|
-
const gitPreflight = createGitPreflight({
|
|
1139
|
-
registry: synapseRegistry,
|
|
1140
|
-
locks: swarmLocks,
|
|
1141
|
-
});
|
|
1142
1124
|
const ctoAutoCollector = createCtoAutoCollector({
|
|
1143
1125
|
registry: synapseRegistry,
|
|
1144
1126
|
runCollect,
|
|
@@ -1437,7 +1419,7 @@ export async function startHub({
|
|
|
1437
1419
|
});
|
|
1438
1420
|
}
|
|
1439
1421
|
|
|
1440
|
-
// ── Synapse Layer 5: session registry
|
|
1422
|
+
// ── Synapse Layer 5: session registry routes ──
|
|
1441
1423
|
// Admin/raw snapshot (loopback-only). Returns raw cwd/pid for local
|
|
1442
1424
|
// admin/HUD use; the redacted peer surface is GET /synapse/peers.
|
|
1443
1425
|
if (path === "/synapse/sessions" && req.method === "GET") {
|
|
@@ -1642,37 +1624,6 @@ export async function startHub({
|
|
|
1642
1624
|
}
|
|
1643
1625
|
}
|
|
1644
1626
|
|
|
1645
|
-
if (path === "/synapse/locks" && req.method === "GET") {
|
|
1646
|
-
return writeJson(res, 200, {
|
|
1647
|
-
ok: true,
|
|
1648
|
-
locks: swarmLocks.snapshot(),
|
|
1649
|
-
ts: Date.now(),
|
|
1650
|
-
});
|
|
1651
|
-
}
|
|
1652
|
-
|
|
1653
|
-
if (path === "/synapse/preflight" && req.method === "POST") {
|
|
1654
|
-
try {
|
|
1655
|
-
const body = await parseBody(req);
|
|
1656
|
-
const { op, args = {}, sessionContext = {} } = body;
|
|
1657
|
-
if (!op || typeof op !== "string") {
|
|
1658
|
-
return writeJson(res, 400, { ok: false, error: "op 필수" });
|
|
1659
|
-
}
|
|
1660
|
-
if (!SYNAPSE_VALID_OPS.has(op)) {
|
|
1661
|
-
return writeJson(res, 400, {
|
|
1662
|
-
ok: false,
|
|
1663
|
-
error: `invalid op: ${op}`,
|
|
1664
|
-
});
|
|
1665
|
-
}
|
|
1666
|
-
const result = gitPreflight.check(op, args, sessionContext);
|
|
1667
|
-
return writeJson(res, 200, { ok: true, ...result });
|
|
1668
|
-
} catch (err) {
|
|
1669
|
-
return writeJson(res, 400, {
|
|
1670
|
-
ok: false,
|
|
1671
|
-
error: String(err?.message || err),
|
|
1672
|
-
});
|
|
1673
|
-
}
|
|
1674
|
-
}
|
|
1675
|
-
|
|
1676
1627
|
if (path.startsWith("/bridge")) {
|
|
1677
1628
|
const isBridgeStatusGet =
|
|
1678
1629
|
path === "/bridge/status" && req.method === "GET";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// hub/team/recovery-store.mjs — Best-effort preservation of worker changes
|
|
2
2
|
// before a dirty worktree is removed. Uses file-based `.patch` + JSON manifest
|
|
3
|
-
// instead of git stash
|
|
3
|
+
// instead of git stash, which is risky to pop under concurrent worktree use.
|
|
4
4
|
|
|
5
5
|
import { execFile } from "node:child_process";
|
|
6
6
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
@@ -23,7 +23,10 @@ import {
|
|
|
23
23
|
} from "node:fs";
|
|
24
24
|
import { dirname, join } from "node:path";
|
|
25
25
|
|
|
26
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
canonicalCodexProfileModel,
|
|
28
|
+
normalizeCodexProfileName,
|
|
29
|
+
} from "../../scripts/lib/codex-profile-config.mjs";
|
|
27
30
|
|
|
28
31
|
export const STATES = Object.freeze({
|
|
29
32
|
PLANNING: "PLANNING",
|
|
@@ -318,6 +321,11 @@ function normalizeEscalationChainEntry(entry, index, source) {
|
|
|
318
321
|
const profile = entry.profile.trim();
|
|
319
322
|
normalized.profile =
|
|
320
323
|
entry.cli === "codex" ? normalizeEscalationProfile(profile) : profile;
|
|
324
|
+
// A remapped legacy profile must not keep reporting its old model.
|
|
325
|
+
if (normalized.profile !== profile) {
|
|
326
|
+
normalized.model =
|
|
327
|
+
canonicalCodexProfileModel(normalized.profile) ?? normalized.model;
|
|
328
|
+
}
|
|
321
329
|
}
|
|
322
330
|
return normalized;
|
|
323
331
|
}
|
|
@@ -48,9 +48,9 @@ function hasDirtyFiles(session) {
|
|
|
48
48
|
// A session is "live" while active OR idle. Idle is an interactive session that
|
|
49
49
|
// missed its heartbeat interval but is still under the TTL — alive but inactive,
|
|
50
50
|
// not presumed dead. getActive() and querySessions() share this single predicate
|
|
51
|
-
// so the liveness contract can't drift: getActive() feeds
|
|
52
|
-
//
|
|
53
|
-
// interactive session
|
|
51
|
+
// so the liveness contract can't drift: getActive() feeds peer discovery,
|
|
52
|
+
// CTO status/hygiene and log retention, so dropping idle there would hide a
|
|
53
|
+
// still-live interactive session from every one of those consumers.
|
|
54
54
|
function isLiveStatus(status) {
|
|
55
55
|
return status === "active" || status === "idle";
|
|
56
56
|
}
|
|
@@ -337,7 +337,7 @@ export function createSynapseRegistry(opts = {}) {
|
|
|
337
337
|
// session resuming: Claude Code re-fires SessionStart with the same
|
|
338
338
|
// session_id on resume/clear/compact, so fall through and re-register
|
|
339
339
|
// (revive) it. Otherwise the resumed-but-live session stays stale forever
|
|
340
|
-
// and vanishes from peer-discovery
|
|
340
|
+
// and vanishes from peer-discovery.
|
|
341
341
|
const existing = sessions.get(sessionId);
|
|
342
342
|
if (existing && isLiveStatus(existing.status)) {
|
|
343
343
|
console.warn(
|
|
@@ -381,10 +381,13 @@ export function createSynapseRegistry(opts = {}) {
|
|
|
381
381
|
}
|
|
382
382
|
|
|
383
383
|
// stale/expired 세션이 cutoff 넘게 누적되면 Map에서 제거.
|
|
384
|
-
// live(active/idle)
|
|
385
|
-
//
|
|
386
|
-
//
|
|
387
|
-
//
|
|
384
|
+
// live(active/idle) rows survive a long-stale lastHeartbeat because peer
|
|
385
|
+
// discovery and CTO status/hygiene still read them.
|
|
386
|
+
// Dirty stale rows keep the historical 24h window. Its original consumer,
|
|
387
|
+
// the git-preflight dirty-file guard, is gone and nothing populates
|
|
388
|
+
// dirtyFiles today, so this split is a candidate for collapsing into one
|
|
389
|
+
// window. Clean stale rows expire quickly to avoid daily dummy rows after
|
|
390
|
+
// overnight operator gaps.
|
|
388
391
|
function pruneExpired(opts2 = {}) {
|
|
389
392
|
if (destroyed) return { removed: [], count: 0 };
|
|
390
393
|
|
|
@@ -510,9 +513,9 @@ export function createSynapseRegistry(opts = {}) {
|
|
|
510
513
|
}
|
|
511
514
|
|
|
512
515
|
function getActive() {
|
|
513
|
-
// Live = active or idle.
|
|
514
|
-
//
|
|
515
|
-
//
|
|
516
|
+
// Live = active or idle. Peer discovery, CTO status/hygiene and log
|
|
517
|
+
// retention read this list; an idle interactive session is still live
|
|
518
|
+
// (process alive) and must stay visible to them.
|
|
516
519
|
return [...sessions.values()]
|
|
517
520
|
.filter((session) => isLiveStatus(session.status))
|
|
518
521
|
.map((session) => cloneSession(session));
|