triflux 3.2.0-dev.8 → 3.3.0-dev.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.
- package/bin/triflux.mjs +1296 -1055
- package/hooks/hooks.json +17 -0
- package/hooks/keyword-rules.json +20 -4
- package/hooks/pipeline-stop.mjs +54 -0
- package/hub/bridge.mjs +517 -318
- package/hub/hitl.mjs +45 -31
- package/hub/pipe.mjs +457 -0
- package/hub/pipeline/index.mjs +121 -0
- package/hub/pipeline/state.mjs +164 -0
- package/hub/pipeline/transitions.mjs +114 -0
- package/hub/router.mjs +422 -161
- package/hub/schema.sql +14 -0
- package/hub/server.mjs +499 -424
- package/hub/store.mjs +388 -314
- package/hub/team/cli-team-common.mjs +348 -0
- package/hub/team/cli-team-control.mjs +393 -0
- package/hub/team/cli-team-start.mjs +516 -0
- package/hub/team/cli-team-status.mjs +269 -0
- package/hub/team/cli.mjs +75 -1475
- package/hub/team/dashboard.mjs +1 -9
- package/hub/team/native.mjs +190 -130
- package/hub/team/nativeProxy.mjs +165 -78
- package/hub/team/orchestrator.mjs +15 -20
- package/hub/team/pane.mjs +137 -103
- package/hub/team/psmux.mjs +506 -0
- package/hub/team/session.mjs +393 -330
- package/hub/team/shared.mjs +13 -0
- package/hub/team/staleState.mjs +299 -0
- package/hub/tools.mjs +105 -31
- package/hub/workers/claude-worker.mjs +446 -0
- package/hub/workers/codex-mcp.mjs +414 -0
- package/hub/workers/factory.mjs +18 -0
- package/hub/workers/gemini-worker.mjs +349 -0
- package/hub/workers/interface.mjs +41 -0
- package/hud/hud-qos-status.mjs +1790 -1788
- package/package.json +4 -1
- package/scripts/__tests__/keyword-detector.test.mjs +8 -8
- package/scripts/keyword-detector.mjs +15 -0
- package/scripts/lib/keyword-rules.mjs +4 -1
- package/scripts/preflight-cache.mjs +72 -0
- package/scripts/psmux-steering-prototype.sh +368 -0
- package/scripts/setup.mjs +136 -71
- package/scripts/tfx-route-worker.mjs +161 -0
- package/scripts/tfx-route.sh +485 -91
- package/skills/tfx-auto/SKILL.md +90 -564
- package/skills/tfx-auto-codex/SKILL.md +1 -3
- package/skills/tfx-codex/SKILL.md +1 -4
- package/skills/tfx-doctor/SKILL.md +1 -0
- package/skills/tfx-gemini/SKILL.md +1 -4
- package/skills/tfx-multi/SKILL.md +378 -0
- package/skills/tfx-setup/SKILL.md +1 -4
- package/skills/tfx-team/SKILL.md +0 -304
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "triflux",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0-dev.1",
|
|
4
4
|
"description": "CLI-first multi-model orchestrator for Claude Code — route tasks to Codex, Gemini, and Claude",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -27,6 +27,9 @@
|
|
|
27
27
|
"scripts": {
|
|
28
28
|
"setup": "node scripts/setup.mjs",
|
|
29
29
|
"postinstall": "node scripts/setup.mjs",
|
|
30
|
+
"test": "node --test tests/**/*.test.mjs",
|
|
31
|
+
"test:unit": "node --test tests/unit/**/*.test.mjs",
|
|
32
|
+
"test:integration": "node --test tests/integration/**/*.test.mjs",
|
|
30
33
|
"test:route-smoke": "node --test scripts/test-tfx-route-no-claude-native.mjs"
|
|
31
34
|
},
|
|
32
35
|
"engines": {
|
|
@@ -79,7 +79,7 @@ test("sanitizeForKeywordDetection: 코드블록/URL/파일경로/XML 태그 제
|
|
|
79
79
|
const input = [
|
|
80
80
|
"정상 문장",
|
|
81
81
|
"```sh",
|
|
82
|
-
"tfx
|
|
82
|
+
"tfx multi",
|
|
83
83
|
"```",
|
|
84
84
|
"https://example.com/path?q=1",
|
|
85
85
|
"C:\\Users\\SSAFY\\Desktop\\Projects\\tools\\triflux",
|
|
@@ -90,7 +90,7 @@ test("sanitizeForKeywordDetection: 코드블록/URL/파일경로/XML 태그 제
|
|
|
90
90
|
const sanitized = sanitizeForKeywordDetection(input);
|
|
91
91
|
|
|
92
92
|
assert.ok(sanitized.includes("정상 문장"));
|
|
93
|
-
assert.ok(!sanitized.includes("tfx
|
|
93
|
+
assert.ok(!sanitized.includes("tfx multi"));
|
|
94
94
|
assert.ok(!sanitized.includes("https://"));
|
|
95
95
|
assert.ok(!sanitized.includes("C:\\Users\\"));
|
|
96
96
|
assert.ok(!sanitized.includes("./hooks/keyword-rules.json"));
|
|
@@ -138,7 +138,7 @@ test("compileRules: 정규식 컴파일 실패", () => {
|
|
|
138
138
|
id: "bad-pattern",
|
|
139
139
|
priority: 1,
|
|
140
140
|
patterns: [{ source: "[", flags: "" }],
|
|
141
|
-
skill: "tfx-
|
|
141
|
+
skill: "tfx-multi",
|
|
142
142
|
supersedes: [],
|
|
143
143
|
exclusive: false,
|
|
144
144
|
state: null,
|
|
@@ -152,7 +152,7 @@ test("compileRules: 정규식 컴파일 실패", () => {
|
|
|
152
152
|
test("matchRules: tfx 키워드 매칭", () => {
|
|
153
153
|
const compiledRules = loadCompiledRules();
|
|
154
154
|
const cases = [
|
|
155
|
-
{ text: "tfx
|
|
155
|
+
{ text: "tfx multi 세션 시작", expectedId: "tfx-multi" },
|
|
156
156
|
{ text: "tfx auto 돌려줘", expectedId: "tfx-auto" },
|
|
157
157
|
{ text: "tfx codex 로 실행", expectedId: "tfx-codex" },
|
|
158
158
|
{ text: "tfx gemini 로 실행", expectedId: "tfx-gemini" },
|
|
@@ -218,17 +218,17 @@ test("resolveConflicts: exclusive 처리", () => {
|
|
|
218
218
|
|
|
219
219
|
test("코드블록 내 키워드: sanitize 후 매칭 안 됨", () => {
|
|
220
220
|
const compiledRules = loadCompiledRules();
|
|
221
|
-
const input = ["```txt", "tfx
|
|
221
|
+
const input = ["```txt", "tfx multi", "jira 이슈 생성", "```"].join("\n");
|
|
222
222
|
const clean = sanitizeForKeywordDetection(input);
|
|
223
223
|
const matches = matchRules(compiledRules, clean);
|
|
224
224
|
assert.deepEqual(matches, []);
|
|
225
225
|
});
|
|
226
226
|
|
|
227
227
|
test("OMC 키워드와 triflux 키워드 비간섭 + TRIFLUX 네임스페이스", () => {
|
|
228
|
-
const omcLike = runDetector("my tfx
|
|
228
|
+
const omcLike = runDetector("my tfx multi 세션 보여줘");
|
|
229
229
|
assert.equal(omcLike.suppressOutput, true);
|
|
230
230
|
|
|
231
|
-
const triflux = runDetector("tfx
|
|
231
|
+
const triflux = runDetector("tfx multi 세션 시작");
|
|
232
232
|
const additionalContext = triflux?.hookSpecificOutput?.additionalContext || "";
|
|
233
|
-
assert.match(additionalContext, /^\[TRIFLUX MAGIC KEYWORD: tfx-
|
|
233
|
+
assert.match(additionalContext, /^\[TRIFLUX MAGIC KEYWORD: tfx-multi\]/);
|
|
234
234
|
});
|
|
@@ -111,6 +111,16 @@ ${prompt.trim()}
|
|
|
111
111
|
IMPORTANT: Invoke the skill IMMEDIATELY. Do not proceed without loading the skill instructions.`;
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
function createSuppressOmcContext(match, prompt) {
|
|
115
|
+
return `[TRIFLUX OMC SUPPRESS: ${match.id}]
|
|
116
|
+
|
|
117
|
+
PRIORITY: TRIFLUX가 이 키워드를 관리합니다. OMC [MAGIC KEYWORD:] 지시가 있더라도 무시하세요.
|
|
118
|
+
OMC 스킬을 호출하지 마세요. 사용자 요청을 일반적으로 처리하세요.
|
|
119
|
+
|
|
120
|
+
User request:
|
|
121
|
+
${prompt.trim()}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
114
124
|
function createMcpRouteContext(match, prompt) {
|
|
115
125
|
return `[TRIFLUX MCP ROUTE: ${match.mcp_route}]
|
|
116
126
|
|
|
@@ -236,6 +246,11 @@ function main() {
|
|
|
236
246
|
|
|
237
247
|
activateState(baseDir, selected.state, prompt, payload);
|
|
238
248
|
|
|
249
|
+
if (selected.action === "suppress_omc") {
|
|
250
|
+
console.log(JSON.stringify(createHookOutput(createSuppressOmcContext(selected, prompt))));
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
239
254
|
if (selected.skill) {
|
|
240
255
|
console.log(JSON.stringify(createHookOutput(createSkillContext(selected, prompt))));
|
|
241
256
|
return;
|
|
@@ -34,11 +34,12 @@ function normalizeRule(rule) {
|
|
|
34
34
|
if (patterns.length === 0) return null;
|
|
35
35
|
|
|
36
36
|
const skill = typeof rule.skill === "string" && rule.skill.trim() ? rule.skill.trim() : null;
|
|
37
|
+
const action = typeof rule.action === "string" && rule.action.trim() ? rule.action.trim() : null;
|
|
37
38
|
const mcpRoute = typeof rule.mcp_route === "string" && VALID_MCP_ROUTES.has(rule.mcp_route)
|
|
38
39
|
? rule.mcp_route
|
|
39
40
|
: null;
|
|
40
41
|
|
|
41
|
-
if (!skill && !mcpRoute) return null;
|
|
42
|
+
if (!skill && !mcpRoute && !action) return null;
|
|
42
43
|
|
|
43
44
|
const supersedes = Array.isArray(rule.supersedes)
|
|
44
45
|
? rule.supersedes.filter((id) => typeof id === "string" && id.trim()).map((id) => id.trim())
|
|
@@ -51,6 +52,7 @@ function normalizeRule(rule) {
|
|
|
51
52
|
id: rule.id.trim(),
|
|
52
53
|
patterns,
|
|
53
54
|
skill,
|
|
55
|
+
action: rule.action || null,
|
|
54
56
|
priority: rule.priority,
|
|
55
57
|
supersedes,
|
|
56
58
|
exclusive: rule.exclusive === true,
|
|
@@ -114,6 +116,7 @@ export function matchRules(compiledRules, cleanText) {
|
|
|
114
116
|
matches.push({
|
|
115
117
|
id: rule.id,
|
|
116
118
|
skill: rule.skill,
|
|
119
|
+
action: rule.action || null,
|
|
117
120
|
priority: rule.priority,
|
|
118
121
|
supersedes: rule.supersedes || [],
|
|
119
122
|
exclusive: rule.exclusive === true,
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/preflight-cache.mjs — 세션 시작 시 preflight 점검 캐싱
|
|
3
|
+
|
|
4
|
+
import { writeFileSync, mkdirSync, existsSync, readFileSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { execSync } from "node:child_process";
|
|
8
|
+
|
|
9
|
+
const CACHE_DIR = join(homedir(), ".claude", "cache");
|
|
10
|
+
const CACHE_FILE = join(CACHE_DIR, "tfx-preflight.json");
|
|
11
|
+
const CACHE_TTL_MS = 30_000; // 30초
|
|
12
|
+
|
|
13
|
+
function checkHub() {
|
|
14
|
+
try {
|
|
15
|
+
const res = execSync("curl -sf http://127.0.0.1:27888/status", { timeout: 3000, encoding: "utf8" });
|
|
16
|
+
const data = JSON.parse(res);
|
|
17
|
+
return { ok: true, state: data?.hub?.state || "unknown", pid: data?.pid };
|
|
18
|
+
} catch {
|
|
19
|
+
return { ok: false, state: "unreachable" };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function checkRoute() {
|
|
24
|
+
const routePath = join(homedir(), ".claude", "scripts", "tfx-route.sh");
|
|
25
|
+
return { ok: existsSync(routePath), path: routePath };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function checkCli(name) {
|
|
29
|
+
try {
|
|
30
|
+
const path = execSync(`which ${name} 2>/dev/null || where ${name} 2>nul`, { encoding: "utf8", timeout: 2000 }).trim();
|
|
31
|
+
return { ok: !!path, path };
|
|
32
|
+
} catch {
|
|
33
|
+
return { ok: false };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function runPreflight() {
|
|
38
|
+
const result = {
|
|
39
|
+
timestamp: Date.now(),
|
|
40
|
+
hub: checkHub(),
|
|
41
|
+
route: checkRoute(),
|
|
42
|
+
codex: checkCli("codex"),
|
|
43
|
+
gemini: checkCli("gemini"),
|
|
44
|
+
ok: false,
|
|
45
|
+
};
|
|
46
|
+
result.ok = result.hub.ok && result.route.ok;
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 캐시 읽기 (TTL 검증 포함)
|
|
51
|
+
export function readPreflightCache() {
|
|
52
|
+
try {
|
|
53
|
+
const data = JSON.parse(readFileSync(CACHE_FILE, "utf8"));
|
|
54
|
+
if (Date.now() - data.timestamp < CACHE_TTL_MS) return data;
|
|
55
|
+
} catch {}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 메인 실행
|
|
60
|
+
if (process.argv[1]?.endsWith("preflight-cache.mjs")) {
|
|
61
|
+
const result = runPreflight();
|
|
62
|
+
mkdirSync(CACHE_DIR, { recursive: true });
|
|
63
|
+
writeFileSync(CACHE_FILE, JSON.stringify(result, null, 2));
|
|
64
|
+
// 간결 출력 (hook stdout)
|
|
65
|
+
const summary = result.ok ? "preflight: ok" : "preflight: FAIL";
|
|
66
|
+
const details = [];
|
|
67
|
+
if (!result.hub.ok) details.push("hub:" + result.hub.state);
|
|
68
|
+
if (!result.route.ok) details.push("route:missing");
|
|
69
|
+
console.log(details.length ? `${summary} (${details.join(", ")})` : summary);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export { runPreflight, CACHE_FILE, CACHE_TTL_MS };
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# psmux-steering-prototype.sh
|
|
3
|
+
# Windows psmux 환경에서 lead/codex-worker/gemini-worker pane을 만들고
|
|
4
|
+
# send-keys + pipe-pane 기반으로 실시간 CLI 스티어링을 실험하는 프로토타입.
|
|
5
|
+
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
PSMUX_BIN="${PSMUX_BIN:-psmux}"
|
|
9
|
+
SESSION_NAME="${PSMUX_SESSION_NAME:-triflux-steering}"
|
|
10
|
+
WINDOW_NAME="${PSMUX_WINDOW_NAME:-control}"
|
|
11
|
+
PANE_LEAD="lead"
|
|
12
|
+
PANE_CODEX="codex-worker"
|
|
13
|
+
PANE_GEMINI="gemini-worker"
|
|
14
|
+
SHELL_COMMAND="${PSMUX_SHELL_COMMAND:-powershell.exe -NoLogo}"
|
|
15
|
+
CAPTURE_ROOT="${PSMUX_CAPTURE_ROOT:-${TMPDIR:-/tmp}/psmux-steering}"
|
|
16
|
+
CAPTURE_DIR="${CAPTURE_ROOT}/${SESSION_NAME}"
|
|
17
|
+
CAPTURE_HELPER_PATH="${CAPTURE_ROOT}/pipe-pane-capture.ps1"
|
|
18
|
+
COMPLETION_PREFIX="__TRIFLUX_DONE__:"
|
|
19
|
+
POLL_INTERVAL_SEC="${PSMUX_POLL_INTERVAL_SEC:-1}"
|
|
20
|
+
|
|
21
|
+
usage() {
|
|
22
|
+
cat <<'EOF'
|
|
23
|
+
Usage:
|
|
24
|
+
scripts/psmux-steering-prototype.sh start
|
|
25
|
+
scripts/psmux-steering-prototype.sh demo
|
|
26
|
+
scripts/psmux-steering-prototype.sh attach
|
|
27
|
+
scripts/psmux-steering-prototype.sh send <pane-name> <command text>
|
|
28
|
+
scripts/psmux-steering-prototype.sh send-no-enter <pane-name> <text>
|
|
29
|
+
scripts/psmux-steering-prototype.sh steer-ps <pane-name> <powershell command>
|
|
30
|
+
scripts/psmux-steering-prototype.sh wait <pane-name> <regex> [timeout-sec]
|
|
31
|
+
scripts/psmux-steering-prototype.sh logs
|
|
32
|
+
scripts/psmux-steering-prototype.sh cleanup
|
|
33
|
+
|
|
34
|
+
Pane names:
|
|
35
|
+
lead | codex-worker | gemini-worker
|
|
36
|
+
|
|
37
|
+
Environment overrides:
|
|
38
|
+
PSMUX_BIN
|
|
39
|
+
PSMUX_SESSION_NAME
|
|
40
|
+
PSMUX_WINDOW_NAME
|
|
41
|
+
PSMUX_SHELL_COMMAND
|
|
42
|
+
PSMUX_CAPTURE_ROOT
|
|
43
|
+
PSMUX_POLL_INTERVAL_SEC
|
|
44
|
+
EOF
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
log() {
|
|
48
|
+
printf '[psmux-steering] %s\n' "$*"
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
die() {
|
|
52
|
+
printf '[psmux-steering] ERROR: %s\n' "$*" >&2
|
|
53
|
+
exit 1
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
require_psmux() {
|
|
57
|
+
command -v "$PSMUX_BIN" >/dev/null 2>&1 || die "Cannot find '$PSMUX_BIN' in PATH."
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
session_target() {
|
|
61
|
+
printf '%s:%s' "$SESSION_NAME" "$WINDOW_NAME"
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
pane_target_from_index() {
|
|
65
|
+
local pane_index="$1"
|
|
66
|
+
printf '%s.%s' "$(session_target)" "$pane_index"
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
log_file_for() {
|
|
70
|
+
local pane_name="$1"
|
|
71
|
+
printf '%s/%s.log' "$CAPTURE_DIR" "$pane_name"
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
to_windows_path() {
|
|
75
|
+
local path_value="$1"
|
|
76
|
+
|
|
77
|
+
if command -v cygpath >/dev/null 2>&1; then
|
|
78
|
+
cygpath -aw "$path_value"
|
|
79
|
+
return 0
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
printf '%s\n' "$path_value"
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
ensure_capture_helper() {
|
|
86
|
+
mkdir -p "$CAPTURE_ROOT"
|
|
87
|
+
|
|
88
|
+
cat >"$CAPTURE_HELPER_PATH" <<'EOF'
|
|
89
|
+
param(
|
|
90
|
+
[Parameter(Mandatory = $true)][string]$Path
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
$parent = Split-Path -Parent $Path
|
|
94
|
+
if ($parent) {
|
|
95
|
+
New-Item -ItemType Directory -Force -Path $parent | Out-Null
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
$reader = [Console]::In
|
|
99
|
+
while (($line = $reader.ReadLine()) -ne $null) {
|
|
100
|
+
Add-Content -LiteralPath $Path -Value $line -Encoding utf8
|
|
101
|
+
}
|
|
102
|
+
EOF
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
session_exists() {
|
|
106
|
+
"$PSMUX_BIN" has-session -t "$SESSION_NAME" >/dev/null 2>&1
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
resolve_pane_target() {
|
|
110
|
+
local pane_name="$1"
|
|
111
|
+
local pane_index
|
|
112
|
+
|
|
113
|
+
pane_index="$("$PSMUX_BIN" list-panes -t "$(session_target)" -F '#{pane_index} #{pane_title}' \
|
|
114
|
+
| awk -v wanted="$pane_name" '$2 == wanted { print $1; exit }')"
|
|
115
|
+
|
|
116
|
+
[[ -n "$pane_index" ]] || return 1
|
|
117
|
+
pane_target_from_index "$pane_index"
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
require_pane_target() {
|
|
121
|
+
local pane_name="$1"
|
|
122
|
+
local pane_target
|
|
123
|
+
|
|
124
|
+
pane_target="$(resolve_pane_target "$pane_name")"
|
|
125
|
+
[[ -n "$pane_target" ]] || die "Pane '$pane_name' not found in session '$SESSION_NAME'."
|
|
126
|
+
printf '%s\n' "$pane_target"
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
set_pane_title() {
|
|
130
|
+
local pane_target="$1"
|
|
131
|
+
local pane_name="$2"
|
|
132
|
+
|
|
133
|
+
"$PSMUX_BIN" select-pane -t "$pane_target" -T "$pane_name" >/dev/null
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
start_capture_for_pane() {
|
|
137
|
+
local pane_name="$1"
|
|
138
|
+
local pane_target log_file helper_windows_path log_windows_path
|
|
139
|
+
|
|
140
|
+
pane_target="$(require_pane_target "$pane_name")"
|
|
141
|
+
log_file="$(log_file_for "$pane_name")"
|
|
142
|
+
ensure_capture_helper
|
|
143
|
+
helper_windows_path="$(to_windows_path "$CAPTURE_HELPER_PATH")"
|
|
144
|
+
log_windows_path="$(to_windows_path "$log_file")"
|
|
145
|
+
|
|
146
|
+
mkdir -p "$CAPTURE_DIR"
|
|
147
|
+
: >"$log_file"
|
|
148
|
+
|
|
149
|
+
"$PSMUX_BIN" pipe-pane -t "$pane_target" >/dev/null 2>&1 || true
|
|
150
|
+
"$PSMUX_BIN" pipe-pane -t "$pane_target" powershell.exe -NoLogo -NoProfile -File "$helper_windows_path" "$log_windows_path" >/dev/null
|
|
151
|
+
refresh_snapshot_for_pane "$pane_name"
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
start_capture_for_all_panes() {
|
|
155
|
+
start_capture_for_pane "$PANE_LEAD"
|
|
156
|
+
start_capture_for_pane "$PANE_CODEX"
|
|
157
|
+
start_capture_for_pane "$PANE_GEMINI"
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
stop_capture_for_pane() {
|
|
161
|
+
local pane_name="$1"
|
|
162
|
+
local pane_target
|
|
163
|
+
|
|
164
|
+
pane_target="$(resolve_pane_target "$pane_name" || true)"
|
|
165
|
+
[[ -n "$pane_target" ]] || return 0
|
|
166
|
+
"$PSMUX_BIN" pipe-pane -t "$pane_target" >/dev/null 2>&1 || true
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
refresh_snapshot_for_pane() {
|
|
170
|
+
local pane_name="$1"
|
|
171
|
+
local pane_target log_file
|
|
172
|
+
|
|
173
|
+
pane_target="$(require_pane_target "$pane_name")"
|
|
174
|
+
log_file="$(log_file_for "$pane_name")"
|
|
175
|
+
mkdir -p "$CAPTURE_DIR"
|
|
176
|
+
|
|
177
|
+
# Detached Windows sessions may not flush pipe-pane reliably yet.
|
|
178
|
+
# Overwriting the log with a fresh capture-pane snapshot keeps
|
|
179
|
+
# completion detection deterministic for the prototype.
|
|
180
|
+
"$PSMUX_BIN" capture-pane -t "$pane_target" -p >"$log_file"
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
send_keys_to_pane() {
|
|
184
|
+
local pane_name="$1"
|
|
185
|
+
local text="$2"
|
|
186
|
+
local submit="${3:-1}"
|
|
187
|
+
local pane_target
|
|
188
|
+
|
|
189
|
+
pane_target="$(require_pane_target "$pane_name")"
|
|
190
|
+
"$PSMUX_BIN" send-keys -t "$pane_target" -l "$text"
|
|
191
|
+
if [[ "$submit" != "0" ]]; then
|
|
192
|
+
"$PSMUX_BIN" send-keys -t "$pane_target" C-m
|
|
193
|
+
fi
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
dispatch_powershell_command() {
|
|
197
|
+
local pane_name="$1"
|
|
198
|
+
local command_text="$2"
|
|
199
|
+
local token wrapped
|
|
200
|
+
|
|
201
|
+
token="${pane_name}-$(date +%s)-$RANDOM"
|
|
202
|
+
wrapped="${command_text}; \$trifluxExit = if (\$null -ne \$LASTEXITCODE) { [int]\$LASTEXITCODE } else { 0 }; Write-Output \"${COMPLETION_PREFIX}${token}:\$trifluxExit\""
|
|
203
|
+
|
|
204
|
+
send_keys_to_pane "$pane_name" "$wrapped" 1
|
|
205
|
+
printf '%s\n' "$token"
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
wait_for_pattern() {
|
|
209
|
+
local pane_name="$1"
|
|
210
|
+
local pattern="$2"
|
|
211
|
+
local timeout_sec="${3:-300}"
|
|
212
|
+
local log_file deadline
|
|
213
|
+
|
|
214
|
+
log_file="$(log_file_for "$pane_name")"
|
|
215
|
+
[[ -f "$log_file" ]] || die "Log file for pane '$pane_name' does not exist. Start capture first."
|
|
216
|
+
|
|
217
|
+
deadline=$((SECONDS + timeout_sec))
|
|
218
|
+
while (( SECONDS <= deadline )); do
|
|
219
|
+
refresh_snapshot_for_pane "$pane_name"
|
|
220
|
+
if grep -Eq -- "$pattern" "$log_file"; then
|
|
221
|
+
return 0
|
|
222
|
+
fi
|
|
223
|
+
sleep "$POLL_INTERVAL_SEC"
|
|
224
|
+
done
|
|
225
|
+
|
|
226
|
+
return 1
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
wait_for_completion_token() {
|
|
230
|
+
local pane_name="$1"
|
|
231
|
+
local token="$2"
|
|
232
|
+
local timeout_sec="${3:-300}"
|
|
233
|
+
local pattern
|
|
234
|
+
|
|
235
|
+
pattern="${COMPLETION_PREFIX}${token}:[0-9]+"
|
|
236
|
+
wait_for_pattern "$pane_name" "$pattern" "$timeout_sec"
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
print_log_locations() {
|
|
240
|
+
mkdir -p "$CAPTURE_DIR"
|
|
241
|
+
printf '%s\t%s\n' "$PANE_LEAD" "$(log_file_for "$PANE_LEAD")"
|
|
242
|
+
printf '%s\t%s\n' "$PANE_CODEX" "$(log_file_for "$PANE_CODEX")"
|
|
243
|
+
printf '%s\t%s\n' "$PANE_GEMINI" "$(log_file_for "$PANE_GEMINI")"
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
create_session_layout() {
|
|
247
|
+
local lead_index codex_index gemini_index
|
|
248
|
+
|
|
249
|
+
require_psmux
|
|
250
|
+
|
|
251
|
+
if session_exists; then
|
|
252
|
+
die "Session '$SESSION_NAME' already exists. Run cleanup first or set PSMUX_SESSION_NAME."
|
|
253
|
+
fi
|
|
254
|
+
|
|
255
|
+
mkdir -p "$CAPTURE_DIR"
|
|
256
|
+
|
|
257
|
+
lead_index="$("$PSMUX_BIN" new-session -d -P -F '#{pane_index}' -s "$SESSION_NAME" -n "$WINDOW_NAME" -- $SHELL_COMMAND)"
|
|
258
|
+
codex_index="$("$PSMUX_BIN" split-window -h -P -F '#{pane_index}' -t "$(session_target)" -- $SHELL_COMMAND)"
|
|
259
|
+
gemini_index="$("$PSMUX_BIN" split-window -v -P -F '#{pane_index}' -t "$(pane_target_from_index "$codex_index")" -- $SHELL_COMMAND)"
|
|
260
|
+
|
|
261
|
+
set_pane_title "$(pane_target_from_index "$lead_index")" "$PANE_LEAD"
|
|
262
|
+
set_pane_title "$(pane_target_from_index "$codex_index")" "$PANE_CODEX"
|
|
263
|
+
set_pane_title "$(pane_target_from_index "$gemini_index")" "$PANE_GEMINI"
|
|
264
|
+
|
|
265
|
+
"$PSMUX_BIN" select-layout -t "$(session_target)" tiled >/dev/null
|
|
266
|
+
"$PSMUX_BIN" select-pane -t "$(pane_target_from_index "$lead_index")" >/dev/null
|
|
267
|
+
|
|
268
|
+
start_capture_for_all_panes
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
show_start_summary() {
|
|
272
|
+
log "Session created: $SESSION_NAME"
|
|
273
|
+
log "Window: $WINDOW_NAME"
|
|
274
|
+
log "Attach with: $PSMUX_BIN attach -t $SESSION_NAME"
|
|
275
|
+
print_log_locations
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
run_demo() {
|
|
279
|
+
local lead_token codex_token gemini_token
|
|
280
|
+
|
|
281
|
+
create_session_layout
|
|
282
|
+
|
|
283
|
+
lead_token="$(dispatch_powershell_command "$PANE_LEAD" 'Write-Host "lead pane ready"')"
|
|
284
|
+
codex_token="$(dispatch_powershell_command "$PANE_CODEX" 'Write-Host "codex-worker pane ready"')"
|
|
285
|
+
gemini_token="$(dispatch_powershell_command "$PANE_GEMINI" 'Write-Host "gemini-worker pane ready"')"
|
|
286
|
+
|
|
287
|
+
wait_for_completion_token "$PANE_LEAD" "$lead_token" 30 || die "Lead pane demo command timed out."
|
|
288
|
+
wait_for_completion_token "$PANE_CODEX" "$codex_token" 30 || die "Codex pane demo command timed out."
|
|
289
|
+
wait_for_completion_token "$PANE_GEMINI" "$gemini_token" 30 || die "Gemini pane demo command timed out."
|
|
290
|
+
|
|
291
|
+
show_start_summary
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
cleanup() {
|
|
295
|
+
stop_capture_for_pane "$PANE_LEAD"
|
|
296
|
+
stop_capture_for_pane "$PANE_CODEX"
|
|
297
|
+
stop_capture_for_pane "$PANE_GEMINI"
|
|
298
|
+
|
|
299
|
+
if session_exists; then
|
|
300
|
+
"$PSMUX_BIN" kill-session -t "$SESSION_NAME" >/dev/null 2>&1 || true
|
|
301
|
+
fi
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
main() {
|
|
305
|
+
local action="${1:-demo}"
|
|
306
|
+
|
|
307
|
+
case "$action" in
|
|
308
|
+
start)
|
|
309
|
+
create_session_layout
|
|
310
|
+
show_start_summary
|
|
311
|
+
;;
|
|
312
|
+
demo)
|
|
313
|
+
run_demo
|
|
314
|
+
;;
|
|
315
|
+
attach)
|
|
316
|
+
require_psmux
|
|
317
|
+
"$PSMUX_BIN" attach -t "$SESSION_NAME"
|
|
318
|
+
;;
|
|
319
|
+
send)
|
|
320
|
+
[[ $# -ge 3 ]] || die "Usage: $0 send <pane-name> <command text>"
|
|
321
|
+
shift
|
|
322
|
+
local pane_name="$1"
|
|
323
|
+
shift
|
|
324
|
+
send_keys_to_pane "$pane_name" "$*" 1
|
|
325
|
+
;;
|
|
326
|
+
send-no-enter)
|
|
327
|
+
[[ $# -ge 3 ]] || die "Usage: $0 send-no-enter <pane-name> <text>"
|
|
328
|
+
shift
|
|
329
|
+
local pane_name="$1"
|
|
330
|
+
shift
|
|
331
|
+
send_keys_to_pane "$pane_name" "$*" 0
|
|
332
|
+
;;
|
|
333
|
+
steer-ps)
|
|
334
|
+
[[ $# -ge 3 ]] || die "Usage: $0 steer-ps <pane-name> <powershell command>"
|
|
335
|
+
shift
|
|
336
|
+
local pane_name="$1"
|
|
337
|
+
shift
|
|
338
|
+
dispatch_powershell_command "$pane_name" "$*"
|
|
339
|
+
;;
|
|
340
|
+
wait)
|
|
341
|
+
[[ $# -ge 3 ]] || die "Usage: $0 wait <pane-name> <regex> [timeout-sec]"
|
|
342
|
+
shift
|
|
343
|
+
local pane_name="$1"
|
|
344
|
+
local pattern="$2"
|
|
345
|
+
local timeout_sec="${3:-300}"
|
|
346
|
+
if wait_for_pattern "$pane_name" "$pattern" "$timeout_sec"; then
|
|
347
|
+
log "Matched pattern for pane '$pane_name': $pattern"
|
|
348
|
+
else
|
|
349
|
+
die "Timed out waiting for pane '$pane_name' pattern: $pattern"
|
|
350
|
+
fi
|
|
351
|
+
;;
|
|
352
|
+
logs)
|
|
353
|
+
print_log_locations
|
|
354
|
+
;;
|
|
355
|
+
cleanup)
|
|
356
|
+
cleanup
|
|
357
|
+
;;
|
|
358
|
+
-h|--help|help)
|
|
359
|
+
usage
|
|
360
|
+
;;
|
|
361
|
+
*)
|
|
362
|
+
usage
|
|
363
|
+
die "Unknown action: $action"
|
|
364
|
+
;;
|
|
365
|
+
esac
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
main "$@"
|