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.
Files changed (47) hide show
  1. package/README.md +2 -0
  2. package/adapters/codex/skills/tfx-harness/SKILL.md +30 -0
  3. package/adapters/codex/skills/tfx-harness/skill.json +5 -0
  4. package/bin/triflux.mjs +14 -0
  5. package/hooks/keyword-rules.json +139 -19
  6. package/hub/cli-adapter-base.mjs +11 -2
  7. package/hub/codex-adapter.mjs +3 -0
  8. package/hub/intent.mjs +24 -32
  9. package/hub/router.mjs +272 -0
  10. package/hub/schema.sql +61 -2
  11. package/hub/server.mjs +42 -0
  12. package/hub/store.mjs +577 -5
  13. package/hub/team/conductor.mjs +23 -2
  14. package/hub/team/execution-mode.mjs +12 -2
  15. package/hub/team/headless.mjs +16 -0
  16. package/hub/team/retry-state-machine.mjs +8 -3
  17. package/hub/team/swarm-hypervisor.mjs +1 -0
  18. package/hub/team/swarm-preflight.mjs +47 -0
  19. package/hub/team/synapse-http.mjs +149 -4
  20. package/hub/team/worktree-lifecycle.mjs +56 -59
  21. package/hub/tools.mjs +11 -0
  22. package/hub/workers/codex-mcp.mjs +20 -4
  23. package/hub/workers/delegator-mcp.mjs +3 -49
  24. package/package.json +2 -1
  25. package/scripts/__tests__/lint-skills.test.mjs +68 -0
  26. package/scripts/__tests__/tfx-route-node-entry.test.mjs +36 -0
  27. package/scripts/keyword-detector.mjs +55 -8
  28. package/scripts/lib/agent-route-policy.mjs +360 -0
  29. package/scripts/lib/cli-codex.mjs +22 -160
  30. package/scripts/lib/codex-profile-config.mjs +29 -3
  31. package/scripts/lib/keyword-rules.mjs +12 -0
  32. package/scripts/lint-skills.mjs +65 -0
  33. package/scripts/pack.mjs +13 -0
  34. package/scripts/release/check-packages-mirror.mjs +5 -0
  35. package/scripts/setup.mjs +94 -2
  36. package/scripts/tfx-route.mjs +14 -1
  37. package/scripts/tfx-route.sh +233 -70
  38. package/skills/tfx-harness/SKILL.md +19 -76
  39. package/skills/tfx-hub/SKILL.md +1 -1
  40. package/skills/tfx-interview/SKILL.md +3 -1
  41. package/skills/tfx-interview/skill.json +1 -13
  42. package/skills/tfx-profile/SKILL.md +25 -15
  43. package/skills/tfx-prune/SKILL.md +3 -1
  44. package/skills/tfx-prune/skill.json +1 -8
  45. package/skills/tfx-setup/SKILL.md +1 -1
  46. package/tui/codex-profile.mjs +11 -2
  47. package/tui/setup.mjs +2 -0
package/hub/router.mjs CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  resolveHardCeilingMs,
10
10
  } from "./lib/timeout-defaults.mjs";
11
11
  import { uuidv7 } from "./lib/uuidv7.mjs";
12
+ import { normalizeRoleKey } from "./role-contract.mjs";
12
13
 
13
14
  const ASSIGN_PENDING_STATUSES = new Set(["queued", "running"]);
14
15
  const ROLE_TOPICS = new Set(["cto"]);
@@ -16,6 +17,258 @@ const ROLE_SOURCE_RANK = Object.freeze({
16
17
  explicit: 2,
17
18
  "topic-fallback": 1,
18
19
  });
20
+ const TRANSPORT_LOCATOR_SCHEMA_VERSION = "tfx.transport-locator.v1";
21
+ const TRANSPORT_KINDS = new Set(["claude-uds", "tmux"]);
22
+ const TRANSPORT_CAPABILITIES = new Set([
23
+ "probe",
24
+ "wake",
25
+ "activate",
26
+ "interrupt",
27
+ ]);
28
+ const HOST_ID_RE = /^hst_[A-Za-z0-9_-]+$/u;
29
+
30
+ function registrationError(code, message = code) {
31
+ const error = new Error(message);
32
+ error.code = code;
33
+ return error;
34
+ }
35
+
36
+ function registrationString(value, code, { maxLength = 256 } = {}) {
37
+ if (typeof value !== "string" || !value.trim() || value.length > maxLength) {
38
+ throw registrationError(code);
39
+ }
40
+ return value.trim();
41
+ }
42
+
43
+ function registrationLogicalRef(value, code, { maxLength = 128 } = {}) {
44
+ const normalized = registrationString(value, code, { maxLength });
45
+ if (!/^[A-Za-z0-9._:-]+$/u.test(normalized)) {
46
+ throw registrationError(code);
47
+ }
48
+ return normalized;
49
+ }
50
+
51
+ function assertExactFields(value, allowed, code) {
52
+ if (
53
+ value === null ||
54
+ typeof value !== "object" ||
55
+ Array.isArray(value) ||
56
+ Object.keys(value).some((field) => !allowed.has(field))
57
+ ) {
58
+ throw registrationError(code);
59
+ }
60
+ }
61
+
62
+ function normalizeTransportLocator(
63
+ value,
64
+ { cli, hostId, now, leaseExpiresMs },
65
+ ) {
66
+ assertExactFields(
67
+ value,
68
+ new Set([
69
+ "schema_version",
70
+ "transport_id",
71
+ "kind",
72
+ "host_id",
73
+ "capabilities",
74
+ "priority",
75
+ "expires_at_ms",
76
+ "locator",
77
+ ]),
78
+ "TRANSPORT_LOCATOR_INVALID",
79
+ );
80
+ if (value.schema_version !== TRANSPORT_LOCATOR_SCHEMA_VERSION) {
81
+ throw registrationError("TRANSPORT_LOCATOR_SCHEMA_UNSUPPORTED");
82
+ }
83
+ const transportId = registrationLogicalRef(
84
+ value.transport_id,
85
+ "TRANSPORT_ID_INVALID",
86
+ { maxLength: 128 },
87
+ );
88
+ if (!TRANSPORT_KINDS.has(value.kind)) {
89
+ throw registrationError("TRANSPORT_KIND_UNSUPPORTED");
90
+ }
91
+ if (cli === "codex" && value.kind !== "tmux") {
92
+ throw registrationError("CODEX_TRANSPORT_UNSUPPORTED");
93
+ }
94
+ const locatorHostId = registrationString(value.host_id, "HOST_ID_INVALID", {
95
+ maxLength: 128,
96
+ });
97
+ if (!HOST_ID_RE.test(locatorHostId) || (hostId && locatorHostId !== hostId)) {
98
+ throw registrationError("HOST_ID_INVALID");
99
+ }
100
+ if (!Array.isArray(value.capabilities)) {
101
+ throw registrationError("TRANSPORT_CAPABILITIES_INVALID");
102
+ }
103
+ const capabilities = uniqueStrings(value.capabilities);
104
+ if (
105
+ capabilities.length !== value.capabilities.length ||
106
+ capabilities.some((capability) => !TRANSPORT_CAPABILITIES.has(capability))
107
+ ) {
108
+ throw registrationError("TRANSPORT_CAPABILITIES_INVALID");
109
+ }
110
+ if (!Number.isSafeInteger(value.priority)) {
111
+ throw registrationError("TRANSPORT_PRIORITY_INVALID");
112
+ }
113
+ if (
114
+ !Number.isSafeInteger(value.expires_at_ms) ||
115
+ value.expires_at_ms <= now ||
116
+ value.expires_at_ms > leaseExpiresMs
117
+ ) {
118
+ throw registrationError("TRANSPORT_EXPIRY_INVALID");
119
+ }
120
+
121
+ let locator;
122
+ if (value.kind === "claude-uds") {
123
+ assertExactFields(
124
+ value.locator,
125
+ new Set(["session_id", "short", "bridge_id"]),
126
+ "TRANSPORT_LOCATOR_INVALID",
127
+ );
128
+ const sessionId = value.locator.session_id;
129
+ const short = value.locator.short;
130
+ if (!sessionId && !short) {
131
+ throw registrationError("CLAUDE_UDS_SESSION_REQUIRED");
132
+ }
133
+ locator = {
134
+ ...(sessionId
135
+ ? {
136
+ session_id: registrationLogicalRef(
137
+ sessionId,
138
+ "CLAUDE_UDS_SESSION_INVALID",
139
+ ),
140
+ }
141
+ : {}),
142
+ ...(short
143
+ ? {
144
+ short: registrationLogicalRef(short, "CLAUDE_UDS_SHORT_INVALID", {
145
+ maxLength: 128,
146
+ }),
147
+ }
148
+ : {}),
149
+ bridge_id: registrationLogicalRef(
150
+ value.locator.bridge_id,
151
+ "CLAUDE_UDS_BRIDGE_INVALID",
152
+ { maxLength: 128 },
153
+ ),
154
+ };
155
+ } else {
156
+ assertExactFields(
157
+ value.locator,
158
+ new Set(["session_name", "mux_server_id"]),
159
+ "TRANSPORT_LOCATOR_INVALID",
160
+ );
161
+ locator = {
162
+ session_name: registrationLogicalRef(
163
+ value.locator.session_name,
164
+ "TMUX_SESSION_INVALID",
165
+ { maxLength: 128 },
166
+ ),
167
+ mux_server_id: registrationLogicalRef(
168
+ value.locator.mux_server_id,
169
+ "TMUX_SERVER_INVALID",
170
+ { maxLength: 128 },
171
+ ),
172
+ };
173
+ }
174
+
175
+ return {
176
+ schema_version: TRANSPORT_LOCATOR_SCHEMA_VERSION,
177
+ transport_id: transportId,
178
+ kind: value.kind,
179
+ host_id: locatorHostId,
180
+ capabilities,
181
+ priority: value.priority,
182
+ expires_at_ms: value.expires_at_ms,
183
+ locator,
184
+ };
185
+ }
186
+
187
+ export function normalizeRegistrationIdentity(args = {}, now = Date.now()) {
188
+ const hasProjectId = Object.hasOwn(args, "project_id");
189
+ const hasSessionId = Object.hasOwn(args, "session_id");
190
+ const hasHostId = Object.hasOwn(args, "host_id");
191
+ const hasLocatorJson = Object.hasOwn(args, "transport_locators_json");
192
+ const hasLocators = Object.hasOwn(args, "transport_locators");
193
+ if (
194
+ !hasProjectId &&
195
+ !hasSessionId &&
196
+ !hasHostId &&
197
+ !hasLocatorJson &&
198
+ !hasLocators
199
+ ) {
200
+ return null;
201
+ }
202
+
203
+ const projectId = hasProjectId
204
+ ? normalizeRoleKey({ project_id: args.project_id, role_kind: "cto" })
205
+ .project_id
206
+ : null;
207
+ const sessionId = hasSessionId
208
+ ? registrationString(args.session_id, "SESSION_ID_INVALID")
209
+ : null;
210
+ const hostId = hasHostId
211
+ ? registrationString(args.host_id, "HOST_ID_INVALID", { maxLength: 128 })
212
+ : null;
213
+ if (hostId && !HOST_ID_RE.test(hostId)) {
214
+ throw registrationError("HOST_ID_INVALID");
215
+ }
216
+
217
+ let rawLocators = [];
218
+ if (hasLocatorJson) {
219
+ if (Array.isArray(args.transport_locators_json)) {
220
+ rawLocators = args.transport_locators_json;
221
+ } else if (typeof args.transport_locators_json === "string") {
222
+ try {
223
+ rawLocators = JSON.parse(args.transport_locators_json);
224
+ } catch {
225
+ throw registrationError("TRANSPORT_LOCATORS_JSON_INVALID");
226
+ }
227
+ } else {
228
+ throw registrationError("TRANSPORT_LOCATORS_JSON_INVALID");
229
+ }
230
+ } else if (hasLocators) {
231
+ rawLocators = args.transport_locators;
232
+ }
233
+ if (!Array.isArray(rawLocators)) {
234
+ throw registrationError("TRANSPORT_LOCATORS_INVALID");
235
+ }
236
+
237
+ const ttlMs = Number(args.heartbeat_ttl_ms ?? 30000);
238
+ if (rawLocators.length > 0 && (!Number.isFinite(ttlMs) || ttlMs <= 0)) {
239
+ throw registrationError("HEARTBEAT_TTL_INVALID");
240
+ }
241
+ const leaseExpiresMs = now + ttlMs;
242
+ const normalizedLocators = rawLocators
243
+ .map((locator) =>
244
+ normalizeTransportLocator(locator, {
245
+ cli: args.cli,
246
+ hostId,
247
+ now,
248
+ leaseExpiresMs,
249
+ }),
250
+ )
251
+ .sort(
252
+ (left, right) =>
253
+ right.priority - left.priority ||
254
+ left.transport_id.localeCompare(right.transport_id),
255
+ );
256
+ const resolvedHostId = hostId || normalizedLocators[0]?.host_id || null;
257
+ if (
258
+ resolvedHostId &&
259
+ normalizedLocators.some((locator) => locator.host_id !== resolvedHostId)
260
+ ) {
261
+ throw registrationError("HOST_ID_INVALID");
262
+ }
263
+
264
+ return {
265
+ project_id: projectId,
266
+ session_id: sessionId,
267
+ host_id: resolvedHostId,
268
+ transport_locators_json:
269
+ hasLocatorJson || hasLocators ? JSON.stringify(normalizedLocators) : null,
270
+ };
271
+ }
19
272
 
20
273
  function uniqueStrings(values = []) {
21
274
  return Array.from(
@@ -123,6 +376,18 @@ export function createRouter(store) {
123
376
  const queuesByAgent = new Map();
124
377
  const liveMessages = new Map();
125
378
  const roleStates = new Map();
379
+ const updateRegistrationIdentity = store.db?.prepare
380
+ ? store.db.prepare(`
381
+ UPDATE agents SET
382
+ project_id=COALESCE(@project_id, project_id),
383
+ session_id=COALESCE(@session_id, session_id),
384
+ host_id=COALESCE(@host_id, host_id),
385
+ transport_locators_json=COALESCE(
386
+ @transport_locators_json,
387
+ transport_locators_json
388
+ )
389
+ WHERE agent_id=@agent_id`)
390
+ : null;
126
391
  const MAX_LATENCY_SAMPLES = 100;
127
392
  let latencyIdx = 0;
128
393
  const deliveryLatencies = new Array(MAX_LATENCY_SAMPLES).fill(0);
@@ -855,10 +1120,17 @@ export function createRouter(store) {
855
1120
  deliveryEmitter,
856
1121
 
857
1122
  registerAgent(args) {
1123
+ const identity = normalizeRegistrationIdentity(args);
858
1124
  const result = store.registerAgent(args);
859
1125
  if (!registerPresenceIsEffective(result)) {
860
1126
  return registerPresenceFailure(args.agent_id, result);
861
1127
  }
1128
+ if (identity && updateRegistrationIdentity) {
1129
+ updateRegistrationIdentity.run({
1130
+ agent_id: args.agent_id,
1131
+ ...identity,
1132
+ });
1133
+ }
862
1134
  upsertRuntimeTopics(args.agent_id, args.topics || [], { replace: true });
863
1135
  refreshRoleCandidateForAgent(args.agent_id);
864
1136
  for (const roleName of ROLE_TOPICS) {
package/hub/schema.sql CHANGED
@@ -11,7 +11,11 @@ CREATE TABLE IF NOT EXISTS agents (
11
11
  last_seen_ms INTEGER NOT NULL,
12
12
  lease_expires_ms INTEGER NOT NULL,
13
13
  status TEXT NOT NULL CHECK (status IN ('online','stale','offline')),
14
- metadata_json TEXT NOT NULL DEFAULT '{}'
14
+ metadata_json TEXT NOT NULL DEFAULT '{}',
15
+ project_id TEXT,
16
+ session_id TEXT,
17
+ host_id TEXT,
18
+ transport_locators_json TEXT NOT NULL DEFAULT '[]'
15
19
  );
16
20
 
17
21
  -- 메시지 테이블
@@ -28,7 +32,61 @@ CREATE TABLE IF NOT EXISTS messages (
28
32
  correlation_id TEXT NOT NULL,
29
33
  trace_id TEXT NOT NULL,
30
34
  payload_json TEXT NOT NULL DEFAULT '{}',
31
- status TEXT NOT NULL CHECK (status IN ('queued','delivered','acked','expired','dead_letter'))
35
+ status TEXT NOT NULL CHECK (status IN ('queued','delivered','acked','expired','dead_letter')),
36
+ role_key_wire TEXT
37
+ );
38
+
39
+ -- 동적 역할 레지스트리 (schema v6)
40
+ CREATE TABLE IF NOT EXISTS role_registry (
41
+ role_key_wire TEXT PRIMARY KEY,
42
+ project_id TEXT NOT NULL,
43
+ role_kind TEXT NOT NULL CHECK (role_kind IN ('cto','lead')),
44
+ scope_id TEXT NOT NULL DEFAULT '',
45
+ state TEXT NOT NULL CHECK (
46
+ state IN (
47
+ 'electable',
48
+ 'elected-probing',
49
+ 'active',
50
+ 'unreachable',
51
+ 'quarantined'
52
+ )
53
+ ),
54
+ holder_agent_id TEXT,
55
+ previous_holder_agent_id TEXT,
56
+ epoch INTEGER NOT NULL DEFAULT 0 CHECK (epoch >= 0),
57
+ activation_seq INTEGER NOT NULL DEFAULT 0 CHECK (activation_seq >= 0),
58
+ activation_id TEXT,
59
+ activation_deadline_ms INTEGER,
60
+ holder_lease_expires_ms INTEGER,
61
+ charter_version TEXT,
62
+ charter_acked_epoch INTEGER,
63
+ retry_count INTEGER NOT NULL DEFAULT 0,
64
+ next_probe_ms INTEGER,
65
+ blocked_reason TEXT,
66
+ last_transition_ms INTEGER NOT NULL,
67
+ last_reason TEXT NOT NULL,
68
+ version INTEGER NOT NULL DEFAULT 0,
69
+ CHECK (
70
+ (role_kind = 'cto' AND scope_id = '') OR
71
+ (role_kind = 'lead' AND scope_id <> '')
72
+ ),
73
+ UNIQUE(project_id, role_kind, scope_id)
74
+ );
75
+
76
+ CREATE TABLE IF NOT EXISTS role_candidates (
77
+ role_key_wire TEXT NOT NULL,
78
+ agent_id TEXT NOT NULL,
79
+ source TEXT NOT NULL CHECK (source IN ('grant','standup','operator')),
80
+ priority INTEGER NOT NULL DEFAULT 0,
81
+ transport_locators_json TEXT NOT NULL DEFAULT '[]',
82
+ consecutive_probe_failures INTEGER NOT NULL DEFAULT 0,
83
+ excluded_until_ms INTEGER,
84
+ last_probe_ms INTEGER,
85
+ last_probe_code TEXT,
86
+ updated_at_ms INTEGER NOT NULL,
87
+ PRIMARY KEY(role_key_wire, agent_id),
88
+ FOREIGN KEY(role_key_wire)
89
+ REFERENCES role_registry(role_key_wire) ON DELETE CASCADE
32
90
  );
33
91
 
34
92
  -- 메시지 수신함 (배달 추적)
@@ -73,6 +131,7 @@ CREATE INDEX IF NOT EXISTS idx_messages_correlation ON messages(correlation_id);
73
131
  CREATE INDEX IF NOT EXISTS idx_messages_trace ON messages(trace_id);
74
132
  CREATE INDEX IF NOT EXISTS idx_messages_expires ON messages(expires_at_ms);
75
133
  CREATE INDEX IF NOT EXISTS idx_messages_priority ON messages(priority DESC, created_at_ms ASC);
134
+ CREATE INDEX IF NOT EXISTS idx_messages_role_key ON messages(role_key_wire, status, expires_at_ms);
76
135
  CREATE INDEX IF NOT EXISTS idx_inbox_agent ON message_inbox(agent_id, delivered_at_ms);
77
136
  CREATE INDEX IF NOT EXISTS idx_inbox_message ON message_inbox(message_id);
78
137
  CREATE INDEX IF NOT EXISTS idx_human_requests_state ON human_requests(state);
package/hub/server.mjs CHANGED
@@ -1558,6 +1558,38 @@ export async function startHub({
1558
1558
  try {
1559
1559
  const body = await parseBody(req);
1560
1560
  const { sessionId } = body || {};
1561
+ if (body?.agent_id && body?.cli) {
1562
+ const registered = router.registerAgent({
1563
+ agent_id: body.agent_id,
1564
+ cli: body.cli,
1565
+ capabilities: Array.isArray(body.capabilities)
1566
+ ? body.capabilities
1567
+ : [],
1568
+ topics: Array.isArray(body.topics) ? body.topics : [],
1569
+ heartbeat_ttl_ms: body.heartbeat_ttl_ms ?? 300000,
1570
+ metadata:
1571
+ body.metadata && typeof body.metadata === "object"
1572
+ ? body.metadata
1573
+ : { source: "session_start" },
1574
+ ...(Object.hasOwn(body, "project_id")
1575
+ ? { project_id: body.project_id }
1576
+ : {}),
1577
+ ...(Object.hasOwn(body, "session_id")
1578
+ ? { session_id: body.session_id }
1579
+ : {}),
1580
+ ...(Object.hasOwn(body, "host_id")
1581
+ ? { host_id: body.host_id }
1582
+ : {}),
1583
+ ...(Object.hasOwn(body, "transport_locators_json")
1584
+ ? { transport_locators_json: body.transport_locators_json }
1585
+ : {}),
1586
+ });
1587
+ if (!registered?.ok) {
1588
+ throw new Error(
1589
+ registered?.error?.message || "hub agent register failed",
1590
+ );
1591
+ }
1592
+ }
1561
1593
  const result = synapseRegistry.register(sessionId, body);
1562
1594
  if (!result?.ok) {
1563
1595
  throw new Error(result?.reason || "register failed");
@@ -1712,6 +1744,10 @@ export async function startHub({
1712
1744
  topics = [],
1713
1745
  capabilities = [],
1714
1746
  metadata = {},
1747
+ project_id,
1748
+ session_id,
1749
+ host_id,
1750
+ transport_locators_json,
1715
1751
  } = body;
1716
1752
  if (!agent_id || !cli) {
1717
1753
  return writeJson(res, 400, {
@@ -1740,6 +1776,12 @@ export async function startHub({
1740
1776
  topics,
1741
1777
  heartbeat_ttl_ms,
1742
1778
  metadata,
1779
+ ...(project_id === undefined ? {} : { project_id }),
1780
+ ...(session_id === undefined ? {} : { session_id }),
1781
+ ...(host_id === undefined ? {} : { host_id }),
1782
+ ...(transport_locators_json === undefined
1783
+ ? {}
1784
+ : { transport_locators_json }),
1743
1785
  });
1744
1786
  if (result.ok) {
1745
1787
  workerSpans.set(agent_id, {