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
@@ -4,169 +4,23 @@
4
4
  // 산출한다. 실제 spawn 은 Phase 1 에서 이관된다 (현재는 tfx-route.sh 가 수행).
5
5
  //
6
6
  // 진입점: tfx-route.mjs → selectAdapter("codex") → plan({...}).
7
- // 출처: scripts/tfx-route.sh route_agent() L1001-L1079 case 미러.
7
+ // 출처: agent-route-policy.mjs canonical Codex role policy.
8
8
  // timeoutSec은 lane 예상 소요 시간(advisory)이다. hard ceiling 집행은 Bash route가 소유한다.
9
9
 
10
10
  export const id = "codex";
11
11
  export const cliType = "codex";
12
12
  export const command = "codex";
13
13
 
14
- const AGENT_PROFILES = {
15
- executor: {
16
- profile: "gpt56_terra_high",
17
- timeoutSec: 1080,
18
- runMode: "fg",
19
- opusOversight: false,
20
- mcpHint: "implement",
21
- },
22
- codex: {
23
- profile: "gpt56_terra_high",
24
- timeoutSec: 1080,
25
- runMode: "fg",
26
- opusOversight: false,
27
- mcpHint: "implement",
28
- },
29
- "build-fixer": {
30
- profile: "gpt56_luna_low",
31
- timeoutSec: 540,
32
- runMode: "fg",
33
- opusOversight: false,
34
- mcpHint: "implement",
35
- },
36
- cleanup: {
37
- profile: "gpt56_terra_med",
38
- timeoutSec: 540,
39
- runMode: "fg",
40
- opusOversight: false,
41
- mcpHint: "implement",
42
- },
43
- deslop: {
44
- profile: "gpt56_terra_med",
45
- timeoutSec: 540,
46
- runMode: "fg",
47
- opusOversight: false,
48
- mcpHint: "implement",
49
- },
50
- debugger: {
51
- profile: "gpt56_sol_xhigh",
52
- timeoutSec: 900,
53
- runMode: "bg",
54
- opusOversight: false,
55
- mcpHint: "implement",
56
- },
57
- "deep-executor": {
58
- profile: "gpt56_sol_xhigh",
59
- timeoutSec: 3600,
60
- runMode: "bg",
61
- opusOversight: true,
62
- mcpHint: "implement",
63
- },
64
- architect: {
65
- profile: "gpt56_sol_xhigh",
66
- timeoutSec: 3600,
67
- runMode: "bg",
68
- opusOversight: true,
69
- mcpHint: "analyze",
70
- },
71
- critic: {
72
- profile: "gpt56_sol_xhigh",
73
- timeoutSec: 3600,
74
- runMode: "bg",
75
- opusOversight: true,
76
- mcpHint: "analyze",
77
- },
78
- planner: {
79
- profile: "gpt56_sol_xhigh",
80
- timeoutSec: 3600,
81
- runMode: "fg",
82
- opusOversight: true,
83
- mcpHint: "analyze",
84
- },
85
- analyst: {
86
- profile: "gpt56_sol_xhigh",
87
- timeoutSec: 3600,
88
- runMode: "fg",
89
- opusOversight: true,
90
- mcpHint: "analyze",
91
- },
92
- "code-reviewer": {
93
- profile: "gpt56_terra_high",
94
- timeoutSec: 1800,
95
- runMode: "bg",
96
- opusOversight: false,
97
- mcpHint: "review",
98
- subcommand: "review",
99
- },
100
- "quality-reviewer": {
101
- profile: "gpt56_terra_high",
102
- timeoutSec: 1800,
103
- runMode: "bg",
104
- opusOversight: false,
105
- mcpHint: "review",
106
- subcommand: "review",
107
- },
108
- "security-reviewer": {
109
- profile: "gpt56_sol_xhigh",
110
- timeoutSec: 1800,
111
- runMode: "bg",
112
- opusOversight: true,
113
- mcpHint: "review",
114
- subcommand: "review",
115
- },
116
- scientist: {
117
- profile: "gpt56_terra_high",
118
- timeoutSec: 1440,
119
- runMode: "bg",
120
- opusOversight: false,
121
- mcpHint: "analyze",
122
- },
123
- "document-specialist": {
124
- profile: "gpt56_terra_high",
125
- timeoutSec: 1440,
126
- runMode: "bg",
127
- opusOversight: false,
128
- mcpHint: "analyze",
129
- },
130
- "scientist-deep": {
131
- profile: "gpt56_sol_xhigh",
132
- timeoutSec: 3600,
133
- runMode: "bg",
134
- opusOversight: false,
135
- mcpHint: "analyze",
136
- },
137
- verifier: {
138
- profile: "gpt56_terra_high",
139
- timeoutSec: 1200,
140
- runMode: "fg",
141
- opusOversight: false,
142
- mcpHint: "review",
143
- subcommand: "review",
144
- },
145
- "test-engineer": {
146
- profile: "gpt56_terra_high",
147
- timeoutSec: 1200,
148
- runMode: "bg",
149
- opusOversight: false,
150
- mcpHint: "implement",
151
- },
152
- "qa-tester": {
153
- profile: "gpt56_terra_high",
154
- timeoutSec: 1200,
155
- runMode: "bg",
156
- opusOversight: false,
157
- mcpHint: "review",
158
- subcommand: "review",
159
- },
160
- spark: {
161
- profile: "gpt56_luna_low",
162
- timeoutSec: 180,
163
- runMode: "fg",
164
- opusOversight: false,
165
- mcpHint: "implement",
166
- },
167
- };
14
+ import {
15
+ CODEX_AGENT_POLICY,
16
+ resolveCodexAgentPolicy,
17
+ resolveCodexAgentProfile,
18
+ resolveNestedCodexAgentProfile,
19
+ } from "./agent-route-policy.mjs";
168
20
 
169
- const DEFAULT_PROFILE = AGENT_PROFILES.executor;
21
+ // Compatibility re-exports for launchers which historically imported these
22
+ // helpers from this adapter. The policy module owns the semantics.
23
+ export { resolveCodexAgentProfile, resolveNestedCodexAgentProfile };
170
24
 
171
25
  export function plan({
172
26
  agent,
@@ -174,18 +28,26 @@ export function plan({
174
28
  mcpProfile = "auto",
175
29
  timeoutSec,
176
30
  contextFile,
31
+ profileOverride = "auto",
32
+ retryProfile = null,
33
+ nested = false,
177
34
  } = {}) {
178
35
  if (!agent) {
179
36
  throw new Error("[cli-codex] agent required");
180
37
  }
181
- const cfg = AGENT_PROFILES[agent] ?? DEFAULT_PROFILE;
38
+ const cfg = resolveCodexAgentPolicy(agent);
39
+ const profile = resolveCodexAgentProfile(agent, {
40
+ profileOverride,
41
+ retryProfile,
42
+ nested,
43
+ });
182
44
  const effectiveTimeoutSec =
183
45
  Number.isFinite(timeoutSec) && timeoutSec > 0 ? timeoutSec : cfg.timeoutSec;
184
46
  return {
185
47
  command,
186
48
  subcommand: cfg.subcommand ?? "exec",
187
- profile: cfg.profile,
188
- effort: cfg.profile,
49
+ profile,
50
+ effort: profile,
189
51
  timeoutMs: effectiveTimeoutSec * 1000,
190
52
  runMode: cfg.runMode,
191
53
  opusOversight: cfg.opusOversight,
@@ -196,5 +58,5 @@ export function plan({
196
58
  }
197
59
 
198
60
  export function describe() {
199
- return { id, cliType, command, agents: Object.keys(AGENT_PROFILES) };
61
+ return { id, cliType, command, agents: Object.keys(CODEX_AGENT_POLICY) };
200
62
  }
@@ -9,6 +9,8 @@ import { homedir } from "node:os";
9
9
  import { dirname, join } from "node:path";
10
10
 
11
11
  const EFFORT_BY_SUFFIX = {
12
+ ultra: "ultra",
13
+ max: "max",
12
14
  xhigh: "xhigh",
13
15
  high: "high",
14
16
  med: "medium",
@@ -16,6 +18,15 @@ const EFFORT_BY_SUFFIX = {
16
18
  low: "low",
17
19
  };
18
20
 
21
+ const CANONICAL_PROFILE_VALUES = Object.freeze({
22
+ gpt56_luna_low: { model: "gpt-5.6-luna", effort: "low" },
23
+ gpt56_terra_med: { model: "gpt-5.6-terra", effort: "medium" },
24
+ gpt56_terra_high: { model: "gpt-5.6-terra", effort: "high" },
25
+ gpt56_sol_xhigh: { model: "gpt-5.6-sol", effort: "xhigh" },
26
+ gpt56_sol_max: { model: "gpt-5.6-sol", effort: "max" },
27
+ gpt56_sol_ultra: { model: "gpt-5.6-sol", effort: "ultra" },
28
+ });
29
+
19
30
  function readProfileScalar(raw, key) {
20
31
  const match = String(raw).match(
21
32
  new RegExp(
@@ -49,8 +60,10 @@ function readProfileScalar(raw, key) {
49
60
  * @param {{ codexHome?: string }} [opts]
50
61
  * @returns {string[]} e.g. ['model="gpt-5.6-terra"', 'model_reasoning_effort="high"']
51
62
  */
52
- export function codexProfileConfigOverrides(profileName, opts = {}) {
53
- if (typeof profileName !== "string" || !profileName) return [];
63
+ export function resolveCodexProfileConfigValues(profileName, opts = {}) {
64
+ if (typeof profileName !== "string" || !profileName) {
65
+ return { model: null, effort: null };
66
+ }
54
67
  const codexHome =
55
68
  opts.codexHome || process.env.CODEX_HOME || join(homedir(), ".codex");
56
69
  let model = null;
@@ -63,9 +76,22 @@ export function codexProfileConfigOverrides(profileName, opts = {}) {
63
76
  model = readProfileScalar(raw, "model");
64
77
  effort = readProfileScalar(raw, "model_reasoning_effort");
65
78
  } catch {
66
- const suffix = /_(xhigh|high|med|medium|low)$/.exec(profileName);
79
+ const suffix = /_(ultra|max|xhigh|high|med|medium|low)$/.exec(profileName);
67
80
  if (suffix) effort = EFFORT_BY_SUFFIX[suffix[1]] || null;
68
81
  }
82
+ return { model, effort };
83
+ }
84
+
85
+ export function codexProfileConfigOverrides(profileName, opts = {}) {
86
+ let { model, effort } = resolveCodexProfileConfigValues(profileName, opts);
87
+ if (opts.enforceCanonicalProfile === true) {
88
+ const canonical = CANONICAL_PROFILE_VALUES[profileName];
89
+ if (canonical) ({ model, effort } = canonical);
90
+ }
91
+ if (opts.disallowUltra === true && effort?.toLowerCase() === "ultra") {
92
+ model = "gpt-5.6-sol";
93
+ effort = "max";
94
+ }
69
95
  const overrides = [];
70
96
  if (model) overrides.push(`model="${model}"`);
71
97
  if (effort) overrides.push(`model_reasoning_effort="${effort}"`);
@@ -26,6 +26,7 @@ function normalizeState(state) {
26
26
 
27
27
  function normalizeRule(rule) {
28
28
  if (!rule || typeof rule !== "object") return null;
29
+ if (rule.disabled === true) return null;
29
30
  if (typeof rule.id !== "string" || !rule.id.trim()) return null;
30
31
  if (!Array.isArray(rule.patterns) || rule.patterns.length === 0) return null;
31
32
  if (typeof rule.priority !== "number" || !Number.isFinite(rule.priority))
@@ -61,6 +62,13 @@ function normalizeRule(rule) {
61
62
  const state = normalizeState(rule.state);
62
63
  if (rule.state != null && state == null) return null;
63
64
 
65
+ // repo_scope: path segment 화이트리스트 (빈 배열 = 전역). explicit: 종결 라우팅 마커.
66
+ const repoScope = Array.isArray(rule.repo_scope)
67
+ ? rule.repo_scope
68
+ .filter((s) => typeof s === "string" && s.trim())
69
+ .map((s) => s.trim())
70
+ : [];
71
+
64
72
  return {
65
73
  id: rule.id.trim(),
66
74
  patterns,
@@ -72,6 +80,8 @@ function normalizeRule(rule) {
72
80
  exclusive: rule.exclusive === true,
73
81
  state,
74
82
  mcp_route: mcpRoute,
83
+ repo_scope: repoScope,
84
+ explicit: rule.explicit === true,
75
85
  };
76
86
  }
77
87
 
@@ -149,6 +159,8 @@ export function matchRules(compiledRules, cleanText) {
149
159
  exclusive: rule.exclusive === true,
150
160
  state: rule.state || null,
151
161
  mcp_route: rule.mcp_route || null,
162
+ repo_scope: rule.repo_scope || [],
163
+ explicit: rule.explicit === true,
152
164
  });
153
165
  }
154
166
 
@@ -121,6 +121,66 @@ function lintBodyModelNames({ file, body, startLine }) {
121
121
  return problems;
122
122
  }
123
123
 
124
+ function lintAgentModels({ file, body, startLine }) {
125
+ const problems = [];
126
+ const agentStartRe = /\bAgent\s*\(/g;
127
+ let match;
128
+ while ((match = agentStartRe.exec(body)) !== null) {
129
+ const openParenIndex = match.index + match[0].lastIndexOf("(");
130
+ let quote = null;
131
+ let depth = 1;
132
+ let closeParenIndex = -1;
133
+
134
+ for (let index = openParenIndex + 1; index < body.length; index += 1) {
135
+ const character = body[index];
136
+
137
+ if (quote) {
138
+ if (character === "\\") {
139
+ index += 1;
140
+ } else if (character === quote) {
141
+ quote = null;
142
+ }
143
+ continue;
144
+ }
145
+
146
+ if (character === '"' || character === "'" || character === "`") {
147
+ quote = character;
148
+ } else if (character === "(") {
149
+ depth += 1;
150
+ } else if (character === ")") {
151
+ depth -= 1;
152
+ if (depth === 0) {
153
+ closeParenIndex = index;
154
+ break;
155
+ }
156
+ }
157
+ }
158
+
159
+ if (closeParenIndex === -1) continue;
160
+
161
+ const call = body.slice(openParenIndex + 1, closeParenIndex);
162
+ agentStartRe.lastIndex = closeParenIndex + 1;
163
+ if (!/\bsubagent_type\s*=/.test(call) || !/\bprompt\s*=/.test(call)) {
164
+ continue;
165
+ }
166
+ if (/\bmodel\s*=/.test(call)) continue;
167
+ const line =
168
+ startLine + body.slice(0, match.index).split(/\r?\n/).length - 1;
169
+ problems.push(
170
+ createProblem({
171
+ file,
172
+ line,
173
+ code: "agent-model-required",
174
+ problem: "executable Agent call is missing model=",
175
+ cause:
176
+ "실행형 Agent 호출은 모델 선택을 명시해야 라우팅 drift를 막을 수 있습니다.",
177
+ fix: "Add model=<resolved-native-model> to the Agent call.",
178
+ }),
179
+ );
180
+ }
181
+ return problems;
182
+ }
183
+
124
184
  function lintSkillFile({ file, rootDir }) {
125
185
  const content = readFileSync(file, "utf8");
126
186
  const { data, body } = parseFrontmatter(content);
@@ -133,6 +193,11 @@ function lintSkillFile({ file, rootDir }) {
133
193
  body,
134
194
  startLine: bodyStartLine(content),
135
195
  }),
196
+ ...lintAgentModels({
197
+ file: relativeFile,
198
+ body,
199
+ startLine: bodyStartLine(content),
200
+ }),
136
201
  ];
137
202
  }
138
203
 
package/scripts/pack.mjs CHANGED
@@ -34,6 +34,8 @@ const CORE_FILES = [
34
34
  "hub/intent.mjs",
35
35
  "hub/token-mode.mjs",
36
36
  "hub/router.mjs",
37
+ "hub/role-contract.mjs",
38
+ "hub/role-control-events.mjs",
37
39
  "hub/hitl.mjs",
38
40
  "hub/assign-callbacks.mjs",
39
41
  "hub/fullcycle.mjs",
@@ -51,6 +53,8 @@ const CORE_FILES = [
51
53
  // Phase 1 dynamic routing (#251 + #263 + #264 + wire-ups)
52
54
  "hub/dynamic-routing-engine.mjs",
53
55
  "hub/routing-snapshot.mjs",
56
+ "hub/role-contract.mjs",
57
+ "hub/role-control-events.mjs",
54
58
  ];
55
59
 
56
60
  const CORE_DIRS = [
@@ -60,6 +64,7 @@ const CORE_DIRS = [
60
64
  "hub/delegator",
61
65
  "hub/lib",
62
66
  "hub/middleware",
67
+ "hub/team/intervention.mjs",
63
68
  "hub/team/retry-state-machine.mjs",
64
69
  "hub/team/claude-agent-session-normalizer.mjs",
65
70
  "hub/team/claude-daemon-control.mjs",
@@ -88,6 +93,7 @@ const REMOTE_FILES = [
88
93
  "cto/brief.mjs",
89
94
  "cto/collect.mjs",
90
95
  "cto/events.mjs",
96
+ "cto/hygiene-actions.mjs",
91
97
  "cto/hygiene-notify.mjs",
92
98
  "cto/hygiene.mjs",
93
99
  "cto/lake-root.mjs",
@@ -96,6 +102,7 @@ const REMOTE_FILES = [
96
102
  ];
97
103
 
98
104
  const REMOTE_DIRS = ["hub/team", "hub/workers", "hub/public"];
105
+ const REMOTE_CORE_PROXIES = ["hub/lib/cto-env.mjs"];
99
106
 
100
107
  const TRIFLUX_DIRS = [
101
108
  "bin",
@@ -301,6 +308,12 @@ function packRemote() {
301
308
  cleanDist(dest);
302
309
  for (const f of REMOTE_FILES) copyItem(f, dest);
303
310
  for (const d of REMOTE_DIRS) copyItem(d, dest);
311
+ for (const proxy of REMOTE_CORE_PROXIES) {
312
+ const target = join(dest, proxy);
313
+ mkdirSync(dirname(target), { recursive: true });
314
+ writeFileSync(target, `export * from "@triflux/core/${proxy}";\n`);
315
+ console.log(` WRITE ${proxy}`);
316
+ }
304
317
  // shared scripts/lib needed by delegator-mcp.mjs (dynamic resolve)
305
318
  copyItem("scripts/lib", dest);
306
319
  writeIndex(dest, REMOTE_INDEX);
@@ -22,6 +22,7 @@ import { fileURLToPath } from "node:url";
22
22
  const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
23
23
  const DEFAULT_REPO_ROOT = join(SCRIPT_DIR, "..", "..");
24
24
  const MIRROR_TOPS = [
25
+ "adapters",
25
26
  "bin",
26
27
  "config",
27
28
  "cto",
@@ -38,6 +39,10 @@ const CORE_FILE_MIRRORS = [
38
39
  source: "hub/bridge.mjs",
39
40
  target: "packages/core/hub/bridge.mjs",
40
41
  },
42
+ {
43
+ source: "hub/router.mjs",
44
+ target: "packages/core/hub/router.mjs",
45
+ },
41
46
  {
42
47
  source: "hub/team/retry-state-machine.mjs",
43
48
  target: "packages/core/hub/team/retry-state-machine.mjs",
package/scripts/setup.mjs CHANGED
@@ -14,6 +14,7 @@ import {
14
14
  mkdirSync,
15
15
  readdirSync,
16
16
  readFileSync,
17
+ renameSync,
17
18
  rmSync,
18
19
  unlinkSync,
19
20
  writeFileSync,
@@ -73,7 +74,15 @@ const HUD_PATH = join(CLAUDE_DIR, "hud", "hud-qos-status.mjs");
73
74
  const WINDOWS_HUB_AUTOSTART_TASK = "TrifluxHubEnsure";
74
75
 
75
76
  const REQUIRED_CODEX_PROFILES = [
76
- // GPT-5.6 family — 기존 xhigh/high/med/low 의미를 모델별 lane으로 유지.
77
+ // GPT-5.6 family — max/ultra are explicit exception lanes, not role defaults.
78
+ {
79
+ name: "gpt56_sol_ultra",
80
+ lines: ['model = "gpt-5.6-sol"', 'model_reasoning_effort = "ultra"'],
81
+ },
82
+ {
83
+ name: "gpt56_sol_max",
84
+ lines: ['model = "gpt-5.6-sol"', 'model_reasoning_effort = "max"'],
85
+ },
77
86
  {
78
87
  name: "gpt56_sol_xhigh",
79
88
  lines: ['model = "gpt-5.6-sol"', 'model_reasoning_effort = "xhigh"'],
@@ -498,6 +507,7 @@ const SKILL_ALIASES = [
498
507
 
499
508
  const DEPRECATED_SKILLS = ["tfx-codex-route", "tfx-gemini-route"];
500
509
  const LOCAL_DEV_SKILL_MARKER = ".triflux-local-skill";
510
+ const MANAGED_CODEX_SKILL_MARKER = ".triflux-managed-skill";
501
511
 
502
512
  // ── 구형 Codex 모델 (마이그레이션 안내 대상) ──
503
513
 
@@ -579,6 +589,76 @@ function isLocalDevSkillDir(skillPath) {
579
589
  return existsSync(join(skillPath, LOCAL_DEV_SKILL_MARKER));
580
590
  }
581
591
 
592
+ function skillTreeMatches(srcDir, dstDir) {
593
+ if (!existsSync(srcDir) || !existsSync(dstDir)) return false;
594
+ for (const entry of readdirSync(srcDir, { withFileTypes: true })) {
595
+ const srcPath = join(srcDir, entry.name);
596
+ const dstPath = join(dstDir, entry.name);
597
+ if (entry.isDirectory()) {
598
+ if (!skillTreeMatches(srcPath, dstPath)) return false;
599
+ continue;
600
+ }
601
+ if (!existsSync(dstPath)) return false;
602
+ if (!readFileSync(srcPath).equals(readFileSync(dstPath))) return false;
603
+ }
604
+ return true;
605
+ }
606
+
607
+ /**
608
+ * Sync the tracked Codex tfx-harness adapter without taking ownership of a
609
+ * pre-existing user skill. A missing source is a hard failure; a conflicting
610
+ * destination is backed up and deliberately left untouched.
611
+ */
612
+ function syncCodexHarnessAdapter({
613
+ sourceDir = join(PLUGIN_ROOT, "adapters", "codex", "skills", "tfx-harness"),
614
+ destinationDir = join(CODEX_DIR, "skills", "tfx-harness"),
615
+ } = {}) {
616
+ const sourceSkill = join(sourceDir, "SKILL.md");
617
+ if (!existsSync(sourceSkill)) {
618
+ return {
619
+ ok: false,
620
+ action: "blocked",
621
+ reason: "codex_harness_adapter_source_missing",
622
+ sourceDir,
623
+ destinationDir,
624
+ };
625
+ }
626
+
627
+ const managed = existsSync(join(destinationDir, MANAGED_CODEX_SKILL_MARKER));
628
+ const current = skillTreeMatches(sourceDir, destinationDir);
629
+ if (current && managed) {
630
+ return { ok: true, action: "noop", sourceDir, destinationDir };
631
+ }
632
+
633
+ if (existsSync(destinationDir) && !managed) {
634
+ const backupDir = `${destinationDir}.triflux-backup-${Date.now()}`;
635
+ cpSync(destinationDir, backupDir, { recursive: true });
636
+ return {
637
+ ok: true,
638
+ action: "skipped",
639
+ reason: "user_owned_codex_skill",
640
+ sourceDir,
641
+ destinationDir,
642
+ backupDir,
643
+ };
644
+ }
645
+
646
+ const tempDir = `${destinationDir}.triflux-tmp-${process.pid}-${Date.now()}`;
647
+ mkdirSync(dirname(destinationDir), { recursive: true });
648
+ cpSync(sourceDir, tempDir, { recursive: true });
649
+ writeFileSync(
650
+ join(tempDir, MANAGED_CODEX_SKILL_MARKER),
651
+ "managed by triflux\n",
652
+ );
653
+
654
+ if (existsSync(destinationDir)) {
655
+ const previousDir = `${destinationDir}.triflux-previous-${Date.now()}`;
656
+ renameSync(destinationDir, previousDir);
657
+ }
658
+ renameSync(tempDir, destinationDir);
659
+ return { ok: true, action: "synced", sourceDir, destinationDir };
660
+ }
661
+
582
662
  /**
583
663
  * 훅 커맨드 문자열에서 스크립트 파일명을 추출한다.
584
664
  * 예: 'node "/path/to/safety-guard.mjs"' → "safety-guard.mjs"
@@ -1526,6 +1606,7 @@ export {
1526
1606
  SYNC_MAP,
1527
1607
  scanHudFiles,
1528
1608
  syncAliasedSkillDir,
1609
+ syncCodexHarnessAdapter,
1529
1610
  syncWorkerPackages,
1530
1611
  validateSchtasksTrLength,
1531
1612
  WINDOWS_HUB_AUTOSTART_TASK,
@@ -1581,6 +1662,16 @@ export async function runDeferred(stdinData) {
1581
1662
  result.action === "updated" ||
1582
1663
  result.action === "removed",
1583
1664
  ).length;
1665
+ const codexHarnessSync = syncCodexHarnessAdapter();
1666
+ if (!codexHarnessSync.ok) {
1667
+ io.log(` \x1b[31m✗\x1b[0m Codex tfx-harness: ${codexHarnessSync.reason}`);
1668
+ return io.result(1);
1669
+ }
1670
+ if (codexHarnessSync.action === "skipped") {
1671
+ io.log(
1672
+ ` \x1b[33m⚠\x1b[0m Codex tfx-harness: user skill preserved; backup ${codexHarnessSync.backupDir}`,
1673
+ );
1674
+ }
1584
1675
  const cloakBrowserResult = ensureCloakBrowser({
1585
1676
  warn: (message) => io.log(` \x1b[33m⚠\x1b[0m ${message}`),
1586
1677
  });
@@ -1601,7 +1692,8 @@ export async function runDeferred(stdinData) {
1601
1692
  return io.result(0);
1602
1693
  }
1603
1694
 
1604
- let synced = claudeRoutingChangedCount;
1695
+ let synced =
1696
+ claudeRoutingChangedCount + (codexHarnessSync.action === "synced" ? 1 : 0);
1605
1697
 
1606
1698
  // ── Memory Doctor (P0 자동 수정) ──
1607
1699
  const isCIEnv = process.env.CI === "true" || process.env.DOCKER === "true";
@@ -165,6 +165,8 @@ export function buildRoutePlan(request, agentMap) {
165
165
  mcpProfile: request.mcpProfile ?? "auto",
166
166
  timeoutSec: request.timeoutSec,
167
167
  contextFile: request.contextFile,
168
+ profileOverride: request.profileOverride,
169
+ nested: request.nested,
168
170
  });
169
171
  return {
170
172
  agent: request.agent,
@@ -272,7 +274,18 @@ async function main(argv) {
272
274
  }
273
275
 
274
276
  const agentMap = loadAgentMap();
275
- const plan = buildRoutePlan(request, agentMap);
277
+ const plan = buildRoutePlan(
278
+ {
279
+ ...request,
280
+ profileOverride: process.env.TFX_CODEX_PROFILE,
281
+ nested: Boolean(
282
+ process.env.TFX_TEAM_NAME ||
283
+ process.env.TFX_WORKER_INDEX ||
284
+ process.env.TFX_WORKER_SANDBOX_SCOPE,
285
+ ),
286
+ },
287
+ agentMap,
288
+ );
276
289
 
277
290
  if (request.mode === "route-print") {
278
291
  process.stdout.write(`${JSON.stringify(plan, null, 2)}\n`);