triflux 10.0.3 → 10.0.5
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/hooks/hook-orchestrator.mjs +48 -1
- package/hooks/keyword-rules.json +12 -26
- package/hub/cli-adapter-base.mjs +7 -6
- package/package.json +16 -4
- package/scripts/__tests__/gen-skill-docs.test.mjs +89 -1
- package/scripts/__tests__/skill-template.test.mjs +90 -0
- package/scripts/__tests__/smoke.test.mjs +34 -34
- package/scripts/gen-skill-docs.mjs +4 -1
- package/scripts/gen-skill-manifest.mjs +83 -0
- package/scripts/lib/skill-template.mjs +24 -0
- package/skills/merge-worktree/skill.json +5 -0
- package/skills/star-prompt/skill.json +4 -0
- package/skills/tfx-analysis/SKILL.md +1 -0
- package/skills/tfx-analysis/skill.json +12 -0
- package/skills/tfx-auto/SKILL.md.tmpl +45 -0
- package/skills/tfx-auto/skill.json +26 -0
- package/skills/tfx-auto-codex/SKILL.md +1 -0
- package/skills/tfx-auto-codex/skill.json +9 -0
- package/skills/tfx-autopilot/SKILL.md +1 -0
- package/skills/tfx-autopilot/skill.json +11 -0
- package/skills/tfx-autoresearch/SKILL.md +1 -0
- package/skills/tfx-autoresearch/skill.json +15 -0
- package/skills/tfx-autoroute/SKILL.md +1 -0
- package/skills/tfx-autoroute/skill.json +13 -0
- package/skills/tfx-codex/SKILL.md.tmpl +1 -0
- package/skills/tfx-codex/skill.json +8 -0
- package/skills/tfx-codex-swarm/skill.json +8 -0
- package/skills/tfx-consensus/SKILL.md +1 -0
- package/skills/tfx-consensus/skill.json +7 -0
- package/skills/tfx-debate/SKILL.md +1 -0
- package/skills/tfx-debate/skill.json +13 -0
- package/skills/tfx-deep-analysis/SKILL.md +1 -0
- package/skills/tfx-deep-analysis/skill.json +11 -0
- package/skills/tfx-deep-interview/skill.json +12 -0
- package/skills/tfx-deep-plan/SKILL.md +1 -0
- package/skills/tfx-deep-plan/skill.json +14 -0
- package/skills/tfx-deep-qa/SKILL.md +1 -0
- package/skills/tfx-deep-qa/skill.json +12 -0
- package/skills/tfx-deep-research/SKILL.md +1 -0
- package/skills/tfx-deep-research/skill.json +15 -0
- package/skills/tfx-deep-review/SKILL.md +1 -0
- package/skills/tfx-deep-review/skill.json +13 -0
- package/skills/tfx-doctor/skill.json +8 -0
- package/skills/tfx-find/SKILL.md.tmpl +1 -0
- package/skills/tfx-find/skill.json +12 -0
- package/skills/tfx-forge/skill.json +12 -0
- package/skills/tfx-fullcycle/SKILL.md +1 -0
- package/skills/tfx-fullcycle/skill.json +12 -0
- package/skills/tfx-gemini/SKILL.md +1 -0
- package/skills/tfx-gemini/skill.json +9 -0
- package/skills/tfx-hooks/skill.json +8 -0
- package/skills/tfx-hub/skill.json +8 -0
- package/skills/tfx-index/skill.json +11 -0
- package/skills/tfx-interview/SKILL.md +1 -0
- package/skills/tfx-interview/skill.json +13 -0
- package/skills/tfx-multi/skill.json +8 -0
- package/skills/tfx-panel/SKILL.md +1 -0
- package/skills/tfx-panel/skill.json +13 -0
- package/skills/tfx-persist/SKILL.md +1 -0
- package/skills/tfx-persist/skill.json +13 -0
- package/skills/tfx-plan/SKILL.md.tmpl +1 -0
- package/skills/tfx-plan/skill.json +11 -0
- package/skills/tfx-profile/skill.json +8 -0
- package/skills/tfx-prune/SKILL.md +1 -0
- package/skills/tfx-prune/skill.json +13 -0
- package/skills/tfx-psmux-rules/skill.json +8 -0
- package/skills/tfx-qa/SKILL.md.tmpl +1 -0
- package/skills/tfx-qa/skill.json +11 -0
- package/skills/tfx-ralph/SKILL.md +1 -0
- package/skills/tfx-ralph/skill.json +9 -0
- package/skills/tfx-remote-setup/skill.json +8 -0
- package/skills/tfx-remote-spawn/skill.json +9 -0
- package/skills/tfx-research/SKILL.md.tmpl +1 -0
- package/skills/tfx-research/skill.json +13 -0
- package/skills/tfx-review/SKILL.md.tmpl +1 -0
- package/skills/tfx-review/skill.json +11 -0
- package/skills/tfx-setup/skill.json +8 -0
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
// CLAUDE_PLUGIN_ROOT — ${PLUGIN_ROOT} 치환용
|
|
20
20
|
// HOME / USERPROFILE — ${HOME} 치환용
|
|
21
21
|
|
|
22
|
-
import { readFileSync, existsSync } from "node:fs";
|
|
22
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
23
23
|
import { join, dirname } from "node:path";
|
|
24
24
|
import { fileURLToPath } from "node:url";
|
|
25
25
|
import { execFileSync, execFile } from "node:child_process";
|
|
@@ -210,8 +210,55 @@ function mergeOutputs(accumulated, newOutput) {
|
|
|
210
210
|
}
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
+
// ── 라우팅 가중치 기록 ────────────────────────────────────────
|
|
214
|
+
function recordRouteOutcome(slug, mode, outcome) {
|
|
215
|
+
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
216
|
+
const projDir = join(home, ".gstack", "projects", slug);
|
|
217
|
+
const weightsPath = join(projDir, "routing-weights.json");
|
|
218
|
+
|
|
219
|
+
mkdirSync(projDir, { recursive: true });
|
|
220
|
+
|
|
221
|
+
const weights = existsSync(weightsPath)
|
|
222
|
+
? JSON.parse(readFileSync(weightsPath, "utf8"))
|
|
223
|
+
: { updated_at: null, total_routes: 0, overrides: 0, weights: { mode_bias: {}, profile_bias: {}, depth_bias: {} } };
|
|
224
|
+
|
|
225
|
+
weights.total_routes++;
|
|
226
|
+
weights.updated_at = new Date().toISOString();
|
|
227
|
+
|
|
228
|
+
const bias = weights.weights.mode_bias;
|
|
229
|
+
const current = bias[mode] || 0;
|
|
230
|
+
|
|
231
|
+
if (outcome === "override") {
|
|
232
|
+
bias[mode] = Math.max(0, current - 0.1);
|
|
233
|
+
weights.overrides++;
|
|
234
|
+
} else if (outcome === "completion") {
|
|
235
|
+
bias[mode] = Math.min(1, current + 0.05);
|
|
236
|
+
} else if (outcome === "abort") {
|
|
237
|
+
bias[mode] = Math.max(0, current - 0.1);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const total = Object.values(bias).reduce((s, v) => s + v, 0);
|
|
241
|
+
if (total > 0) {
|
|
242
|
+
for (const key of Object.keys(bias)) {
|
|
243
|
+
bias[key] = +(bias[key] / total).toFixed(3);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
writeFileSync(weightsPath, JSON.stringify(weights, null, 2), "utf8");
|
|
248
|
+
}
|
|
249
|
+
|
|
213
250
|
// ── 메인 ────────────────────────────────────────────────────
|
|
214
251
|
async function main() {
|
|
252
|
+
// CLI: --record-route <slug> <mode> <outcome>
|
|
253
|
+
const rrIdx = process.argv.indexOf("--record-route");
|
|
254
|
+
if (rrIdx !== -1) {
|
|
255
|
+
const slug = process.argv[rrIdx + 1];
|
|
256
|
+
const mode = process.argv[rrIdx + 2];
|
|
257
|
+
const outcome = process.argv[rrIdx + 3];
|
|
258
|
+
if (slug && mode && outcome) recordRouteOutcome(slug, mode, outcome);
|
|
259
|
+
process.exit(0);
|
|
260
|
+
}
|
|
261
|
+
|
|
215
262
|
const stdinRaw = readStdin();
|
|
216
263
|
const registry = loadRegistry();
|
|
217
264
|
|
package/hooks/keyword-rules.json
CHANGED
|
@@ -13,8 +13,7 @@
|
|
|
13
13
|
"priority": 0,
|
|
14
14
|
"supersedes": [
|
|
15
15
|
"tfx-multi",
|
|
16
|
-
"tfx-
|
|
17
|
-
"tfx-auto-codex",
|
|
16
|
+
"tfx-unified",
|
|
18
17
|
"tfx-codex",
|
|
19
18
|
"tfx-gemini"
|
|
20
19
|
],
|
|
@@ -71,34 +70,21 @@
|
|
|
71
70
|
"mcp_route": null
|
|
72
71
|
},
|
|
73
72
|
{
|
|
74
|
-
"id": "tfx-
|
|
73
|
+
"id": "tfx-unified",
|
|
75
74
|
"patterns": [
|
|
76
|
-
{
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
"
|
|
85
|
-
"tfx-codex"
|
|
86
|
-
],
|
|
87
|
-
"exclusive": false,
|
|
88
|
-
"state": null,
|
|
89
|
-
"mcp_route": null
|
|
90
|
-
},
|
|
91
|
-
{
|
|
92
|
-
"id": "tfx-auto",
|
|
93
|
-
"patterns": [
|
|
94
|
-
{
|
|
95
|
-
"source": "\\btfx[\\s-]?auto\\b",
|
|
96
|
-
"flags": "i"
|
|
97
|
-
}
|
|
75
|
+
{ "source": "\\btfx[\\s-]?auto\\b", "flags": "i" },
|
|
76
|
+
{ "source": "\\btfx[\\s-]?auto[\\s-]?codex\\b", "flags": "i" },
|
|
77
|
+
{ "source": "(?:만들어|고쳐|구현해|짜줘|수정해|바꿔)", "flags": "i" },
|
|
78
|
+
{ "source": "(?:리뷰해|검토해|봐줘|괜찮아)", "flags": "i" },
|
|
79
|
+
{ "source": "(?:테스트|검증|돌려봐|QA)", "flags": "i" },
|
|
80
|
+
{ "source": "(?:분석해|계획|설계해)", "flags": "i" },
|
|
81
|
+
{ "source": "(?:찾아봐|조사해|검색해)", "flags": "i" },
|
|
82
|
+
{ "source": "(?:정리해|슬롭|클린업)", "flags": "i" },
|
|
83
|
+
{ "source": "\\b(?:implement|build|fix|review|test|plan|analyze)\\b", "flags": "i" }
|
|
98
84
|
],
|
|
99
85
|
"skill": "tfx-auto",
|
|
100
86
|
"priority": 2,
|
|
101
|
-
"supersedes": [],
|
|
87
|
+
"supersedes": ["tfx-auto-codex"],
|
|
102
88
|
"exclusive": false,
|
|
103
89
|
"state": null,
|
|
104
90
|
"mcp_route": null
|
package/hub/cli-adapter-base.mjs
CHANGED
|
@@ -38,15 +38,16 @@ export function gte(minMinor) {
|
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
40
|
* Codex CLI 기능별 분기 객체.
|
|
41
|
-
*
|
|
41
|
+
* 실측 기반 임계값: 0.114.0에서 exec/skip-git-repo-check/color 확인됨.
|
|
42
|
+
* --output-last-message는 0.114.0에 없음 (0.117+ 추정).
|
|
42
43
|
*/
|
|
43
44
|
export const FEATURES = {
|
|
44
|
-
/** exec 서브커맨드 사용 가능 여부 */
|
|
45
|
-
get execSubcommand() { return gte(
|
|
46
|
-
/** --output-last-message 플래그 지원 여부 */
|
|
45
|
+
/** exec 서브커맨드 사용 가능 여부 (0.110+ 이전부터 존재) */
|
|
46
|
+
get execSubcommand() { return gte(110); },
|
|
47
|
+
/** --output-last-message 플래그 지원 여부 (0.117+) */
|
|
47
48
|
get outputLastMessage() { return gte(117); },
|
|
48
|
-
/** --color
|
|
49
|
-
get colorNever() { return gte(
|
|
49
|
+
/** --color <COLOR> 플래그 지원 여부 (exec와 동시 도입) */
|
|
50
|
+
get colorNever() { return gte(110); },
|
|
50
51
|
/** 플러그인 시스템 지원 여부 (향후 확장용) */
|
|
51
52
|
get pluginSystem() { return gte(120); },
|
|
52
53
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "triflux",
|
|
3
|
-
"version": "10.0.
|
|
3
|
+
"version": "10.0.5",
|
|
4
4
|
"description": "CLI-first multi-model orchestrator for Claude Code — route tasks to Codex, Gemini, and Claude",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -13,9 +13,11 @@
|
|
|
13
13
|
"tfx-doctor-tui": "bin/tfx-doctor-tui.mjs",
|
|
14
14
|
"tfx-setup-tui": "bin/tfx-setup-tui.mjs"
|
|
15
15
|
},
|
|
16
|
-
"engines": {
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=18.0.0"
|
|
18
|
+
},
|
|
17
19
|
"dependencies": {
|
|
18
|
-
"@triflux/core": "
|
|
20
|
+
"@triflux/core": "10.0.1",
|
|
19
21
|
"@triflux/remote": "^10.0.0-alpha.1"
|
|
20
22
|
},
|
|
21
23
|
"files": [
|
|
@@ -31,7 +33,17 @@
|
|
|
31
33
|
"README.md",
|
|
32
34
|
"LICENSE"
|
|
33
35
|
],
|
|
34
|
-
"keywords": [
|
|
36
|
+
"keywords": [
|
|
37
|
+
"claude-code",
|
|
38
|
+
"plugin",
|
|
39
|
+
"codex",
|
|
40
|
+
"gemini",
|
|
41
|
+
"cli-routing",
|
|
42
|
+
"orchestration",
|
|
43
|
+
"multi-model",
|
|
44
|
+
"triflux",
|
|
45
|
+
"tfx"
|
|
46
|
+
],
|
|
35
47
|
"author": "tellang",
|
|
36
48
|
"license": "MIT",
|
|
37
49
|
"homepage": "https://github.com/tellang/triflux#readme",
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { describe, it } from "node:test";
|
|
6
6
|
|
|
7
7
|
import { generateSkillDocs } from "../gen-skill-docs.mjs";
|
|
8
|
+
import { generateSkillManifests } from "../gen-skill-manifest.mjs";
|
|
8
9
|
|
|
9
10
|
function makeTempDir() {
|
|
10
11
|
return mkdtempSync(join(tmpdir(), "tfx-gen-skill-docs-"));
|
|
@@ -84,4 +85,91 @@ describe("gen-skill-docs", () => {
|
|
|
84
85
|
rmSync(root, { recursive: true, force: true });
|
|
85
86
|
}
|
|
86
87
|
});
|
|
88
|
+
|
|
89
|
+
it("partial 내부의 {{#include}}를 해석한다", () => {
|
|
90
|
+
const root = makeTempDir();
|
|
91
|
+
try {
|
|
92
|
+
const skillsDir = join(root, "skills");
|
|
93
|
+
const templatesDir = join(skillsDir, "_templates");
|
|
94
|
+
const sharedDir = join(skillsDir, "shared");
|
|
95
|
+
const skillDir = join(skillsDir, "tfx-inc");
|
|
96
|
+
|
|
97
|
+
mkdirSync(templatesDir, { recursive: true });
|
|
98
|
+
mkdirSync(sharedDir, { recursive: true });
|
|
99
|
+
mkdirSync(skillDir, { recursive: true });
|
|
100
|
+
|
|
101
|
+
writeFileSync(join(sharedDir, "args.md"), "ARGS={{SKILL_NAME}}", "utf8");
|
|
102
|
+
writeFileSync(join(templatesDir, "base.md"), "{{#include shared/args.md}}", "utf8");
|
|
103
|
+
writeFileSync(
|
|
104
|
+
join(skillDir, "SKILL.md.tmpl"),
|
|
105
|
+
"---\nname: tfx-inc\ndescription: inc test\n---\n{{> base}}\nend",
|
|
106
|
+
"utf8",
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const result = generateSkillDocs({ skillsDir, templatesDir, write: true });
|
|
110
|
+
assert.equal(result.count, 1);
|
|
111
|
+
|
|
112
|
+
const output = readFileSync(join(skillDir, "SKILL.md"), "utf8");
|
|
113
|
+
assert.match(output, /ARGS=tfx-inc/);
|
|
114
|
+
} finally {
|
|
115
|
+
rmSync(root, { recursive: true, force: true });
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe("gen-skill-manifest", () => {
|
|
121
|
+
it("SKILL.md frontmatter에서 skill.json을 생성한다", () => {
|
|
122
|
+
const root = makeTempDir();
|
|
123
|
+
try {
|
|
124
|
+
const skillsDir = join(root, "skills");
|
|
125
|
+
const skillDir = join(skillsDir, "tfx-manifest-test");
|
|
126
|
+
|
|
127
|
+
mkdirSync(skillDir, { recursive: true });
|
|
128
|
+
writeFileSync(
|
|
129
|
+
join(skillDir, "SKILL.md"),
|
|
130
|
+
[
|
|
131
|
+
"---",
|
|
132
|
+
"name: tfx-manifest-test",
|
|
133
|
+
"description: test manifest",
|
|
134
|
+
"triggers:",
|
|
135
|
+
" - test",
|
|
136
|
+
" - manifest",
|
|
137
|
+
"argument-hint: <arg>",
|
|
138
|
+
"internal: true",
|
|
139
|
+
"---",
|
|
140
|
+
"body",
|
|
141
|
+
].join("\n"),
|
|
142
|
+
"utf8",
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const result = generateSkillManifests({ skillsDir, write: true });
|
|
146
|
+
assert.equal(result.count, 1);
|
|
147
|
+
|
|
148
|
+
const manifest = JSON.parse(readFileSync(join(skillDir, "skill.json"), "utf8"));
|
|
149
|
+
assert.equal(manifest.name, "tfx-manifest-test");
|
|
150
|
+
assert.equal(manifest.description, "test manifest");
|
|
151
|
+
assert.deepEqual(manifest.triggers, ["test", "manifest"]);
|
|
152
|
+
assert.equal(manifest.argument_hint, "<arg>");
|
|
153
|
+
assert.equal(manifest.internal, true);
|
|
154
|
+
} finally {
|
|
155
|
+
rmSync(root, { recursive: true, force: true });
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("frontmatter가 없는 SKILL.md는 건너뛴다", () => {
|
|
160
|
+
const root = makeTempDir();
|
|
161
|
+
try {
|
|
162
|
+
const skillsDir = join(root, "skills");
|
|
163
|
+
const skillDir = join(skillsDir, "tfx-no-fm");
|
|
164
|
+
|
|
165
|
+
mkdirSync(skillDir, { recursive: true });
|
|
166
|
+
writeFileSync(join(skillDir, "SKILL.md"), "no frontmatter", "utf8");
|
|
167
|
+
|
|
168
|
+
const result = generateSkillManifests({ skillsDir, write: true });
|
|
169
|
+
assert.equal(result.count, 0);
|
|
170
|
+
assert.equal(existsSync(join(skillDir, "skill.json")), false);
|
|
171
|
+
} finally {
|
|
172
|
+
rmSync(root, { recursive: true, force: true });
|
|
173
|
+
}
|
|
174
|
+
});
|
|
87
175
|
});
|
|
@@ -6,11 +6,14 @@ import { describe, it } from "node:test";
|
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
8
|
buildSkillTemplateContext,
|
|
9
|
+
loadSkillManifest,
|
|
9
10
|
loadTemplatePartials,
|
|
10
11
|
parseFrontmatter,
|
|
12
|
+
parseFrontmatterWithManifest,
|
|
11
13
|
renderSkillTemplate,
|
|
12
14
|
} from "../lib/skill-template.mjs";
|
|
13
15
|
import { generateSkillDocs } from "../gen-skill-docs.mjs";
|
|
16
|
+
import { generateSkillManifests } from "../gen-skill-manifest.mjs";
|
|
14
17
|
|
|
15
18
|
function makeTempDir() {
|
|
16
19
|
return mkdtempSync(join(tmpdir(), "tfx-skill-template-"));
|
|
@@ -159,6 +162,93 @@ describe("skill-template engine", () => {
|
|
|
159
162
|
assert.equal(renderedLines.at(-1), "name=big-template");
|
|
160
163
|
});
|
|
161
164
|
|
|
165
|
+
it("{{#include shared/*.md}}로 파일을 인라인 확장한다", () => {
|
|
166
|
+
const root = makeTempDir();
|
|
167
|
+
try {
|
|
168
|
+
const sharedDir = join(root, "shared");
|
|
169
|
+
mkdirSync(sharedDir, { recursive: true });
|
|
170
|
+
writeFileSync(join(sharedDir, "telemetry.md"), "TEL={{SKILL_NAME}}", "utf8");
|
|
171
|
+
|
|
172
|
+
const template = "before\n{{#include shared/telemetry.md}}\nafter";
|
|
173
|
+
const output = renderSkillTemplate(template, { SKILL_NAME: "test-skill" }, {
|
|
174
|
+
partials: {},
|
|
175
|
+
includeBaseDir: root,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
assert.match(output, /before/);
|
|
179
|
+
assert.match(output, /TEL=test-skill/);
|
|
180
|
+
assert.match(output, /after/);
|
|
181
|
+
} finally {
|
|
182
|
+
rmSync(root, { recursive: true, force: true });
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("loadSkillManifest는 skill.json이 있으면 파싱한다", () => {
|
|
187
|
+
const root = makeTempDir();
|
|
188
|
+
try {
|
|
189
|
+
writeFileSync(join(root, "skill.json"), JSON.stringify({
|
|
190
|
+
name: "tfx-test",
|
|
191
|
+
description: "test skill",
|
|
192
|
+
triggers: ["test"],
|
|
193
|
+
}), "utf8");
|
|
194
|
+
|
|
195
|
+
const manifest = loadSkillManifest(root);
|
|
196
|
+
assert.equal(manifest.name, "tfx-test");
|
|
197
|
+
assert.deepEqual(manifest.triggers, ["test"]);
|
|
198
|
+
} finally {
|
|
199
|
+
rmSync(root, { recursive: true, force: true });
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("loadSkillManifest는 skill.json이 없으면 null을 반환한다", () => {
|
|
204
|
+
const root = makeTempDir();
|
|
205
|
+
try {
|
|
206
|
+
assert.equal(loadSkillManifest(root), null);
|
|
207
|
+
} finally {
|
|
208
|
+
rmSync(root, { recursive: true, force: true });
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("parseFrontmatterWithManifest는 skill.json 우선으로 병합한다", () => {
|
|
213
|
+
const root = makeTempDir();
|
|
214
|
+
try {
|
|
215
|
+
writeFileSync(join(root, "skill.json"), JSON.stringify({
|
|
216
|
+
name: "manifest-name",
|
|
217
|
+
description: "manifest-desc",
|
|
218
|
+
internal: true,
|
|
219
|
+
}), "utf8");
|
|
220
|
+
|
|
221
|
+
const source = [
|
|
222
|
+
"---",
|
|
223
|
+
"name: yaml-name",
|
|
224
|
+
"description: yaml-desc",
|
|
225
|
+
"deep: true",
|
|
226
|
+
"---",
|
|
227
|
+
"body",
|
|
228
|
+
].join("\n");
|
|
229
|
+
|
|
230
|
+
const result = parseFrontmatterWithManifest(source, root);
|
|
231
|
+
assert.equal(result.data.name, "manifest-name");
|
|
232
|
+
assert.equal(result.data.description, "manifest-desc");
|
|
233
|
+
assert.equal(result.data.internal, true);
|
|
234
|
+
assert.equal(result.data.deep, true);
|
|
235
|
+
assert.equal(result.body, "body");
|
|
236
|
+
} finally {
|
|
237
|
+
rmSync(root, { recursive: true, force: true });
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("parseFrontmatterWithManifest는 skill.json 없으면 YAML fallback한다", () => {
|
|
242
|
+
const root = makeTempDir();
|
|
243
|
+
try {
|
|
244
|
+
const source = "---\nname: yaml-only\n---\nbody";
|
|
245
|
+
const result = parseFrontmatterWithManifest(source, root);
|
|
246
|
+
assert.equal(result.data.name, "yaml-only");
|
|
247
|
+
} finally {
|
|
248
|
+
rmSync(root, { recursive: true, force: true });
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
162
252
|
it("gen-skill-docs에서 누락 partial 참조 시 에러를 전파한다", () => {
|
|
163
253
|
const root = makeTempDir();
|
|
164
254
|
try {
|
|
@@ -1,34 +1,34 @@
|
|
|
1
|
-
import { describe, it } from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
|
|
4
|
-
describe("smoke: 주요 모듈 import 검증", () => {
|
|
5
|
-
it("scripts/lib/keyword-rules.mjs — 순수 함수 export", async () => {
|
|
6
|
-
const mod = await import("../lib/keyword-rules.mjs");
|
|
7
|
-
assert.equal(typeof mod.loadRules, "function");
|
|
8
|
-
assert.equal(typeof mod.compileRules, "function");
|
|
9
|
-
assert.equal(typeof mod.matchRules, "function");
|
|
10
|
-
assert.equal(typeof mod.resolveConflicts, "function");
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
it("hub/team/shared.mjs — ANSI 상수 export", async () => {
|
|
14
|
-
const mod = await import("../../hub/team/shared.mjs");
|
|
15
|
-
assert.equal(typeof mod.AMBER, "string");
|
|
16
|
-
assert.equal(typeof mod.RESET, "string");
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it("hub/team/staleState.mjs — stale 상태 유틸 export", async () => {
|
|
20
|
-
const mod = await import("../../hub/team/staleState.mjs");
|
|
21
|
-
assert.equal(typeof mod.TEAM_STATE_FILE_NAME, "string");
|
|
22
|
-
assert.equal(typeof mod.STALE_TEAM_MAX_AGE_MS, "number");
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it("hub/pipeline/transitions.mjs — 파이프라인 전이 규칙 export", async () => {
|
|
26
|
-
const mod = await import("../../hub/pipeline/transitions.mjs");
|
|
27
|
-
assert.ok(Array.isArray(mod.PHASES));
|
|
28
|
-
assert.ok(mod.PHASES.includes("plan"));
|
|
29
|
-
assert.ok(mod.PHASES.includes("complete"));
|
|
30
|
-
assert.ok(mod.TERMINAL instanceof Set);
|
|
31
|
-
assert.ok(mod.TERMINAL.has("complete"));
|
|
32
|
-
assert.ok(mod.TERMINAL.has("failed"));
|
|
33
|
-
});
|
|
34
|
-
});
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
describe("smoke: 주요 모듈 import 검증", () => {
|
|
5
|
+
it("scripts/lib/keyword-rules.mjs — 순수 함수 export", async () => {
|
|
6
|
+
const mod = await import("../lib/keyword-rules.mjs");
|
|
7
|
+
assert.equal(typeof mod.loadRules, "function");
|
|
8
|
+
assert.equal(typeof mod.compileRules, "function");
|
|
9
|
+
assert.equal(typeof mod.matchRules, "function");
|
|
10
|
+
assert.equal(typeof mod.resolveConflicts, "function");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("hub/team/shared.mjs — ANSI 상수 export", async () => {
|
|
14
|
+
const mod = await import("../../hub/team/shared.mjs");
|
|
15
|
+
assert.equal(typeof mod.AMBER, "string");
|
|
16
|
+
assert.equal(typeof mod.RESET, "string");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("hub/team/staleState.mjs — stale 상태 유틸 export", async () => {
|
|
20
|
+
const mod = await import("../../hub/team/staleState.mjs");
|
|
21
|
+
assert.equal(typeof mod.TEAM_STATE_FILE_NAME, "string");
|
|
22
|
+
assert.equal(typeof mod.STALE_TEAM_MAX_AGE_MS, "number");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("hub/pipeline/transitions.mjs — 파이프라인 전이 규칙 export", async () => {
|
|
26
|
+
const mod = await import("../../hub/pipeline/transitions.mjs");
|
|
27
|
+
assert.ok(Array.isArray(mod.PHASES));
|
|
28
|
+
assert.ok(mod.PHASES.includes("plan"));
|
|
29
|
+
assert.ok(mod.PHASES.includes("complete"));
|
|
30
|
+
assert.ok(mod.TERMINAL instanceof Set);
|
|
31
|
+
assert.ok(mod.TERMINAL.has("complete"));
|
|
32
|
+
assert.ok(mod.TERMINAL.has("failed"));
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -65,7 +65,10 @@ export function generateSkillDocs({
|
|
|
65
65
|
for (const templatePath of templateFiles) {
|
|
66
66
|
const templateContent = readFileSync(templatePath, "utf8");
|
|
67
67
|
const context = createRenderContext(templateContent, templatePath);
|
|
68
|
-
const rendered = renderSkillTemplate(templateContent, context, {
|
|
68
|
+
const rendered = renderSkillTemplate(templateContent, context, {
|
|
69
|
+
partials,
|
|
70
|
+
includeBaseDir: skillsDir,
|
|
71
|
+
});
|
|
69
72
|
const outputPath = resolveOutputPath(templatePath);
|
|
70
73
|
|
|
71
74
|
if (write) {
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
import { parseFrontmatter } from "./lib/skill-template.mjs";
|
|
6
|
+
|
|
7
|
+
function collectSkillDirs(skillsDir) {
|
|
8
|
+
return readdirSync(skillsDir, { withFileTypes: true })
|
|
9
|
+
.filter((entry) => entry.isDirectory() && !entry.name.startsWith("_"))
|
|
10
|
+
.map((entry) => ({
|
|
11
|
+
name: entry.name,
|
|
12
|
+
dir: join(skillsDir, entry.name),
|
|
13
|
+
}))
|
|
14
|
+
.filter(({ dir }) => existsSync(join(dir, "SKILL.md")))
|
|
15
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function extractManifest(skillMdContent) {
|
|
19
|
+
const { data } = parseFrontmatter(skillMdContent);
|
|
20
|
+
if (!data.name) return null;
|
|
21
|
+
|
|
22
|
+
const manifest = { name: data.name };
|
|
23
|
+
if (data.description) manifest.description = data.description;
|
|
24
|
+
if (data.triggers) manifest.triggers = data.triggers;
|
|
25
|
+
if (data["argument-hint"]) manifest.argument_hint = data["argument-hint"];
|
|
26
|
+
if (data.internal === true || data.internal === "true") manifest.internal = true;
|
|
27
|
+
|
|
28
|
+
return manifest;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function generateSkillManifests({ skillsDir, write = true } = {}) {
|
|
32
|
+
if (!skillsDir) throw new Error("skillsDir is required");
|
|
33
|
+
|
|
34
|
+
const skillDirs = collectSkillDirs(skillsDir);
|
|
35
|
+
const generated = [];
|
|
36
|
+
|
|
37
|
+
for (const { name, dir } of skillDirs) {
|
|
38
|
+
const skillMdPath = join(dir, "SKILL.md");
|
|
39
|
+
const content = readFileSync(skillMdPath, "utf8");
|
|
40
|
+
const manifest = extractManifest(content);
|
|
41
|
+
|
|
42
|
+
if (!manifest) continue;
|
|
43
|
+
|
|
44
|
+
const manifestPath = join(dir, "skill.json");
|
|
45
|
+
const json = JSON.stringify(manifest, null, 2) + "\n";
|
|
46
|
+
|
|
47
|
+
if (write) {
|
|
48
|
+
const existing = existsSync(manifestPath)
|
|
49
|
+
? readFileSync(manifestPath, "utf8")
|
|
50
|
+
: null;
|
|
51
|
+
if (existing !== json) {
|
|
52
|
+
writeFileSync(manifestPath, json, "utf8");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
generated.push({ name, manifestPath, manifest });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return { generated, count: generated.length };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function runCli() {
|
|
63
|
+
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
|
64
|
+
const repoRoot = resolve(scriptDir, "..");
|
|
65
|
+
const skillsDir = join(repoRoot, "skills");
|
|
66
|
+
|
|
67
|
+
const result = generateSkillManifests({ skillsDir });
|
|
68
|
+
console.log(`Generated ${result.count} skill.json file(s).`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const isDirectRun = process.argv[1]
|
|
72
|
+
? resolve(process.argv[1]) === fileURLToPath(import.meta.url)
|
|
73
|
+
: false;
|
|
74
|
+
|
|
75
|
+
if (isDirectRun) {
|
|
76
|
+
try {
|
|
77
|
+
runCli();
|
|
78
|
+
} catch (error) {
|
|
79
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
80
|
+
console.error(`[gen-skill-manifest] ${message}`);
|
|
81
|
+
process.exitCode = 1;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -317,6 +317,30 @@ export function loadTemplatePartials(partialsDir) {
|
|
|
317
317
|
return partials;
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
+
export function loadSkillManifest(skillDir) {
|
|
321
|
+
const manifestPath = join(skillDir, "skill.json");
|
|
322
|
+
if (!existsSync(manifestPath)) return null;
|
|
323
|
+
|
|
324
|
+
const raw = readFileSync(manifestPath, "utf8");
|
|
325
|
+
return JSON.parse(raw);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export function parseFrontmatterWithManifest(source, skillDir) {
|
|
329
|
+
const manifest = skillDir ? loadSkillManifest(skillDir) : null;
|
|
330
|
+
const { data: yamlData, body } = parseFrontmatter(source);
|
|
331
|
+
|
|
332
|
+
if (!manifest) return { data: yamlData, body };
|
|
333
|
+
|
|
334
|
+
const merged = { ...yamlData };
|
|
335
|
+
if (manifest.name) merged.name = manifest.name;
|
|
336
|
+
if (manifest.description) merged.description = manifest.description;
|
|
337
|
+
if (manifest.triggers) merged.triggers = manifest.triggers;
|
|
338
|
+
if (manifest.argument_hint) merged["argument-hint"] = manifest.argument_hint;
|
|
339
|
+
if (manifest.internal != null) merged.internal = manifest.internal;
|
|
340
|
+
|
|
341
|
+
return { data: merged, body };
|
|
342
|
+
}
|
|
343
|
+
|
|
320
344
|
export function renderSkillTemplate(template, context = {}, options = {}) {
|
|
321
345
|
const { partials = {}, includes = {}, includeBaseDir = "" } = options;
|
|
322
346
|
return renderWithContext(template, context, partials, {
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "star-prompt",
|
|
3
|
+
"description": "CLI 프로젝트의 setup/postinstall 흐름에 GitHub 스타 요청 프롬프트를 추가한다. gh CLI 인증 확인 → 이미 스타 여부 감지 → 인터랙티브 confirm → gh API로 자동 스타. Apple 스타일 UX 카피 포함. 'star prompt', '스타 요청', '리포 스타', 'star request', '깃헙 스타 넣어줘', 'star 눌러달라고', '응원 요청' 같은 요청에 사용한다."
|
|
4
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tfx-analysis",
|
|
3
|
+
"description": "코드나 아키텍처를 분석해야 할 때 사용한다. '코드 분석', 'code analysis', '아키텍처 분석', '이 코드 어떻게 돌아가?', '구조 파악' 같은 요청에 반드시 사용. 코드 품질, 보안, 성능, 복잡도 분석이 필요한 모든 상황에 적극 활용.",
|
|
4
|
+
"triggers": [
|
|
5
|
+
"코드 분석",
|
|
6
|
+
"code analysis",
|
|
7
|
+
"아키텍처 분석",
|
|
8
|
+
"analysis"
|
|
9
|
+
],
|
|
10
|
+
"argument_hint": "<분석 대상 — 파일, 디렉토리, 또는 주제>",
|
|
11
|
+
"internal": true
|
|
12
|
+
}
|