triflux 10.42.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 (84) 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/tfx-live.mjs +3 -1
  5. package/bin/triflux.mjs +14 -0
  6. package/config/routing-policy.json +1 -1
  7. package/hooks/keyword-rules.json +139 -19
  8. package/hooks/pipeline-stop.mjs +1 -1
  9. package/hub/bridge.mjs +80 -1
  10. package/hub/cli-adapter-base.mjs +111 -21
  11. package/hub/codex-adapter.mjs +8 -1
  12. package/hub/dynamic-routing-engine.mjs +1 -1
  13. package/hub/gemini-adapter.mjs +3 -1
  14. package/hub/intent.mjs +24 -28
  15. package/hub/lib/cto-env.mjs +46 -0
  16. package/hub/lib/memory-store.mjs +25 -13
  17. package/hub/lib/timeout-defaults.mjs +45 -0
  18. package/hub/lib/worker-lifecycle.mjs +101 -0
  19. package/hub/middleware/request-logger.mjs +4 -1
  20. package/hub/pipe.mjs +9 -0
  21. package/hub/role-contract.mjs +314 -0
  22. package/hub/role-control-events.mjs +113 -0
  23. package/hub/router.mjs +383 -29
  24. package/hub/schema.sql +62 -2
  25. package/hub/server.mjs +130 -43
  26. package/hub/store.mjs +609 -19
  27. package/hub/team/claude-daemon-control.mjs +28 -15
  28. package/hub/team/cli/commands/start/parse-args.mjs +2 -2
  29. package/hub/team/conductor.mjs +23 -2
  30. package/hub/team/execution-mode.mjs +12 -2
  31. package/hub/team/headless.mjs +311 -24
  32. package/hub/team/intervention.mjs +530 -0
  33. package/hub/team/retry-state-machine.mjs +12 -3
  34. package/hub/team/swarm-hypervisor.mjs +21 -2
  35. package/hub/team/swarm-locks.mjs +17 -1
  36. package/hub/team/swarm-preflight.mjs +47 -0
  37. package/hub/team/synapse-http.mjs +149 -4
  38. package/hub/team/uds-orchestrator.mjs +62 -9
  39. package/hub/team/worktree-lifecycle.mjs +56 -59
  40. package/hub/tools.mjs +19 -3
  41. package/hub/workers/codex-app-server-worker.mjs +54 -5
  42. package/hub/workers/codex-mcp.mjs +21 -5
  43. package/hub/workers/delegator-mcp.mjs +3 -49
  44. package/hud/constants.mjs +21 -0
  45. package/hud/context-monitor.mjs +18 -21
  46. package/hud/hud-qos-status.mjs +1 -1
  47. package/hud/providers/codex-probe.mjs +216 -0
  48. package/hud/providers/codex.mjs +206 -29
  49. package/hud/renderers.mjs +6 -9
  50. package/package.json +2 -1
  51. package/scripts/__tests__/lint-skills.test.mjs +68 -0
  52. package/scripts/__tests__/tfx-route-bash-node-parity.test.mjs +1 -1
  53. package/scripts/__tests__/tfx-route-node-entry.test.mjs +42 -6
  54. package/scripts/headless-guard.mjs +70 -23
  55. package/scripts/keyword-detector.mjs +55 -8
  56. package/scripts/lib/agent-route-policy.mjs +360 -0
  57. package/scripts/lib/cli-agy.mjs +1 -0
  58. package/scripts/lib/cli-claude.mjs +1 -0
  59. package/scripts/lib/cli-codex.mjs +23 -160
  60. package/scripts/lib/codex-profile-config.mjs +31 -5
  61. package/scripts/lib/keyword-rules.mjs +12 -0
  62. package/scripts/lint-skills.mjs +65 -0
  63. package/scripts/pack.mjs +13 -0
  64. package/scripts/release/check-packages-mirror.mjs +5 -0
  65. package/scripts/setup.mjs +108 -12
  66. package/scripts/tfx-gate-activate.mjs +1 -1
  67. package/scripts/tfx-route-worker.mjs +5 -0
  68. package/scripts/tfx-route.mjs +14 -1
  69. package/scripts/tfx-route.sh +333 -118
  70. package/skills/tfx-analysis/SKILL.md +3 -1
  71. package/skills/tfx-harness/SKILL.md +19 -76
  72. package/skills/tfx-hub/SKILL.md +2 -2
  73. package/skills/tfx-interview/SKILL.md +3 -1
  74. package/skills/tfx-interview/skill.json +1 -13
  75. package/skills/tfx-plan/SKILL.md +3 -1
  76. package/skills/tfx-profile/SKILL.md +32 -22
  77. package/skills/tfx-prune/SKILL.md +7 -3
  78. package/skills/tfx-prune/skill.json +1 -8
  79. package/skills/tfx-qa/SKILL.md +6 -2
  80. package/skills/tfx-research/SKILL.md +3 -1
  81. package/skills/tfx-review/SKILL.md +5 -3
  82. package/skills/tfx-setup/SKILL.md +1 -1
  83. package/tui/codex-profile.mjs +14 -3
  84. package/tui/setup.mjs +6 -4
@@ -0,0 +1,314 @@
1
+ const PROJECT_ID_RE = /^prj_[A-Za-z0-9_-]{22}$/u;
2
+ const SCOPE_ID_RE = /^scp_[A-Za-z0-9_-]{22}$/u;
3
+ const BASE64URL_RE = /^[A-Za-z0-9_-]+$/u;
4
+
5
+ export const ROLE_KINDS = Object.freeze(["cto", "lead"]);
6
+ export const ROLE_REACHABILITY_STATES = Object.freeze([
7
+ "electable",
8
+ "elected-probing",
9
+ "active",
10
+ "unreachable",
11
+ "quarantined",
12
+ ]);
13
+ export const ROLE_STATUS_SCHEMA_VERSION = "tfx.roles.v2";
14
+ export const LEGACY_ROLE_SNAPSHOT_FIELDS = Object.freeze([
15
+ "role",
16
+ "status",
17
+ "leader_agent_id",
18
+ "previous_leader_agent_id",
19
+ "leader_epoch",
20
+ "lease_expires_ms",
21
+ "candidate_source",
22
+ "candidate_count",
23
+ "live_candidate_count",
24
+ "pending_count",
25
+ "transferred_count",
26
+ "last_transition_ms",
27
+ "last_reason",
28
+ "candidates",
29
+ ]);
30
+
31
+ function roleContractError(code, message = code) {
32
+ const error = new Error(message);
33
+ error.code = code;
34
+ return error;
35
+ }
36
+
37
+ function isRecord(value) {
38
+ return value !== null && typeof value === "object" && !Array.isArray(value);
39
+ }
40
+
41
+ function canonicalJsonRoleKey(key) {
42
+ return key.role_kind === "lead"
43
+ ? `{"project_id":${JSON.stringify(key.project_id)},"role_kind":"lead","scope_id":${JSON.stringify(key.scope_id)}}`
44
+ : `{"project_id":${JSON.stringify(key.project_id)},"role_kind":"cto"}`;
45
+ }
46
+
47
+ /**
48
+ * Validate and normalize the only two valid dynamic role identities.
49
+ *
50
+ * @param {unknown} input
51
+ * @returns {{project_id: string, role_kind: "cto"} | {project_id: string, role_kind: "lead", scope_id: string}}
52
+ */
53
+ export function normalizeRoleKey(input) {
54
+ if (!isRecord(input)) throw roleContractError("ROLE_KEY_INVALID");
55
+
56
+ const fields = Object.keys(input);
57
+ const allowed = new Set(["project_id", "role_kind", "scope_id"]);
58
+ if (fields.some((field) => !allowed.has(field))) {
59
+ throw roleContractError("ROLE_KEY_FIELD_UNSUPPORTED");
60
+ }
61
+
62
+ const { project_id: projectId, role_kind: roleKind } = input;
63
+ if (typeof projectId !== "string" || !PROJECT_ID_RE.test(projectId)) {
64
+ throw roleContractError("PROJECT_ID_INVALID");
65
+ }
66
+ if (!ROLE_KINDS.includes(roleKind)) {
67
+ throw roleContractError("ROLE_KIND_UNSUPPORTED");
68
+ }
69
+
70
+ const hasScope = Object.hasOwn(input, "scope_id");
71
+ if (roleKind === "cto") {
72
+ if (hasScope) throw roleContractError("ROLE_SCOPE_FORBIDDEN");
73
+ return { project_id: projectId, role_kind: "cto" };
74
+ }
75
+
76
+ if (!hasScope || typeof input.scope_id !== "string") {
77
+ throw roleContractError("SCOPE_REQUIRED");
78
+ }
79
+ if (!SCOPE_ID_RE.test(input.scope_id)) {
80
+ throw roleContractError("SCOPE_ID_INVALID");
81
+ }
82
+ return { project_id: projectId, role_kind: "lead", scope_id: input.scope_id };
83
+ }
84
+
85
+ /** @param {unknown} key */
86
+ export function encodeRoleKey(key) {
87
+ const normalized = normalizeRoleKey(key);
88
+ const wire = `rk2.${Buffer.from(canonicalJsonRoleKey(normalized), "utf8").toString("base64url")}`;
89
+ if (Buffer.byteLength(wire, "utf8") > 512) {
90
+ throw roleContractError("ROLE_KEY_WIRE_TOO_LONG");
91
+ }
92
+ return wire;
93
+ }
94
+
95
+ /** @param {unknown} wire */
96
+ export function decodeRoleKey(wire) {
97
+ if (typeof wire !== "string" || Buffer.byteLength(wire, "utf8") > 512) {
98
+ throw roleContractError("ROLE_KEY_WIRE_INVALID");
99
+ }
100
+ if (!wire.startsWith("rk2.")) throw roleContractError("ROLE_KEY_WIRE_PREFIX");
101
+
102
+ const encoded = wire.slice(4);
103
+ if (!encoded || !BASE64URL_RE.test(encoded)) {
104
+ throw roleContractError("ROLE_KEY_NON_CANONICAL");
105
+ }
106
+
107
+ let parsed;
108
+ try {
109
+ const canonicalSource = Buffer.from(encoded, "base64url").toString("utf8");
110
+ parsed = JSON.parse(canonicalSource);
111
+ } catch {
112
+ throw roleContractError("ROLE_KEY_NON_CANONICAL");
113
+ }
114
+
115
+ let normalized;
116
+ try {
117
+ normalized = normalizeRoleKey(parsed);
118
+ } catch (error) {
119
+ if (error?.code) throw error;
120
+ throw roleContractError("ROLE_KEY_NON_CANONICAL");
121
+ }
122
+ if (encodeRoleKey(normalized) !== wire) {
123
+ throw roleContractError("ROLE_KEY_NON_CANONICAL");
124
+ }
125
+ return normalized;
126
+ }
127
+
128
+ /** @param {unknown} key */
129
+ export function roleTopicAddress(key) {
130
+ return `topic:role:${encodeRoleKey(key)}`;
131
+ }
132
+
133
+ const TRANSITION_ROWS = [
134
+ ["electable", "candidate_selected", "elected-probing", "reserve_holder"],
135
+ ["electable", "standup_reserved", "elected-probing", "reserve_standup"],
136
+ ["electable", "no_candidate", "electable", "record_blocked_no_candidate"],
137
+ [
138
+ "electable",
139
+ "candidates_excluded",
140
+ "quarantined",
141
+ "schedule_exclusion_expiry",
142
+ ],
143
+ ["elected-probing", "probe_succeeded", "elected-probing", "send_activation"],
144
+ ["elected-probing", "activation_acked", "active", "record_activation_ack"],
145
+ [
146
+ "elected-probing",
147
+ "probe_failed",
148
+ "unreachable",
149
+ "exclude_failed_candidate",
150
+ ],
151
+ ["elected-probing", "wake_failed", "unreachable", "exclude_failed_candidate"],
152
+ ["elected-probing", "ack_timeout", "unreachable", "exclude_failed_candidate"],
153
+ [
154
+ "elected-probing",
155
+ "holder_lease_expired",
156
+ "electable",
157
+ "revoke_holder_epoch_fence",
158
+ ],
159
+ [
160
+ "active",
161
+ "holder_lease_expired",
162
+ "electable",
163
+ "revoke_holder_preserve_backlog",
164
+ ],
165
+ ["active", "holder_offline", "electable", "revoke_holder_preserve_backlog"],
166
+ ["active", "transport_failed", "unreachable", "fence_active_authority"],
167
+ ["active", "charter_reissued", "elected-probing", "reserve_reack"],
168
+ [
169
+ "unreachable",
170
+ "alternate_candidate_available",
171
+ "electable",
172
+ "failover_candidate",
173
+ ],
174
+ ["unreachable", "retry_due", "elected-probing", "reserve_retry_epoch"],
175
+ ["unreachable", "retries_exhausted", "quarantined", "schedule_quarantine"],
176
+ [
177
+ "quarantined",
178
+ "exclusion_expired",
179
+ "electable",
180
+ "restore_candidate_eligibility",
181
+ ],
182
+ ["quarantined", "candidate_registered", "electable", "clear_quarantine"],
183
+ ["quarantined", "locator_registered", "electable", "clear_quarantine"],
184
+ ["quarantined", "capability_registered", "electable", "clear_quarantine"],
185
+ ...ROLE_REACHABILITY_STATES.flatMap((state) => [
186
+ [state, "stale_ack", state, "reject_stale_ack"],
187
+ [state, "scope_closed", null, "remove_registry"],
188
+ ]),
189
+ ];
190
+
191
+ export const ROLE_REACHABILITY_TRANSITIONS = Object.freeze(
192
+ TRANSITION_ROWS.map(([from, trigger, to, action]) =>
193
+ Object.freeze({ from, trigger, to, action }),
194
+ ),
195
+ );
196
+
197
+ const TRANSITIONS_BY_KEY = new Map(
198
+ ROLE_REACHABILITY_TRANSITIONS.map((transition) => [
199
+ `${transition.from}:${transition.trigger}`,
200
+ transition,
201
+ ]),
202
+ );
203
+
204
+ /**
205
+ * Apply one already-guarded reachability event. Guard evaluation and all side
206
+ * effects deliberately remain outside this inert contract kernel.
207
+ */
208
+ export function reduceRoleReachabilityTransition(state, trigger) {
209
+ if (!ROLE_REACHABILITY_STATES.includes(state)) {
210
+ throw roleContractError("ROLE_REACHABILITY_STATE_INVALID");
211
+ }
212
+ const transition = TRANSITIONS_BY_KEY.get(`${state}:${trigger}`);
213
+ if (!transition) throw roleContractError("INVALID_ROLE_TRANSITION");
214
+ return trigger === "stale_ack"
215
+ ? { state: transition.to, action: transition.action, code: "STALE_EPOCH" }
216
+ : { state: transition.to, action: transition.action };
217
+ }
218
+
219
+ export const reduceRoleReachability = reduceRoleReachabilityTransition;
220
+
221
+ function snapshotLegacyDetails(snapshot) {
222
+ const legacy = isRecord(snapshot.legacy) ? snapshot.legacy : {};
223
+ return {
224
+ candidate_source:
225
+ legacy.candidate_source ?? snapshot.candidate_source ?? null,
226
+ transferred_count:
227
+ legacy.transferred_count ?? snapshot.transferred_count ?? 0,
228
+ candidates: legacy.candidates ?? snapshot.candidates ?? [],
229
+ };
230
+ }
231
+
232
+ /**
233
+ * Construct the v2 status envelope in deterministic role-key order.
234
+ * `generatedAtMs` is supplied by the caller so the kernel has no clock.
235
+ */
236
+ export function createRoleStatusEnvelopeV2({
237
+ items = [],
238
+ generatedAtMs,
239
+ projectId,
240
+ } = {}) {
241
+ if (!Number.isFinite(generatedAtMs)) {
242
+ throw roleContractError("ROLE_STATUS_GENERATED_AT_REQUIRED");
243
+ }
244
+ if (
245
+ projectId !== undefined &&
246
+ (typeof projectId !== "string" || !PROJECT_ID_RE.test(projectId))
247
+ ) {
248
+ throw roleContractError("PROJECT_ID_INVALID");
249
+ }
250
+ if (!Array.isArray(items))
251
+ throw roleContractError("ROLE_STATUS_ITEMS_INVALID");
252
+
253
+ return {
254
+ schema_version: ROLE_STATUS_SCHEMA_VERSION,
255
+ generated_at_ms: generatedAtMs,
256
+ ...(projectId === undefined ? {} : { project_id: projectId }),
257
+ items: [...items].sort((left, right) =>
258
+ String(left?.role_key_wire || "").localeCompare(
259
+ String(right?.role_key_wire || ""),
260
+ ),
261
+ ),
262
+ };
263
+ }
264
+
265
+ /**
266
+ * Project the single exact-project CTO into the legacy `roles.cto` shape.
267
+ * The optional `legacy` detail preserves fields that v2 callers do not use.
268
+ */
269
+ export function projectLegacyCtoStatus(envelope, projectId) {
270
+ if (
271
+ !isRecord(envelope) ||
272
+ envelope.schema_version !== ROLE_STATUS_SCHEMA_VERSION ||
273
+ !Array.isArray(envelope.items)
274
+ ) {
275
+ throw roleContractError("ROLE_STATUS_ENVELOPE_INVALID");
276
+ }
277
+ if (typeof projectId !== "string" || !PROJECT_ID_RE.test(projectId)) {
278
+ throw roleContractError("SCOPE_REQUIRED");
279
+ }
280
+
281
+ const snapshot = envelope.items.find((item) => {
282
+ const key = item?.role_key;
283
+ return key?.project_id === projectId && key?.role_kind === "cto";
284
+ });
285
+ if (!snapshot) return { cto: null };
286
+
287
+ const status =
288
+ snapshot.state === "active"
289
+ ? "active"
290
+ : snapshot.state === "electable" || snapshot.state === "elected-probing"
291
+ ? "electable"
292
+ : "offline";
293
+ const legacy = snapshotLegacyDetails(snapshot);
294
+ return {
295
+ cto: {
296
+ role: "cto",
297
+ status,
298
+ leader_agent_id:
299
+ status === "active" ? (snapshot.holder_agent_id ?? null) : null,
300
+ previous_leader_agent_id: snapshot.previous_holder_agent_id ?? null,
301
+ leader_epoch: snapshot.epoch,
302
+ lease_expires_ms:
303
+ status === "active" ? (snapshot.lease_expires_ms ?? null) : null,
304
+ candidate_source: legacy.candidate_source,
305
+ candidate_count: snapshot.candidate_count,
306
+ live_candidate_count: snapshot.live_candidate_count,
307
+ pending_count: snapshot.pending_count,
308
+ transferred_count: legacy.transferred_count,
309
+ last_transition_ms: snapshot.last_transition_ms,
310
+ last_reason: snapshot.last_reason,
311
+ candidates: legacy.candidates,
312
+ },
313
+ };
314
+ }
@@ -0,0 +1,113 @@
1
+ const EVENT_NAMES = new Set(["capability.degraded", "drift.detected"]);
2
+ const CONTROL_ACTIONS = new Set([
3
+ "log_only",
4
+ "candidate_exclude",
5
+ "adapter_disable",
6
+ "role_failover",
7
+ "dispatch_block",
8
+ "manager_disable",
9
+ ]);
10
+ const OPTIONAL_FIELDS = [
11
+ "project_id",
12
+ "role_key_wire",
13
+ "candidate_agent_id",
14
+ "transport_id",
15
+ "capability",
16
+ "expected_version",
17
+ "observed_version",
18
+ "trace_id",
19
+ "error_code",
20
+ ];
21
+
22
+ export const ROLE_CONTROL_EVENT_SCHEMA_VERSION = "tfx.role-control-event.v1";
23
+ export const ROLE_CONTROL_EVENT_FIELDS = Object.freeze([
24
+ "schema_version",
25
+ "event",
26
+ "event_id",
27
+ "module",
28
+ "dur_ms",
29
+ "control_action",
30
+ "reason_code",
31
+ ...OPTIONAL_FIELDS,
32
+ ]);
33
+
34
+ function eventError(code, message = code) {
35
+ const error = new Error(message);
36
+ error.code = code;
37
+ return error;
38
+ }
39
+
40
+ function defaultEventId() {
41
+ return (
42
+ globalThis.crypto?.randomUUID?.() ??
43
+ `evt_${Math.random().toString(36).slice(2)}`
44
+ );
45
+ }
46
+
47
+ function validateRequired(event) {
48
+ if (!EVENT_NAMES.has(event.event))
49
+ throw eventError("ROLE_CONTROL_EVENT_INVALID");
50
+ if (typeof event.event_id !== "string" || !event.event_id) {
51
+ throw eventError("ROLE_CONTROL_EVENT_REQUIRED");
52
+ }
53
+ if (typeof event.module !== "string" || !event.module) {
54
+ throw eventError("ROLE_CONTROL_EVENT_REQUIRED");
55
+ }
56
+ if (!Number.isFinite(event.dur_ms) || event.dur_ms < 0) {
57
+ throw eventError("ROLE_CONTROL_EVENT_REQUIRED");
58
+ }
59
+ if (!CONTROL_ACTIONS.has(event.control_action)) {
60
+ throw eventError("ROLE_CONTROL_EVENT_REQUIRED");
61
+ }
62
+ if (typeof event.reason_code !== "string" || !event.reason_code) {
63
+ throw eventError("ROLE_CONTROL_EVENT_REQUIRED");
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Build an allowlisted event object. Unknown domain fields are intentionally
69
+ * discarded before the event reaches a logger.
70
+ */
71
+ export function buildRoleControlEvent(
72
+ input = {},
73
+ { eventId = defaultEventId } = {},
74
+ ) {
75
+ const event = {
76
+ schema_version: ROLE_CONTROL_EVENT_SCHEMA_VERSION,
77
+ event: input.event,
78
+ event_id: input.event_id ?? eventId(),
79
+ module: input.module,
80
+ dur_ms: input.dur_ms,
81
+ control_action: input.control_action,
82
+ reason_code: input.reason_code,
83
+ };
84
+ for (const field of OPTIONAL_FIELDS) {
85
+ if (input[field] !== undefined) event[field] = input[field];
86
+ }
87
+ validateRequired(event);
88
+ return event;
89
+ }
90
+
91
+ export function buildCapabilityDegradedEvent(input = {}, options = {}) {
92
+ return buildRoleControlEvent(
93
+ { ...input, event: "capability.degraded" },
94
+ options,
95
+ );
96
+ }
97
+
98
+ export function buildDriftDetectedEvent(input = {}, options = {}) {
99
+ return buildRoleControlEvent({ ...input, event: "drift.detected" }, options);
100
+ }
101
+
102
+ /**
103
+ * Pino-style loggers receive event as an object field and as the message.
104
+ */
105
+ export function emitRoleControlEvent(log, event, level = "warn") {
106
+ if (!log || typeof log[level] !== "function") {
107
+ throw eventError("ROLE_CONTROL_LOGGER_INVALID");
108
+ }
109
+ const sanitized = buildRoleControlEvent(event, {
110
+ eventId: () => event?.event_id,
111
+ });
112
+ log[level](sanitized, sanitized.event);
113
+ }