triflux 10.30.1 → 10.31.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.
package/hub/server.mjs CHANGED
@@ -2054,6 +2054,16 @@ export async function startHub({
2054
2054
  }, 60000);
2055
2055
  sessionTimer.unref();
2056
2056
 
2057
+ const synapsePruneTimer = setInterval(() => {
2058
+ try {
2059
+ const { count } = synapseRegistry.pruneExpired();
2060
+ if (count > 0) hubLog.info({ count }, "synapse.prune");
2061
+ } catch (err) {
2062
+ hubLog.warn({ err }, "synapse.prune_failed");
2063
+ }
2064
+ }, 60000);
2065
+ synapsePruneTimer.unref();
2066
+
2057
2067
  // 고아 node.exe 프로세스 + stale spawn 세션 주기적 정리 (5분마다)
2058
2068
  // TFX_DISABLE_ORPHAN_CLEANUP=1 로 비활성 (active SSH/swarm 세션 보호 회피용 hotfix gate)
2059
2069
  const orphanCleanupTimer = setInterval(
@@ -2288,6 +2298,7 @@ export async function startHub({
2288
2298
  router.stopSweeper();
2289
2299
  clearInterval(hitlTimer);
2290
2300
  clearInterval(sessionTimer);
2301
+ clearInterval(synapsePruneTimer);
2291
2302
  clearInterval(rateLimitTimer);
2292
2303
  clearInterval(orphanCleanupTimer);
2293
2304
  if (idleTimer) {
@@ -5,6 +5,7 @@ const DEFAULT_LOCAL_HEARTBEAT_INTERVAL_MS = 5_000;
5
5
  const DEFAULT_LOCAL_TIMEOUT_MS = 30_000;
6
6
  const DEFAULT_REMOTE_HEARTBEAT_INTERVAL_MS = 15_000;
7
7
  const DEFAULT_REMOTE_TIMEOUT_MS = 90_000;
8
+ const DEFAULT_EXPIRE_TIMEOUT_MS = 24 * 60 * 60 * 1000;
8
9
  // Interactive (Claude/Codex) sessions idle for long stretches; a 30s local
9
10
  // timeout produces stale false-positives. 5-minute TTL + an `idle` state
10
11
  // distinguishes "alive but inactive" from "presumed dead".
@@ -133,6 +134,7 @@ export function createSynapseRegistry(opts = {}) {
133
134
  remoteTimeoutMs = DEFAULT_REMOTE_TIMEOUT_MS,
134
135
  interactiveHeartbeatIntervalMs = DEFAULT_INTERACTIVE_HEARTBEAT_INTERVAL_MS,
135
136
  interactiveTimeoutMs = DEFAULT_INTERACTIVE_TIMEOUT_MS,
137
+ expireTimeoutMs = DEFAULT_EXPIRE_TIMEOUT_MS,
136
138
  } = opts;
137
139
 
138
140
  const sessions = new Map();
@@ -355,10 +357,89 @@ export function createSynapseRegistry(opts = {}) {
355
357
  return true;
356
358
  }
357
359
 
360
+ // stale/expired 세션이 expireTimeoutMs 넘게 누적되면 Map에서 제거.
361
+ // live(active/idle)는 lastHeartbeat가 오래돼도 보존 — git-preflight dirty-file 가드가 의존.
362
+ function pruneExpired(opts2 = {}) {
363
+ if (destroyed) return { removed: [], count: 0 };
364
+
365
+ const cutoff =
366
+ typeof opts2.olderThanMs === "number"
367
+ ? opts2.olderThanMs
368
+ : expireTimeoutMs;
369
+ const removed = [];
370
+ const currentTime = now();
371
+
372
+ for (const [sessionId, session] of sessions) {
373
+ if (
374
+ isLiveStatus(session.status) ||
375
+ currentTime - session.lastHeartbeat <= cutoff
376
+ ) {
377
+ continue;
378
+ }
379
+
380
+ stopMonitor(sessionId);
381
+ sessions.delete(sessionId);
382
+ removed.push(sessionId);
383
+ notifyRemoved(session);
384
+ }
385
+
386
+ if (removed.length > 0) persist();
387
+ return { removed, count: removed.length };
388
+ }
389
+
358
390
  function heartbeat(sessionId, partialMeta = null) {
359
391
  const normalized = normalizeSessionId(sessionId);
360
392
  const session = sessions.get(normalized);
361
- if (!session) return false;
393
+ if (!session) {
394
+ // self-heal은 위치 정보(worktreePath 또는 cwd)가 있는 heartbeat에서만
395
+ // 발동한다. UserPromptSubmit heartbeat는 항상 worktreePath를 보내므로
396
+ // SessionStart register POST drop 복구 경로는 커버되고, taskSummary만 담은
397
+ // 빈약한 heartbeat로 위치 없는 가비지 세션이 register 되는 것은 막는다
398
+ // (그 경우는 기존대로 false → 400/heartbeat failed).
399
+ const hasLocator =
400
+ partialMeta &&
401
+ typeof partialMeta === "object" &&
402
+ !Array.isArray(partialMeta) &&
403
+ ((typeof partialMeta.worktreePath === "string" &&
404
+ partialMeta.worktreePath.length > 0) ||
405
+ (typeof partialMeta.cwd === "string" && partialMeta.cwd.length > 0));
406
+ const canSelfHeal = Boolean(normalized) && hasLocator;
407
+ if (!canSelfHeal) return false;
408
+
409
+ // 미등록 세션 heartbeat = SessionStart register POST가 drop된 경우.
410
+ // self-heal로 register 복구(영구 미등록 방지).
411
+ //
412
+ // sessionKind는 명시값이 없으면 "interactive"로 본다. 실제 복구 경로인
413
+ // heartbeatInteractiveSession은 worktreePath/host/taskSummary만 보내고
414
+ // sessionKind는 생략하는데, sanitizeSession 기본값("headless")으로 떨어지면
415
+ // 복구된 Claude 세션이 30초 local timeout을 받아 idle/5분 TTL을 잃고 곧장
416
+ // stale로 전이돼 getActive()/peer discovery에서 사라진다(이 PRD가 고치려던
417
+ // 원래 증상의 재현). heartbeat 기반 liveness로 살아나는 세션은 interactive다.
418
+ const sanitizedKind =
419
+ typeof partialMeta.sessionKind === "string"
420
+ ? partialMeta.sessionKind
421
+ : "interactive";
422
+ const healed = sanitizeSession(
423
+ {
424
+ ...partialMeta,
425
+ sessionId: normalized,
426
+ sessionKind: sanitizedKind,
427
+ status: "active",
428
+ lastHeartbeat: now(),
429
+ },
430
+ normalized,
431
+ );
432
+ if (!healed) return false;
433
+
434
+ sessions.set(normalized, healed);
435
+ startMonitor(normalized);
436
+ persist();
437
+ emitter?.emit("synapse.session.started", {
438
+ sessionId: normalized,
439
+ session: cloneSession(healed),
440
+ });
441
+ return true;
442
+ }
362
443
 
363
444
  const wasRemote = session.isRemote;
364
445
  const updated = { ...session, lastHeartbeat: now(), status: "active" };
@@ -492,6 +573,7 @@ export function createSynapseRegistry(opts = {}) {
492
573
  return Object.freeze({
493
574
  register,
494
575
  unregister,
576
+ pruneExpired,
495
577
  heartbeat,
496
578
  getActive,
497
579
  getAll,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triflux",
3
- "version": "10.30.1",
3
+ "version": "10.31.0",
4
4
  "description": "CLI-first multi-model orchestrator for Claude Code — route tasks to Codex, Antigravity, and Claude",
5
5
  "type": "module",
6
6
  "bin": {
@@ -464,6 +464,85 @@ prepend_codex_north_star() {
464
464
  rm -f "$tmp_file" 2>/dev/null || true
465
465
  }
466
466
 
467
+ # prepend_skill: opt-in 으로 등록된 스킬의 SKILL.md 본문을 프롬프트 앞에 주입한다.
468
+ # 활성 조건은 TFX_INJECT_SKILL env (tfx-auto 가 --skill <name> 으로 set). 미설정이면
469
+ # 현행 동작 그대로 (no-op). CLI-agnostic — codex/agy 레인에서 동일하게 재사용한다.
470
+ # 전달은 prepend_codex_north_star 와 동일하게 printf/cat → temp file 만 사용하므로
471
+ # $ / 백슬래시(₩) 같은 특수문자를 셸 재확장 없이 리터럴로 보존한다 (parse-safe).
472
+ # 스킬 경로: ${TFX_SKILLS_DIR:-<repo>/skills}/<name>/SKILL.md.
473
+ prepend_skill() {
474
+ local prompt="${1:-}"
475
+ local skill_name="${TFX_INJECT_SKILL:-}"
476
+ if [[ -z "$skill_name" ]]; then
477
+ printf '%s' "$prompt"
478
+ return 0
479
+ fi
480
+ # 경로 탈출 방지: skill_name 은 skills/ 하위 단일 디렉토리 이름만 허용한다.
481
+ if [[ "$skill_name" == *"/"* || "$skill_name" == *".."* ]]; then
482
+ echo "[tfx-route] WARNING: TFX_INJECT_SKILL='${skill_name}' 에 경로 구분자/.. 포함 — 주입 거부" >&2
483
+ printf '%s' "$prompt"
484
+ return 0
485
+ fi
486
+
487
+ local skills_dir="${TFX_SKILLS_DIR:-}"
488
+ if [[ -z "$skills_dir" ]]; then
489
+ skills_dir="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/.." 2>/dev/null && pwd)/skills"
490
+ fi
491
+ local skill_file="${skills_dir}/${skill_name}/SKILL.md"
492
+ if [[ ! -r "$skill_file" ]]; then
493
+ echo "[tfx-route] WARNING: TFX_INJECT_SKILL='${skill_name}' 스킬을 찾지 못함 ($skill_file) — 주입 생략" >&2
494
+ printf '%s' "$prompt"
495
+ return 0
496
+ fi
497
+
498
+ local tmp_file
499
+ tmp_file="$(mktemp "${TFX_TMP:-${TMPDIR:-/tmp}}/tfx-skill-prompt.XXXXXX" 2>/dev/null)" || {
500
+ printf '%s' "$prompt"
501
+ return 0
502
+ }
503
+
504
+ if ! {
505
+ printf '%s\n' "--- SKILL: ${skill_name} (apply this methodology to the task below) ---"
506
+ cat "$skill_file"
507
+ if [[ -s "$skill_file" ]]; then
508
+ local last_byte
509
+ last_byte="$(tail -c 1 "$skill_file" 2>/dev/null || true)"
510
+ [[ -n "$last_byte" ]] && printf '\n'
511
+ fi
512
+ printf '%s\n' "--- END SKILL ---"
513
+ printf '%s' "$prompt"
514
+ } > "$tmp_file"; then
515
+ rm -f "$tmp_file" 2>/dev/null || true
516
+ printf '%s' "$prompt"
517
+ return 0
518
+ fi
519
+
520
+ cat "$tmp_file" 2>/dev/null || printf '%s' "$prompt"
521
+ rm -f "$tmp_file" 2>/dev/null || true
522
+ }
523
+
524
+ # append_agy_anti_overclaim: agy(Gemini 3.x) 레인 전용 완료/grounding 규율 블록을
525
+ # 프롬프트 *뒤*에 붙인다. Gemini 3.x 는 과신(overconfidence) outlier 라 거짓 완료
526
+ # 주장·환각이 잦다 (AA-Omniscience 실증). 부정 제약은 Gemini 3 공식 가이드 권고대로
527
+ # 프롬프트 END 에 배치하고, blanket "do not guess" 대신 "주어진 context 로 추론 +
528
+ # 정확한 abstention 우선" 형태로 적는다 (open-ended negative 는 역효과). TFX_AGY_ANTI_OVERCLAIM=0 으로 opt-out.
529
+ append_agy_anti_overclaim() {
530
+ local prompt="${1:-}"
531
+ case "${TFX_AGY_ANTI_OVERCLAIM:-1}" in
532
+ 0|false|FALSE|off|OFF|no|NO)
533
+ printf '%s' "$prompt"
534
+ return 0
535
+ ;;
536
+ esac
537
+
538
+ printf '%s' "$prompt"
539
+ printf '\n\n%s\n' "--- COMPLETION & GROUNDING DISCIPLINE (apply strictly) ---"
540
+ printf '%s\n' "- Claim done/fixed/passing ONLY after running the check in this turn and seeing its output. With no fresh evidence, report what is still unverified instead of asserting success."
541
+ printf '%s\n' "- Ground every answer in the provided context and the actual repository. If something is not verifiable from what you can see, say 'No Info / 확인 불가' and stop rather than inventing plausible-but-unconfirmed details."
542
+ printf '%s\n' "- Use the provided context for deductions and prefer accurate abstention over confident guessing."
543
+ printf '%s' "--- END DISCIPLINE ---"
544
+ }
545
+
467
546
  read_probe_state() {
468
547
  local pid="$1"
469
548
  local state_file="${TFX_PROBE_STATE_FILE:-${TFX_PROBE_DIR}/${pid}.json}"
@@ -2678,6 +2757,14 @@ FALLBACK_EOF
2678
2757
  _codex_full_prompt_with_sentinel="$(prepend_codex_north_star "$FULL_PROMPT"; printf '%s' "$_codex_prompt_sentinel")"
2679
2758
  FULL_PROMPT="${_codex_full_prompt_with_sentinel%"$_codex_prompt_sentinel"}"
2680
2759
  fi
2760
+ # Opt-in 스킬 주입 (TFX_INJECT_SKILL). north-star 와 동일한 sentinel 패턴으로
2761
+ # trailing newline 을 보존한다. 미설정이면 prepend_skill 이 no-op.
2762
+ if [[ -n "${TFX_INJECT_SKILL:-}" ]]; then
2763
+ local _codex_skill_sentinel="__TFX_CODEX_SKILL_END_${$}_${RANDOM}__"
2764
+ local _codex_skill_prompt
2765
+ _codex_skill_prompt="$(prepend_skill "$FULL_PROMPT"; printf '%s' "$_codex_skill_sentinel")"
2766
+ FULL_PROMPT="${_codex_skill_prompt%"$_codex_skill_sentinel"}"
2767
+ fi
2681
2768
  codex_transport_effective="exec"
2682
2769
  if [[ "$TFX_CODEX_TRANSPORT" != "exec" ]]; then
2683
2770
  run_codex_mcp "$FULL_PROMPT" "$use_tee" || exit_code=$?
@@ -2782,6 +2869,18 @@ EOF
2782
2869
  _agy_full_prompt_with_sentinel="$(prepend_codex_north_star "$FULL_PROMPT"; printf '%s' "$_agy_prompt_sentinel")"
2783
2870
  FULL_PROMPT="${_agy_full_prompt_with_sentinel%"$_agy_prompt_sentinel"}"
2784
2871
  fi
2872
+ # Opt-in 스킬 주입 (TFX_INJECT_SKILL) — codex 레인과 동일.
2873
+ if [[ -n "${TFX_INJECT_SKILL:-}" ]]; then
2874
+ local _agy_skill_sentinel="__TFX_AGY_SKILL_END_${$}_${RANDOM}__"
2875
+ local _agy_skill_prompt
2876
+ _agy_skill_prompt="$(prepend_skill "$FULL_PROMPT"; printf '%s' "$_agy_skill_sentinel")"
2877
+ FULL_PROMPT="${_agy_skill_prompt%"$_agy_skill_sentinel"}"
2878
+ fi
2879
+ # agy(Gemini 3.x) anti-overclaim 규율 블록을 프롬프트 END 에 append. opt-out: TFX_AGY_ANTI_OVERCLAIM=0.
2880
+ local _agy_aoc_sentinel="__TFX_AGY_AOC_END_${$}_${RANDOM}__"
2881
+ local _agy_aoc_prompt
2882
+ _agy_aoc_prompt="$(append_agy_anti_overclaim "$FULL_PROMPT"; printf '%s' "$_agy_aoc_sentinel")"
2883
+ FULL_PROMPT="${_agy_aoc_prompt%"$_agy_aoc_sentinel"}"
2785
2884
  run_antigravity_exec "$FULL_PROMPT" "$use_tee" || exit_code=$?
2786
2885
  if [[ "$exit_code" -ne 0 && "$exit_code" -ne 124 ]]; then
2787
2886
  local agy_stderr_bytes=0
@@ -28,7 +28,7 @@ triggers:
28
28
  - spec-panel
29
29
  - business-panel
30
30
  - index-repo
31
- argument-hint: "<command|task> [args...] [--cli auto|codex|antigravity|claude] [--mode quick|deep|consensus] [--risk-tier auto|low|medium|high] [--shape consensus|debate|panel] [--cli-set triad|no-antigravity|custom] [--parallel 1|N|swarm] [--retry 0|1|ralph] [--isolation none|worktree] [--remote <host>|none]"
31
+ argument-hint: "<command|task> [args...] [--cli auto|codex|antigravity|claude] [--mode quick|deep|consensus] [--risk-tier auto|low|medium|high] [--shape consensus|debate|panel] [--cli-set triad|no-antigravity|custom] [--parallel 1|N|swarm] [--retry 0|1|ralph] [--isolation none|worktree] [--remote <host>|none] [--skill <name>]"
32
32
  ---
33
33
 
34
34
  # tfx-auto — 통합 CLI 오케스트레이터
@@ -169,6 +169,7 @@ ARGUMENTS 에 아래 플래그가 있으면 Step 0 스마트 라우팅의 내부
169
169
  | `--no-claude-native` | false (기본) | Claude native sub-agent 경로 유지 | — |
170
170
  | `--no-claude-native` | true | Claude native 경로 disable, CLI 기반 worker 강제 | tfx-route.sh |
171
171
  | `--max-iterations` | `0` (기본, unlimited) | `--retry ralph`/`auto-escalate` 상한 | retry-state-machine.mjs |
172
+ | `--skill` | `<name>` | `skills/<name>/SKILL.md` 본문을 codex/agy 프롬프트 앞에 주입 (`TFX_INJECT_SKILL`). 미지정 시 no-op | tfx-route.sh |
172
173
 
173
174
  ### `--risk-tier` 계약
174
175
 
@@ -204,6 +205,37 @@ ARGUMENTS 에 아래 플래그가 있으면 Step 0 스마트 라우팅의 내부
204
205
  - `--retry ralph` → true ralph state machine (Phase 3, retry-state-machine.mjs)
205
206
  - `--retry auto-escalate` → CLI 승격 체인 (Phase 3)
206
207
  - `--max-iterations N` (N>0) → ralph/auto-escalate 에 상한 부여
208
+ - `--skill <name>` → `TFX_INJECT_SKILL=<name>` 로 tfx-route.sh 에 전달. 스킬 파일 부재 시 warning 후 주입 생략 (fail-open, 작업은 계속)
209
+
210
+ ### 스킬 주입 (`--skill <name>`)
211
+
212
+ codex/agy 워커 프롬프트 앞에 등록된 스킬의 방법론을 주입하는 **opt-in** 프레임워크. 설계 근거는 `decision-skill-passing` (omc 레퍼런스 + codex/agy 네이티브 스킬 조사):
213
+
214
+ - **메커니즘**: `--skill <name>` → tfx-route.sh `TFX_INJECT_SKILL=<name>`. `skills/<name>/SKILL.md` 본문을 `--- SKILL: <name> (apply this methodology...) ---` delimited block 으로 프롬프트 앞에 prepend. codex·agy 레인 공통 (CLI-agnostic, `prepend_skill`).
215
+ - **기본 off**: 미지정이면 no-op → 현행 동작 그대로. 회귀 위험 없음.
216
+ - **파싱 안전**: prepend 는 `printf`/`cat` → temp file 만 사용. codex 는 argv `--` 뒤, agy 는 stdin `printf %s` 로 전달되므로 `$`(codex)·`₩`/백슬래시(agy) 같은 특수문자를 셸 재확장 없이 **리터럴 보존**. 스킬 본문을 그대로 넘겨도 깨지지 않음.
217
+ - **네이티브 스킬을 안 쓰는 이유**: codex `$skill-name`/`/name` 슬래시와 agy `/name` 은 둘 다 **인터랙티브 TUI 전용** → headless one-shot 에서 named-skill 강제 호출 불가 (미문서). 그래서 prose 주입을 채택 (레퍼런스 omc 도 role .md 를 raw prepend, 네이티브 회피).
218
+
219
+ #### 커맨드→스킬 매핑 (tfx-auto convention, opt-in)
220
+
221
+ tfx-auto 가 커맨드/agent 별로 주입할 스킬을 고를 때 쓰는 **권고 테이블**. 기본은 매핑 없음 (명시 `--skill` 만 작동) — 부적절한 스킬이 모든 프롬프트에 새는 것을 막는다. 프로젝트가 명시적으로 활성화할 때만 아래를 적용해 `--skill` 을 자동 부여한다.
222
+
223
+ | 커맨드/agent | 권고 스킬 | 비고 |
224
+ |--------------|-----------|------|
225
+ | (기본) | 없음 | explicit `--skill` 우선 |
226
+ | 프로젝트 정의 | 프로젝트가 지정 | 활성화 시 tfx-auto 가 해당 `--skill` set |
227
+
228
+ > 매핑된 스킬 파일이 없으면 `prepend_skill` 이 warning 후 주입을 생략한다 (fail-open). 존재하지 않는 매핑을 "주입됨"으로 가정하지 않는다.
229
+
230
+ #### agy anti-overclaim (자동, agy 레인 전용)
231
+
232
+ agy(Gemini 3.x) 레인은 `TFX_AGY_ANTI_OVERCLAIM`(기본 on) 으로 완료/grounding 규율 블록을 프롬프트 **END** 에 자동 append (`append_agy_anti_overclaim`). Gemini 3.x 과신(AA-Omniscience 실측: 정확도 53-56% 대비 환각률 88-91%) 대응:
233
+
234
+ - fresh 증거 없이 done/fixed/passing 주장 금지.
235
+ - 검증 불가 시 `No Info / 확인 불가` 후 중단 (날조 금지).
236
+ - 추론은 주어진 context 로, confident guessing 보다 accurate abstention 우선.
237
+
238
+ 부정 제약은 Gemini 3 공식 가이드대로 **END 배치**하고 blanket "do not guess" 는 역효과라 피한다. opt-out: `TFX_AGY_ANTI_OVERCLAIM=0`.
207
239
 
208
240
  ### Retry state machine 계약 (legacy autoroute / persist 이관)
209
241