triflux 10.0.0 → 10.0.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.md +171 -0
- package/README.md +32 -15
- package/bin/triflux.mjs +62 -5
- package/hooks/agent-route-guard.mjs +109 -0
- package/hooks/cross-review-tracker.mjs +122 -0
- package/hooks/error-context.mjs +148 -0
- package/hooks/hook-adaptive-collector.mjs +86 -0
- package/hooks/hook-manager.mjs +365 -0
- package/hooks/hook-orchestrator.mjs +312 -0
- package/hooks/hook-registry.json +246 -0
- package/hooks/hooks.json +89 -0
- package/hooks/keyword-rules.json +574 -0
- package/hooks/lib/resolve-root.mjs +59 -0
- package/hooks/mcp-config-watcher.mjs +80 -0
- package/hooks/pipeline-stop.mjs +76 -0
- package/hooks/safety-guard.mjs +169 -0
- package/hooks/subagent-verifier.mjs +80 -0
- package/hub/account-broker.mjs +251 -0
- package/hub/adaptive-diagnostic.mjs +323 -0
- package/hub/adaptive-inject.mjs +186 -0
- package/hub/adaptive-memory.mjs +163 -0
- package/hub/adaptive.mjs +143 -0
- package/hub/assign-callbacks.mjs +133 -0
- package/hub/bridge.mjs +799 -0
- package/hub/cli-adapter-base.mjs +280 -0
- package/hub/codex-adapter.mjs +199 -0
- package/hub/codex-compat.mjs +11 -0
- package/hub/codex-preflight.mjs +166 -0
- package/hub/delegator/contracts.mjs +37 -0
- package/hub/delegator/index.mjs +14 -0
- package/hub/delegator/schema/delegator-tools.schema.json +250 -0
- package/hub/delegator/service.mjs +307 -0
- package/hub/delegator/tool-definitions.mjs +35 -0
- package/hub/fullcycle.mjs +96 -0
- package/hub/gemini-adapter.mjs +180 -0
- package/hub/hitl.mjs +143 -0
- package/hub/intent.mjs +193 -0
- package/hub/lib/cache-guard.mjs +114 -0
- package/hub/lib/known-errors.json +72 -0
- package/hub/lib/memory-store.mjs +748 -0
- package/hub/lib/process-utils.mjs +361 -0
- package/hub/lib/ssh-command.mjs +211 -0
- package/hub/lib/ssh-retry.mjs +59 -0
- package/hub/lib/uuidv7.mjs +44 -0
- package/hub/memory-doctor.mjs +480 -0
- package/hub/middleware/request-logger.mjs +161 -0
- package/hub/paths.mjs +30 -0
- package/hub/pipe.mjs +664 -0
- package/hub/pipeline/gates/confidence.mjs +56 -0
- package/hub/pipeline/gates/consensus.mjs +94 -0
- package/hub/pipeline/gates/index.mjs +5 -0
- package/hub/pipeline/gates/selfcheck.mjs +82 -0
- package/hub/pipeline/index.mjs +318 -0
- package/hub/pipeline/state.mjs +191 -0
- package/hub/pipeline/transitions.mjs +124 -0
- package/hub/platform.mjs +225 -0
- package/hub/public/dashboard.html +355 -0
- package/hub/public/tray-icon.ico +0 -0
- package/hub/public/tray-icon.png +0 -0
- package/hub/quality/deslop.mjs +253 -0
- package/hub/reflexion.mjs +372 -0
- package/hub/research.mjs +146 -0
- package/hub/router.mjs +791 -0
- package/hub/routing/complexity.mjs +166 -0
- package/hub/routing/index.mjs +117 -0
- package/hub/routing/q-learning.mjs +336 -0
- package/hub/schema.sql +148 -0
- package/hub/server.mjs +1264 -0
- package/hub/session-fingerprint.mjs +352 -0
- package/hub/state.mjs +258 -0
- package/hub/store-adapter.mjs +118 -0
- package/hub/store.mjs +857 -0
- package/hub/team/agent-map.json +11 -0
- package/hub/team/ansi.mjs +379 -0
- package/hub/team/backend.mjs +90 -0
- package/hub/team/cli/commands/attach.mjs +37 -0
- package/hub/team/cli/commands/control.mjs +43 -0
- package/hub/team/cli/commands/debug.mjs +74 -0
- package/hub/team/cli/commands/focus.mjs +53 -0
- package/hub/team/cli/commands/interrupt.mjs +36 -0
- package/hub/team/cli/commands/kill.mjs +37 -0
- package/hub/team/cli/commands/list.mjs +24 -0
- package/hub/team/cli/commands/send.mjs +37 -0
- package/hub/team/cli/commands/start/index.mjs +106 -0
- package/hub/team/cli/commands/start/parse-args.mjs +130 -0
- package/hub/team/cli/commands/start/start-headless.mjs +109 -0
- package/hub/team/cli/commands/start/start-in-process.mjs +40 -0
- package/hub/team/cli/commands/start/start-mux.mjs +73 -0
- package/hub/team/cli/commands/start/start-wt.mjs +69 -0
- package/hub/team/cli/commands/status.mjs +87 -0
- package/hub/team/cli/commands/stop.mjs +31 -0
- package/hub/team/cli/commands/task.mjs +30 -0
- package/hub/team/cli/commands/tasks.mjs +13 -0
- package/hub/team/cli/help.mjs +42 -0
- package/hub/team/cli/index.mjs +41 -0
- package/hub/team/cli/manifest.mjs +29 -0
- package/hub/team/cli/render.mjs +30 -0
- package/hub/team/cli/services/attach-fallback.mjs +54 -0
- package/hub/team/cli/services/hub-client.mjs +227 -0
- package/hub/team/cli/services/member-selector.mjs +30 -0
- package/hub/team/cli/services/native-control.mjs +117 -0
- package/hub/team/cli/services/runtime-mode.mjs +62 -0
- package/hub/team/cli/services/state-store.mjs +48 -0
- package/hub/team/cli/services/task-model.mjs +30 -0
- package/hub/team/conductor-mesh-bridge.mjs +121 -0
- package/hub/team/conductor.mjs +671 -0
- package/hub/team/dashboard-anchor.mjs +14 -0
- package/hub/team/dashboard-layout.mjs +33 -0
- package/hub/team/dashboard-open.mjs +153 -0
- package/hub/team/dashboard.mjs +274 -0
- package/hub/team/event-log.mjs +76 -0
- package/hub/team/handoff.mjs +303 -0
- package/hub/team/headless.mjs +1156 -0
- package/hub/team/health-probe.mjs +272 -0
- package/hub/team/launcher-template.mjs +95 -0
- package/hub/team/lead-control.mjs +104 -0
- package/hub/team/native-supervisor.mjs +392 -0
- package/hub/team/native.mjs +649 -0
- package/hub/team/nativeProxy.mjs +688 -0
- package/hub/team/notify.mjs +293 -0
- package/hub/team/orchestrator.mjs +161 -0
- package/hub/team/pane.mjs +153 -0
- package/hub/team/process-cleanup.mjs +342 -0
- package/hub/team/psmux.mjs +1354 -0
- package/hub/team/remote-probe.mjs +276 -0
- package/hub/team/remote-session.mjs +299 -0
- package/hub/team/remote-watcher.mjs +478 -0
- package/hub/team/routing.mjs +223 -0
- package/hub/team/session-sync.mjs +169 -0
- package/hub/team/session.mjs +611 -0
- package/hub/team/shared.mjs +13 -0
- package/hub/team/staleState.mjs +361 -0
- package/hub/team/swarm-hypervisor.mjs +589 -0
- package/hub/team/swarm-locks.mjs +204 -0
- package/hub/team/swarm-planner.mjs +260 -0
- package/hub/team/swarm-reconciler.mjs +137 -0
- package/hub/team/tui-lite.mjs +380 -0
- package/hub/team/tui-remote-adapter.mjs +393 -0
- package/hub/team/tui-viewer.mjs +463 -0
- package/hub/team/tui.mjs +1449 -0
- package/hub/team/worktree-lifecycle.mjs +193 -0
- package/hub/team/wt-manager.mjs +407 -0
- package/hub/team/wt-templates.json +43 -0
- package/hub/team-bridge.mjs +27 -0
- package/hub/token-mode.mjs +224 -0
- package/hub/tools.mjs +636 -0
- package/hub/tray.mjs +376 -0
- package/hub/workers/claude-worker.mjs +475 -0
- package/hub/workers/codex-mcp.mjs +507 -0
- package/hub/workers/delegator-mcp.mjs +1076 -0
- package/hub/workers/factory.mjs +21 -0
- package/hub/workers/gemini-worker.mjs +374 -0
- package/hub/workers/interface.mjs +52 -0
- package/hub/workers/worker-utils.mjs +104 -0
- package/hud/colors.mjs +88 -0
- package/hud/constants.mjs +88 -0
- package/hud/context-monitor.mjs +403 -0
- package/hud/hud-qos-status.mjs +210 -0
- package/hud/providers/claude.mjs +314 -0
- package/hud/providers/codex.mjs +151 -0
- package/hud/providers/gemini.mjs +320 -0
- package/hud/renderers.mjs +442 -0
- package/hud/terminal.mjs +140 -0
- package/hud/utils.mjs +313 -0
- package/mesh/index.mjs +63 -0
- package/mesh/mesh-budget.mjs +128 -0
- package/mesh/mesh-heartbeat.mjs +100 -0
- package/mesh/mesh-protocol.mjs +96 -0
- package/mesh/mesh-queue.mjs +165 -0
- package/mesh/mesh-registry.mjs +78 -0
- package/mesh/mesh-router.mjs +76 -0
- package/package.json +8 -1
- package/references/hosts.json +33 -0
- package/scripts/__tests__/gen-skill-docs.test.mjs +87 -0
- package/scripts/__tests__/keyword-detector.test.mjs +234 -0
- package/scripts/__tests__/mcp-guard-engine.test.mjs +118 -0
- package/scripts/__tests__/remote-spawn-transfer.test.mjs +117 -0
- package/scripts/__tests__/remote-spawn.test.mjs +92 -0
- package/scripts/__tests__/skill-template.test.mjs +193 -0
- package/scripts/__tests__/smoke.test.mjs +34 -0
- package/scripts/cache-buildup.mjs +30 -0
- package/scripts/cache-doctor.mjs +149 -0
- package/scripts/cache-warmup.mjs +557 -0
- package/scripts/claudemd-sync.mjs +148 -0
- package/scripts/cli-route.sh +3 -0
- package/scripts/completions/tfx.bash +47 -0
- package/scripts/completions/tfx.fish +44 -0
- package/scripts/completions/tfx.zsh +83 -0
- package/scripts/cross-review-gate.mjs +126 -0
- package/scripts/cross-review-tracker.mjs +238 -0
- package/scripts/gen-skill-docs.mjs +111 -0
- package/scripts/headless-guard-fast.sh +21 -0
- package/scripts/headless-guard.mjs +360 -0
- package/scripts/hub-ensure.mjs +120 -0
- package/scripts/keyword-detector.mjs +272 -0
- package/scripts/keyword-rules-expander.mjs +521 -0
- package/scripts/lib/claudemd-scanner.mjs +218 -0
- package/scripts/lib/context.mjs +67 -0
- package/scripts/lib/cross-review-utils.mjs +51 -0
- package/scripts/lib/env-probe.mjs +241 -0
- package/scripts/lib/gemini-profiles.mjs +85 -0
- package/scripts/lib/handoff.mjs +171 -0
- package/scripts/lib/hook-utils.mjs +14 -0
- package/scripts/lib/keyword-rules.mjs +166 -0
- package/scripts/lib/logger.mjs +105 -0
- package/scripts/lib/mcp-filter.mjs +739 -0
- package/scripts/lib/mcp-guard-engine.mjs +954 -0
- package/scripts/lib/mcp-manifest.mjs +79 -0
- package/scripts/lib/mcp-server-catalog.mjs +118 -0
- package/scripts/lib/psmux-info.mjs +119 -0
- package/scripts/lib/remote-spawn-transfer.mjs +196 -0
- package/scripts/lib/skill-template.mjs +326 -0
- package/scripts/mcp-check.mjs +237 -0
- package/scripts/mcp-cleanup.ps1 +17 -0
- package/scripts/mcp-gateway-config.mjs +207 -0
- package/scripts/mcp-gateway-ensure.mjs +85 -0
- package/scripts/mcp-gateway-integration-test.mjs +228 -0
- package/scripts/mcp-gateway-start.mjs +226 -0
- package/scripts/mcp-gateway-start.ps1 +141 -0
- package/scripts/mcp-gateway-verify.mjs +77 -0
- package/scripts/mcp-safety-guard.mjs +44 -0
- package/scripts/notion-read.mjs +556 -0
- package/scripts/pack.mjs +295 -0
- package/scripts/preflight-cache.mjs +69 -0
- package/scripts/preinstall.mjs +96 -0
- package/scripts/remote-spawn.mjs +1376 -0
- package/scripts/run.cjs +79 -0
- package/scripts/session-spawn-helper.mjs +185 -0
- package/scripts/setup.mjs +1178 -0
- package/scripts/test-lock.mjs +71 -0
- package/scripts/test-tfx-route-no-claude-native.mjs +57 -0
- package/scripts/tfx-batch-stats.mjs +96 -0
- package/scripts/tfx-gate-activate.mjs +89 -0
- package/scripts/tfx-route-post.mjs +505 -0
- package/scripts/tfx-route-worker.mjs +223 -0
- package/scripts/tfx-route.sh +2014 -0
- package/scripts/tmp-cleanup.mjs +103 -0
- package/scripts/token-snapshot.mjs +575 -0
- package/skills/tfx-auto/SKILL.md.tmpl +2 -3
- package/skills/tfx-autoresearch/SKILL.md +6 -5
- package/skills/tfx-codex/SKILL.md.tmpl +2 -3
- 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-deep-interview/SKILL.md +1 -2
- package/skills/tfx-plan/SKILL.md.tmpl +2 -3
- package/skills/tfx-psmux-rules/SKILL.md +11 -2
- package/skills/tfx-qa/SKILL.md.tmpl +2 -3
- package/skills/tfx-remote-spawn/SKILL.md +8 -11
- package/skills/tfx-research/SKILL.md.tmpl +2 -3
- package/skills/tfx-review/SKILL.md.tmpl +2 -3
- package/skills/tfx-workspace/async-tests/run-tests.sh +203 -0
- package/skills/tfx-workspace/evals/evals.json +79 -0
- package/skills/tfx-workspace/iteration-1/benchmark.json +162 -0
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/eval_metadata.json +11 -0
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/grading.json +9 -0
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/outputs/analysis.md +154 -0
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/grading.json +9 -0
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/outputs/analysis.md +126 -0
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/eval_metadata.json +11 -0
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/grading.json +9 -0
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/outputs/analysis.md +119 -0
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/grading.json +9 -0
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/outputs/analysis.md +115 -0
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/eval_metadata.json +10 -0
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/grading.json +8 -0
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/outputs/analysis.md +86 -0
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/grading.json +8 -0
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/outputs/analysis.md +81 -0
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-1/multi-team-creation/eval_metadata.json +12 -0
- package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/grading.json +10 -0
- package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/outputs/analysis.md +316 -0
- package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/grading.json +10 -0
- package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/outputs/analysis.md +352 -0
- package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-1/review.html +1325 -0
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/eval_metadata.json +12 -0
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/grading.json +10 -0
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/outputs/analysis.md +97 -0
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/grading.json +10 -0
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/outputs/analysis.md +94 -0
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/eval_metadata.json +12 -0
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/grading.json +10 -0
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/outputs/analysis.md +209 -0
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/grading.json +10 -0
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/outputs/analysis.md +193 -0
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-2/benchmark.json +62 -0
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/eval_metadata.json +13 -0
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/grading.json +11 -0
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/outputs/analysis.md +382 -0
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/grading.json +11 -0
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/outputs/analysis.md +333 -0
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-2/review.html +1325 -0
- package/skills/tfx-workspace/skill-snapshot/tfx-auto/SKILL.md +217 -0
- package/skills/{tfx-auto-codex/SKILL.md.tmpl → tfx-workspace/skill-snapshot/tfx-auto-codex/SKILL.md} +3 -31
- package/skills/tfx-workspace/skill-snapshot/tfx-codex/SKILL.md +65 -0
- package/skills/tfx-workspace/skill-snapshot/tfx-doctor/SKILL.md +94 -0
- package/skills/{tfx-gemini/SKILL.md.tmpl → tfx-workspace/skill-snapshot/tfx-gemini/SKILL.md} +6 -14
- package/skills/tfx-workspace/skill-snapshot/tfx-hub/SKILL.md +133 -0
- package/skills/tfx-workspace/skill-snapshot/tfx-multi/SKILL.md +426 -0
- package/skills/tfx-workspace/skill-snapshot/tfx-setup/SKILL.md +101 -0
- package/skills/merge-worktree/SKILL.md.tmpl +0 -144
- package/skills/shared/arguments-processing.md +0 -2
- package/skills/shared/mandatory-rules.md +0 -6
- package/skills/shared/telemetry-segment.md +0 -6
- package/skills/star-prompt/SKILL.md.tmpl +0 -122
- package/skills/tfx-analysis/SKILL.md.tmpl +0 -106
- package/skills/tfx-analysis/skill.json +0 -11
- package/skills/tfx-auto/skill.json +0 -26
- package/skills/tfx-auto-codex/skill.json +0 -8
- package/skills/tfx-autopilot/SKILL.md.tmpl +0 -115
- package/skills/tfx-autopilot/skill.json +0 -10
- package/skills/tfx-autoresearch/SKILL.md.tmpl +0 -135
- package/skills/tfx-autoresearch/skill.json +0 -14
- package/skills/tfx-autoroute/SKILL.md.tmpl +0 -188
- package/skills/tfx-autoroute/skill.json +0 -12
- package/skills/tfx-codex/skill.json +0 -8
- package/skills/tfx-codex-swarm/SKILL.md.tmpl +0 -16
- package/skills/tfx-codex-swarm/skill.json +0 -5
- package/skills/tfx-consensus/SKILL.md.tmpl +0 -145
- package/skills/tfx-consensus/skill.json +0 -8
- package/skills/tfx-debate/SKILL.md.tmpl +0 -191
- package/skills/tfx-debate/skill.json +0 -12
- package/skills/tfx-deep-analysis/SKILL.md.tmpl +0 -227
- package/skills/tfx-deep-analysis/skill.json +0 -10
- package/skills/tfx-deep-interview/SKILL.md.tmpl +0 -203
- package/skills/tfx-deep-interview/skill.json +0 -12
- package/skills/tfx-deep-plan/SKILL.md.tmpl +0 -281
- package/skills/tfx-deep-plan/skill.json +0 -13
- package/skills/tfx-deep-qa/SKILL.md.tmpl +0 -164
- package/skills/tfx-deep-qa/skill.json +0 -11
- package/skills/tfx-deep-research/SKILL.md.tmpl +0 -216
- package/skills/tfx-deep-research/skill.json +0 -14
- package/skills/tfx-deep-review/SKILL.md.tmpl +0 -178
- package/skills/tfx-deep-review/skill.json +0 -12
- package/skills/tfx-doctor/SKILL.md.tmpl +0 -172
- package/skills/tfx-doctor/skill.json +0 -8
- package/skills/tfx-find/skill.json +0 -12
- package/skills/tfx-forge/SKILL.md.tmpl +0 -187
- package/skills/tfx-forge/skill.json +0 -12
- package/skills/tfx-fullcycle/SKILL.md.tmpl +0 -285
- package/skills/tfx-fullcycle/skill.json +0 -11
- package/skills/tfx-gemini/skill.json +0 -8
- package/skills/tfx-hooks/SKILL.md.tmpl +0 -216
- package/skills/tfx-hooks/skill.json +0 -8
- package/skills/tfx-hub/SKILL.md.tmpl +0 -212
- package/skills/tfx-hub/skill.json +0 -8
- package/skills/tfx-index/skill.json +0 -11
- package/skills/tfx-interview/SKILL.md.tmpl +0 -284
- package/skills/tfx-interview/skill.json +0 -12
- package/skills/tfx-multi/SKILL.md.tmpl +0 -183
- package/skills/tfx-multi/skill.json +0 -8
- package/skills/tfx-panel/SKILL.md.tmpl +0 -188
- package/skills/tfx-panel/skill.json +0 -12
- package/skills/tfx-persist/SKILL.md.tmpl +0 -269
- package/skills/tfx-persist/skill.json +0 -12
- package/skills/tfx-plan/skill.json +0 -11
- package/skills/tfx-profile/SKILL.md.tmpl +0 -239
- package/skills/tfx-profile/skill.json +0 -8
- package/skills/tfx-prune/SKILL.md.tmpl +0 -199
- package/skills/tfx-prune/skill.json +0 -12
- package/skills/tfx-psmux-rules/SKILL.md.tmpl +0 -317
- package/skills/tfx-psmux-rules/skill.json +0 -8
- package/skills/tfx-qa/skill.json +0 -11
- package/skills/tfx-ralph/SKILL.md.tmpl +0 -27
- package/skills/tfx-ralph/skill.json +0 -8
- package/skills/tfx-remote-setup/SKILL.md.tmpl +0 -576
- package/skills/tfx-remote-setup/skill.json +0 -8
- package/skills/tfx-remote-spawn/SKILL.md.tmpl +0 -263
- package/skills/tfx-remote-spawn/skill.json +0 -9
- package/skills/tfx-research/skill.json +0 -13
- package/skills/tfx-review/skill.json +0 -11
- package/skills/tfx-setup/SKILL.md.tmpl +0 -380
- package/skills/tfx-setup/skill.json +0 -8
- package/skills/tfx-swarm/SKILL.md.tmpl +0 -154
- package/skills/tfx-swarm/skill.json +0 -5
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
// hub/lib/process-utils.mjs
|
|
2
|
+
// 프로세스 관련 공유 유틸리티
|
|
3
|
+
|
|
4
|
+
import { execSync } from "node:child_process";
|
|
5
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from "node:fs";
|
|
6
|
+
import { homedir, tmpdir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { IS_WINDOWS, killProcess } from "../platform.mjs";
|
|
9
|
+
|
|
10
|
+
const CLEANUP_SCRIPT_DIR = join(tmpdir(), "tfx-process-utils");
|
|
11
|
+
const SCAN_SCRIPT_PATH = join(CLEANUP_SCRIPT_DIR, "scan-processes.ps1");
|
|
12
|
+
const TREE_SCRIPT_PATH = join(CLEANUP_SCRIPT_DIR, "get-ancestor-tree.ps1");
|
|
13
|
+
|
|
14
|
+
// 스크립트 버전 — 내용 변경 시 증가하여 캐시된 스크립트를 갱신
|
|
15
|
+
const SCRIPT_VERSION = 3;
|
|
16
|
+
const VERSION_FILE = join(CLEANUP_SCRIPT_DIR, ".version");
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 주어진 PID의 프로세스가 살아있는지 확인한다.
|
|
20
|
+
* EPERM: 프로세스는 존재하지만 signal 권한 없음 → alive
|
|
21
|
+
* ESRCH: 프로세스가 존재하지 않음 → dead
|
|
22
|
+
*/
|
|
23
|
+
export function isPidAlive(pid) {
|
|
24
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
process.kill(pid, 0);
|
|
28
|
+
return true;
|
|
29
|
+
} catch (e) {
|
|
30
|
+
if (e?.code === 'EPERM') return true;
|
|
31
|
+
if (e?.code === 'ESRCH') return false;
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 동기적 sleep. Atomics.wait 우선, 불가 시 busy-wait 폴백.
|
|
38
|
+
*/
|
|
39
|
+
function sleepSyncMs(ms) {
|
|
40
|
+
try {
|
|
41
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
42
|
+
} catch {
|
|
43
|
+
const end = Date.now() + ms;
|
|
44
|
+
while (Date.now() < end) { /* spin */ }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 고아 PID 목록에 SIGTERM → 3초 대기 → SIGKILL 에스컬레이션을 적용한다.
|
|
50
|
+
* PID 재사용 레이스 방어: SIGTERM 전 alive 확인, SIGKILL 전 재검증.
|
|
51
|
+
* @param {number[]} orphanPids
|
|
52
|
+
* @param {Map<number, {ppid: number, name: string}>} [procMap] PID 재사용 감지용 스냅샷
|
|
53
|
+
* @returns {number} killed count
|
|
54
|
+
*/
|
|
55
|
+
function killWithEscalation(orphanPids, procMap) {
|
|
56
|
+
if (orphanPids.length === 0) return 0;
|
|
57
|
+
|
|
58
|
+
// SIGTERM 전 alive 스냅샷 — 이미 죽은 PID는 카운트에서 제외
|
|
59
|
+
const aliveBeforeKill = new Set(orphanPids.filter(pid => isPidAlive(pid)));
|
|
60
|
+
|
|
61
|
+
for (const pid of aliveBeforeKill) {
|
|
62
|
+
killProcess(pid, { signal: "SIGTERM" });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
sleepSyncMs(3000);
|
|
66
|
+
|
|
67
|
+
let killed = 0;
|
|
68
|
+
for (const pid of aliveBeforeKill) {
|
|
69
|
+
if (isPidAlive(pid)) {
|
|
70
|
+
// PID 재사용 방어: procMap이 있으면 스캔 시점의 ppid와 현재 ppid 비교
|
|
71
|
+
// ppid가 변경되었으면 PID가 재사용된 것이므로 kill하지 않���
|
|
72
|
+
if (procMap) {
|
|
73
|
+
const snapshot = procMap.get(pid);
|
|
74
|
+
if (snapshot) {
|
|
75
|
+
try {
|
|
76
|
+
const current = execSync(
|
|
77
|
+
IS_WINDOWS
|
|
78
|
+
? `powershell -NoProfile -WindowStyle Hidden -Command "(Get-CimInstance Win32_Process -Filter 'ProcessId=${pid}' -ErrorAction SilentlyContinue).ParentProcessId"`
|
|
79
|
+
: `ps -o ppid= -p ${pid}`,
|
|
80
|
+
{ encoding: "utf8", timeout: 3000, stdio: ["pipe", "pipe", "pipe"], windowsHide: true },
|
|
81
|
+
);
|
|
82
|
+
const currentPpid = Number.parseInt(current.trim(), 10);
|
|
83
|
+
if (Number.isFinite(currentPpid) && currentPpid !== snapshot.ppid) {
|
|
84
|
+
continue; // PID 재사용 감지 — skip
|
|
85
|
+
}
|
|
86
|
+
} catch {
|
|
87
|
+
// 조회 실패 시 안전하게 skip
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
killProcess(pid, { signal: "SIGKILL", force: true });
|
|
93
|
+
}
|
|
94
|
+
if (!isPidAlive(pid)) killed++;
|
|
95
|
+
}
|
|
96
|
+
return killed;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* PowerShell 헬퍼 스크립트를 임시 디렉토리에 생성한다.
|
|
101
|
+
* bash의 $_ 이스케이핑 문제를 피하기 위해 -File로 실행.
|
|
102
|
+
*/
|
|
103
|
+
function ensureHelperScripts() {
|
|
104
|
+
mkdirSync(CLEANUP_SCRIPT_DIR, { recursive: true });
|
|
105
|
+
|
|
106
|
+
// 버전 체크 — 스크립트 갱신 필요 여부
|
|
107
|
+
let needsUpdate = true;
|
|
108
|
+
try {
|
|
109
|
+
if (existsSync(VERSION_FILE)) {
|
|
110
|
+
const cached = Number.parseInt(readFileSync(VERSION_FILE, "utf8").trim(), 10);
|
|
111
|
+
if (cached === SCRIPT_VERSION) needsUpdate = false;
|
|
112
|
+
}
|
|
113
|
+
} catch {}
|
|
114
|
+
|
|
115
|
+
if (needsUpdate) {
|
|
116
|
+
// 기존 스크립트 삭제 후 재생성
|
|
117
|
+
try { unlinkSync(SCAN_SCRIPT_PATH); } catch {}
|
|
118
|
+
try { unlinkSync(TREE_SCRIPT_PATH); } catch {}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!existsSync(TREE_SCRIPT_PATH)) {
|
|
122
|
+
writeFileSync(TREE_SCRIPT_PATH, [
|
|
123
|
+
"param([int]$StartPid)",
|
|
124
|
+
"$p = $StartPid",
|
|
125
|
+
"for ($i = 0; $i -lt 10; $i++) {",
|
|
126
|
+
" if ($p -le 0) { break }",
|
|
127
|
+
" Write-Output $p",
|
|
128
|
+
' $parent = (Get-CimInstance Win32_Process -Filter "ProcessId=$p" -ErrorAction SilentlyContinue).ParentProcessId',
|
|
129
|
+
" if ($null -eq $parent -or $parent -le 0) { break }",
|
|
130
|
+
" $p = $parent",
|
|
131
|
+
"}",
|
|
132
|
+
].join("\n"), "utf8");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!existsSync(SCAN_SCRIPT_PATH)) {
|
|
136
|
+
// CLI + 쉘 + 런타임 전체를 스캔하여 PID,ParentPID,Name 출력
|
|
137
|
+
// codex/claude/pwsh/uvx 누락 시 중간 프로세스가 alive 판정되어 고아 트리 전체가 보호됨
|
|
138
|
+
// 예: WT(dead)→pwsh(alive,미스캔)→codex→cmd→node — pwsh에서 isPidAlive=true로 끊김
|
|
139
|
+
writeFileSync(SCAN_SCRIPT_PATH, [
|
|
140
|
+
"$ErrorActionPreference = 'SilentlyContinue'",
|
|
141
|
+
"Get-CimInstance Win32_Process -Filter \"Name='node.exe' OR Name='bash.exe' OR Name='cmd.exe' OR Name='codex.exe' OR Name='claude.exe' OR Name='pwsh.exe' OR Name='uvx.exe'\" | ForEach-Object {",
|
|
142
|
+
' Write-Output "$($_.ProcessId),$($_.ParentProcessId),$($_.Name)"',
|
|
143
|
+
"}",
|
|
144
|
+
].join("\n"), "utf8");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (needsUpdate) {
|
|
148
|
+
writeFileSync(VERSION_FILE, String(SCRIPT_VERSION), "utf8");
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* PID → 루트 조상까지의 체인에서 살아있는 조상이 있는지 확인한다.
|
|
154
|
+
* 프로세스 맵을 사용하여 O(depth) 탐색.
|
|
155
|
+
* @param {number} pid
|
|
156
|
+
* @param {Map<number, {ppid: number, name: string}>} procMap
|
|
157
|
+
* @param {Set<number>} protectedPids
|
|
158
|
+
* @returns {boolean} true = 보호됨 (활성 조상 체인이 있음)
|
|
159
|
+
*/
|
|
160
|
+
function hasLiveAncestorChain(pid, procMap, protectedPids) {
|
|
161
|
+
const visited = new Set();
|
|
162
|
+
let current = pid;
|
|
163
|
+
|
|
164
|
+
while (current > 0 && !visited.has(current)) {
|
|
165
|
+
visited.add(current);
|
|
166
|
+
|
|
167
|
+
if (protectedPids.has(current)) return true;
|
|
168
|
+
|
|
169
|
+
const info = procMap.get(current);
|
|
170
|
+
if (!info) {
|
|
171
|
+
// 프로세스 맵에 없음 → 살아있는지 직접 확인
|
|
172
|
+
return isPidAlive(current);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const ppid = info.ppid;
|
|
176
|
+
if (!Number.isFinite(ppid) || ppid <= 0) {
|
|
177
|
+
// 루트 프로세스 (ppid=0) — 시스템 프로세스이므로 보호
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 부모가 맵에 없고 죽었으면 → 고아 체인
|
|
182
|
+
if (!procMap.has(ppid) && !isPidAlive(ppid)) return false;
|
|
183
|
+
|
|
184
|
+
current = ppid;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// kill 대상 프로세스 이름 (Windows)
|
|
191
|
+
// codex/claude도 포함: protectedPids + hasLiveAncestorChain이 활성 인스턴스를 보호하므로
|
|
192
|
+
// 고아(부모 dead + 자식 dead)만 kill됨. pwsh.exe는 사용자 인터랙티브 쉘이므로 제외.
|
|
193
|
+
const KILLABLE_NAMES = new Set([
|
|
194
|
+
"node.exe", "bash.exe", "cmd.exe", "uvx.exe",
|
|
195
|
+
"codex.exe", "claude.exe",
|
|
196
|
+
]);
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* 고아 프로세스 트리를 정리한다 (node.exe + bash.exe + cmd.exe + uvx.exe).
|
|
200
|
+
* Windows 전용 — Agent 서브프로세스가 MCP 서버, bash 래퍼, cmd 래퍼를 남기는 문제 대응.
|
|
201
|
+
*
|
|
202
|
+
* 전략: 부모 체인을 루트까지 추적하여, 체인 중간에 죽은 프로세스가 있으면
|
|
203
|
+
* 해당 프로세스 아래의 전체 트리를 고아로 판정하고 정리.
|
|
204
|
+
* 스캔 범위에는 codex/claude/pwsh도 포함하여 체인 추적 정확도를 높인다.
|
|
205
|
+
*
|
|
206
|
+
* 보호 대상: 현재 프로세스 조상 트리, Hub PID
|
|
207
|
+
* @returns {{ killed: number, remaining: number }}
|
|
208
|
+
*/
|
|
209
|
+
export function cleanupOrphanNodeProcesses() {
|
|
210
|
+
if (!IS_WINDOWS) return cleanupOrphansUnix();
|
|
211
|
+
|
|
212
|
+
ensureHelperScripts();
|
|
213
|
+
|
|
214
|
+
const myPid = process.pid;
|
|
215
|
+
|
|
216
|
+
// Hub PID 보호
|
|
217
|
+
let hubPid = null;
|
|
218
|
+
try {
|
|
219
|
+
const hubPidPath = join(homedir(), ".claude", "cache", "tfx-hub", "hub.pid");
|
|
220
|
+
if (existsSync(hubPidPath)) {
|
|
221
|
+
const hubInfo = JSON.parse(readFileSync(hubPidPath, "utf8"));
|
|
222
|
+
hubPid = Number(hubInfo?.pid);
|
|
223
|
+
}
|
|
224
|
+
} catch {}
|
|
225
|
+
|
|
226
|
+
// 보호 PID 세트: 현재 프로세스 + Hub + 현재 프로세스의 조상 트리
|
|
227
|
+
const protectedPids = new Set();
|
|
228
|
+
protectedPids.add(myPid);
|
|
229
|
+
if (Number.isFinite(hubPid) && hubPid > 0) protectedPids.add(hubPid);
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
const treeOutput = execSync(
|
|
233
|
+
`powershell -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File "${TREE_SCRIPT_PATH}" -StartPid ${myPid}`,
|
|
234
|
+
{ encoding: "utf8", timeout: 8000, stdio: ["pipe", "pipe", "pipe"], windowsHide: true },
|
|
235
|
+
);
|
|
236
|
+
for (const line of treeOutput.split(/\r?\n/)) {
|
|
237
|
+
const pid = Number.parseInt(line.trim(), 10);
|
|
238
|
+
if (Number.isFinite(pid) && pid > 0) protectedPids.add(pid);
|
|
239
|
+
}
|
|
240
|
+
} catch {}
|
|
241
|
+
|
|
242
|
+
// 전체 프로세스 맵 구축 (node + bash + cmd + codex + claude + pwsh + uvx)
|
|
243
|
+
const procMap = new Map();
|
|
244
|
+
try {
|
|
245
|
+
const output = execSync(
|
|
246
|
+
`powershell -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File "${SCAN_SCRIPT_PATH}"`,
|
|
247
|
+
{ encoding: "utf8", timeout: 15000, stdio: ["pipe", "pipe", "pipe"], windowsHide: true },
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
for (const line of output.split(/\r?\n/)) {
|
|
251
|
+
const trimmed = line.trim();
|
|
252
|
+
if (!trimmed) continue;
|
|
253
|
+
const [pidStr, ppidStr, name] = trimmed.split(",");
|
|
254
|
+
const pid = Number.parseInt(pidStr, 10);
|
|
255
|
+
const ppid = Number.parseInt(ppidStr, 10);
|
|
256
|
+
if (Number.isFinite(pid) && pid > 0) {
|
|
257
|
+
procMap.set(pid, { ppid, name: name || "unknown" });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
} catch {}
|
|
261
|
+
|
|
262
|
+
// 고아 판정 + 정리 (SIGTERM → 3s → SIGKILL 에스컬레이션)
|
|
263
|
+
// CLI 도구(codex/claude/pwsh)는 체인 추적용으로만 스캔 — kill 대상에서 제외
|
|
264
|
+
const orphanPids = [];
|
|
265
|
+
for (const [pid, info] of procMap) {
|
|
266
|
+
if (protectedPids.has(pid)) continue;
|
|
267
|
+
if (!KILLABLE_NAMES.has(info.name?.toLowerCase())) continue;
|
|
268
|
+
if (hasLiveAncestorChain(pid, procMap, protectedPids)) continue;
|
|
269
|
+
orphanPids.push(pid);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const killed = killWithEscalation(orphanPids, procMap);
|
|
273
|
+
|
|
274
|
+
// 남은 프로세스 수 확인
|
|
275
|
+
let remaining = 0;
|
|
276
|
+
try {
|
|
277
|
+
const countOutput = execSync(
|
|
278
|
+
`powershell -NoProfile -WindowStyle Hidden -Command "(Get-Process node -ErrorAction SilentlyContinue).Count"`,
|
|
279
|
+
{ encoding: "utf8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"], windowsHide: true },
|
|
280
|
+
);
|
|
281
|
+
remaining = Number.parseInt(countOutput.trim(), 10) || 0;
|
|
282
|
+
} catch {}
|
|
283
|
+
|
|
284
|
+
return { killed, remaining };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Unix/macOS 고아 프로세스 정리.
|
|
289
|
+
* `ps -eo pid,ppid,comm` 기반 프로세스 맵 → 동일한 조상 체인 판정 → SIGKILL 에스컬레이션.
|
|
290
|
+
* @returns {{ killed: number, remaining: number }}
|
|
291
|
+
*/
|
|
292
|
+
function cleanupOrphansUnix() {
|
|
293
|
+
const myPid = process.pid;
|
|
294
|
+
|
|
295
|
+
// Hub PID 보호
|
|
296
|
+
const protectedPids = new Set();
|
|
297
|
+
protectedPids.add(myPid);
|
|
298
|
+
try {
|
|
299
|
+
const hubPidPath = join(homedir(), ".claude", "cache", "tfx-hub", "hub.pid");
|
|
300
|
+
if (existsSync(hubPidPath)) {
|
|
301
|
+
const hubPid = Number(JSON.parse(readFileSync(hubPidPath, "utf8"))?.pid);
|
|
302
|
+
if (Number.isFinite(hubPid) && hubPid > 0) protectedPids.add(hubPid);
|
|
303
|
+
}
|
|
304
|
+
} catch {}
|
|
305
|
+
|
|
306
|
+
// 현재 프로세스의 조상 트리 보호
|
|
307
|
+
try {
|
|
308
|
+
let current = myPid;
|
|
309
|
+
for (let i = 0; i < 10; i++) {
|
|
310
|
+
protectedPids.add(current);
|
|
311
|
+
const output = execSync(`ps -o ppid= -p ${current}`, {
|
|
312
|
+
encoding: "utf8", timeout: 3000, stdio: ["pipe", "pipe", "pipe"],
|
|
313
|
+
});
|
|
314
|
+
const ppid = Number.parseInt(output.trim(), 10);
|
|
315
|
+
if (!Number.isFinite(ppid) || ppid <= 1) break;
|
|
316
|
+
current = ppid;
|
|
317
|
+
}
|
|
318
|
+
} catch {}
|
|
319
|
+
|
|
320
|
+
// 프로세스 맵 구축 (런타임 + CLI — 체인 추적 정확도를 위해 CLI도 포함)
|
|
321
|
+
const procMap = new Map();
|
|
322
|
+
try {
|
|
323
|
+
const output = execSync("ps -eo pid,ppid,comm", {
|
|
324
|
+
encoding: "utf8", timeout: 10000, stdio: ["pipe", "pipe", "pipe"],
|
|
325
|
+
});
|
|
326
|
+
for (const line of output.split("\n").slice(1)) {
|
|
327
|
+
const parts = line.trim().split(/\s+/);
|
|
328
|
+
if (parts.length < 3) continue;
|
|
329
|
+
const pid = Number.parseInt(parts[0], 10);
|
|
330
|
+
const ppid = Number.parseInt(parts[1], 10);
|
|
331
|
+
const name = parts.slice(2).join(" ");
|
|
332
|
+
if (Number.isFinite(pid) && pid > 0 && /^(node|bash|sh|python|codex|claude|uvx)/.test(name)) {
|
|
333
|
+
procMap.set(pid, { ppid, name });
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
} catch {}
|
|
337
|
+
|
|
338
|
+
// kill 대상: node, python, codex, claude, uvx — bash/sh는 사용자 인터랙티브 쉘 가능성
|
|
339
|
+
const killableUnix = /^(node|python|codex|claude|uvx)/;
|
|
340
|
+
|
|
341
|
+
// 고아 판정 + SIGKILL 에스컬레이션
|
|
342
|
+
const orphanPids = [];
|
|
343
|
+
for (const [pid, info] of procMap) {
|
|
344
|
+
if (protectedPids.has(pid)) continue;
|
|
345
|
+
if (!killableUnix.test(info.name)) continue;
|
|
346
|
+
if (hasLiveAncestorChain(pid, procMap, protectedPids)) continue;
|
|
347
|
+
orphanPids.push(pid);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const killed = killWithEscalation(orphanPids, procMap);
|
|
351
|
+
|
|
352
|
+
let remaining = 0;
|
|
353
|
+
try {
|
|
354
|
+
const output = execSync("ps -eo comm | grep -c '^node$'", {
|
|
355
|
+
encoding: "utf8", timeout: 3000, stdio: ["pipe", "pipe", "pipe"],
|
|
356
|
+
});
|
|
357
|
+
remaining = Number.parseInt(output.trim(), 10) || 0;
|
|
358
|
+
} catch {}
|
|
359
|
+
|
|
360
|
+
return { killed, remaining };
|
|
361
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
// hub/lib/ssh-command.mjs — OS-aware SSH command builder
|
|
2
|
+
// PowerShell 호스트에 bash 문법(2>/dev/null, &&, $())을 보내는 사고를 방지한다.
|
|
3
|
+
// 모든 SSH 명령 생성 코드에서 이 유틸리티를 사용할 것.
|
|
4
|
+
|
|
5
|
+
import { readFileSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
|
|
8
|
+
/** hosts.json 캐시 (프로세스 수명 동안 유지) */
|
|
9
|
+
let hostsCache = null;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* hosts.json을 로드하여 캐시한다.
|
|
13
|
+
* @param {string} [repoRoot] — 프로젝트 루트 경로
|
|
14
|
+
*/
|
|
15
|
+
function loadHostsCache(repoRoot) {
|
|
16
|
+
if (hostsCache) return;
|
|
17
|
+
try {
|
|
18
|
+
const hostsPath = repoRoot
|
|
19
|
+
? join(repoRoot, "references", "hosts.json")
|
|
20
|
+
: join(process.cwd(), "references", "hosts.json");
|
|
21
|
+
hostsCache = JSON.parse(readFileSync(hostsPath, "utf8"));
|
|
22
|
+
} catch {
|
|
23
|
+
hostsCache = { hosts: {} };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* hosts.json에서 호스트 OS를 조회한다.
|
|
29
|
+
* @param {string} hostAlias — hosts.json의 키 (e.g. "ultra4")
|
|
30
|
+
* @param {string} [repoRoot] — 프로젝트 루트 경로
|
|
31
|
+
* @returns {"windows"|"posix"} 호스트 OS 유형
|
|
32
|
+
*/
|
|
33
|
+
export function detectHostOs(hostAlias, repoRoot) {
|
|
34
|
+
loadHostsCache(repoRoot);
|
|
35
|
+
|
|
36
|
+
const hostCfg = hostsCache.hosts?.[hostAlias];
|
|
37
|
+
if (hostCfg?.os === "windows") return "windows";
|
|
38
|
+
if (hostCfg?.os) return "posix";
|
|
39
|
+
|
|
40
|
+
// IP 주소나 unknown alias → posix 기본값
|
|
41
|
+
return "posix";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* OS에 맞는 셸 쿼팅을 적용한다.
|
|
46
|
+
* - posix: 싱글쿼트 래핑 + 내부 싱글쿼트 이스케이프
|
|
47
|
+
* - windows: PowerShell 싱글쿼트 래핑 + 내부 싱글쿼트 더블링
|
|
48
|
+
* @param {string} value
|
|
49
|
+
* @param {"windows"|"posix"} os
|
|
50
|
+
* @returns {string}
|
|
51
|
+
*/
|
|
52
|
+
export function shellQuoteForHost(value, os) {
|
|
53
|
+
const s = String(value);
|
|
54
|
+
if (os === "windows") {
|
|
55
|
+
// PowerShell: 싱글쿼트 내부에서 '' 로 이스케이프
|
|
56
|
+
return `'${s.replace(/'/g, "''")}'`;
|
|
57
|
+
}
|
|
58
|
+
// POSIX: 싱글쿼트 내부에서 '\'' 로 이스케이프
|
|
59
|
+
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* OS에 맞는 stderr 억제 구문을 반환한다.
|
|
64
|
+
* @param {"windows"|"posix"} os
|
|
65
|
+
* @returns {string} "2>$null" 또는 "2>/dev/null"
|
|
66
|
+
*/
|
|
67
|
+
export function suppressStderr(os) {
|
|
68
|
+
return os === "windows" ? "2>$null" : "2>/dev/null";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* OS에 맞는 명령 연결자를 반환한다.
|
|
73
|
+
* @param {"windows"|"posix"} os
|
|
74
|
+
* @returns {string} "; " (PowerShell) 또는 " && " (bash)
|
|
75
|
+
*/
|
|
76
|
+
export function commandJoin(os) {
|
|
77
|
+
return os === "windows" ? "; " : " && ";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* OS에 맞는 null 디바이스 경로를 반환한다.
|
|
82
|
+
* @param {"windows"|"posix"} os
|
|
83
|
+
* @returns {string}
|
|
84
|
+
*/
|
|
85
|
+
export function nullDevice(os) {
|
|
86
|
+
return os === "windows" ? "$null" : "/dev/null";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* SSH execFileSync 인자 배열을 안전하게 구성한다.
|
|
91
|
+
* psmux 명령처럼 원격 셸에서 실행할 명령을 배열로 받아
|
|
92
|
+
* 적절한 쿼팅을 적용한 SSH 인자 배열을 반환한다.
|
|
93
|
+
*
|
|
94
|
+
* @param {string} host — SSH 호스트 (user@ip 또는 alias)
|
|
95
|
+
* @param {string[]} remoteCmd — 원격에서 실행할 명령 + 인자
|
|
96
|
+
* @param {object} [opts]
|
|
97
|
+
* @param {number} [opts.connectTimeout=5] — SSH 연결 타임아웃(초)
|
|
98
|
+
* @param {boolean} [opts.keepalive=true] — ServerAliveInterval/CountMax 활성화
|
|
99
|
+
* @param {"windows"|"posix"} [opts.os] — 호스트 OS (미지정 시 자동 감지)
|
|
100
|
+
* @param {string} [opts.repoRoot] — hosts.json 탐색 경로
|
|
101
|
+
* @returns {string[]} execFileSync('ssh', returnValue) 형태로 사용
|
|
102
|
+
*/
|
|
103
|
+
export function buildSshArgs(host, remoteCmd, opts = {}) {
|
|
104
|
+
const connectTimeout = opts.connectTimeout ?? 5;
|
|
105
|
+
const keepalive = opts.keepalive !== false;
|
|
106
|
+
const hostAlias = host.includes("@") ? host.split("@").pop() : host;
|
|
107
|
+
const os = opts.os ?? detectHostOs(hostAlias, opts.repoRoot);
|
|
108
|
+
|
|
109
|
+
// 명령어(첫 요소)는 쿼팅하지 않고, 인자만 쿼팅
|
|
110
|
+
const [cmd, ...args] = remoteCmd;
|
|
111
|
+
const quotedArgs = args.map((a) => shellQuoteForHost(a, os));
|
|
112
|
+
const remoteCmdStr = [cmd, ...quotedArgs].join(" ");
|
|
113
|
+
|
|
114
|
+
const sshArgs = [
|
|
115
|
+
"-o", `ConnectTimeout=${connectTimeout}`,
|
|
116
|
+
"-o", "BatchMode=yes",
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
if (keepalive) {
|
|
120
|
+
sshArgs.push("-o", "ServerAliveInterval=30", "-o", "ServerAliveCountMax=3");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
sshArgs.push(host, remoteCmdStr);
|
|
124
|
+
return sshArgs;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* 검증: 문자열에 호스트 OS와 맞지 않는 셸 문법이 포함되었는지 검사한다.
|
|
129
|
+
* SSH 명령 생성 시 안전 게이트로 사용.
|
|
130
|
+
*
|
|
131
|
+
* @param {string} command — 원격 실행할 명령 문자열
|
|
132
|
+
* @param {"windows"|"posix"} os — 대상 호스트 OS
|
|
133
|
+
* @returns {{ safe: boolean, violations: string[] }}
|
|
134
|
+
*/
|
|
135
|
+
export function validateCommandForOs(command, os) {
|
|
136
|
+
const violations = [];
|
|
137
|
+
|
|
138
|
+
if (os === "windows") {
|
|
139
|
+
// PowerShell에서 해석 불가능한 bash 문법 검출
|
|
140
|
+
if (/2>\/dev\/null/.test(command)) {
|
|
141
|
+
violations.push("2>/dev/null → PowerShell에서는 2>$null 사용");
|
|
142
|
+
}
|
|
143
|
+
if (/\$\(/.test(command) && !/\$\(/.test(command) === false) {
|
|
144
|
+
// $() bash substitution vs PowerShell $() — 문맥 의존
|
|
145
|
+
// PowerShell도 $()를 사용하므로 여기선 경고만
|
|
146
|
+
}
|
|
147
|
+
if (/\s&&\s/.test(command)) {
|
|
148
|
+
violations.push("&& → PowerShell에서는 ; 또는 -and 사용");
|
|
149
|
+
}
|
|
150
|
+
if (/\s\|\|\s/.test(command)) {
|
|
151
|
+
violations.push("|| → PowerShell에서는 ; 또는 -or 사용");
|
|
152
|
+
}
|
|
153
|
+
if (/>\s*\/dev\/null/.test(command)) {
|
|
154
|
+
violations.push(">/dev/null → PowerShell에서는 >$null 사용");
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return { safe: violations.length === 0, violations };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* hosts.json에서 호스트 전체 설정을 조회한다.
|
|
163
|
+
* @param {string} hostAlias — hosts.json의 키 (e.g. "ultra4")
|
|
164
|
+
* @param {string} [repoRoot]
|
|
165
|
+
* @returns {object|null}
|
|
166
|
+
*/
|
|
167
|
+
export function getHostConfig(hostAlias, repoRoot) {
|
|
168
|
+
loadHostsCache(repoRoot);
|
|
169
|
+
return hostsCache.hosts?.[hostAlias] ?? null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* 별칭(alias)으로 호스트 키를 찾는다.
|
|
174
|
+
* @param {string} alias — 호스트 키 또는 aliases 배열 내 값
|
|
175
|
+
* @param {string} [repoRoot]
|
|
176
|
+
* @returns {string|null} 호스트 키 (e.g. "ultra4") 또는 null
|
|
177
|
+
*/
|
|
178
|
+
export function resolveHostAlias(alias, repoRoot) {
|
|
179
|
+
loadHostsCache(repoRoot);
|
|
180
|
+
|
|
181
|
+
if (hostsCache.hosts?.[alias]) return alias;
|
|
182
|
+
|
|
183
|
+
for (const [name, cfg] of Object.entries(hostsCache.hosts || {})) {
|
|
184
|
+
if (cfg.aliases?.includes(alias)) return name;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* 특정 capability를 보유한 호스트 목록을 반환한다.
|
|
192
|
+
* @param {string} capability — e.g. "codex", "claude", "gpu"
|
|
193
|
+
* @param {string} [repoRoot]
|
|
194
|
+
* @returns {Array<{ name: string, config: object }>}
|
|
195
|
+
*/
|
|
196
|
+
export function selectHostForCapability(capability, repoRoot) {
|
|
197
|
+
loadHostsCache(repoRoot);
|
|
198
|
+
|
|
199
|
+
const matches = [];
|
|
200
|
+
for (const [name, cfg] of Object.entries(hostsCache.hosts || {})) {
|
|
201
|
+
if (cfg.capabilities?.includes(capability)) {
|
|
202
|
+
matches.push({ name, config: cfg });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return matches;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** hosts.json 캐시 초기화 (테스트용) */
|
|
209
|
+
export function resetHostsCache() {
|
|
210
|
+
hostsCache = null;
|
|
211
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// hub/lib/ssh-retry.mjs — SSH command retry with exponential backoff
|
|
2
|
+
// transient SSH 에러(connection reset, broken pipe 등)를 감지하여 자동 재시도한다.
|
|
3
|
+
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
|
+
|
|
6
|
+
const DEFAULT_MAX_RETRIES = 2;
|
|
7
|
+
const DEFAULT_BASE_DELAY_MS = 1000;
|
|
8
|
+
|
|
9
|
+
/** transient SSH 에러 패턴 */
|
|
10
|
+
const TRANSIENT_RE =
|
|
11
|
+
/connection (reset|refused|timed out)|broken pipe|network is unreachable|ssh_exchange_identification|kex_exchange_identification/i;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* execFileSync('ssh', args) 를 retry 래핑한다.
|
|
15
|
+
* transient 에러만 재시도하고, 그 외 에러는 즉시 throw.
|
|
16
|
+
*
|
|
17
|
+
* @param {string[]} args — ssh 인자 배열
|
|
18
|
+
* @param {object} [opts] — execFileSync 옵션 + retry 설정
|
|
19
|
+
* @param {number} [opts.maxRetries=2]
|
|
20
|
+
* @param {number} [opts.baseDelayMs=1000]
|
|
21
|
+
* @returns {string|Buffer} ssh stdout
|
|
22
|
+
*/
|
|
23
|
+
export function execSshWithRetry(args, opts = {}) {
|
|
24
|
+
const { maxRetries = DEFAULT_MAX_RETRIES, baseDelayMs = DEFAULT_BASE_DELAY_MS, ...execOpts } = opts;
|
|
25
|
+
|
|
26
|
+
let lastError;
|
|
27
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
28
|
+
try {
|
|
29
|
+
return execFileSync("ssh", args, execOpts);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
lastError = err;
|
|
32
|
+
if (attempt < maxRetries && isTransientSshError(err)) {
|
|
33
|
+
sleepSync(baseDelayMs * 2 ** attempt);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
throw err;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
throw lastError;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* transient SSH 에러인지 판별한다.
|
|
44
|
+
* @param {Error} err
|
|
45
|
+
* @returns {boolean}
|
|
46
|
+
*/
|
|
47
|
+
export function isTransientSshError(err) {
|
|
48
|
+
const msg = String(err.stderr || err.message || "");
|
|
49
|
+
return TRANSIENT_RE.test(msg);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 동기 sleep (Atomics.wait 기반).
|
|
54
|
+
* @param {number} ms
|
|
55
|
+
*/
|
|
56
|
+
function sleepSync(ms) {
|
|
57
|
+
if (ms <= 0) return;
|
|
58
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
59
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// hub/lib/uuidv7.mjs — UUIDv7 generator (RFC 9562, monotonic)
|
|
2
|
+
import { randomBytes } from 'node:crypto';
|
|
3
|
+
|
|
4
|
+
let _rndPool = Buffer.alloc(0), _rndOff = 0;
|
|
5
|
+
|
|
6
|
+
function pooledRandom(n) {
|
|
7
|
+
if (_rndOff + n > _rndPool.length) {
|
|
8
|
+
_rndPool = randomBytes(256);
|
|
9
|
+
_rndOff = 0;
|
|
10
|
+
}
|
|
11
|
+
const out = Buffer.from(_rndPool.subarray(_rndOff, _rndOff + n));
|
|
12
|
+
_rndOff += n;
|
|
13
|
+
return out;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let _lastMs = 0n;
|
|
17
|
+
let _seq = 0;
|
|
18
|
+
|
|
19
|
+
/** UUIDv7 생성 (RFC 9562, 단조 증가 보장) */
|
|
20
|
+
export function uuidv7() {
|
|
21
|
+
let now = BigInt(Date.now());
|
|
22
|
+
if (now <= _lastMs) {
|
|
23
|
+
_seq++;
|
|
24
|
+
if (_seq > 0xfff) {
|
|
25
|
+
now = _lastMs + 1n;
|
|
26
|
+
_seq = 0;
|
|
27
|
+
}
|
|
28
|
+
} else {
|
|
29
|
+
_seq = 0;
|
|
30
|
+
}
|
|
31
|
+
_lastMs = now;
|
|
32
|
+
const buf = pooledRandom(16);
|
|
33
|
+
buf[0] = Number((now >> 40n) & 0xffn);
|
|
34
|
+
buf[1] = Number((now >> 32n) & 0xffn);
|
|
35
|
+
buf[2] = Number((now >> 24n) & 0xffn);
|
|
36
|
+
buf[3] = Number((now >> 16n) & 0xffn);
|
|
37
|
+
buf[4] = Number((now >> 8n) & 0xffn);
|
|
38
|
+
buf[5] = Number(now & 0xffn);
|
|
39
|
+
buf[6] = ((_seq >> 8) & 0x0f) | 0x70;
|
|
40
|
+
buf[7] = _seq & 0xff;
|
|
41
|
+
buf[8] = (buf[8] & 0x3f) | 0x80;
|
|
42
|
+
const h = buf.toString('hex');
|
|
43
|
+
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
|
|
44
|
+
}
|