@triflux/remote 10.43.0 → 10.45.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 (48) hide show
  1. package/cto/hygiene.mjs +1 -1
  2. package/hub/lib/cto-env.mjs +1 -0
  3. package/hub/pipe.mjs +10 -9
  4. package/hub/schema.sql +62 -2
  5. package/hub/server.mjs +276 -132
  6. package/hub/store.mjs +699 -41
  7. package/hub/team/backend.mjs +4 -2
  8. package/hub/team/claude-daemon-control.mjs +2 -0
  9. package/hub/team/cli/commands/start/start-headless.mjs +2 -0
  10. package/hub/team/cli/commands/start/start-mux.mjs +24 -3
  11. package/hub/team/cli/services/runtime-mode.mjs +23 -9
  12. package/hub/team/conductor.mjs +101 -24
  13. package/hub/team/cto-auto-collect.mjs +2 -6
  14. package/hub/team/cto-pull.mjs +4 -0
  15. package/hub/team/dashboard-open.mjs +1 -0
  16. package/hub/team/execution-mode.mjs +50 -9
  17. package/hub/team/headless.mjs +539 -92
  18. package/hub/team/interactive-tui-transport.mjs +15 -1
  19. package/hub/team/intervention.mjs +1 -1
  20. package/hub/team/launcher-template.mjs +17 -1
  21. package/hub/team/notify.mjs +31 -11
  22. package/hub/team/orchestrator.mjs +36 -9
  23. package/hub/team/pane.mjs +176 -14
  24. package/hub/team/psmux.mjs +79 -70
  25. package/hub/team/retry-state-machine.mjs +41 -13
  26. package/hub/team/session.mjs +57 -0
  27. package/hub/team/swarm-hypervisor.mjs +4 -2
  28. package/hub/team/swarm-preflight.mjs +47 -0
  29. package/hub/team/synapse-http.mjs +149 -4
  30. package/hub/team/terminal-opener.mjs +68 -13
  31. package/hub/team/tui-viewer.mjs +65 -14
  32. package/hub/team/worktree-lifecycle.mjs +56 -59
  33. package/hub/tools.mjs +22 -1
  34. package/hub/workers/codex-mcp.mjs +26 -5
  35. package/hub/workers/delegator-mcp.mjs +3 -49
  36. package/package.json +1 -1
  37. package/scripts/lib/agent-route-policy.mjs +367 -0
  38. package/scripts/lib/cli-agy.mjs +24 -4
  39. package/scripts/lib/cli-codex.mjs +22 -160
  40. package/scripts/lib/codex-profile-config.mjs +48 -4
  41. package/scripts/lib/codex-recovery.sh +19 -0
  42. package/scripts/lib/gemini-profiles.mjs +113 -29
  43. package/scripts/lib/keyword-rules.mjs +24 -0
  44. package/scripts/lib/logger.mjs +60 -43
  45. package/scripts/lib/machine-profile.mjs +293 -0
  46. package/scripts/lib/mcp-health.mjs +44 -18
  47. package/cto/steward.mjs +0 -362
  48. package/hub/log-retention.mjs +0 -726
@@ -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 { access, mkdir, readdir, realpath, rm } from "node:fs/promises";
8
- import { normalize, relative, resolve } from "node:path";
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: capture caller's branch BEFORE any checkout. Without
331
- // restoring it in finally, this function leaks main repo HEAD onto
332
- // integrationBranch and every subsequent Edit/commit lands on the swarm
333
- // temp branch silently (observed 2026-04-20: probe + edits + commits all
334
- // ended up on swarm/.../merge while user thought they were on main).
335
- let originalBranch = null;
336
- try {
337
- const head = await git(["rev-parse", "--abbrev-ref", "HEAD"], rootDir);
338
- if (head && head !== "HEAD") originalBranch = head;
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(["checkout", integrationBranch], rootDir);
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], rootDir);
368
- const headCommit = await git(["rev-parse", "HEAD"], rootDir);
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"], rootDir);
390
+ await git(["merge", "--abort"], integrationWorktree);
380
391
  } catch {
381
392
  /* ff-only usually leaves no merge state */
382
393
  }
383
394
  try {
384
- await git(["reset", "--hard", backupCommit], rootDir);
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], rootDir);
408
+ await git(["cherry-pick", sha], integrationWorktree);
395
409
  }
396
410
 
397
- const headCommit = await git(["rev-parse", "HEAD"], rootDir);
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
- try {
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(["reset", "--hard", backupCommit], rootDir);
425
+ await git(["cherry-pick", "--abort"], integrationWorktree);
435
426
  } catch {
436
- /* best-effort */
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
- await git(["branch", "-f", integrationBranch, backupCommit], rootDir);
430
+ // #129 rollback is now local to the disposable integration worktree.
431
+ await git(["reset", "--hard", backupCommit], integrationWorktree);
443
432
  } catch {
444
- /* best-effort: branch may be checked out in another worktree */
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
- // Always restore caller's branch. Skip if originalBranch is null
457
- // (detached HEAD case) or already on target.
458
- if (originalBranch) {
445
+ if (integrationWorktreeAdded) {
459
446
  try {
460
- await git(["checkout", originalBranch], rootDir);
447
+ await git(
448
+ ["worktree", "remove", "--force", integrationWorktree],
449
+ rootDir,
450
+ );
461
451
  } catch {
462
- /* best-effort — caller will see HEAD on integrationBranch */
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
@@ -2,6 +2,11 @@
2
2
  // register/status/publish/ask/poll/handoff/HITL + team proxy
3
3
  // 모든 도구 응답: { ok: boolean, error?: { code, message }, data?: ... }
4
4
 
5
+ import {
6
+ DEFAULT_ASSIGN_TIMEOUT_MS,
7
+ DEFAULT_ASSIGN_TTL_MS,
8
+ DEFAULT_HANDOFF_TTL_MS,
9
+ } from "@triflux/core/hub/lib/timeout-defaults.mjs";
5
10
  import { createPipeline, ensurePipelineTable } from "@triflux/core/hub/pipeline/index.mjs";
6
11
  import {
7
12
  initPipelineState,
@@ -10,7 +15,6 @@ import {
10
15
  } from "@triflux/core/hub/pipeline/state.mjs";
11
16
  import { sendInputToConductorSession } from "./team/conductor-registry.mjs";
12
17
  import { getTeamBridge } from "@triflux/core/hub/team-bridge.mjs";
13
- import { DEFAULT_ASSIGN_TIMEOUT_MS, DEFAULT_ASSIGN_TTL_MS, DEFAULT_HANDOFF_TTL_MS } from "@triflux/core/hub/lib/timeout-defaults.mjs";
14
18
 
15
19
  const TEAM_BRIDGE_NOT_REGISTERED = "bridge_not_registered";
16
20
 
@@ -149,6 +153,19 @@ export function createTools(store, router, hitl, pipe = null) {
149
153
  },
150
154
  topics: { type: "array", items: { type: "string" }, maxItems: 64 },
151
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
+ },
167
+ hub_instance_id: { type: "string" },
168
+ store_fingerprint: { type: "string" },
152
169
  heartbeat_ttl_ms: {
153
170
  type: "integer",
154
171
  minimum: 10000,
@@ -174,6 +191,8 @@ export function createTools(store, router, hitl, pipe = null) {
174
191
  agent_id: { type: "string" },
175
192
  trace_id: { type: "string" },
176
193
  include_metrics: { type: "boolean", default: true },
194
+ hub_instance_id: { type: "string" },
195
+ store_fingerprint: { type: "string" },
177
196
  },
178
197
  },
179
198
  handler: wrap("STATUS_FAILED", (args) => {
@@ -194,6 +213,8 @@ export function createTools(store, router, hitl, pipe = null) {
194
213
  agent_id: { type: "string", pattern: "^[a-zA-Z0-9._:-]{3,64}$" },
195
214
  reason: { type: "string" },
196
215
  requested_by: { type: "string" },
216
+ hub_instance_id: { type: "string" },
217
+ store_fingerprint: { type: "string" },
197
218
  },
198
219
  },
199
220
  handler: wrap("TAKEOVER_ROLE_FAILED", (args) => {
@@ -8,6 +8,7 @@ import { fileURLToPath } from "node:url";
8
8
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
9
9
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
10
10
 
11
+ import { normalizeCodexProfileName } from "../../scripts/lib/codex-profile-config.mjs";
11
12
  import {
12
13
  CODEX_MCP_EXECUTION_EXIT_CODE,
13
14
  CODEX_MCP_TRANSPORT_EXIT_CODE,
@@ -134,11 +135,13 @@ function readTomlScalar(raw, key) {
134
135
  export function resolveCodexProfileConfig(profileName) {
135
136
  const result = { model: null, reasoningEffort: null };
136
137
  if (typeof profileName !== "string" || !profileName) return result;
138
+ const normalizedProfileName = normalizeCodexProfileName(profileName);
139
+ if (!normalizedProfileName) return result;
137
140
 
138
141
  const codexHome = process.env.CODEX_HOME || join(homedir(), ".codex");
139
142
  try {
140
143
  const raw = readFileSync(
141
- join(codexHome, `${profileName}.config.toml`),
144
+ join(codexHome, `${normalizedProfileName}.config.toml`),
142
145
  "utf8",
143
146
  );
144
147
  result.model = readTomlScalar(raw, "model");
@@ -146,9 +149,13 @@ export function resolveCodexProfileConfig(profileName) {
146
149
  } catch {
147
150
  // Profile file absent (non-file alias). Derive effort from the naming
148
151
  // convention; leave model unset so codex uses its config.toml default.
149
- const suffix = /_(xhigh|high|med|medium|low)$/.exec(profileName);
152
+ const suffix = /_(ultra|max|xhigh|high|med|medium|low)$/.exec(
153
+ normalizedProfileName,
154
+ );
150
155
  if (suffix) {
151
156
  const map = {
157
+ ultra: "ultra",
158
+ max: "max",
152
159
  xhigh: "xhigh",
153
160
  high: "high",
154
161
  med: "medium",
@@ -193,16 +200,27 @@ export function buildCodexArguments(prompt, opts = {}) {
193
200
  // profile effort merges on top of any opts.config.
194
201
  if (typeof opts.profile === "string" && opts.profile) {
195
202
  const resolved = resolveCodexProfileConfig(opts.profile);
196
- if (resolved.model && typeof args.model !== "string") {
197
- args.model = resolved.model;
203
+ const profileDerivedUltra =
204
+ resolved.reasoningEffort?.toLowerCase() === "ultra";
205
+ if (typeof args.model !== "string") {
206
+ if (profileDerivedUltra) args.model = "gpt-6-astra";
207
+ else if (resolved.model) args.model = resolved.model;
198
208
  }
199
209
  if (resolved.reasoningEffort) {
200
210
  args.config = {
201
211
  ...(args.config || {}),
202
- model_reasoning_effort: resolved.reasoningEffort,
212
+ model_reasoning_effort: profileDerivedUltra
213
+ ? "max"
214
+ : resolved.reasoningEffort,
203
215
  };
204
216
  }
205
217
  }
218
+ if (typeof opts.reasoningEffort === "string" && opts.reasoningEffort) {
219
+ args.config = {
220
+ ...(args.config || {}),
221
+ model_reasoning_effort: opts.reasoningEffort,
222
+ };
223
+ }
206
224
 
207
225
  return args;
208
226
  }
@@ -694,6 +712,9 @@ function parseCliArgs(argv) {
694
712
  case "--model":
695
713
  options.model = next();
696
714
  break;
715
+ case "--reasoning-effort":
716
+ options.reasoningEffort = next();
717
+ break;
697
718
  case "--approval-policy":
698
719
  options.approvalPolicy = next();
699
720
  break;
@@ -11,6 +11,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
11
11
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
12
12
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
13
13
  import * as z from "zod";
14
+ import { resolveCodexAgentPolicy } from "../../scripts/lib/agent-route-policy.mjs";
14
15
  import { resolveBashExecutable } from "@triflux/core/hub/lib/bash-path.mjs";
15
16
  import { runHeadlessWithCleanup } from "../team/headless.mjs";
16
17
  import { buildWorkerSandboxEnv } from "../team/worker-sandbox.mjs";
@@ -33,57 +34,11 @@ const { buildPromptHint, getCodexMcpConfig, SUPPORTED_MCP_PROFILES } =
33
34
  await import(pathToFileURL(mcpFilterPath).href);
34
35
  const SERVER_INFO = { name: "triflux-delegator", version: "1.0.0" };
35
36
  const DEFAULT_CONTEXT_BYTES = 32 * 1024;
36
- const DEFAULT_ROUTE_TIMEOUT_SEC = 120;
37
37
  const DIRECT_PROGRESS_START = 5;
38
38
  const DIRECT_PROGRESS_RUNNING = 60;
39
39
  const DIRECT_PROGRESS_DONE = 100;
40
40
  const SEARCH_ENGINE_CACHE_PATH = [".omc", "state", "search-engines.json"];
41
41
 
42
- const AGENT_TIMEOUT_SEC = Object.freeze({
43
- executor: 1080,
44
- "build-fixer": 540,
45
- debugger: 900,
46
- "deep-executor": 3600,
47
- architect: 3600,
48
- planner: 3600,
49
- critic: 3600,
50
- analyst: 3600,
51
- "code-reviewer": 1800,
52
- "security-reviewer": 1800,
53
- "quality-reviewer": 1800,
54
- scientist: 1440,
55
- "scientist-deep": 3600,
56
- "document-specialist": 1440,
57
- designer: 900,
58
- writer: 900,
59
- explore: 300,
60
- verifier: 1200,
61
- "test-engineer": 300,
62
- "qa-tester": 300,
63
- spark: 180,
64
- });
65
-
66
- const CODEX_PROFILE_BY_AGENT = Object.freeze({
67
- executor: "gpt56_terra_high",
68
- "build-fixer": "gpt56_luna_low",
69
- debugger: "gpt56_sol_xhigh",
70
- "deep-executor": "gpt56_sol_xhigh",
71
- architect: "gpt56_sol_xhigh",
72
- planner: "gpt56_sol_xhigh",
73
- critic: "gpt56_sol_xhigh",
74
- analyst: "gpt56_sol_xhigh",
75
- "code-reviewer": "gpt56_terra_high",
76
- "security-reviewer": "gpt56_sol_xhigh",
77
- "quality-reviewer": "gpt56_terra_high",
78
- scientist: "gpt56_terra_high",
79
- "scientist-deep": "gpt56_sol_xhigh",
80
- "document-specialist": "gpt56_terra_high",
81
- verifier: "gpt56_terra_high",
82
- designer: "gpt56_sol_xhigh",
83
- writer: "gpt56_luna_low",
84
- spark: "gpt56_luna_low",
85
- });
86
-
87
42
  const REVIEW_INSTRUCTION_BY_AGENT = Object.freeze({
88
43
  "code-reviewer":
89
44
  "코드 리뷰 모드로 동작하라. 버그, 리스크, 회귀, 테스트 누락을 우선 식별하라.",
@@ -137,7 +92,7 @@ function resolveRouteScript(explicitPath, cwd = process.cwd()) {
137
92
  }
138
93
 
139
94
  function resolveCodexProfile(agentType) {
140
- return CODEX_PROFILE_BY_AGENT[agentType] || "high";
95
+ return resolveCodexAgentPolicy(agentType).profile;
141
96
  }
142
97
 
143
98
  function normalizeProvider(provider = "auto") {
@@ -154,8 +109,7 @@ function resolveTimeoutMs(agentType, timeoutMs) {
154
109
  return Math.trunc(timeoutMs);
155
110
  }
156
111
 
157
- const timeoutSec = AGENT_TIMEOUT_SEC[agentType] || DEFAULT_ROUTE_TIMEOUT_SEC;
158
- return timeoutSec * 1000;
112
+ return resolveCodexAgentPolicy(agentType).timeoutSec * 1000;
159
113
  }
160
114
 
161
115
  function resolveTimeoutSec(agentType, timeoutMs) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@triflux/remote",
3
- "version": "10.43.0",
3
+ "version": "10.45.0",
4
4
  "description": "triflux remote — team mode, psmux, MCP workers, SQLite store.",
5
5
  "type": "module",
6
6
  "main": "hub/index.mjs",