triflux 10.0.6 → 10.1.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 (61) hide show
  1. package/CLAUDE.md +22 -0
  2. package/bin/triflux.mjs +30 -20
  3. package/hooks/error-context.mjs +35 -7
  4. package/hooks/hook-orchestrator.mjs +14 -0
  5. package/hooks/hook-registry.json +256 -246
  6. package/hooks/safety-guard.mjs +70 -1
  7. package/hub/assign-callbacks.mjs +4 -4
  8. package/hub/bridge.mjs +1 -1
  9. package/hub/codex-adapter.mjs +1 -1
  10. package/hub/codex-compat.mjs +1 -0
  11. package/hub/fullcycle.mjs +1 -0
  12. package/hub/intent.mjs +4 -1
  13. package/hub/lib/memory-store.mjs +22 -0
  14. package/hub/promote-penalties.mjs +120 -0
  15. package/hub/public/dashboard.html +7 -7
  16. package/hub/quality/deslop.mjs +1 -1
  17. package/hub/reflexion.mjs +40 -56
  18. package/hub/research.mjs +1 -0
  19. package/hub/router.mjs +1 -1
  20. package/hub/server.mjs +23 -0
  21. package/hub/session-fingerprint.mjs +2 -2
  22. package/hub/store-adapter.mjs +51 -20
  23. package/hub/store.mjs +8 -8
  24. package/hub/team/cli/commands/start/start-headless.mjs +5 -3
  25. package/hub/team/cli/services/hub-client.mjs +1 -1
  26. package/hub/team/conductor.mjs +1 -1
  27. package/hub/team/dashboard-open.mjs +1 -1
  28. package/hub/team/headless.mjs +4 -4
  29. package/hub/team/orchestrator.mjs +2 -2
  30. package/hub/team/psmux.mjs +67 -5
  31. package/hub/team/remote-probe.mjs +1 -1
  32. package/hub/team/remote-watcher.mjs +1 -1
  33. package/hub/team/session.mjs +1 -1
  34. package/hub/team/swarm-hypervisor.mjs +3 -4
  35. package/hub/team/tui-remote-adapter.mjs +3 -3
  36. package/hub/team/tui-viewer.mjs +0 -1
  37. package/hub/team/tui.mjs +12 -15
  38. package/hub/team/worktree-lifecycle.mjs +1 -1
  39. package/hub/token-mode.mjs +4 -1
  40. package/hub/workers/delegator-mcp.mjs +1 -2
  41. package/package.json +1 -1
  42. package/scripts/__tests__/skill-template.test.mjs +0 -1
  43. package/scripts/claudemd-sync.mjs +1 -1
  44. package/scripts/cli-route.sh +3 -3
  45. package/scripts/cross-review-gate.mjs +1 -1
  46. package/scripts/headless-guard.mjs +44 -16
  47. package/scripts/lib/mcp-filter.mjs +2 -2
  48. package/scripts/lib/mcp-guard-engine.mjs +1 -1
  49. package/scripts/lib/psmux-info.mjs +2 -2
  50. package/scripts/lib/skill-template.mjs +4 -4
  51. package/scripts/mcp-check.mjs +1 -1
  52. package/scripts/mcp-gateway-config.mjs +1 -1
  53. package/scripts/mcp-gateway-ensure.mjs +1 -1
  54. package/scripts/mcp-gateway-start.mjs +1 -1
  55. package/scripts/notion-read.mjs +3 -3
  56. package/scripts/remote-spawn.mjs +1 -1
  57. package/scripts/session-spawn-helper.mjs +1 -3
  58. package/scripts/setup.mjs +1 -1
  59. package/scripts/test-lock.mjs +1 -1
  60. package/scripts/tfx-route-post.mjs +6 -6
  61. package/scripts/tfx-route.sh +6 -0
package/CLAUDE.md CHANGED
@@ -1,3 +1,4 @@
1
+ <!-- prompt-hygiene:ignore line_count_warning -->
1
2
  # triflux — Claude Code 운영 가이드
2
3
 
3
4
  <core-systems>
@@ -81,6 +82,12 @@ tfx-deep-review, tfx-deep-qa, tfx-deep-plan, tfx-deep-research, tfx-consensus, t
81
82
  - "auto" 단독 → tfx-auto. "알아서 해" → tfx-autopilot
82
83
  - "코드에서 찾아" → tfx-find. "알아봐" → tfx-research
83
84
  - 복합 의도: "구현하고 리뷰까지" → tfx-auto → cross-review hook
85
+
86
+ ### Q-Learning 동적 라우팅 (실험적)
87
+
88
+ - `TRIFLUX_DYNAMIC_ROUTING=true` 설정 시 Q-Learning 기반 동적 스킬 라우팅 활성화
89
+ - `routing-weights.json` + Q-table로 스킬 선택 최적화
90
+ - 기본 비활성
84
91
  </routing>
85
92
 
86
93
  <session-context>
@@ -161,6 +168,21 @@ codex를 SSH 너머로 직접 실행하지 않는다. config.toml 충돌 + TTY
161
168
  - `~` → `$HOME` 변환 필수, 원격 기본 셸 = PowerShell
162
169
  </remote>
163
170
 
171
+ <headless-retrieval>
172
+ ## Headless 결과 회수
173
+
174
+ background로 실행한 headless 결과는 **반드시 task-notification 완료 후** 읽는다.
175
+
176
+ | 패턴 | 올바름 | 이유 |
177
+ |------|--------|------|
178
+ | task-notification 후 output 파일 읽기 | YES | 프로세스 종료 = 워커 전부 완료 |
179
+ | task-notification 전 output 파일 tail | NO | 시작 메시지만 보이고 "실패"로 오진 |
180
+ | psmux capture-pane으로 중간 체크 | NO | 워커 진행 중이면 빈 화면일 수 있음 |
181
+
182
+ 완료 마커: `=== HEADLESS_COMPLETE succeeded=N failed=N total=N ===`
183
+ 워커 상세: `$TMPDIR/tfx-headless/{sessionName}-worker-N.txt`
184
+ </headless-retrieval>
185
+
164
186
  <cross-review>
165
187
  ## 교차 검증
166
188
 
package/bin/triflux.mjs CHANGED
@@ -27,7 +27,7 @@ import {
27
27
  import {
28
28
  SYNC_MAP, SKILL_ALIASES, REQUIRED_CODEX_PROFILES, LEGACY_CODEX_MODELS,
29
29
  syncAliasedSkillDir, hasProfileSection, replaceProfileSection,
30
- ensureCodexProfiles, getVersion, cleanupStaleSkills, DEPRECATED_SKILLS,
30
+ ensureCodexProfiles, getVersion, cleanupStaleSkills,
31
31
  extractManagedHookFilename, getManagedRegistryHooks, ensureHooksInSettings,
32
32
  ensureCodexHubServerConfig,
33
33
  } from "../scripts/setup.mjs";
@@ -65,14 +65,14 @@ const GREEN_BRIGHT = "\x1b[38;5;82m";
65
65
  const RED_BRIGHT = "\x1b[38;5;196m";
66
66
 
67
67
  // ── 브랜드 요소 ──
68
- const BRAND = `${AMBER}${BOLD}triflux${RESET}`;
68
+ const _BRAND = `${AMBER}${BOLD}triflux${RESET}`;
69
69
  const VER = `${DIM}v${PKG.version}${RESET}`;
70
70
  const LINE = `${GRAY}${"─".repeat(48)}${RESET}`;
71
- const DOT = `${GRAY}·${RESET}`;
71
+ const _DOT = `${GRAY}·${RESET}`;
72
72
  const STALE_TEAM_MAX_AGE_SEC = 3600;
73
73
  const ANSI_PATTERN = /\x1B\[[0-?]*[ -/]*[@-~]/g;
74
74
 
75
- const EXIT_SUCCESS = 0;
75
+ const _EXIT_SUCCESS = 0;
76
76
  const EXIT_ERROR = 1;
77
77
  const EXIT_ARG_ERROR = 2;
78
78
  const EXIT_CLI_MISSING = 3;
@@ -719,12 +719,11 @@ function previewMcpRegistrationActions(mcpUrl) {
719
719
  function previewClaudeRoutingAction() {
720
720
  const globalClaudePath = join(CLAUDE_DIR, "CLAUDE.md");
721
721
  const projectClaudePath = join(PKG_ROOT, "CLAUDE.md");
722
- const projectContent = existsSync(projectClaudePath)
723
- ? readFileSync(projectClaudePath, "utf8")
724
- : "";
725
- const projectSection = extractMarkdownSection(projectContent, TFX_SECTION_HEADING);
726
722
 
727
- if (!projectSection) {
723
+ let _routingTable;
724
+ try {
725
+ _routingTable = getLatestRoutingTable();
726
+ } catch {
728
727
  return {
729
728
  type: "claude-guidance",
730
729
  path: globalClaudePath,
@@ -734,18 +733,23 @@ function previewClaudeRoutingAction() {
734
733
  };
735
734
  }
736
735
 
737
- const globalContent = existsSync(globalClaudePath)
738
- ? readFileSync(globalClaudePath, "utf8")
739
- : "";
740
- const preview = ensureTfxSection(globalContent, { scope: "global", projectSection });
736
+ if (!existsSync(globalClaudePath)) {
737
+ return {
738
+ type: "claude-guidance",
739
+ path: globalClaudePath,
740
+ source: projectClaudePath,
741
+ change: "create",
742
+ };
743
+ }
744
+
745
+ const globalContent = readFileSync(globalClaudePath, "utf8");
746
+ const hasRouting = globalContent.includes("<routing>") || globalContent.includes("## triflux CLI 라우팅");
741
747
 
742
748
  return {
743
749
  type: "claude-guidance",
744
750
  path: globalClaudePath,
745
751
  source: projectClaudePath,
746
- change: preview.changed ? (existsSync(globalClaudePath) ? "update" : "create") : "noop",
747
- heading: TFX_SECTION_HEADING,
748
- summary: TFX_GLOBAL_SUMMARY_SECTION,
752
+ change: hasRouting ? "noop" : "create",
749
753
  };
750
754
  }
751
755
 
@@ -2073,7 +2077,7 @@ async function cmdDoctor(options = {}) {
2073
2077
  // 14. Stale Teams (Claude teams/ + tasks/ 자동 감지)
2074
2078
  section("Stale Teams");
2075
2079
  const teamsDir = join(CLAUDE_DIR, "teams");
2076
- const tasksDir = join(CLAUDE_DIR, "tasks");
2080
+ const _tasksDir = join(CLAUDE_DIR, "tasks");
2077
2081
  if (existsSync(teamsDir)) {
2078
2082
  try {
2079
2083
  const teamDirs = readdirSync(teamsDir).filter(d => {
@@ -2130,8 +2134,8 @@ async function cmdDoctor(options = {}) {
2130
2134
  // 프로세스 명령줄에서 세션 ID 매칭 (tmux 없는 in-process 팀 지원)
2131
2135
  if (!hasActiveMember && teamConfig.leadSessionId) {
2132
2136
  try {
2133
- const sessionToken = teamConfig.leadSessionId.toLowerCase();
2134
- const safeToken = teamConfig.leadSessionId.slice(0, 8).replace(/[^a-zA-Z0-9\-]/g, '');
2137
+ const _sessionToken = teamConfig.leadSessionId.toLowerCase();
2138
+ const safeToken = teamConfig.leadSessionId.slice(0, 8).replace(/[^a-zA-Z0-9-]/g, '');
2135
2139
  // Claude Code 프로세스에서 세션 ID 검색
2136
2140
  if (process.platform === "win32") {
2137
2141
  const psOut = execSync(
@@ -3723,7 +3727,7 @@ async function cmdHub(args = [], options = {}) {
3723
3727
  process.kill(info.pid, "SIGTERM");
3724
3728
  try { unlinkSync(HUB_PID_FILE); } catch {}
3725
3729
  console.log(`\n ${GREEN_BRIGHT}✓${RESET} hub 종료됨 (PID ${info.pid})\n`);
3726
- } catch (e) {
3730
+ } catch (_e) {
3727
3731
  try { unlinkSync(HUB_PID_FILE); } catch {}
3728
3732
  console.log(`\n ${DIM}hub 프로세스 없음 — PID 파일 정리됨${RESET}\n`);
3729
3733
  }
@@ -3884,6 +3888,12 @@ async function main() {
3884
3888
  case "hub":
3885
3889
  await cmdHub(cmdArgs, { json: JSON_OUTPUT && (cmdArgs[0] || "status") === "status" });
3886
3890
  return;
3891
+ case "monitor": {
3892
+ const { createMonitor } = await import("../tui/monitor.mjs");
3893
+ const mon = createMonitor();
3894
+ await mon.start();
3895
+ break;
3896
+ }
3887
3897
  case "tray": {
3888
3898
  const trayUrl = new URL("../hub/tray.mjs", import.meta.url);
3889
3899
  const trayPath = fileURLToPath(trayUrl);
@@ -4,7 +4,8 @@
4
4
  // 도구 실패 시 에러 패턴을 분석하여 해결 힌트를 additionalContext로 주입한다.
5
5
  // Claude가 동일 에러를 반복하지 않도록 구체적 가이드를 제공.
6
6
 
7
- import { readFileSync } from "node:fs";
7
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
8
+ import { join } from "node:path";
8
9
 
9
10
  // ── 에러 패턴 → 해결 힌트 매핑 ─────────────────────────────
10
11
  const ERROR_HINTS = [
@@ -128,16 +129,43 @@ function main() {
128
129
  JSON.stringify(input.tool_result || ""),
129
130
  ].join("\n");
130
131
 
132
+ // ── reflexion 적응형 학습: safety-guard/headless-guard 차단을 패널티로 기록 ──
133
+ const isSafetyBlock = /\[(?:safety-guard|headless-guard)\].*(?:BLOCKED|차단)/i.test(errorText);
134
+ if (isSafetyBlock) {
135
+ try {
136
+ const home = process.env.HOME || process.env.USERPROFILE || "";
137
+ const penaltyDir = join(home, ".triflux", "reflexion");
138
+ mkdirSync(penaltyDir, { recursive: true });
139
+ const penaltyFile = join(penaltyDir, "pending-penalties.jsonl");
140
+ const command = input.tool_input?.command || "";
141
+ const entry = {
142
+ ts: new Date().toISOString(),
143
+ type: "guard_block",
144
+ tool: input.tool_name || "Bash",
145
+ error_pattern: errorText.match(/\[.*?\]\s*(.{0,120})/)?.[1] || errorText.slice(0, 120),
146
+ command_preview: command.slice(0, 200),
147
+ source: errorText.includes("safety-guard") ? "safety-guard" : "headless-guard",
148
+ };
149
+ writeFileSync(penaltyFile, JSON.stringify(entry) + "\n", { flag: "a" });
150
+ } catch { /* reflexion 기록 실패는 무시 — 힌트 출력에 영향 주지 않음 */ }
151
+ }
152
+
131
153
  const hints = findHints(errorText);
132
- if (hints.length === 0) process.exit(0);
154
+ // safety-guard 차단에는 guard가 이미 구체적 안내를 제공하므로 추가 힌트 불필요
155
+ if (hints.length === 0 && !isSafetyBlock) process.exit(0);
133
156
 
134
157
  const toolName = input.tool_name || "Unknown";
135
- const output = {
136
- systemMessage:
137
- `[error-context] ${toolName} 실패 — 해결 힌트:\n` +
138
- hints.map((h) => ` → ${h}`).join("\n"),
139
- };
158
+ const parts = [];
159
+ if (hints.length > 0) {
160
+ parts.push(`[error-context] ${toolName} 실패 — 해결 힌트:\n` + hints.map((h) => ` → ${h}`).join("\n"));
161
+ }
162
+ if (isSafetyBlock) {
163
+ parts.push("[reflexion] 이 패턴이 적응형 학습에 기록되었습니다. 다음 세션에서 동일 패턴 시 사전 차단됩니다.");
164
+ }
165
+
166
+ if (parts.length === 0) process.exit(0);
140
167
 
168
+ const output = { systemMessage: parts.join("\n") };
141
169
  process.stdout.write(JSON.stringify(output));
142
170
  }
143
171
 
@@ -340,6 +340,20 @@ async function main() {
340
340
  }
341
341
  }
342
342
 
343
+ // ── PostToolUse:Skill 완료 시 라우팅 가중치 기록 ──
344
+ if (eventName === "PostToolUse" && toolName === "Skill" && !blocked) {
345
+ try {
346
+ const input = JSON.parse(stdinRaw);
347
+ const skillName = input.tool_input?.skill || "";
348
+ if (skillName && skillName.startsWith("tfx-")) {
349
+ const mode = skillName.replace(/^tfx-/, "");
350
+ const gitRoot = process.env.GIT_WORK_TREE || process.cwd();
351
+ const slug = gitRoot.split(/[\\/]/).pop() || "unknown";
352
+ recordRouteOutcome(slug, mode, "completion");
353
+ }
354
+ } catch { /* 가중치 기록 실패 무시 */ }
355
+ }
356
+
343
357
  // 결과 출력
344
358
  if (blocked) {
345
359
  process.exit(2);