triflux 10.0.4 → 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.
Files changed (76) hide show
  1. package/hooks/hook-orchestrator.mjs +48 -1
  2. package/hooks/keyword-rules.json +12 -26
  3. package/hub/cli-adapter-base.mjs +7 -6
  4. package/package.json +1 -1
  5. package/scripts/__tests__/gen-skill-docs.test.mjs +89 -1
  6. package/scripts/__tests__/skill-template.test.mjs +90 -0
  7. package/scripts/gen-skill-docs.mjs +4 -1
  8. package/scripts/gen-skill-manifest.mjs +83 -0
  9. package/scripts/lib/skill-template.mjs +24 -0
  10. package/skills/merge-worktree/skill.json +5 -0
  11. package/skills/star-prompt/skill.json +4 -0
  12. package/skills/tfx-analysis/SKILL.md +1 -0
  13. package/skills/tfx-analysis/skill.json +12 -0
  14. package/skills/tfx-auto/SKILL.md.tmpl +45 -0
  15. package/skills/tfx-auto/skill.json +26 -0
  16. package/skills/tfx-auto-codex/SKILL.md +1 -0
  17. package/skills/tfx-auto-codex/skill.json +9 -0
  18. package/skills/tfx-autopilot/SKILL.md +1 -0
  19. package/skills/tfx-autopilot/skill.json +11 -0
  20. package/skills/tfx-autoresearch/SKILL.md +1 -0
  21. package/skills/tfx-autoresearch/skill.json +15 -0
  22. package/skills/tfx-autoroute/SKILL.md +1 -0
  23. package/skills/tfx-autoroute/skill.json +13 -0
  24. package/skills/tfx-codex/SKILL.md.tmpl +1 -0
  25. package/skills/tfx-codex/skill.json +8 -0
  26. package/skills/tfx-codex-swarm/skill.json +8 -0
  27. package/skills/tfx-consensus/SKILL.md +1 -0
  28. package/skills/tfx-consensus/skill.json +7 -0
  29. package/skills/tfx-debate/SKILL.md +1 -0
  30. package/skills/tfx-debate/skill.json +13 -0
  31. package/skills/tfx-deep-analysis/SKILL.md +1 -0
  32. package/skills/tfx-deep-analysis/skill.json +11 -0
  33. package/skills/tfx-deep-interview/skill.json +12 -0
  34. package/skills/tfx-deep-plan/SKILL.md +1 -0
  35. package/skills/tfx-deep-plan/skill.json +14 -0
  36. package/skills/tfx-deep-qa/SKILL.md +1 -0
  37. package/skills/tfx-deep-qa/skill.json +12 -0
  38. package/skills/tfx-deep-research/SKILL.md +1 -0
  39. package/skills/tfx-deep-research/skill.json +15 -0
  40. package/skills/tfx-deep-review/SKILL.md +1 -0
  41. package/skills/tfx-deep-review/skill.json +13 -0
  42. package/skills/tfx-doctor/skill.json +8 -0
  43. package/skills/tfx-find/SKILL.md.tmpl +1 -0
  44. package/skills/tfx-find/skill.json +12 -0
  45. package/skills/tfx-forge/skill.json +12 -0
  46. package/skills/tfx-fullcycle/SKILL.md +1 -0
  47. package/skills/tfx-fullcycle/skill.json +12 -0
  48. package/skills/tfx-gemini/SKILL.md +1 -0
  49. package/skills/tfx-gemini/skill.json +9 -0
  50. package/skills/tfx-hooks/skill.json +8 -0
  51. package/skills/tfx-hub/skill.json +8 -0
  52. package/skills/tfx-index/skill.json +11 -0
  53. package/skills/tfx-interview/SKILL.md +1 -0
  54. package/skills/tfx-interview/skill.json +13 -0
  55. package/skills/tfx-multi/skill.json +8 -0
  56. package/skills/tfx-panel/SKILL.md +1 -0
  57. package/skills/tfx-panel/skill.json +13 -0
  58. package/skills/tfx-persist/SKILL.md +1 -0
  59. package/skills/tfx-persist/skill.json +13 -0
  60. package/skills/tfx-plan/SKILL.md.tmpl +1 -0
  61. package/skills/tfx-plan/skill.json +11 -0
  62. package/skills/tfx-profile/skill.json +8 -0
  63. package/skills/tfx-prune/SKILL.md +1 -0
  64. package/skills/tfx-prune/skill.json +13 -0
  65. package/skills/tfx-psmux-rules/skill.json +8 -0
  66. package/skills/tfx-qa/SKILL.md.tmpl +1 -0
  67. package/skills/tfx-qa/skill.json +11 -0
  68. package/skills/tfx-ralph/SKILL.md +1 -0
  69. package/skills/tfx-ralph/skill.json +9 -0
  70. package/skills/tfx-remote-setup/skill.json +8 -0
  71. package/skills/tfx-remote-spawn/skill.json +9 -0
  72. package/skills/tfx-research/SKILL.md.tmpl +1 -0
  73. package/skills/tfx-research/skill.json +13 -0
  74. package/skills/tfx-review/SKILL.md.tmpl +1 -0
  75. package/skills/tfx-review/skill.json +11 -0
  76. 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
 
@@ -13,8 +13,7 @@
13
13
  "priority": 0,
14
14
  "supersedes": [
15
15
  "tfx-multi",
16
- "tfx-auto",
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-auto-codex",
73
+ "id": "tfx-unified",
75
74
  "patterns": [
76
- {
77
- "source": "\\btfx[\\s-]?auto[\\s-]?codex\\b",
78
- "flags": "i"
79
- }
80
- ],
81
- "skill": "tfx-auto-codex",
82
- "priority": 1,
83
- "supersedes": [
84
- "tfx-auto",
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
@@ -38,15 +38,16 @@ export function gte(minMinor) {
38
38
 
39
39
  /**
40
40
  * Codex CLI 기능별 분기 객체.
41
- * 117 = 0.117.0 (Rust 리라이트, exec 서브커맨드 도입)
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(117); },
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 never 플래그 지원 여부 */
49
- get colorNever() { return gte(117); },
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.4",
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": {
@@ -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 {
@@ -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, { partials });
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,5 @@
1
+ {
2
+ "name": "merge-worktree",
3
+ "description": "워크트리 브랜치를 main으로 squash-merge + conventional commit 자동 생성. codex-swarm 워크트리 자동 인식. '머지해', 'merge worktree', '워크트리 머지', '결과 수집', 'squash merge' 요청에 사용.",
4
+ "argument_hint": "[target-branch]"
5
+ }
@@ -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
+ }
@@ -1,4 +1,5 @@
1
1
  ---
2
+ internal: true
2
3
  name: tfx-analysis
3
4
  description: "코드나 아키텍처를 분석해야 할 때 사용한다. '코드 분석', 'code analysis', '아키텍처 분석', '이 코드 어떻게 돌아가?', '구조 파악' 같은 요청에 반드시 사용. 코드 품질, 보안, 성능, 복잡도 분석이 필요한 모든 상황에 적극 활용."
4
5
  triggers:
@@ -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
+ }
@@ -31,6 +31,51 @@ argument-hint: "<command|task> [args...]"
31
31
 
32
32
  {{> base}}
33
33
 
34
+ ### Step 0: 스마트 라우팅 (tfx-auto 진입 시 자동 실행)
35
+
36
+ preamble에서 routing-weights.json을 읽고, 사용자 입력을 분석하여 dispatch 결정.
37
+
38
+ ```bash
39
+ SLUG=$(basename "$(git rev-parse --show-toplevel 2>/dev/null)" 2>/dev/null || echo "unknown")
40
+ WEIGHTS_FILE="$HOME/.gstack/projects/$SLUG/routing-weights.json"
41
+ USER_MODE=""
42
+ if [ -f "$WEIGHTS_FILE" ]; then
43
+ USER_MODE=$(node -e "
44
+ const w=JSON.parse(require('fs').readFileSync('$WEIGHTS_FILE','utf8'));
45
+ const m=w.weights?.mode_bias||{};
46
+ const top=Object.entries(m).sort((a,b)=>b[1]-a[1])[0];
47
+ if(top && top[1]>0.3) console.log(top[0]);
48
+ " 2>/dev/null)
49
+ fi
50
+ echo "USER_PREFERRED_MODE: ${USER_MODE:-none}"
51
+ ```
52
+
53
+ 판단 기준 (우선순위 순):
54
+
55
+ 1. **사용자 명시 키워드** (최우선):
56
+ - "병렬", "swarm", "PRD 돌려" → `Skill("tfx-swarm")` dispatch
57
+ - "꼼꼼히", "제대로", "deep" → 해당 `tfx-deep-*` dispatch
58
+ - "끝까지", "멈추지마", "ralph" → `Skill("tfx-persist")` dispatch
59
+ - "multi", "팀", "협업" → `Skill("tfx-multi")` dispatch
60
+ - "codex로", "gemini로" → `Skill("tfx-codex")` 또는 `Skill("tfx-gemini")` dispatch
61
+
62
+ 2. **PRD 인자 분석**:
63
+ - PRD 경로 2개 이상 → `Skill("tfx-swarm")` dispatch
64
+ - PRD 1개 + XL 규모 → `Skill("tfx-fullcycle")` dispatch
65
+
66
+ 3. **선호도 가중치** (tiebreaker):
67
+ - USER_PREFERRED_MODE가 있고 가중치 > 0.3이면 제안
68
+ - "[tfx] 사용자 선호: {mode}. 이 모드로 실행할까요?" 1줄 표시
69
+ - 응답 없으면 기본(auto) 진행
70
+
71
+ 4. **기본**: 기존 tfx-auto 워크플로우 그대로 실행
72
+
73
+ dispatch 시 해당 스킬을 Skill 도구로 호출하고 **이 워크플로우를 종료**한다. dispatch하지 않으면 아래 기존 워크플로우 진행.
74
+
75
+ 라우팅 결정 후 1줄 표시:
76
+ ```
77
+ [tfx] 규모: {S/M/L/XL}, 모드: {mode} ({profile}) — 오버라이드: /tfx-multi, /tfx-swarm 등
78
+ ```
34
79
 
35
80
  > **MANDATORY RULES**
36
81
  >
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "tfx-auto",
3
+ "description": "통합 CLI 오케스트레이터. 커맨드 숏컷(단일) + 자동 분류/분해(병렬) + 수동 병렬. tfx-route.sh 기반. '코드 짜줘', '구현해줘', '만들어줘', '수정해줘', '고쳐줘', 'implement', 'build', 'fix' 같은 구현/수정 요청에 사용. CLI 라우팅이 필요한 모든 작업에 적극 활용.",
4
+ "triggers": [
5
+ "tfx-auto",
6
+ "implement",
7
+ "build",
8
+ "research",
9
+ "brainstorm",
10
+ "design",
11
+ "test",
12
+ "analyze",
13
+ "troubleshoot",
14
+ "improve",
15
+ "cleanup",
16
+ "explain",
17
+ "document",
18
+ "pm",
19
+ "reflect",
20
+ "estimate",
21
+ "spec-panel",
22
+ "business-panel",
23
+ "index-repo"
24
+ ],
25
+ "argument_hint": "<command|task> [args...]"
26
+ }
@@ -1,4 +1,5 @@
1
1
  ---
2
+ internal: true
2
3
  name: tfx-auto-codex
3
4
  description: >
4
5
  Codex 리드형 tfx-auto. Claude 네이티브 역할을 Codex로 치환하고 Gemini 사용은 유지합니다.
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "tfx-auto-codex",
3
+ "description": "Codex 리드형 tfx-auto. Claude 네이티브 역할을 Codex로 치환하고 Gemini 사용은 유지합니다. '코덱스 리드', '코덱스가 주도', 'codex lead' 같은 요청에 사용.",
4
+ "triggers": [
5
+ "tfx-auto-codex"
6
+ ],
7
+ "argument_hint": "\\\"작업 설명\\\" | N:agent_type \\\"작업 설명\\\"",
8
+ "internal": true
9
+ }
@@ -1,4 +1,5 @@
1
1
  ---
2
+ internal: true
2
3
  name: tfx-autopilot
3
4
  description: "단일 파일/모듈 수준의 간단한 작업을 자율 구현할 때 사용한다. 구현+검증까지 5분 이내 완료 가능한 작업, '간단히', '빠르게', '하나만', '자동으로', '알아서 해', '그냥 해줘' 같은 수식어가 있을 때 사용. 복잡한 멀티파일 작업은 tfx-fullcycle을 사용하세요."
4
5
  triggers:
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "tfx-autopilot",
3
+ "description": "단일 파일/모듈 수준의 간단한 작업을 자율 구현할 때 사용한다. 구현+검증까지 5분 이내 완료 가능한 작업, '간단히', '빠르게', '하나만', '자동으로', '알아서 해', '그냥 해줘' 같은 수식어가 있을 때 사용. 복잡한 멀티파일 작업은 tfx-fullcycle을 사용하세요.",
4
+ "triggers": [
5
+ "autopilot",
6
+ "자동",
7
+ "알아서 해"
8
+ ],
9
+ "argument_hint": "<구현할 작업 설명>",
10
+ "internal": true
11
+ }
@@ -1,4 +1,5 @@
1
1
  ---
2
+ internal: true
2
3
  name: tfx-autoresearch
3
4
  description: >
4
5
  자율 웹 리서치 → 실행 가능 계획 도출.