triflux 10.3.2 → 10.3.4

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 (65) hide show
  1. package/.claude-plugin/plugin.json +22 -22
  2. package/LICENSE +21 -21
  3. package/README.ko.md +16 -0
  4. package/README.md +8 -0
  5. package/hooks/hook-registry.json +256 -256
  6. package/hub/adaptive-inject.mjs +1 -1
  7. package/hub/assign-callbacks.mjs +120 -120
  8. package/hub/delegator/index.mjs +14 -14
  9. package/hub/delegator/tool-definitions.mjs +35 -35
  10. package/hub/hitl.mjs +143 -143
  11. package/hub/lib/path-utils.mjs +167 -0
  12. package/hub/router.mjs +791 -791
  13. package/hub/session-fingerprint.mjs +1 -1
  14. package/hub/team/cli/commands/attach.mjs +37 -37
  15. package/hub/team/cli/commands/debug.mjs +74 -74
  16. package/hub/team/cli/commands/focus.mjs +53 -53
  17. package/hub/team/cli/commands/list.mjs +24 -24
  18. package/hub/team/cli/commands/start/start-in-process.mjs +40 -40
  19. package/hub/team/cli/commands/start/start-mux.mjs +73 -73
  20. package/hub/team/cli/commands/start/start-wt.mjs +69 -69
  21. package/hub/team/cli/commands/tasks.mjs +13 -13
  22. package/hub/team/cli/render.mjs +30 -30
  23. package/hub/team/cli/services/attach-fallback.mjs +54 -54
  24. package/hub/team/cli/services/member-selector.mjs +30 -30
  25. package/hub/team/cli/services/native-control.mjs +116 -116
  26. package/hub/team/cli/services/task-model.mjs +30 -30
  27. package/hub/team/notify.mjs +1 -1
  28. package/hub/team/orchestrator.mjs +161 -161
  29. package/hub/team/runtime-strategy.mjs +74 -0
  30. package/hub/team/session.mjs +611 -611
  31. package/hub/team/shared.mjs +13 -13
  32. package/hub/team/worktree-lifecycle.mjs +61 -2
  33. package/hub/tray.mjs +368 -368
  34. package/hub/workers/codex-mcp.mjs +507 -507
  35. package/hub/workers/factory.mjs +21 -21
  36. package/hud/hud-qos-status.mjs +17 -3
  37. package/hud/mission-board.mjs +53 -0
  38. package/hud/providers/claude.mjs +95 -22
  39. package/hud/renderers.mjs +39 -5
  40. package/mesh/index.mjs +63 -0
  41. package/mesh/mesh-budget.mjs +128 -0
  42. package/mesh/mesh-heartbeat.mjs +100 -0
  43. package/mesh/mesh-protocol.mjs +96 -0
  44. package/mesh/mesh-queue.mjs +165 -0
  45. package/mesh/mesh-registry.mjs +78 -0
  46. package/mesh/mesh-router.mjs +76 -0
  47. package/package.json +2 -1
  48. package/scripts/completions/tfx.bash +47 -47
  49. package/scripts/completions/tfx.fish +44 -44
  50. package/scripts/completions/tfx.zsh +83 -83
  51. package/scripts/demo.mjs +169 -0
  52. package/scripts/headless-guard.mjs +16 -4
  53. package/scripts/hub-ensure.mjs +120 -120
  54. package/scripts/keyword-detector.mjs +272 -272
  55. package/scripts/keyword-rules-expander.mjs +521 -521
  56. package/scripts/lib/mcp-server-catalog.mjs +118 -118
  57. package/scripts/lib/skill-state.mjs +220 -0
  58. package/scripts/notion-read.mjs +553 -553
  59. package/scripts/test-tfx-route-no-claude-native.mjs +57 -57
  60. package/scripts/tfx-batch-stats.mjs +96 -96
  61. package/skills/.omc/state/agent-replay-8f0e10a9-9693-4410-96f5-a6b07e8ed995.jsonl +0 -1
  62. package/skills/.omc/state/idle-notif-cooldown.json +0 -3
  63. package/skills/.omc/state/last-tool-error.json +0 -7
  64. package/skills/.omc/state/subagent-tracking.json +0 -7
  65. package/skills/tfx-remote-spawn/references/hosts.json +0 -16
@@ -1,118 +1,118 @@
1
- export const SEARCH_SERVER_ORDER = Object.freeze(['brave-search', 'tavily', 'exa']);
2
-
3
- export const MCP_SERVER_TOOL_CATALOG = Object.freeze({
4
- context7: Object.freeze(['resolve-library-id', 'query-docs']),
5
- 'brave-search': Object.freeze(['brave_web_search', 'brave_news_search']),
6
- exa: Object.freeze(['web_search_exa', 'get_code_context_exa']),
7
- tavily: Object.freeze(['tavily_search', 'tavily_extract']),
8
- playwright: Object.freeze([
9
- 'browser_navigate',
10
- 'browser_navigate_back',
11
- 'browser_snapshot',
12
- 'browser_take_screenshot',
13
- 'browser_wait_for',
14
- ]),
15
- 'sequential-thinking': Object.freeze(['sequentialthinking']),
16
- });
17
-
18
- export const MCP_SERVER_DOMAIN_TAGS = Object.freeze({
19
- context7: Object.freeze(['docs', 'reference', 'api', 'sdk', 'library']),
20
- 'brave-search': Object.freeze(['web', 'search', 'news', 'current']),
21
- exa: Object.freeze(['code', 'repository', 'examples', 'search']),
22
- tavily: Object.freeze(['research', 'search', 'news', 'verification', 'current']),
23
- playwright: Object.freeze(['browser', 'ui', 'visual', 'e2e']),
24
- 'sequential-thinking': Object.freeze(['analysis', 'planning', 'reasoning', 'security', 'review']),
25
- });
26
-
27
- export const DOMAIN_TAG_KEYWORDS = Object.freeze({
28
- docs: Object.freeze(['docs', 'documentation', 'manual', 'guide', '문서', '가이드', '매뉴얼']),
29
- reference: Object.freeze(['reference', 'spec', 'schema', 'official', '레퍼런스', '공식', '스펙', '스키마']),
30
- api: Object.freeze(['api', 'endpoint', 'interface', 'sdk', '호출', '엔드포인트']),
31
- sdk: Object.freeze(['sdk', 'library', 'package', 'framework', '라이브러리', '패키지', '프레임워크']),
32
- library: Object.freeze(['library', 'package', 'framework', 'module', '라이브러리', '패키지', '모듈']),
33
- web: Object.freeze(['web', 'site', 'article', 'forum', 'blog', 'reddit', '웹', '사이트', '기사', '포럼', '블로그']),
34
- search: Object.freeze(['search', 'browse', 'lookup', 'find', '검색', '조회', '탐색', '찾아']),
35
- news: Object.freeze(['latest', 'recent', 'news', 'today', 'release', 'announcement', '최신', '최근', '뉴스', '오늘', '릴리즈', '공지']),
36
- current: Object.freeze(['current', 'status', 'pricing', 'changelog', 'up-to-date', '현재', '상태', '가격', '변경사항']),
37
- research: Object.freeze(['research', 'verify', 'fact-check', 'investigate', '리서치', '검증', '조사']),
38
- verification: Object.freeze(['verify', 'validation', 'fact-check', 'audit', '검증', '확인', '감사']),
39
- code: Object.freeze(['code', 'repo', 'repository', 'source', 'implementation', 'bug', 'fix', 'test', 'snippet', 'cli', '코드', '리포', '저장소', '구현', '버그', '테스트', '예제', '스크립트']),
40
- repository: Object.freeze(['repo', 'repository', 'source', 'git', 'github', '리포', '저장소', '소스']),
41
- examples: Object.freeze(['example', 'examples', 'snippet', 'sample', '예제', '샘플']),
42
- browser: Object.freeze(['browser', 'page', 'dom', 'screenshot', 'render', '브라우저', '페이지', '스크린샷', '렌더']),
43
- ui: Object.freeze(['ui', 'ux', 'layout', 'responsive', 'css', 'html', '디자인', '레이아웃', '반응형']),
44
- visual: Object.freeze(['visual', 'screenshot', 'layout', 'render', 'screen', '화면', '시각', '스크린샷']),
45
- e2e: Object.freeze(['playwright', 'e2e', 'click', 'navigate', 'automation', 'playwright', '클릭', '이동', '자동화']),
46
- analysis: Object.freeze(['analysis', 'analyze', 'audit', 'compare', 'root cause', '분석', '검토', '비교', '원인']),
47
- planning: Object.freeze(['plan', 'planning', 'strategy', 'design', '계획', '전략', '설계']),
48
- reasoning: Object.freeze(['reason', 'reasoning', 'think', 'critique', '추론', '사고', '비평']),
49
- security: Object.freeze(['security', 'risk', 'threat', 'vulnerability', '보안', '위험', '취약점']),
50
- review: Object.freeze(['review', 'reviewer', 'inspect', '리뷰', '검수']),
51
- });
52
-
53
- export const SERVER_EXPLICIT_KEYWORDS = Object.freeze({
54
- context7: Object.freeze(['context7']),
55
- 'brave-search': Object.freeze(['brave', 'brave-search']),
56
- exa: Object.freeze(['exa']),
57
- tavily: Object.freeze(['tavily']),
58
- playwright: Object.freeze(['playwright']),
59
- 'sequential-thinking': Object.freeze(['sequential-thinking', 'sequential thinking']),
60
- });
61
-
62
- export function uniqueStrings(values = []) {
63
- return [...new Set(
64
- values
65
- .filter((value) => typeof value === 'string' && value.trim())
66
- .map((value) => value.trim()),
67
- )];
68
- }
69
-
70
- export function inferDomainTagsFromText(text = '') {
71
- if (typeof text !== 'string' || !text.trim()) return [];
72
- const normalized = text.toLocaleLowerCase();
73
- const matched = [];
74
-
75
- for (const [tag, keywords] of Object.entries(DOMAIN_TAG_KEYWORDS)) {
76
- if (keywords.some((keyword) => normalized.includes(String(keyword).toLocaleLowerCase()))) {
77
- matched.push(tag);
78
- }
79
- }
80
-
81
- return uniqueStrings(matched);
82
- }
83
-
84
- export function getDefaultServerMetadata(serverName = '') {
85
- const toolCount = MCP_SERVER_TOOL_CATALOG[serverName]?.length || 0;
86
- const domainTags = uniqueStrings([
87
- ...(MCP_SERVER_DOMAIN_TAGS[serverName] || []),
88
- ...inferDomainTagsFromText(serverName),
89
- ]);
90
-
91
- return {
92
- tool_count: toolCount,
93
- domain_tags: domainTags,
94
- };
95
- }
96
-
97
- export function normalizeServerMetadata(serverName = '', metadata = {}) {
98
- const fallback = getDefaultServerMetadata(serverName);
99
- const toolCount = Number.isFinite(metadata.tool_count)
100
- ? Math.max(0, Math.trunc(metadata.tool_count))
101
- : fallback.tool_count;
102
- const domainTags = uniqueStrings([
103
- ...fallback.domain_tags,
104
- ...(Array.isArray(metadata.domain_tags) ? metadata.domain_tags : []),
105
- ...inferDomainTagsFromText([
106
- serverName,
107
- typeof metadata.command === 'string' ? metadata.command : '',
108
- typeof metadata.url === 'string' ? metadata.url : '',
109
- ...(Array.isArray(metadata.args) ? metadata.args : []),
110
- ...(metadata.env && typeof metadata.env === 'object' ? Object.keys(metadata.env) : []),
111
- ].join(' ')),
112
- ]);
113
-
114
- return {
115
- tool_count: toolCount,
116
- domain_tags: domainTags,
117
- };
118
- }
1
+ export const SEARCH_SERVER_ORDER = Object.freeze(['brave-search', 'tavily', 'exa']);
2
+
3
+ export const MCP_SERVER_TOOL_CATALOG = Object.freeze({
4
+ context7: Object.freeze(['resolve-library-id', 'query-docs']),
5
+ 'brave-search': Object.freeze(['brave_web_search', 'brave_news_search']),
6
+ exa: Object.freeze(['web_search_exa', 'get_code_context_exa']),
7
+ tavily: Object.freeze(['tavily_search', 'tavily_extract']),
8
+ playwright: Object.freeze([
9
+ 'browser_navigate',
10
+ 'browser_navigate_back',
11
+ 'browser_snapshot',
12
+ 'browser_take_screenshot',
13
+ 'browser_wait_for',
14
+ ]),
15
+ 'sequential-thinking': Object.freeze(['sequentialthinking']),
16
+ });
17
+
18
+ export const MCP_SERVER_DOMAIN_TAGS = Object.freeze({
19
+ context7: Object.freeze(['docs', 'reference', 'api', 'sdk', 'library']),
20
+ 'brave-search': Object.freeze(['web', 'search', 'news', 'current']),
21
+ exa: Object.freeze(['code', 'repository', 'examples', 'search']),
22
+ tavily: Object.freeze(['research', 'search', 'news', 'verification', 'current']),
23
+ playwright: Object.freeze(['browser', 'ui', 'visual', 'e2e']),
24
+ 'sequential-thinking': Object.freeze(['analysis', 'planning', 'reasoning', 'security', 'review']),
25
+ });
26
+
27
+ export const DOMAIN_TAG_KEYWORDS = Object.freeze({
28
+ docs: Object.freeze(['docs', 'documentation', 'manual', 'guide', '문서', '가이드', '매뉴얼']),
29
+ reference: Object.freeze(['reference', 'spec', 'schema', 'official', '레퍼런스', '공식', '스펙', '스키마']),
30
+ api: Object.freeze(['api', 'endpoint', 'interface', 'sdk', '호출', '엔드포인트']),
31
+ sdk: Object.freeze(['sdk', 'library', 'package', 'framework', '라이브러리', '패키지', '프레임워크']),
32
+ library: Object.freeze(['library', 'package', 'framework', 'module', '라이브러리', '패키지', '모듈']),
33
+ web: Object.freeze(['web', 'site', 'article', 'forum', 'blog', 'reddit', '웹', '사이트', '기사', '포럼', '블로그']),
34
+ search: Object.freeze(['search', 'browse', 'lookup', 'find', '검색', '조회', '탐색', '찾아']),
35
+ news: Object.freeze(['latest', 'recent', 'news', 'today', 'release', 'announcement', '최신', '최근', '뉴스', '오늘', '릴리즈', '공지']),
36
+ current: Object.freeze(['current', 'status', 'pricing', 'changelog', 'up-to-date', '현재', '상태', '가격', '변경사항']),
37
+ research: Object.freeze(['research', 'verify', 'fact-check', 'investigate', '리서치', '검증', '조사']),
38
+ verification: Object.freeze(['verify', 'validation', 'fact-check', 'audit', '검증', '확인', '감사']),
39
+ code: Object.freeze(['code', 'repo', 'repository', 'source', 'implementation', 'bug', 'fix', 'test', 'snippet', 'cli', '코드', '리포', '저장소', '구현', '버그', '테스트', '예제', '스크립트']),
40
+ repository: Object.freeze(['repo', 'repository', 'source', 'git', 'github', '리포', '저장소', '소스']),
41
+ examples: Object.freeze(['example', 'examples', 'snippet', 'sample', '예제', '샘플']),
42
+ browser: Object.freeze(['browser', 'page', 'dom', 'screenshot', 'render', '브라우저', '페이지', '스크린샷', '렌더']),
43
+ ui: Object.freeze(['ui', 'ux', 'layout', 'responsive', 'css', 'html', '디자인', '레이아웃', '반응형']),
44
+ visual: Object.freeze(['visual', 'screenshot', 'layout', 'render', 'screen', '화면', '시각', '스크린샷']),
45
+ e2e: Object.freeze(['playwright', 'e2e', 'click', 'navigate', 'automation', 'playwright', '클릭', '이동', '자동화']),
46
+ analysis: Object.freeze(['analysis', 'analyze', 'audit', 'compare', 'root cause', '분석', '검토', '비교', '원인']),
47
+ planning: Object.freeze(['plan', 'planning', 'strategy', 'design', '계획', '전략', '설계']),
48
+ reasoning: Object.freeze(['reason', 'reasoning', 'think', 'critique', '추론', '사고', '비평']),
49
+ security: Object.freeze(['security', 'risk', 'threat', 'vulnerability', '보안', '위험', '취약점']),
50
+ review: Object.freeze(['review', 'reviewer', 'inspect', '리뷰', '검수']),
51
+ });
52
+
53
+ export const SERVER_EXPLICIT_KEYWORDS = Object.freeze({
54
+ context7: Object.freeze(['context7']),
55
+ 'brave-search': Object.freeze(['brave', 'brave-search']),
56
+ exa: Object.freeze(['exa']),
57
+ tavily: Object.freeze(['tavily']),
58
+ playwright: Object.freeze(['playwright']),
59
+ 'sequential-thinking': Object.freeze(['sequential-thinking', 'sequential thinking']),
60
+ });
61
+
62
+ export function uniqueStrings(values = []) {
63
+ return [...new Set(
64
+ values
65
+ .filter((value) => typeof value === 'string' && value.trim())
66
+ .map((value) => value.trim()),
67
+ )];
68
+ }
69
+
70
+ export function inferDomainTagsFromText(text = '') {
71
+ if (typeof text !== 'string' || !text.trim()) return [];
72
+ const normalized = text.toLocaleLowerCase();
73
+ const matched = [];
74
+
75
+ for (const [tag, keywords] of Object.entries(DOMAIN_TAG_KEYWORDS)) {
76
+ if (keywords.some((keyword) => normalized.includes(String(keyword).toLocaleLowerCase()))) {
77
+ matched.push(tag);
78
+ }
79
+ }
80
+
81
+ return uniqueStrings(matched);
82
+ }
83
+
84
+ export function getDefaultServerMetadata(serverName = '') {
85
+ const toolCount = MCP_SERVER_TOOL_CATALOG[serverName]?.length || 0;
86
+ const domainTags = uniqueStrings([
87
+ ...(MCP_SERVER_DOMAIN_TAGS[serverName] || []),
88
+ ...inferDomainTagsFromText(serverName),
89
+ ]);
90
+
91
+ return {
92
+ tool_count: toolCount,
93
+ domain_tags: domainTags,
94
+ };
95
+ }
96
+
97
+ export function normalizeServerMetadata(serverName = '', metadata = {}) {
98
+ const fallback = getDefaultServerMetadata(serverName);
99
+ const toolCount = Number.isFinite(metadata.tool_count)
100
+ ? Math.max(0, Math.trunc(metadata.tool_count))
101
+ : fallback.tool_count;
102
+ const domainTags = uniqueStrings([
103
+ ...fallback.domain_tags,
104
+ ...(Array.isArray(metadata.domain_tags) ? metadata.domain_tags : []),
105
+ ...inferDomainTagsFromText([
106
+ serverName,
107
+ typeof metadata.command === 'string' ? metadata.command : '',
108
+ typeof metadata.url === 'string' ? metadata.url : '',
109
+ ...(Array.isArray(metadata.args) ? metadata.args : []),
110
+ ...(metadata.env && typeof metadata.env === 'object' ? Object.keys(metadata.env) : []),
111
+ ].join(' ')),
112
+ ]);
113
+
114
+ return {
115
+ tool_count: toolCount,
116
+ domain_tags: domainTags,
117
+ };
118
+ }
@@ -0,0 +1,220 @@
1
+ import {
2
+ access,
3
+ mkdir,
4
+ open,
5
+ readdir,
6
+ readFile,
7
+ rename,
8
+ rm,
9
+ writeFile,
10
+ } from "node:fs/promises";
11
+ import { basename, join } from "node:path";
12
+
13
+ const DEFAULT_STATE_DIR = join(process.cwd(), ".tfx", "state");
14
+ const STOP_HOOKS = new Map();
15
+
16
+ function stateFilePath(stateDir, skillName) {
17
+ return join(stateDir, `${skillName}-active.json`);
18
+ }
19
+
20
+ function assertValidSkillName(skillName) {
21
+ if (basename(skillName) !== skillName) {
22
+ throw new Error(`Invalid skill name: ${skillName}`);
23
+ }
24
+ }
25
+
26
+ function assertValidOnStop(onStop) {
27
+ if (onStop !== undefined && typeof onStop !== "function") {
28
+ throw new TypeError("onStop must be a function");
29
+ }
30
+ }
31
+
32
+ async function readStateFile(filePath) {
33
+ try {
34
+ const raw = await readFile(filePath, "utf8");
35
+ return JSON.parse(raw);
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+
41
+ function rememberStopHook(filePath, onStop) {
42
+ if (typeof onStop === "function") {
43
+ STOP_HOOKS.set(filePath, onStop);
44
+ return;
45
+ }
46
+ STOP_HOOKS.delete(filePath);
47
+ }
48
+
49
+ function logStopHookWarning(message, error) {
50
+ if (error) {
51
+ console.warn(message, error);
52
+ return;
53
+ }
54
+ console.warn(message);
55
+ }
56
+
57
+ async function runStopHook(filePath, state, onStop) {
58
+ if (!state?.hasStopHook) {
59
+ STOP_HOOKS.delete(filePath);
60
+ return;
61
+ }
62
+
63
+ const hook = onStop ?? STOP_HOOKS.get(filePath);
64
+ STOP_HOOKS.delete(filePath);
65
+
66
+ if (typeof hook !== "function") {
67
+ return;
68
+ }
69
+
70
+ try {
71
+ await hook({
72
+ skillName: state.skillName,
73
+ filePath,
74
+ state,
75
+ });
76
+ } catch (error) {
77
+ logStopHookWarning(
78
+ `Failed to run stop-hook for skill: ${state.skillName}`,
79
+ error,
80
+ );
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Activate a skill by writing its state file.
86
+ * Throws if the skill is already active.
87
+ *
88
+ * @param {string} skillName
89
+ * @param {{ stateDir?: string, onStop?: (() => Promise<void> | void) }} options
90
+ */
91
+ export async function activateSkill(
92
+ skillName,
93
+ { stateDir = DEFAULT_STATE_DIR, onStop } = {},
94
+ ) {
95
+ assertValidSkillName(skillName);
96
+ assertValidOnStop(onStop);
97
+
98
+ await mkdir(stateDir, { recursive: true });
99
+
100
+ const filePath = stateFilePath(stateDir, skillName);
101
+ const lockPath = `${filePath}.lock`;
102
+ const tmpPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
103
+ let lockHandle;
104
+
105
+ try {
106
+ lockHandle = await open(lockPath, "wx");
107
+ try {
108
+ await access(filePath);
109
+ throw new Error(`Skill already active: ${skillName}`);
110
+ } catch (error) {
111
+ if (error?.message === `Skill already active: ${skillName}`) {
112
+ throw error;
113
+ }
114
+ if (error?.code !== "ENOENT") {
115
+ throw error;
116
+ }
117
+ }
118
+
119
+ const state = {
120
+ skillName,
121
+ pid: process.pid,
122
+ activatedAt: Date.now(),
123
+ hasStopHook: typeof onStop === "function",
124
+ };
125
+ await writeFile(tmpPath, JSON.stringify(state), "utf8");
126
+ await rename(tmpPath, filePath);
127
+ rememberStopHook(filePath, onStop);
128
+ } finally {
129
+ await rm(tmpPath, { force: true }).catch(() => {});
130
+ if (lockHandle) {
131
+ await lockHandle.close().catch(() => {});
132
+ }
133
+ await rm(lockPath, { force: true }).catch(() => {});
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Deactivate a skill by removing its state file.
139
+ * Does not throw if the file does not exist.
140
+ *
141
+ * @param {string} skillName
142
+ * @param {{ stateDir?: string, onStop?: (() => Promise<void> | void) }} options
143
+ */
144
+ export async function deactivateSkill(
145
+ skillName,
146
+ { stateDir = DEFAULT_STATE_DIR, onStop } = {},
147
+ ) {
148
+ assertValidOnStop(onStop);
149
+
150
+ const filePath = stateFilePath(stateDir, skillName);
151
+ const state = await readStateFile(filePath);
152
+
153
+ try {
154
+ await runStopHook(filePath, state, onStop);
155
+ } finally {
156
+ STOP_HOOKS.delete(filePath);
157
+ await rm(filePath, { force: true }).catch(() => {});
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Return all currently active skills by scanning *-active.json files.
163
+ *
164
+ * @param {{ stateDir?: string }} options
165
+ * @returns {Promise<Array<{ skillName: string, pid: number, activatedAt: number, hasStopHook?: boolean }>>}
166
+ */
167
+ export async function getActiveSkills({ stateDir = DEFAULT_STATE_DIR } = {}) {
168
+ let entries;
169
+ try {
170
+ entries = await readdir(stateDir);
171
+ } catch {
172
+ return [];
173
+ }
174
+
175
+ const results = [];
176
+ for (const entry of entries) {
177
+ if (!entry.endsWith("-active.json")) continue;
178
+ const state = await readStateFile(join(stateDir, entry));
179
+ if (state) {
180
+ results.push(state);
181
+ }
182
+ }
183
+ return results;
184
+ }
185
+
186
+ /**
187
+ * Remove state files for skills whose processes are no longer alive.
188
+ *
189
+ * @param {{ stateDir?: string }} options
190
+ * @returns {Promise<string[]>} list of pruned skill names
191
+ */
192
+ export async function pruneOrphanSkillStates({
193
+ stateDir = DEFAULT_STATE_DIR,
194
+ } = {}) {
195
+ const active = await getActiveSkills({ stateDir });
196
+ const pruned = [];
197
+
198
+ for (const state of active) {
199
+ let alive = true;
200
+ try {
201
+ process.kill(state.pid, 0);
202
+ } catch {
203
+ alive = false;
204
+ }
205
+
206
+ if (!alive) {
207
+ const filePath = stateFilePath(stateDir, state.skillName);
208
+ STOP_HOOKS.delete(filePath);
209
+ if (state.hasStopHook) {
210
+ logStopHookWarning(
211
+ `Skipping stop-hook for orphaned skill state: ${state.skillName}`,
212
+ );
213
+ }
214
+ await rm(filePath, { force: true }).catch(() => {});
215
+ pruned.push(state.skillName);
216
+ }
217
+ }
218
+
219
+ return pruned;
220
+ }