triflux 10.24.0 → 10.25.1

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 (107) hide show
  1. package/bin/triflux.mjs +118 -4
  2. package/config/mcp-registry.json +85 -0
  3. package/config/routing-policy.json +124 -0
  4. package/hooks/safety-guard.mjs +8 -1
  5. package/hub/team/backend.mjs +44 -6
  6. package/hub/team/claude-daemon-control.mjs +219 -0
  7. package/hub/team/claude-native-bridge.mjs +785 -0
  8. package/hub/team/claude-session-projection.mjs +65 -0
  9. package/hub/team/cli/commands/start/index.mjs +7 -0
  10. package/hub/team/cli/commands/start/parse-args.mjs +51 -1
  11. package/hub/team/cli/commands/start/start-headless.mjs +12 -0
  12. package/hub/team/headless-bridge-session.mjs +17 -0
  13. package/hub/team/headless.mjs +418 -50
  14. package/hub/team/swarm-cli.mjs +15 -0
  15. package/hub/team/swarm-hypervisor.mjs +171 -7
  16. package/hub/team/worktree-lifecycle.mjs +4 -16
  17. package/hud/constants.mjs +6 -0
  18. package/hud/hud-qos-status.mjs +11 -3
  19. package/hud/renderers.mjs +5 -11
  20. package/package.json +6 -3
  21. package/scripts/__tests__/install-mcp-gateway-startup.test.mjs +367 -0
  22. package/scripts/__tests__/mcp-gateway-health-check.test.mjs +208 -0
  23. package/scripts/__tests__/mcp-gateway-wrapper-check.test.mjs +54 -0
  24. package/scripts/__tests__/mcp-guard-engine-http-headers.test.mjs +49 -3
  25. package/scripts/__tests__/mcp-guard-engine-policy-sync.test.mjs +203 -0
  26. package/scripts/__tests__/mcp-guard-engine-stdio-sync.test.mjs +171 -0
  27. package/scripts/__tests__/mcp-guard-engine-sync-http-headers.test.mjs +47 -1
  28. package/scripts/__tests__/mcp-guard-engine-watch-http-headers.test.mjs +3 -4
  29. package/scripts/__tests__/mcp-guard-engine.test.mjs +87 -2
  30. package/scripts/__tests__/release-governance.test.mjs +161 -1
  31. package/scripts/__tests__/tfx-route-bash-node-parity.test.mjs +204 -0
  32. package/scripts/__tests__/tfx-route-node-entry.test.mjs +403 -0
  33. package/scripts/__tests__/tfx-route-phase1-modules.test.mjs +268 -0
  34. package/scripts/codex-mcp-gateway-sync.mjs +1 -1
  35. package/scripts/doctor-diagnose.mjs +14 -8
  36. package/scripts/headless-guard.mjs +9 -4
  37. package/scripts/install-mcp-gateway-startup.mjs +503 -0
  38. package/scripts/lib/agent-json.mjs +27 -0
  39. package/scripts/lib/async.mjs +174 -0
  40. package/scripts/lib/cli-agy.mjs +62 -0
  41. package/scripts/lib/cli-claude.mjs +78 -0
  42. package/scripts/lib/cli-codex.mjs +199 -0
  43. package/scripts/lib/cli-gemini.mjs +67 -0
  44. package/scripts/lib/env.mjs +14 -0
  45. package/scripts/lib/hub.mjs +48 -0
  46. package/scripts/lib/mcp-gateway-health-check.mjs +159 -0
  47. package/scripts/lib/mcp-gateway-servers.mjs +70 -0
  48. package/scripts/lib/mcp-gateway-wrapper-check.mjs +35 -0
  49. package/scripts/lib/mcp-guard-engine.mjs +524 -25
  50. package/scripts/lib/mcp-manifest.mjs +2 -2
  51. package/scripts/lib/pid.mjs +63 -0
  52. package/scripts/lib/quota.mjs +47 -0
  53. package/scripts/lib/team.mjs +75 -0
  54. package/scripts/lib/timeout.mjs +66 -0
  55. package/scripts/lib/tmp.mjs +26 -0
  56. package/scripts/lib/toml.mjs +113 -0
  57. package/scripts/mcp-gateway-config.mjs +2 -31
  58. package/scripts/mcp-gateway-ensure.mjs +24 -6
  59. package/scripts/mcp-gateway-start.mjs +16 -59
  60. package/scripts/mcp-safety-guard.mjs +3 -1
  61. package/scripts/pack.mjs +23 -18
  62. package/scripts/preflight-cache.mjs +79 -9
  63. package/scripts/release/check-packages-mirror.mjs +20 -4
  64. package/scripts/release/prepare.mjs +2 -1
  65. package/scripts/release/publish.mjs +100 -7
  66. package/scripts/release/verify.mjs +39 -13
  67. package/scripts/release/version-manifest.json +8 -0
  68. package/scripts/setup.mjs +40 -7
  69. package/scripts/sync-hub-mcp-settings.mjs +353 -28
  70. package/scripts/tfx-route.mjs +302 -0
  71. package/scripts/tfx-route.sh +163 -26
  72. package/skills/tfx-analysis/SKILL.md +12 -12
  73. package/skills/tfx-auto/SKILL.md +14 -12
  74. package/skills/tfx-auto/SKILL.md.tmpl +5 -5
  75. package/skills/tfx-consensus/SKILL.md.tmpl +8 -8
  76. package/skills/tfx-debate/SKILL.md +1 -1
  77. package/skills/tfx-debate/SKILL.md.tmpl +5 -5
  78. package/skills/tfx-doctor/SKILL.md +5 -5
  79. package/skills/tfx-doctor/SKILL.md.tmpl +5 -5
  80. package/skills/tfx-fullcycle/SKILL.md.tmpl +8 -8
  81. package/skills/tfx-hub/SKILL.md +2 -2
  82. package/skills/tfx-hub/SKILL.md.tmpl +2 -2
  83. package/skills/tfx-index/SKILL.md +9 -9
  84. package/skills/tfx-index/SKILL.md.tmpl +9 -9
  85. package/skills/tfx-interview/SKILL.md +29 -29
  86. package/skills/tfx-interview/SKILL.md.tmpl +24 -24
  87. package/skills/tfx-multi/SKILL.md.tmpl +4 -4
  88. package/skills/tfx-multi/references/agent-wrapper-rules.md +3 -3
  89. package/skills/tfx-panel/SKILL.md.tmpl +5 -5
  90. package/skills/tfx-persist/SKILL.md.tmpl +15 -15
  91. package/skills/tfx-plan/SKILL.md +14 -14
  92. package/skills/tfx-plan/SKILL.md.tmpl +6 -6
  93. package/skills/tfx-plan/skill.json +1 -1
  94. package/skills/tfx-profile/SKILL.md +10 -10
  95. package/skills/tfx-profile/SKILL.md.tmpl +10 -10
  96. package/skills/tfx-profile/skill.json +1 -1
  97. package/skills/tfx-prune/SKILL.md +4 -4
  98. package/skills/tfx-prune/SKILL.md.tmpl +4 -4
  99. package/skills/tfx-qa/SKILL.md +4 -4
  100. package/skills/tfx-research/SKILL.md +9 -9
  101. package/skills/tfx-research/SKILL.md.tmpl +12 -12
  102. package/skills/tfx-research/skill.json +1 -1
  103. package/skills/tfx-review/SKILL.md +13 -13
  104. package/skills/tfx-setup/SKILL.md +25 -8
  105. package/skills/tfx-setup/SKILL.md.tmpl +8 -8
  106. package/skills/tfx-ship/SKILL.md +4 -5
  107. package/skills/tfx-swarm/SKILL.md.tmpl +4 -4
package/bin/triflux.mjs CHANGED
@@ -61,6 +61,7 @@ import { serializeHandoff } from "../scripts/lib/handoff.mjs";
61
61
  import {
62
62
  addRegistryServer,
63
63
  createDefaultRegistry,
64
+ discoverProjectMcpTargets,
64
65
  inspectRegistry,
65
66
  inspectRegistryStatus,
66
67
  removeRegistryServer,
@@ -333,7 +334,8 @@ const CLI_COMMAND_SCHEMAS = Object.freeze({
333
334
  ],
334
335
  },
335
336
  sync: {
336
- usage: "tfx mcp sync [--json]",
337
+ usage:
338
+ "tfx mcp sync [--json] [--all-projects [root]] [--dry-run] [--exclude <glob>]",
337
339
  options: [
338
340
  {
339
341
  name: "--json",
@@ -2515,11 +2517,12 @@ function buildMcpStatusRows(statusInfo) {
2515
2517
  .filter((row) => row.type === "registry")
2516
2518
  .map((row) => {
2517
2519
  let detail = "";
2518
- if (row.status === "present") detail = row.actualUrl || row.expectedUrl;
2520
+ if (row.status === "present")
2521
+ detail = row.actualUrl || row.actualCommand || row.expectedUrl;
2519
2522
  else if (row.status === "missing") detail = "registry only";
2520
2523
  else if (row.status === "missing-file") detail = "config missing";
2521
2524
  else if (row.status === "mismatch")
2522
- detail = `expected ${row.expectedUrl}`;
2525
+ detail = `expected ${row.expectedUrl || row.expectedCommand}`;
2523
2526
  else if (row.status === "invalid-config") detail = "parse error";
2524
2527
  else if (row.status === "stdio") detail = "configured as stdio";
2525
2528
  return [
@@ -4667,6 +4670,68 @@ async function cmdDoctor(options = {}) {
4667
4670
  }
4668
4671
  }
4669
4672
 
4673
+ // ── MCP Gateway Health ──
4674
+ // install-mcp-gateway-startup 으로 띄운 LaunchAgent/systemd daemon 의 stdout
4675
+ // (~/.local/state/triflux/mcp-gateway.out.log) 를 파싱해 missing-env 등으로
4676
+ // skip 된 server 를 잡는다. 로그가 없으면 gateway 미설치/미실행으로 침묵.
4677
+ section("MCP Gateway Health");
4678
+ {
4679
+ const { checkMcpGatewayHealth, summarizeMcpGatewayHealth } = await import(
4680
+ "../scripts/lib/mcp-gateway-health-check.mjs"
4681
+ );
4682
+ const gatewayHealth = checkMcpGatewayHealth();
4683
+ const summary = summarizeMcpGatewayHealth(gatewayHealth);
4684
+ addDoctorCheck(report, {
4685
+ name: "mcp-gateway-health",
4686
+ status: summary.level === "warn" ? "warning" : "ok",
4687
+ log_path: gatewayHealth.logPath,
4688
+ findings: gatewayHealth.findings,
4689
+ started: gatewayHealth.started,
4690
+ skipped: gatewayHealth.skipped,
4691
+ ...(summary.fix ? { fix: summary.fix } : {}),
4692
+ });
4693
+ if (summary.level === "skip") {
4694
+ info(summary.message);
4695
+ } else if (summary.level === "ok") {
4696
+ ok(summary.message);
4697
+ } else {
4698
+ warn(summary.message);
4699
+ if (summary.fix) info(`수정: ${summary.fix}`);
4700
+ issues++;
4701
+ }
4702
+ }
4703
+
4704
+ // ── MCP Gateway Wrapper ──
4705
+ // 로그가 생기기 전 단계에서 wrapper 자체가 secrets.env 를 source 하는지 확인한다.
4706
+ section("MCP Gateway Wrapper");
4707
+ {
4708
+ const { checkWrapperSourcing } = await import(
4709
+ "../scripts/lib/mcp-gateway-wrapper-check.mjs"
4710
+ );
4711
+ const wrapperCheck = await checkWrapperSourcing();
4712
+ addDoctorCheck(report, {
4713
+ name: "mcp-gateway-wrapper-sourcing",
4714
+ status:
4715
+ wrapperCheck.status === "warn" ? "warning" : wrapperCheck.status,
4716
+ path: wrapperCheck.wrapperPath,
4717
+ ...(wrapperCheck.message ? { message: wrapperCheck.message } : {}),
4718
+ ...(wrapperCheck.suggestedFix
4719
+ ? { fix: wrapperCheck.suggestedFix }
4720
+ : {}),
4721
+ });
4722
+
4723
+ if (wrapperCheck.status === "ok") {
4724
+ ok("wrapper sources secrets.env");
4725
+ } else if (wrapperCheck.status === "warn") {
4726
+ warn(wrapperCheck.message);
4727
+ info(`수정: ${wrapperCheck.suggestedFix}`);
4728
+ issues++;
4729
+ } else {
4730
+ warn("mcp-gateway wrapper not installed");
4731
+ if (wrapperCheck.message) info(wrapperCheck.message);
4732
+ }
4733
+ }
4734
+
4670
4735
  // ── Codex Config Health (BUG-H #132) ──
4671
4736
  // _codex_config_swap 의 restore 가 Windows lock/ACL 로 실패하면
4672
4737
  // ~/.codex/config.toml.pre-exec 가 남아 [mcp_servers.*] 섹션이 영구 손실된다.
@@ -5776,10 +5841,45 @@ function cmdMcp(args = [], options = {}) {
5776
5841
 
5777
5842
  case "sync": {
5778
5843
  const registryState = ensureValidRegistryState();
5779
- const result = syncRegistryTargets({ registry: registryState.registry });
5844
+ const allProjectsIndex = args.indexOf("--all-projects");
5845
+ const allProjectsRoot =
5846
+ allProjectsIndex >= 0 &&
5847
+ args[allProjectsIndex + 1] &&
5848
+ !String(args[allProjectsIndex + 1]).startsWith("--")
5849
+ ? args[allProjectsIndex + 1]
5850
+ : null;
5851
+ const dryRun = args.includes("--dry-run");
5852
+ const excludes = args.flatMap((arg, index) =>
5853
+ arg === "--exclude" && args[index + 1] ? [args[index + 1]] : [],
5854
+ );
5855
+ const allProjects =
5856
+ allProjectsIndex >= 0
5857
+ ? discoverProjectMcpTargets({
5858
+ root: allProjectsRoot || undefined,
5859
+ exclude: excludes,
5860
+ })
5861
+ : null;
5862
+ const result = dryRun
5863
+ ? { actions: [] }
5864
+ : syncRegistryTargets({
5865
+ registry: registryState.registry,
5866
+ ...(allProjects ? { targets: allProjects.targets } : {}),
5867
+ });
5780
5868
  if (json) {
5781
5869
  printJson({
5782
5870
  registry_path: registryState.path,
5871
+ ...(allProjects
5872
+ ? {
5873
+ dry_run: dryRun,
5874
+ all_projects: {
5875
+ root: allProjects.root,
5876
+ maxDepth: allProjects.maxDepth,
5877
+ exclude: allProjects.exclude,
5878
+ count: allProjects.targets.length,
5879
+ },
5880
+ targets: allProjects.targets,
5881
+ }
5882
+ : {}),
5783
5883
  actions: result.actions,
5784
5884
  });
5785
5885
  return;
@@ -5787,8 +5887,22 @@ function cmdMcp(args = [], options = {}) {
5787
5887
 
5788
5888
  console.log(`\n ${AMBER}${BOLD}⬡ triflux mcp sync${RESET} ${VER}\n`);
5789
5889
  console.log(` ${LINE}`);
5890
+ if (allProjects) {
5891
+ section("Project Targets");
5892
+ info(`${allProjects.targets.length}개 파일 (${allProjects.root})`);
5893
+ for (const target of allProjects.targets) {
5894
+ info(formatPathForDisplay(target.filePath));
5895
+ }
5896
+ if (dryRun) {
5897
+ console.log("");
5898
+ return;
5899
+ }
5900
+ }
5790
5901
  section("Actions");
5791
5902
  for (const action of result.actions) {
5903
+ for (const warning of action.warnings || []) {
5904
+ process.stderr.write(`${warning}\n`);
5905
+ }
5792
5906
  const label = `${action.label} ${DIM}(${formatPathForDisplay(action.filePath)})${RESET}`;
5793
5907
  if (action.status === "updated") ok(`${label} → updated`);
5794
5908
  else if (action.status === "warning") warn(`${label} → warning`);
@@ -0,0 +1,85 @@
1
+ {
2
+ "$schema": "mcp-registry-schema",
3
+ "version": 1,
4
+ "description": "MCP 서버 중앙 레지스트리 — 진실의 원천",
5
+ "defaults": {
6
+ "transport": "hub-url",
7
+ "hub_base": "http://127.0.0.1:27888"
8
+ },
9
+ "policy_notes": {
10
+ "policy": "Server policy is the SSOT for client sync shape: hosted writes url+headers, gateway-sse writes http://127.0.0.1:81XX/sse with SSE metadata, and stdio writes command/args/env.",
11
+ "transport": "Server transport accepts \"hub-url\" for triflux Hub URL flow, \"http\" for direct Streamable HTTP MCP endpoints, or \"stdio\" for upstream-stdio-only MCP servers. Gateway-backed stdio upstreams use policy:\"gateway-sse\" plus gateway_port so clients receive URL/SSE config.",
12
+ "headers": "Optional headers are allowed only for HTTP-compatible transports. Each header value must be a descriptor: {\"value\":\"literal\"} for non-secret static values, {\"env\":\"ENV_VAR_NAME\"} for secrets resolved at sync/runtime, or {\"env\":\"ENV_VAR_NAME\",\"prefix\":\"Bearer \"} for common authorization formats.",
13
+ "secret_safety": "Resolved secret values must not be written back to this registry file. Missing env vars warn during sync and do not emit empty secret headers.",
14
+ "sync_denylist": "Array of client:server strings skipped by proactive registry sync, for example gemini:tfx-hub."
15
+ },
16
+ "servers": {
17
+ "tfx-hub": {
18
+ "policy": "hosted",
19
+ "transport": "hub-url",
20
+ "url": "http://127.0.0.1:27888/mcp",
21
+ "safe": true,
22
+ "targets": ["claude", "gemini", "codex", "antigravity"],
23
+ "description": "triflux Hub MCP 서버"
24
+ },
25
+ "context7": {
26
+ "policy": "hosted",
27
+ "transport": "http",
28
+ "url": "https://mcp.context7.com/mcp",
29
+ "safe": true,
30
+ "targets": ["claude", "gemini", "codex", "antigravity"],
31
+ "description": "Upstash Context7 — 라이브러리 문서/코드 컨텍스트 (HTTP MCP, API key 불필요)"
32
+ },
33
+ "exa": {
34
+ "policy": "hosted",
35
+ "transport": "http",
36
+ "url": "https://mcp.exa.ai/mcp",
37
+ "headers": {
38
+ "Authorization": { "env": "EXA_API_KEY", "prefix": "Bearer " }
39
+ },
40
+ "safe": true,
41
+ "targets": ["claude", "gemini", "codex", "antigravity"],
42
+ "description": "Exa neural/semantic web search — 학술/기술 깊이. Key 발급: https://exa.ai/dashboard → secrets.env의 EXA_API_KEY"
43
+ },
44
+ "serena": {
45
+ "policy": "gateway-sse",
46
+ "gateway_port": 8105,
47
+ "safe": true,
48
+ "targets": ["claude", "gemini", "codex", "antigravity"],
49
+ "description": "Serena MCP — supergateway singleton SSE endpoint (:8105)"
50
+ },
51
+ "brave-search": {
52
+ "policy": "gateway-sse",
53
+ "gateway_port": 8101,
54
+ "safe": true,
55
+ "targets": ["claude", "gemini", "codex", "antigravity"],
56
+ "description": "Brave Search MCP — supergateway singleton SSE endpoint (:8101). BRAVE_API_KEY 환경변수 필요 (https://brave.com/search/api/). secrets.env 의 BRAVE_API_KEY 참조."
57
+ },
58
+ "tavily": {
59
+ "policy": "hosted",
60
+ "transport": "http",
61
+ "url": "https://mcp.tavily.com/mcp",
62
+ "headers": {
63
+ "Authorization": { "env": "TAVILY_API_KEY", "prefix": "Bearer " }
64
+ },
65
+ "safe": true,
66
+ "targets": ["claude", "gemini", "codex", "antigravity"],
67
+ "description": "Tavily research — 비용/운영/DX/일반 웹. Key 발급: https://app.tavily.com/home → secrets.env의 TAVILY_API_KEY"
68
+ }
69
+ },
70
+ "policies": {
71
+ "stdio_action": "replace-with-hub",
72
+ "unknown_server_action": "warn",
73
+ "sync_denylist": [],
74
+ "watched_paths": [
75
+ "~/.gemini/settings.json",
76
+ "~/.codex/config.toml",
77
+ "~/.claude.json",
78
+ "~/.claude/settings.json",
79
+ "~/.claude/settings.local.json",
80
+ ".claude/mcp.json",
81
+ ".mcp.json",
82
+ "~/.gemini/config/mcp_config.json"
83
+ ]
84
+ }
85
+ }
@@ -0,0 +1,124 @@
1
+ {
2
+ "version": 1,
3
+ "freshness": {
4
+ "max_snapshot_age_ms": 900000,
5
+ "max_cross_provider_skew_ms": 300000,
6
+ "local_clock_skew_guard_ms": 60000
7
+ },
8
+ "thresholds": {
9
+ "reset_imminent_ms": 86400000,
10
+ "exhaustion_high_percent": 80,
11
+ "exhaustion_low_percent": 20,
12
+ "expiry_drain_window_ms": 604800000,
13
+ "drain_usage_ceiling_percent": 50
14
+ },
15
+ "scenarios": [
16
+ {
17
+ "id": "S1",
18
+ "name": "claude-reset-imminent-drain",
19
+ "when": {
20
+ "provider": "claude",
21
+ "weeklyResetsAtLteMs": 86400000,
22
+ "weeklyPercentLt": 100,
23
+ "hasMultipleAccounts": true
24
+ },
25
+ "then": {
26
+ "lane": "multi",
27
+ "primary_cli": "claude",
28
+ "reserve_codex_share": 0.1,
29
+ "reason": "claude-weekly-reset-imminent-drain-fleet"
30
+ }
31
+ },
32
+ {
33
+ "id": "S2",
34
+ "name": "codex-reset-imminent-drain",
35
+ "when": {
36
+ "provider": "codex",
37
+ "weeklyResetsAtLteMs": 86400000,
38
+ "weeklyPercentLt": 100
39
+ },
40
+ "then": {
41
+ "lane": "swarm",
42
+ "primary_cli": "codex",
43
+ "reserve_claude_share": 0.1,
44
+ "reason": "codex-weekly-reset-imminent-drain-fleet"
45
+ }
46
+ },
47
+ {
48
+ "id": "S3",
49
+ "name": "claude-burned-codex-fresh",
50
+ "when": {
51
+ "claude_weekly_percent_gte": 80,
52
+ "codex_weekly_percent_lt": 20,
53
+ "claude_weekly_resets_at_gt_ms": 172800000
54
+ },
55
+ "then": {
56
+ "lane": "single",
57
+ "primary_cli": "codex",
58
+ "claude_lanes_allowed": ["explore", "final-challenge"],
59
+ "reason": "claude-burned-codex-fresh-rebalance"
60
+ }
61
+ },
62
+ {
63
+ "id": "S4",
64
+ "name": "claude-multi-account-staggered",
65
+ "when": {
66
+ "provider": "claude",
67
+ "accountCountGte": 2,
68
+ "staggered": true
69
+ },
70
+ "then": {
71
+ "lane": "multi",
72
+ "primary_cli": "claude",
73
+ "rank_by": "expiring_capacity_score",
74
+ "reason": "claude-multi-account-staggered-reset"
75
+ }
76
+ },
77
+ {
78
+ "id": "S5",
79
+ "name": "claude-parallel-leasemany",
80
+ "when": {
81
+ "provider": "claude",
82
+ "request_team_size_gte": 2,
83
+ "accountCountGte": 2
84
+ },
85
+ "then": {
86
+ "lane": "swarm",
87
+ "primary_cli": "claude",
88
+ "atomic": "leaseMany",
89
+ "atomic_policy": "balanced",
90
+ "reason": "claude-parallel-multi-account"
91
+ }
92
+ },
93
+ {
94
+ "id": "S6",
95
+ "name": "account-expiry-drain",
96
+ "when": {
97
+ "expiresAtLteMs": 604800000,
98
+ "weeklyPercentLt": 50,
99
+ "circuitStateNotOpen": true
100
+ },
101
+ "then": {
102
+ "lane": "single",
103
+ "riskTier": "drain",
104
+ "elevated_weight": 2.0,
105
+ "reason": "account-expiry-drain-before-loss"
106
+ }
107
+ }
108
+ ],
109
+ "fallback": {
110
+ "default_mode": "codex-default",
111
+ "default_cli": "codex",
112
+ "default_model": "gpt-5.5",
113
+ "triggers": [
114
+ "hud_null",
115
+ "hud_stale_gte_15min",
116
+ "weekly_reset_in_past",
117
+ "cross_provider_skew_exceeded",
118
+ "broker_empty",
119
+ "all_circuits_open",
120
+ "mirror_drift",
121
+ "flag_disabled"
122
+ ]
123
+ }
124
+ }
@@ -340,7 +340,14 @@ function main() {
340
340
  return hasSegmentInvocation(cmd, WT_DIRECT_PATTERNS);
341
341
  }
342
342
 
343
- if (isWtDirectInvocation(command)) {
343
+ // wt.exe는 Windows Terminal 전용. macOS/Linux에서는 PATH에 존재하지 않아
344
+ // 차단해도 dormant이며, 문서/로그 텍스트의 wt 단어가 false positive로 잡힐
345
+ // 위험만 있다. win32에서만 차단 적용.
346
+ // SAFETY_GUARD_FORCE_PLATFORM env로 테스트에서 platform 시뮬레이션 가능
347
+ // (mac CI에서 win32 차단 동작을 검증하기 위함).
348
+ const guardPlatform =
349
+ process.env.SAFETY_GUARD_FORCE_PLATFORM || process.platform;
350
+ if (guardPlatform === "win32" && isWtDirectInvocation(command)) {
344
351
  blockCommand(WT_DIRECT_BLOCK_MESSAGE, command);
345
352
  }
346
353
 
@@ -1,6 +1,7 @@
1
1
  // hub/team/backend.mjs — CLI 백엔드 추상화 레이어
2
- // 각 CLI(codex/gemini/claude)의 명령 빌드 로직을 클래스로 캡슐화한다.
2
+ // 각 CLI(codex/gemini/claude/antigravity)의 명령 빌드 로직을 클래스로 캡슐화한다.
3
3
  // v7.2.2
4
+ import { writeFileSync } from "node:fs";
4
5
  import { createRequire } from "node:module";
5
6
 
6
7
  import { buildExecArgs } from "../codex-adapter.mjs";
@@ -16,6 +17,22 @@ export function buildGeminiCommand(prompt, resultFile, { isWindows } = {}) {
16
17
  return `gemini --yolo --prompt ${prompt} --output-format text > '${resultFile}' 2>'${resultFile}.err' < /dev/null`;
17
18
  }
18
19
 
20
+ export function buildAntigravityCommand(
21
+ prompt,
22
+ resultFile,
23
+ { isWindows } = {},
24
+ ) {
25
+ // Persist prompt to a sibling file so the shell never interpolates raw
26
+ // prompt content. Without this the Unix branch let `$()`, backticks, `\\`,
27
+ // and `"` reach the shell verbatim (P1: shell injection).
28
+ const promptFile = `${resultFile}.prompt`;
29
+ writeFileSync(promptFile, prompt);
30
+ if (isWindows) {
31
+ return `Get-Content -Raw '${promptFile}' | agy --print --dangerously-skip-permissions > '${resultFile}' 2>'${resultFile}.err'`;
32
+ }
33
+ return `agy --print --dangerously-skip-permissions < '${promptFile}' > '${resultFile}' 2>'${resultFile}.err'`;
34
+ }
35
+
19
36
  const _require = createRequire(import.meta.url);
20
37
 
21
38
  // ── 백엔드 클래스 ──────────────────────────────────────────────────────────
@@ -77,19 +94,40 @@ export class ClaudeBackend {
77
94
  }
78
95
  }
79
96
 
97
+ export class AntigravityBackend {
98
+ name() {
99
+ return "antigravity";
100
+ }
101
+ command() {
102
+ return "agy";
103
+ }
104
+
105
+ buildArgs(prompt, resultFile, opts = {}) {
106
+ return buildAntigravityCommand(prompt, resultFile, {
107
+ isWindows: IS_WINDOWS,
108
+ ...opts,
109
+ });
110
+ }
111
+
112
+ env() {
113
+ return {};
114
+ }
115
+ }
116
+
80
117
  // ── 레지스트리 ─────────────────────────────────────────────────────────────
81
118
 
82
- /** @type {Map<string, CodexBackend|GeminiBackend|ClaudeBackend>} */
119
+ /** @type {Map<string, CodexBackend|GeminiBackend|ClaudeBackend|AntigravityBackend>} */
83
120
  const backends = new Map([
84
121
  ["codex", new CodexBackend()],
85
122
  ["gemini", new GeminiBackend()],
86
123
  ["claude", new ClaudeBackend()],
124
+ ["antigravity", new AntigravityBackend()],
87
125
  ]);
88
126
 
89
127
  /**
90
128
  * 백엔드 이름으로 조회한다.
91
- * @param {string} name — "codex" | "gemini" | "claude"
92
- * @returns {CodexBackend|GeminiBackend|ClaudeBackend}
129
+ * @param {string} name — "codex" | "gemini" | "claude" | "antigravity"
130
+ * @returns {CodexBackend|GeminiBackend|ClaudeBackend|AntigravityBackend}
93
131
  * @throws {Error} 등록되지 않은 이름
94
132
  */
95
133
  export function getBackend(name) {
@@ -102,7 +140,7 @@ export function getBackend(name) {
102
140
  * 에이전트명 또는 CLI명을 Backend로 해석한다.
103
141
  * agent-map.json을 통해 에이전트명 → CLI명으로 변환 후 레지스트리에서 조회한다.
104
142
  * @param {string} agentOrCli — "executor", "codex", "designer" 등
105
- * @returns {CodexBackend|GeminiBackend|ClaudeBackend}
143
+ * @returns {CodexBackend|GeminiBackend|ClaudeBackend|AntigravityBackend}
106
144
  */
107
145
  export function getBackendForAgent(agentOrCli) {
108
146
  const agentMap = _require("./agent-map.json");
@@ -112,7 +150,7 @@ export function getBackendForAgent(agentOrCli) {
112
150
 
113
151
  /**
114
152
  * 등록된 모든 백엔드를 반환한다.
115
- * @returns {Array<CodexBackend|GeminiBackend|ClaudeBackend>}
153
+ * @returns {Array<CodexBackend|GeminiBackend|ClaudeBackend|AntigravityBackend>}
116
154
  */
117
155
  export function listBackends() {
118
156
  return Array.from(backends.values());