triflux 8.11.2 → 8.12.2
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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/hooks/hooks.json +56 -51
- package/hub/pipeline/index.mjs +318 -318
- package/hub/routing/q-learning.mjs +2 -1
- package/hub/schema.sql +146 -146
- package/hub/team/cli/commands/kill.mjs +37 -37
- package/hub/team/cli/commands/start/index.mjs +3 -2
- package/hub/team/cli/commands/start/parse-args.mjs +5 -0
- package/hub/team/cli/commands/start/start-headless.mjs +2 -1
- package/hub/team/cli/commands/stop.mjs +31 -31
- package/hub/team/cli/commands/task.mjs +30 -30
- package/hub/team/cli/help.mjs +41 -40
- package/hub/team/cli/services/hub-client.mjs +208 -208
- package/hub/team/cli/services/native-control.mjs +118 -118
- package/hub/team/cli/services/runtime-mode.mjs +62 -62
- package/hub/team/cli/services/state-store.mjs +48 -48
- package/hub/team/dashboard-anchor.mjs +14 -0
- package/hub/team/dashboard.mjs +274 -274
- package/hub/team/headless.mjs +44 -19
- package/hub/team/native.mjs +649 -649
- package/hub/tools.mjs +554 -554
- package/hub/workers/delegator-mcp.mjs +21 -1
- package/package.json +1 -1
- package/scripts/__tests__/remote-spawn-transfer.test.mjs +117 -0
- package/scripts/__tests__/remote-spawn.test.mjs +78 -0
- package/scripts/cache-buildup.mjs +401 -0
- package/scripts/headless-guard.mjs +10 -3
- package/scripts/hub-ensure.mjs +120 -120
- package/scripts/lib/mcp-filter.mjs +720 -720
- package/scripts/lib/remote-spawn-transfer.mjs +196 -0
- package/scripts/mcp-check.mjs +237 -127
- package/scripts/mcp-gateway-ensure.mjs +6 -8
- package/scripts/mcp-gateway-integration-test.mjs +228 -0
- package/scripts/mcp-gateway-start.mjs +42 -16
- package/scripts/mcp-gateway-start.ps1 +17 -14
- package/scripts/mcp-gateway-verify.mjs +1 -1
- package/scripts/preflight-cache.mjs +137 -137
- package/scripts/remote-spawn.mjs +404 -40
- package/scripts/setup.mjs +18 -0
- package/scripts/tfx-route-worker.mjs +165 -165
- package/scripts/tfx-route.sh +85 -16
- package/scripts/token-snapshot.mjs +575 -575
- package/skills/tfx-analysis/SKILL.md +101 -101
- package/skills/tfx-autopilot/SKILL.md +112 -112
- package/skills/tfx-autoresearch/SKILL.md +1 -0
- package/skills/tfx-autoroute/SKILL.md +184 -184
- package/skills/tfx-codex-swarm/SKILL.md +449 -0
- package/skills/tfx-codex-swarm/evals/evals.json +26 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/benchmark.json +33 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/eval_metadata.json +42 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/grading.json +11 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/outputs/analysis.md +87 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/outputs/classification.md +35 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/outputs/commands.sh +275 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/outputs/routing.md +56 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/timing.json +5 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/grading.json +11 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/outputs/analysis.md +92 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/outputs/classification.md +71 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/outputs/commands.sh +264 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/outputs/routing.md +113 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/timing.json +5 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/eval_metadata.json +32 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/grading.json +9 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/outputs/analysis.md +96 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/outputs/classification.md +38 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/outputs/commands.sh +151 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/outputs/routing.md +51 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/timing.json +5 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/grading.json +9 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/outputs/analysis.md +127 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/outputs/classification.md +57 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/outputs/commands.sh +129 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/outputs/routing.md +84 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/timing.json +5 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/eval_metadata.json +27 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/grading.json +8 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/outputs/analysis.md +98 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/outputs/classification.md +65 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/outputs/commands.sh +123 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/outputs/routing.md +66 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/timing.json +5 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/grading.json +8 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/outputs/analysis.md +88 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/outputs/classification.md +40 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/outputs/commands.sh +130 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/outputs/routing.md +61 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/timing.json +5 -0
- package/skills/tfx-profile/SKILL.md +141 -51
- package/skills/tfx-qa/SKILL.md +117 -117
- package/skills/tfx-remote-spawn/references/hosts.json +16 -16
- package/skills/tfx-review/SKILL.md +51 -51
- package/skills/tfx-setup/SKILL.md +20 -0
- package/tui/gemini-profile.mjs +254 -0
- package/tui/setup.mjs +36 -1
- package/scripts/claude-logged.ps1 +0 -54
- package/scripts/demo-tui.mjs +0 -59
- package/skills/.omc/state/agent-replay-8f0e10a9-9693-4410-96f5-a6b07e8ed995.jsonl +0 -1
- package/skills/.omc/state/idle-notif-cooldown.json +0 -3
- package/skills/.omc/state/last-tool-error.json +0 -7
- package/skills/.omc/state/subagent-tracking.json +0 -7
|
@@ -39,6 +39,7 @@ const DEFAULT_ROUTE_TIMEOUT_SEC = 120;
|
|
|
39
39
|
const DIRECT_PROGRESS_START = 5;
|
|
40
40
|
const DIRECT_PROGRESS_RUNNING = 60;
|
|
41
41
|
const DIRECT_PROGRESS_DONE = 100;
|
|
42
|
+
const SEARCH_ENGINE_CACHE_PATH = ['.omc', 'state', 'search-engines.json'];
|
|
42
43
|
|
|
43
44
|
const AGENT_TIMEOUT_SEC = Object.freeze({
|
|
44
45
|
executor: 1080,
|
|
@@ -197,6 +198,22 @@ function joinInstructions(...values) {
|
|
|
197
198
|
.trim();
|
|
198
199
|
}
|
|
199
200
|
|
|
201
|
+
function loadAvailableServersFromSearchEngineCache(cwd = process.cwd()) {
|
|
202
|
+
const cacheFile = resolve(cwd, ...SEARCH_ENGINE_CACHE_PATH);
|
|
203
|
+
if (!existsSync(cacheFile)) return null;
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
const parsed = JSON.parse(readFileSync(cacheFile, 'utf8'));
|
|
207
|
+
if (!Array.isArray(parsed?.engines)) return [];
|
|
208
|
+
return parsed.engines
|
|
209
|
+
.filter((engine) => engine?.status === 'available')
|
|
210
|
+
.map((engine) => (typeof engine?.name === 'string' ? engine.name.trim() : ''))
|
|
211
|
+
.filter(Boolean);
|
|
212
|
+
} catch {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
200
217
|
function parseRouteType(stderr = '') {
|
|
201
218
|
const match = stderr.match(/type=([a-z-]+)/);
|
|
202
219
|
if (!match) return null;
|
|
@@ -630,13 +647,16 @@ export class DelegatorMcpWorker {
|
|
|
630
647
|
}
|
|
631
648
|
|
|
632
649
|
_getMcpPolicyOptions(args) {
|
|
650
|
+
const cachedAvailableServers = loadAvailableServersFromSearchEngineCache(args.cwd || this.cwd);
|
|
633
651
|
return {
|
|
634
652
|
agentType: args.agentType || 'executor',
|
|
635
653
|
requestedProfile: args.mcpProfile || 'auto',
|
|
636
654
|
searchTool: args.searchTool,
|
|
637
655
|
workerIndex: Number.isInteger(args.workerIndex) ? args.workerIndex : undefined,
|
|
638
656
|
taskText: withContext(String(args.prompt ?? ''), args.contextFile),
|
|
639
|
-
availableServers: Array.isArray(args.availableServers)
|
|
657
|
+
availableServers: Array.isArray(args.availableServers)
|
|
658
|
+
? args.availableServers
|
|
659
|
+
: cachedAvailableServers ?? undefined,
|
|
640
660
|
};
|
|
641
661
|
}
|
|
642
662
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
buildRemoteTransferPlan,
|
|
9
|
+
extractExplicitFileTokens,
|
|
10
|
+
} from "../lib/remote-spawn-transfer.mjs";
|
|
11
|
+
|
|
12
|
+
function makeTempDir() {
|
|
13
|
+
return mkdtempSync(join(tmpdir(), "tfx-remote-spawn-transfer-"));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("remote-spawn transfer plan", () => {
|
|
17
|
+
it("extractExplicitFileTokens는 명시적 파일 토큰만 추출한다", () => {
|
|
18
|
+
const source = [
|
|
19
|
+
"Read [PRD](.omx/plans/prd.md) and `docs/spec.md`.",
|
|
20
|
+
"Also open \"./notes.txt\" and 'http://example.com'.",
|
|
21
|
+
].join("\n");
|
|
22
|
+
|
|
23
|
+
const tokens = extractExplicitFileTokens(source);
|
|
24
|
+
|
|
25
|
+
assert.deepEqual(tokens.sort(), ["./notes.txt", ".omx/plans/prd.md", "docs/spec.md"].sort());
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("handoff + 참조 파일 전송 계획과 프롬프트 재작성", () => {
|
|
29
|
+
const root = makeTempDir();
|
|
30
|
+
try {
|
|
31
|
+
mkdirSync(join(root, "docs"), { recursive: true });
|
|
32
|
+
writeFileSync(join(root, "prd.md"), "prd body", "utf8");
|
|
33
|
+
writeFileSync(join(root, "docs", "spec.md"), "spec body", "utf8");
|
|
34
|
+
writeFileSync(
|
|
35
|
+
join(root, "handoff.md"),
|
|
36
|
+
[
|
|
37
|
+
"Open \"./prd.md\" before anything else.",
|
|
38
|
+
"Then inspect `docs/spec.md` and [again](docs/spec.md).",
|
|
39
|
+
].join("\n"),
|
|
40
|
+
"utf8",
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const plan = buildRemoteTransferPlan({
|
|
44
|
+
cwd: root,
|
|
45
|
+
handoffPath: "handoff.md",
|
|
46
|
+
maxBytes: 1024 * 1024,
|
|
47
|
+
remoteStageRoot: "/remote/stage/abc123",
|
|
48
|
+
userPrompt: "continue with execution",
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
assert.equal(plan.transfers.length, 3);
|
|
52
|
+
assert.equal(plan.transfers[0].type, "handoff");
|
|
53
|
+
assert.equal(plan.transfers[1].type, "reference");
|
|
54
|
+
assert.equal(plan.transfers[2].type, "reference");
|
|
55
|
+
|
|
56
|
+
assert.match(plan.prompt, /Staged handoff file: \/remote\/stage\/abc123\/handoff\/handoff\.md/);
|
|
57
|
+
assert.match(plan.prompt, /\/remote\/stage\/abc123\/refs\/\d{2}-prd\.md/);
|
|
58
|
+
assert.match(plan.prompt, /\/remote\/stage\/abc123\/refs\/\d{2}-spec\.md/);
|
|
59
|
+
assert.equal(plan.prompt.includes("./prd.md"), false);
|
|
60
|
+
assert.equal(plan.prompt.includes("docs/spec.md"), false);
|
|
61
|
+
assert.match(plan.prompt, /---\n\ncontinue with execution$/);
|
|
62
|
+
} finally {
|
|
63
|
+
rmSync(root, { force: true, recursive: true });
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("누락된 참조 파일은 SSH 전 로컬에서 실패한다", () => {
|
|
68
|
+
const root = makeTempDir();
|
|
69
|
+
try {
|
|
70
|
+
writeFileSync(join(root, "handoff.md"), "Read `./missing.md`", "utf8");
|
|
71
|
+
assert.throws(
|
|
72
|
+
() => buildRemoteTransferPlan({
|
|
73
|
+
cwd: root,
|
|
74
|
+
handoffPath: "handoff.md",
|
|
75
|
+
maxBytes: 1024 * 1024,
|
|
76
|
+
remoteStageRoot: "/remote/stage/abc123",
|
|
77
|
+
}),
|
|
78
|
+
/referenced file not found:/,
|
|
79
|
+
);
|
|
80
|
+
} finally {
|
|
81
|
+
rmSync(root, { force: true, recursive: true });
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("MAX_HANDOFF_BYTES 제한을 handoff와 참조 파일 모두에 적용한다", () => {
|
|
86
|
+
const root = makeTempDir();
|
|
87
|
+
try {
|
|
88
|
+
writeFileSync(join(root, "big-ref.txt"), "x".repeat(32), "utf8");
|
|
89
|
+
writeFileSync(join(root, "handoff.md"), "Open ./big-ref.txt", "utf8");
|
|
90
|
+
|
|
91
|
+
assert.throws(
|
|
92
|
+
() => buildRemoteTransferPlan({
|
|
93
|
+
cwd: root,
|
|
94
|
+
handoffPath: "handoff.md",
|
|
95
|
+
maxBytes: 16,
|
|
96
|
+
remoteStageRoot: "/remote/stage/abc123",
|
|
97
|
+
}),
|
|
98
|
+
/referenced file too large:/,
|
|
99
|
+
);
|
|
100
|
+
} finally {
|
|
101
|
+
rmSync(root, { force: true, recursive: true });
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("handoff가 없으면 기존 prompt 전달 동작을 유지한다", () => {
|
|
106
|
+
const plan = buildRemoteTransferPlan({
|
|
107
|
+
handoffPath: null,
|
|
108
|
+
maxBytes: 1024,
|
|
109
|
+
remoteStageRoot: "/remote/stage/abc123",
|
|
110
|
+
userPrompt: "only prompt",
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
assert.equal(plan.prompt, "only prompt");
|
|
114
|
+
assert.deepEqual(plan.transfers, []);
|
|
115
|
+
assert.equal(plan.stagedHandoffPath, null);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
import { startSpawnExitWatcher, watchSpawnSessionExit } from "../remote-spawn.mjs";
|
|
5
|
+
|
|
6
|
+
describe("remote-spawn watcher", () => {
|
|
7
|
+
it("lead pane dead 상태가 grace 기간 유지되면 세션을 정리한다", async () => {
|
|
8
|
+
let nowMs = 0;
|
|
9
|
+
let pollIndex = 0;
|
|
10
|
+
const killed = [];
|
|
11
|
+
const statuses = [
|
|
12
|
+
{ isDead: false, exitCode: null },
|
|
13
|
+
{ isDead: true, exitCode: 0 },
|
|
14
|
+
{ isDead: true, exitCode: 0 },
|
|
15
|
+
{ isDead: true, exitCode: 0 },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const result = await watchSpawnSessionExit("tfx-spawn-unit", {
|
|
19
|
+
pollMs: 100,
|
|
20
|
+
graceMs: 200,
|
|
21
|
+
maxWaitMs: 1000,
|
|
22
|
+
sessionExists: () => true,
|
|
23
|
+
getPaneStatus: () => statuses[Math.min(pollIndex++, statuses.length - 1)],
|
|
24
|
+
killSession: (name) => killed.push(name),
|
|
25
|
+
now: () => nowMs,
|
|
26
|
+
sleep: async (ms) => { nowMs += ms; },
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
assert.equal(result.cleaned, true);
|
|
30
|
+
assert.equal(result.reason, "pane-dead");
|
|
31
|
+
assert.deepEqual(killed, ["tfx-spawn-unit"]);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("live session은 정리하지 않고 timeout으로 종료한다", async () => {
|
|
35
|
+
let nowMs = 0;
|
|
36
|
+
let killCount = 0;
|
|
37
|
+
|
|
38
|
+
const result = await watchSpawnSessionExit("tfx-spawn-timeout", {
|
|
39
|
+
pollMs: 100,
|
|
40
|
+
graceMs: 200,
|
|
41
|
+
maxWaitMs: 250,
|
|
42
|
+
sessionExists: () => true,
|
|
43
|
+
getPaneStatus: () => ({ isDead: false, exitCode: null }),
|
|
44
|
+
killSession: () => { killCount += 1; },
|
|
45
|
+
now: () => nowMs,
|
|
46
|
+
sleep: async (ms) => { nowMs += ms; },
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
assert.equal(result.cleaned, false);
|
|
50
|
+
assert.equal(result.reason, "timeout");
|
|
51
|
+
assert.equal(killCount, 0);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("detached watcher를 --watch-exit 인자로 실행한다", () => {
|
|
55
|
+
const calls = [];
|
|
56
|
+
const started = startSpawnExitWatcher("tfx-spawn-detached", {
|
|
57
|
+
force: true,
|
|
58
|
+
execPath: "node-test",
|
|
59
|
+
scriptPath: "C:/tmp/remote-spawn.mjs",
|
|
60
|
+
spawnFn: (file, args, options) => {
|
|
61
|
+
calls.push({ file, args, options, unrefCalled: false });
|
|
62
|
+
return {
|
|
63
|
+
unref() {
|
|
64
|
+
calls[0].unrefCalled = true;
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
assert.equal(started, true);
|
|
71
|
+
assert.equal(calls.length, 1);
|
|
72
|
+
assert.equal(calls[0].file, "node-test");
|
|
73
|
+
assert.deepEqual(calls[0].args, ["C:/tmp/remote-spawn.mjs", "--watch-exit", "tfx-spawn-detached"]);
|
|
74
|
+
assert.equal(calls[0].options.detached, true);
|
|
75
|
+
assert.equal(calls[0].options.stdio, "ignore");
|
|
76
|
+
assert.equal(calls[0].unrefCalled, true);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/cache-buildup.mjs — Phase 1 캐시 빌드업
|
|
3
|
+
// setup.mjs에서 백그라운드 스폰. CWD의 .omc/cache/ 및 .omc/state/에 기록.
|
|
4
|
+
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { execSync } from "node:child_process";
|
|
7
|
+
import { join, basename } from "node:path";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
|
|
10
|
+
import { readPreflightCache } from "./preflight-cache.mjs";
|
|
11
|
+
import { SEARCH_SERVER_ORDER, MCP_SERVER_DOMAIN_TAGS } from "./lib/mcp-server-catalog.mjs";
|
|
12
|
+
|
|
13
|
+
const CWD = process.cwd();
|
|
14
|
+
const CACHE_DIR = join(CWD, ".omc", "cache");
|
|
15
|
+
const STATE_DIR = join(CWD, ".omc", "state");
|
|
16
|
+
const HOME = homedir();
|
|
17
|
+
|
|
18
|
+
function ensureDirs() {
|
|
19
|
+
mkdirSync(CACHE_DIR, { recursive: true });
|
|
20
|
+
mkdirSync(STATE_DIR, { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function writeJSON(path, data) {
|
|
24
|
+
writeFileSync(path, JSON.stringify(data, null, 2) + "\n");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ── 1. Codex 스킬 스캔 (Task D: 일반화된 발견) ──
|
|
28
|
+
|
|
29
|
+
const ROLE_KEYWORDS = {
|
|
30
|
+
plan: ["plan", "계획", "decompos", "strategy", "설계"],
|
|
31
|
+
auto: ["autonomous", "자율", "auto-execute", "autopilot", "full auto"],
|
|
32
|
+
persist: ["loop", "반복", "completion", "persist", "until", "끝까지"],
|
|
33
|
+
investigate: ["investigate", "research", "조사", "분석", "analysis"],
|
|
34
|
+
review: ["review", "리뷰", "inspect", "검수"],
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function classifyRole(description) {
|
|
38
|
+
const lower = (description || "").toLowerCase();
|
|
39
|
+
for (const [role, keywords] of Object.entries(ROLE_KEYWORDS)) {
|
|
40
|
+
if (keywords.some((kw) => lower.includes(kw))) return role;
|
|
41
|
+
}
|
|
42
|
+
return "general";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseSkillFrontmatter(content) {
|
|
46
|
+
const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
47
|
+
if (!match) return null;
|
|
48
|
+
|
|
49
|
+
const fm = match[1];
|
|
50
|
+
const name = fm.match(/^name:\s*(.+)$/m)?.[1]?.trim() || null;
|
|
51
|
+
const desc = fm.match(/^description:\s*(.+)$/m)?.[1]?.trim() || null;
|
|
52
|
+
return name ? { name, description: desc } : null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function scanCodexSkills() {
|
|
56
|
+
const codexSkillsDir = join(HOME, ".codex", "skills");
|
|
57
|
+
const skills = [];
|
|
58
|
+
|
|
59
|
+
// 1) OMX/커스텀 스킬 (~/.codex/skills/*)
|
|
60
|
+
if (existsSync(codexSkillsDir)) {
|
|
61
|
+
for (const entry of readdirSync(codexSkillsDir, { withFileTypes: true })) {
|
|
62
|
+
if (!entry.isDirectory()) continue;
|
|
63
|
+
const skillMd = join(codexSkillsDir, entry.name, "SKILL.md");
|
|
64
|
+
if (!existsSync(skillMd)) continue;
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const content = readFileSync(skillMd, "utf8");
|
|
68
|
+
const fm = parseSkillFrontmatter(content);
|
|
69
|
+
const name = fm?.name || entry.name;
|
|
70
|
+
const description = fm?.description || "";
|
|
71
|
+
skills.push({
|
|
72
|
+
name,
|
|
73
|
+
role: classifyRole(description),
|
|
74
|
+
description: description.slice(0, 200),
|
|
75
|
+
source: "custom",
|
|
76
|
+
path: skillMd,
|
|
77
|
+
});
|
|
78
|
+
} catch {
|
|
79
|
+
skills.push({ name: entry.name, role: "general", description: "", source: "custom", path: skillMd });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 2) OpenAI 빌트인 스킬 (codex에 내장된 /명령들)
|
|
85
|
+
const BUILTIN_SKILLS = [
|
|
86
|
+
{ name: "web-clone", role: "general", description: "Clone and analyze web pages" },
|
|
87
|
+
{ name: "help", role: "general", description: "Show available commands" },
|
|
88
|
+
{ name: "note", role: "general", description: "Save notes during session" },
|
|
89
|
+
{ name: "worker", role: "auto", description: "Spawn background worker for tasks" },
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
for (const builtin of BUILTIN_SKILLS) {
|
|
93
|
+
// 이미 커스텀으로 오버라이드된 경우 스킵
|
|
94
|
+
if (skills.some((s) => s.name === builtin.name)) continue;
|
|
95
|
+
// 실제 디렉토리 존재 확인
|
|
96
|
+
if (existsSync(join(codexSkillsDir, builtin.name))) continue;
|
|
97
|
+
skills.push({ ...builtin, source: "builtin" });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
scanned_at: new Date().toISOString(),
|
|
102
|
+
codex_skills_dir: codexSkillsDir,
|
|
103
|
+
total: skills.length,
|
|
104
|
+
skills,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── 2. Tier 환경 프로브 ──
|
|
109
|
+
|
|
110
|
+
function probeCliVersion(command, runExec = execSync) {
|
|
111
|
+
try {
|
|
112
|
+
runExec(command, { stdio: "ignore", timeout: 3000, windowsHide: true });
|
|
113
|
+
return true;
|
|
114
|
+
} catch {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function resolveCachedCliCheck(preflightCheck, command, runExec = execSync) {
|
|
120
|
+
if (typeof preflightCheck?.ok === "boolean") {
|
|
121
|
+
return preflightCheck.ok;
|
|
122
|
+
}
|
|
123
|
+
return probeCliVersion(command, runExec);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function probeTierEnvironment({
|
|
127
|
+
readPreflight = readPreflightCache,
|
|
128
|
+
runExec = execSync,
|
|
129
|
+
platform = process.platform,
|
|
130
|
+
} = {}) {
|
|
131
|
+
const preflight = readPreflight();
|
|
132
|
+
const checks = {
|
|
133
|
+
psmux: false,
|
|
134
|
+
hub: false,
|
|
135
|
+
codex: resolveCachedCliCheck(preflight?.codex, "codex --version", runExec),
|
|
136
|
+
gemini: resolveCachedCliCheck(preflight?.gemini, "gemini --version", runExec),
|
|
137
|
+
wt: false,
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// psmux
|
|
141
|
+
try {
|
|
142
|
+
runExec("psmux --version", { stdio: "ignore", timeout: 3000, windowsHide: true });
|
|
143
|
+
checks.psmux = true;
|
|
144
|
+
} catch {}
|
|
145
|
+
|
|
146
|
+
// hub (preflight에서 가져오거나 직접 확인)
|
|
147
|
+
if (preflight?.hub?.ok) {
|
|
148
|
+
checks.hub = true;
|
|
149
|
+
} else {
|
|
150
|
+
try {
|
|
151
|
+
runExec("curl -sf http://127.0.0.1:27888/status", { stdio: "ignore", timeout: 2000, windowsHide: true });
|
|
152
|
+
checks.hub = true;
|
|
153
|
+
} catch {}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Windows Terminal
|
|
157
|
+
if (platform === "win32") {
|
|
158
|
+
try {
|
|
159
|
+
runExec("where wt.exe", { stdio: "ignore", timeout: 2000, windowsHide: true });
|
|
160
|
+
checks.wt = true;
|
|
161
|
+
} catch {}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Tier 분류
|
|
165
|
+
let tier = "minimal"; // claude-only
|
|
166
|
+
if (checks.codex || checks.gemini) tier = "standard"; // multi-cli
|
|
167
|
+
if (checks.psmux && checks.hub && (checks.codex || checks.gemini)) tier = "full"; // orchestration-ready
|
|
168
|
+
|
|
169
|
+
const agents = [];
|
|
170
|
+
agents.push("claude");
|
|
171
|
+
if (checks.codex) agents.push("codex");
|
|
172
|
+
if (checks.gemini) agents.push("gemini");
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
probed_at: new Date().toISOString(),
|
|
176
|
+
tier,
|
|
177
|
+
checks,
|
|
178
|
+
available_agents: agents,
|
|
179
|
+
codex_plan: preflight?.codex_plan || { plan: "unknown" },
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ── 3. 프로젝트 메타 추출 ──
|
|
184
|
+
|
|
185
|
+
export function extractProjectMeta() {
|
|
186
|
+
let name = basename(CWD);
|
|
187
|
+
let description = "";
|
|
188
|
+
let lang = "unknown";
|
|
189
|
+
let testCmd = null;
|
|
190
|
+
let isGit = false;
|
|
191
|
+
|
|
192
|
+
// git
|
|
193
|
+
try {
|
|
194
|
+
const toplevel = execSync("git rev-parse --show-toplevel", {
|
|
195
|
+
encoding: "utf8", timeout: 3000, windowsHide: true, stdio: ["pipe", "pipe", "ignore"],
|
|
196
|
+
}).trim();
|
|
197
|
+
name = basename(toplevel);
|
|
198
|
+
isGit = true;
|
|
199
|
+
} catch {}
|
|
200
|
+
|
|
201
|
+
// package.json
|
|
202
|
+
const pkgPath = join(CWD, "package.json");
|
|
203
|
+
if (existsSync(pkgPath)) {
|
|
204
|
+
try {
|
|
205
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
206
|
+
description = pkg.description || "";
|
|
207
|
+
testCmd = pkg.scripts?.test || null;
|
|
208
|
+
lang = "JavaScript/ESM (Node.js)";
|
|
209
|
+
} catch {}
|
|
210
|
+
} else if (existsSync(join(CWD, "pyproject.toml")) || existsSync(join(CWD, "setup.py"))) {
|
|
211
|
+
lang = "Python";
|
|
212
|
+
} else if (existsSync(join(CWD, "Cargo.toml"))) {
|
|
213
|
+
lang = "Rust";
|
|
214
|
+
} else if (existsSync(join(CWD, "go.mod"))) {
|
|
215
|
+
lang = "Go";
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
extracted_at: new Date().toISOString(),
|
|
220
|
+
name,
|
|
221
|
+
description: description.slice(0, 300),
|
|
222
|
+
lang,
|
|
223
|
+
test_cmd: testCmd,
|
|
224
|
+
is_git: isGit,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ── 4. 검색 엔진 확인 ──
|
|
229
|
+
|
|
230
|
+
function loadMcpInventory() {
|
|
231
|
+
const invPath = join(HOME, ".claude", "cache", "mcp-inventory.json");
|
|
232
|
+
try {
|
|
233
|
+
return JSON.parse(readFileSync(invPath, "utf8"));
|
|
234
|
+
} catch {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function loadMcpConfigs() {
|
|
240
|
+
const servers = {};
|
|
241
|
+
// Claude 설정 파일들에서 MCP 서버 읽기
|
|
242
|
+
const configPaths = [
|
|
243
|
+
join(HOME, ".claude", "settings.json"),
|
|
244
|
+
join(HOME, ".claude", "settings.local.json"),
|
|
245
|
+
join(CWD, ".mcp.json"),
|
|
246
|
+
join(CWD, ".claude", ".mcp.json"),
|
|
247
|
+
];
|
|
248
|
+
|
|
249
|
+
for (const cfgPath of configPaths) {
|
|
250
|
+
try {
|
|
251
|
+
const data = JSON.parse(readFileSync(cfgPath, "utf8"));
|
|
252
|
+
const mcpServers = data.mcpServers || {};
|
|
253
|
+
for (const [name, config] of Object.entries(mcpServers)) {
|
|
254
|
+
servers[name] = { configured: true, source: basename(cfgPath), ...config };
|
|
255
|
+
}
|
|
256
|
+
} catch {}
|
|
257
|
+
}
|
|
258
|
+
return servers;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function checkSearchEngines() {
|
|
262
|
+
const inventory = loadMcpInventory();
|
|
263
|
+
const configuredServers = loadMcpConfigs();
|
|
264
|
+
const engines = [];
|
|
265
|
+
|
|
266
|
+
// 알려진 검색 서버 (우선순위순)
|
|
267
|
+
const KNOWN_SEARCH_SERVERS = [...SEARCH_SERVER_ORDER, "context7"];
|
|
268
|
+
|
|
269
|
+
// MCP 인벤토리에서 검색 가능 서버 탐지
|
|
270
|
+
const allServerNames = new Set(KNOWN_SEARCH_SERVERS);
|
|
271
|
+
|
|
272
|
+
// 인벤토리의 Claude MCP 서버들도 확인
|
|
273
|
+
if (inventory) {
|
|
274
|
+
for (const scope of ["codex", "gemini", "claude"]) {
|
|
275
|
+
const scopeData = inventory[scope];
|
|
276
|
+
if (!scopeData?.servers) continue;
|
|
277
|
+
for (const srv of scopeData.servers) {
|
|
278
|
+
const tags = srv.domain_tags || MCP_SERVER_DOMAIN_TAGS[srv.name] || [];
|
|
279
|
+
if (tags.includes("search") || tags.includes("web") || tags.includes("research")) {
|
|
280
|
+
allServerNames.add(srv.name);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// 설정된 서버도 추가
|
|
287
|
+
for (const name of Object.keys(configuredServers)) {
|
|
288
|
+
const tags = MCP_SERVER_DOMAIN_TAGS[name] || [];
|
|
289
|
+
if (tags.includes("search") || tags.includes("web") || KNOWN_SEARCH_SERVERS.includes(name)) {
|
|
290
|
+
allServerNames.add(name);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// 각 검색 서버의 상태 판정
|
|
295
|
+
for (const name of allServerNames) {
|
|
296
|
+
const configured = !!configuredServers[name];
|
|
297
|
+
const tags = MCP_SERVER_DOMAIN_TAGS[name] || [];
|
|
298
|
+
|
|
299
|
+
// 인벤토리에서 실제 활성 상태 확인
|
|
300
|
+
let inventoryStatus = null;
|
|
301
|
+
if (inventory) {
|
|
302
|
+
for (const scope of ["codex", "gemini", "claude"]) {
|
|
303
|
+
const srv = inventory[scope]?.servers?.find((s) => s.name === name);
|
|
304
|
+
if (srv) {
|
|
305
|
+
inventoryStatus = { scope, status: srv.status, tool_count: srv.tool_count };
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
let status = "unavailable";
|
|
312
|
+
if (inventoryStatus?.status === "enabled" || inventoryStatus?.status === "configured" || inventoryStatus?.status === "available") {
|
|
313
|
+
status = "available";
|
|
314
|
+
} else if (configured) {
|
|
315
|
+
status = "configured"; // 설정은 있지만 인벤토리 미확인
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
engines.push({
|
|
319
|
+
name,
|
|
320
|
+
status,
|
|
321
|
+
domain_tags: tags,
|
|
322
|
+
configured,
|
|
323
|
+
inventory: inventoryStatus,
|
|
324
|
+
source: configuredServers[name]?.source || null,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// 우선순위순 정렬: SEARCH_SERVER_ORDER 먼저, 나머지 알파벳순
|
|
329
|
+
engines.sort((a, b) => {
|
|
330
|
+
const ai = SEARCH_SERVER_ORDER.indexOf(a.name);
|
|
331
|
+
const bi = SEARCH_SERVER_ORDER.indexOf(b.name);
|
|
332
|
+
if (ai !== -1 && bi !== -1) return ai - bi;
|
|
333
|
+
if (ai !== -1) return -1;
|
|
334
|
+
if (bi !== -1) return 1;
|
|
335
|
+
return a.name.localeCompare(b.name);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
const available = engines.filter((e) => e.status === "available");
|
|
339
|
+
const primary = available[0]?.name || null;
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
checked_at: new Date().toISOString(),
|
|
343
|
+
primary_engine: primary,
|
|
344
|
+
available_count: available.length,
|
|
345
|
+
total_count: engines.length,
|
|
346
|
+
engines,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ── 메인 실행 ──
|
|
351
|
+
|
|
352
|
+
function main() {
|
|
353
|
+
ensureDirs();
|
|
354
|
+
|
|
355
|
+
const results = {};
|
|
356
|
+
|
|
357
|
+
try {
|
|
358
|
+
const skills = scanCodexSkills();
|
|
359
|
+
writeJSON(join(CACHE_DIR, "codex-skills.json"), skills);
|
|
360
|
+
results.codex_skills = { ok: true, count: skills.total };
|
|
361
|
+
} catch (e) {
|
|
362
|
+
results.codex_skills = { ok: false, error: e.message };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
try {
|
|
366
|
+
const tier = probeTierEnvironment();
|
|
367
|
+
writeJSON(join(STATE_DIR, "tier-environment.json"), tier);
|
|
368
|
+
results.tier = { ok: true, tier: tier.tier };
|
|
369
|
+
} catch (e) {
|
|
370
|
+
results.tier = { ok: false, error: e.message };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
try {
|
|
374
|
+
const meta = extractProjectMeta();
|
|
375
|
+
writeJSON(join(CACHE_DIR, "project-meta.json"), meta);
|
|
376
|
+
results.project_meta = { ok: true, name: meta.name };
|
|
377
|
+
} catch (e) {
|
|
378
|
+
results.project_meta = { ok: false, error: e.message };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
try {
|
|
382
|
+
const search = checkSearchEngines();
|
|
383
|
+
writeJSON(join(STATE_DIR, "search-engines.json"), search);
|
|
384
|
+
results.search_engines = { ok: true, primary: search.primary_engine, available: search.available_count };
|
|
385
|
+
} catch (e) {
|
|
386
|
+
results.search_engines = { ok: false, error: e.message };
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// 간결 stdout (hook/로그용)
|
|
390
|
+
const ok = Object.values(results).every((r) => r.ok);
|
|
391
|
+
const summary = ok ? "cache-buildup: ok" : "cache-buildup: partial";
|
|
392
|
+
const details = [];
|
|
393
|
+
if (results.codex_skills?.ok) details.push(`skills:${results.codex_skills.count}`);
|
|
394
|
+
if (results.tier?.ok) details.push(`tier:${results.tier.tier}`);
|
|
395
|
+
if (results.search_engines?.ok) details.push(`search:${results.search_engines.primary || "none"}(${results.search_engines.available})`);
|
|
396
|
+
console.log(details.length ? `${summary} (${details.join(", ")})` : summary);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (process.argv[1]?.endsWith("cache-buildup.mjs")) {
|
|
400
|
+
main();
|
|
401
|
+
}
|
|
@@ -152,6 +152,11 @@ function deny(reason) {
|
|
|
152
152
|
process.exit(2);
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
const HEADLESS_FALLBACK_COMMAND =
|
|
156
|
+
'Bash("tfx multi --teammate-mode headless --assign \'codex:prompt:role\' ...")';
|
|
157
|
+
const DIRECT_CLI_BYPASS_HINT =
|
|
158
|
+
"로컬 디버깅이 목적이면 TFX_ALLOW_DIRECT_CLI=1로 일시 우회할 수 있습니다.";
|
|
159
|
+
|
|
155
160
|
async function main() {
|
|
156
161
|
// P0: TFX_ALLOW_DIRECT_CLI 환경변수 바이패스 — psmux 세션 생성 불가 시 수동 활성화
|
|
157
162
|
if (process.env.TFX_ALLOW_DIRECT_CLI === "1") {
|
|
@@ -202,8 +207,9 @@ async function main() {
|
|
|
202
207
|
// codex/gemini 직접 CLI 호출 → deny
|
|
203
208
|
if (/\bcodex\s+exec\b/.test(cmd) || /\bgemini\s+(-p|--prompt)\b/.test(cmd)) {
|
|
204
209
|
deny(
|
|
205
|
-
"[headless-guard] codex/gemini 직접
|
|
206
|
-
|
|
210
|
+
"[headless-guard] codex/gemini 직접 호출은 spawn-session에서 차단됩니다. " +
|
|
211
|
+
`승인된 경로: ${HEADLESS_FALLBACK_COMMAND}. ` +
|
|
212
|
+
DIRECT_CLI_BYPASS_HINT,
|
|
207
213
|
);
|
|
208
214
|
}
|
|
209
215
|
|
|
@@ -310,7 +316,8 @@ async function main() {
|
|
|
310
316
|
if (cliPatterns.some((p) => p.test(combined))) {
|
|
311
317
|
deny(
|
|
312
318
|
"[headless-guard] Codex/Gemini를 Agent()로 래핑하지 마세요. " +
|
|
313
|
-
|
|
319
|
+
`승인된 경로: ${HEADLESS_FALLBACK_COMMAND}. ` +
|
|
320
|
+
DIRECT_CLI_BYPASS_HINT,
|
|
314
321
|
);
|
|
315
322
|
}
|
|
316
323
|
}
|