deuk-agent-flow 5.0.2 → 5.0.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 (98) hide show
  1. package/CHANGELOG.ko.md +18 -0
  2. package/CHANGELOG.md +25 -0
  3. package/bundled/deuk-agent-flow.vsix +0 -0
  4. package/core-rules/AGENTS.md +5 -5
  5. package/package.json +20 -2
  6. package/scripts/bundle-vscode-vsix.ts +1 -1
  7. package/scripts/cli-context-commands.ts +35 -0
  8. package/scripts/cli-init-claude.ts +179 -0
  9. package/scripts/cli-init-core.ts +89 -0
  10. package/scripts/cli-init-instructions.ts +489 -0
  11. package/scripts/cli-init-migrate.ts +504 -0
  12. package/scripts/cli-init-workspace.ts +63 -0
  13. package/scripts/cli-locales.ts +85 -0
  14. package/scripts/cli-skill-commands.ts +3 -1
  15. package/scripts/cli-ticket-commands.ts +58 -0
  16. package/scripts/cli-ticket-docmeta.ts +103 -0
  17. package/scripts/cli-ticket-document.ts +153 -0
  18. package/scripts/cli-ticket-evidence.ts +312 -0
  19. package/scripts/cli-ticket-home.ts +449 -0
  20. package/scripts/cli-ticket-scan.ts +102 -0
  21. package/scripts/cli-ticket-surface.ts +56 -0
  22. package/scripts/oss-prepack.js +32 -0
  23. package/scripts/out/scripts/bundle-vscode-vsix.js +68 -0
  24. package/scripts/out/scripts/bundle-vscode-vsix.js.map +1 -0
  25. package/scripts/out/scripts/cli-args.js +374 -0
  26. package/scripts/out/scripts/cli-args.js.map +1 -0
  27. package/scripts/out/scripts/cli-context-commands.js +31 -0
  28. package/scripts/out/scripts/cli-context-commands.js.map +1 -0
  29. package/scripts/out/scripts/cli-init-claude.js +176 -0
  30. package/scripts/out/scripts/cli-init-claude.js.map +1 -0
  31. package/scripts/out/scripts/cli-init-commands.js +87 -0
  32. package/scripts/out/scripts/cli-init-commands.js.map +1 -0
  33. package/scripts/out/scripts/cli-init-core.js +89 -0
  34. package/scripts/out/scripts/cli-init-core.js.map +1 -0
  35. package/scripts/out/scripts/cli-init-instructions.js +460 -0
  36. package/scripts/out/scripts/cli-init-instructions.js.map +1 -0
  37. package/scripts/out/scripts/cli-init-logic.js +41 -0
  38. package/scripts/out/scripts/cli-init-logic.js.map +1 -0
  39. package/scripts/out/scripts/cli-init-migrate.js +497 -0
  40. package/scripts/out/scripts/cli-init-migrate.js.map +1 -0
  41. package/scripts/out/scripts/cli-init-workspace.js +57 -0
  42. package/scripts/out/scripts/cli-init-workspace.js.map +1 -0
  43. package/scripts/out/scripts/cli-locales.js +83 -0
  44. package/scripts/out/scripts/cli-locales.js.map +1 -0
  45. package/scripts/out/scripts/cli-prompts.js +101 -0
  46. package/scripts/out/scripts/cli-prompts.js.map +1 -0
  47. package/scripts/out/scripts/cli-rule-compiler.js +121 -0
  48. package/scripts/out/scripts/cli-rule-compiler.js.map +1 -0
  49. package/scripts/out/scripts/cli-skill-commands.js +708 -0
  50. package/scripts/out/scripts/cli-skill-commands.js.map +1 -0
  51. package/scripts/out/scripts/cli-telemetry-commands.js +604 -0
  52. package/scripts/out/scripts/cli-telemetry-commands.js.map +1 -0
  53. package/scripts/out/scripts/cli-ticket-command-shared.js +2 -0
  54. package/scripts/out/scripts/cli-ticket-command-shared.js.map +1 -0
  55. package/scripts/out/scripts/cli-ticket-commands.js +3530 -0
  56. package/scripts/out/scripts/cli-ticket-commands.js.map +1 -0
  57. package/scripts/out/scripts/cli-ticket-docmeta.js +90 -0
  58. package/scripts/out/scripts/cli-ticket-docmeta.js.map +1 -0
  59. package/scripts/out/scripts/cli-ticket-document.js +121 -0
  60. package/scripts/out/scripts/cli-ticket-document.js.map +1 -0
  61. package/scripts/out/scripts/cli-ticket-evidence.js +286 -0
  62. package/scripts/out/scripts/cli-ticket-evidence.js.map +1 -0
  63. package/scripts/out/scripts/cli-ticket-home.js +440 -0
  64. package/scripts/out/scripts/cli-ticket-home.js.map +1 -0
  65. package/scripts/out/scripts/cli-ticket-index.js +272 -0
  66. package/scripts/out/scripts/cli-ticket-index.js.map +1 -0
  67. package/scripts/out/scripts/cli-ticket-migration.js +261 -0
  68. package/scripts/out/scripts/cli-ticket-migration.js.map +1 -0
  69. package/scripts/out/scripts/cli-ticket-parser.js +90 -0
  70. package/scripts/out/scripts/cli-ticket-parser.js.map +1 -0
  71. package/scripts/out/scripts/cli-ticket-scan.js +93 -0
  72. package/scripts/out/scripts/cli-ticket-scan.js.map +1 -0
  73. package/scripts/out/scripts/cli-ticket-surface.js +53 -0
  74. package/scripts/out/scripts/cli-ticket-surface.js.map +1 -0
  75. package/scripts/out/scripts/cli-usage-commands.js +310 -0
  76. package/scripts/out/scripts/cli-usage-commands.js.map +1 -0
  77. package/scripts/out/scripts/cli-utils.js +1544 -0
  78. package/scripts/out/scripts/cli-utils.js.map +1 -0
  79. package/scripts/out/scripts/cli.js +696 -0
  80. package/scripts/out/scripts/cli.js.map +1 -0
  81. package/scripts/out/scripts/lint-md.js +238 -0
  82. package/scripts/out/scripts/lint-md.js.map +1 -0
  83. package/scripts/out/scripts/lint-rules.js +193 -0
  84. package/scripts/out/scripts/lint-rules.js.map +1 -0
  85. package/scripts/out/scripts/merge-logic.js +38 -0
  86. package/scripts/out/scripts/merge-logic.js.map +1 -0
  87. package/scripts/out/scripts/plan-parser.js +52 -0
  88. package/scripts/out/scripts/plan-parser.js.map +1 -0
  89. package/scripts/out/scripts/ticket-machine.js +94 -0
  90. package/scripts/out/scripts/ticket-machine.js.map +1 -0
  91. package/scripts/out/scripts/ticket-workflow.js +498 -0
  92. package/scripts/out/scripts/ticket-workflow.js.map +1 -0
  93. package/scripts/ticket-machine.ts +99 -0
  94. package/scripts/ticket-workflow.ts +544 -0
  95. package/templates/skills/role-closer/SKILL.md +27 -0
  96. package/templates/skills/role-implementer/SKILL.md +30 -0
  97. package/templates/skills/role-planner/SKILL.md +29 -0
  98. package/templates/skills/role-qa-verifier/SKILL.md +30 -0
@@ -0,0 +1,708 @@
1
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, rmSync } from "fs";
2
+ import { dirname, join, relative } from "path";
3
+ import { homedir } from "os";
4
+ import { DEUK_ROOT_DIR, getUserSkillsConfigPath, getUserSkillsDir, makePath, parseFrontMatter, resolvePackageRoot } from "./cli-utils.js";
5
+ const SKILL_ROOT = "templates/skills";
6
+ const CONFIG_FILE = `${DEUK_ROOT_DIR}/skills.json`;
7
+ const PACKAGE_ROOT = resolvePackageRoot({ fromUrl: import.meta.url });
8
+ const SKILL_PATHS = {
9
+ installedRoot: makePath(DEUK_ROOT_DIR, "skills"),
10
+ templateRoot: SKILL_ROOT,
11
+ claudeRoot: makePath(".claude", "skills"),
12
+ cursorPointer: makePath(".cursor", "rules", "deuk-agent-skills.mdc"),
13
+ };
14
+ // 에이전트(플랫폼)별 스킬 노출 전략 레지스트리.
15
+ // kind "native": 에이전트 고유 스킬 디렉토리에 SKILL.md를 복사.
16
+ // kind "pointer": 에이전트 룰/인스트럭션 파일에 스킬 포인터 섹션을 주입.
17
+ const SKILL_PLATFORMS = [
18
+ { id: "claude", kind: "native", nativeDir: join(homedir(), ".claude", "skills") },
19
+ { id: "cursor", kind: "pointer", pointerFile: makePath(".cursor", "rules", "deuk-agent-skills.mdc"), format: "mdc" },
20
+ { id: "codex", kind: "pointer", pointerFile: makePath(".codex", "AGENTS.md"), format: "markdown" },
21
+ { id: "copilot", kind: "pointer", pointerFile: makePath(".github", "copilot-instructions.md"), format: "markdown" },
22
+ { id: "antigravity", kind: "pointer", pointerFile: "GEMINI.md", format: "markdown" },
23
+ ];
24
+ const SKILL_PLATFORM_IDS = SKILL_PLATFORMS.map(p => p.id);
25
+ const SKILL_POINTER_BEGIN = "<!-- DEUK_AGENT_SKILLS_BEGIN -->";
26
+ const SKILL_POINTER_END = "<!-- DEUK_AGENT_SKILLS_END -->";
27
+ // native 플랫폼 flat 파일 prefix — 타 도구 스킬과 충돌 방지.
28
+ const NATIVE_SKILL_PREFIX = "daf-";
29
+ function getPlatform(id) {
30
+ const platform = SKILL_PLATFORMS.find(p => p.id === id);
31
+ if (!platform)
32
+ throw new Error(`skill expose requires --platform ${SKILL_PLATFORM_IDS.join("|")}`);
33
+ return platform;
34
+ }
35
+ function safeReadJson(absPath, fallback = null) {
36
+ if (!existsSync(absPath))
37
+ return fallback;
38
+ try {
39
+ return JSON.parse(readFileSync(absPath, "utf8"));
40
+ }
41
+ catch {
42
+ return fallback;
43
+ }
44
+ }
45
+ function getAvailableSkills(cwd) {
46
+ const ids = new Set();
47
+ const scanDir = (dir) => {
48
+ if (existsSync(dir)) {
49
+ readdirSync(dir, { withFileTypes: true })
50
+ .filter(d => d.isDirectory() && existsSync(makePath(dir, d.name, "SKILL.md")))
51
+ .forEach(d => ids.add(d.name));
52
+ }
53
+ };
54
+ scanDir(getUserSkillsDir());
55
+ scanDir(makePath(cwd, SKILL_PATHS.installedRoot));
56
+ scanDir(makePath(cwd, SKILL_PATHS.templateRoot));
57
+ scanDir(makePath(PACKAGE_ROOT, SKILL_ROOT));
58
+ return Array.from(ids);
59
+ }
60
+ function userSkillPath(id) {
61
+ return makePath(getUserSkillsDir(), id, "SKILL.md");
62
+ }
63
+ // 스킬 본문 원본 경로. 전역 사용자본 > repo 템플릿 > 번들 순으로 우선.
64
+ function sourceSkillPath(cwd, id) {
65
+ // #622-D: the package template is the SSOT (main). It wins so package updates
66
+ // (e.g. the latest persona-maid) propagate instead of being shadowed by a stale
67
+ // user copy. The user store is only the source for skills the package does NOT
68
+ // ship (genuinely user-authored skills).
69
+ const repoTemplate = makePath(cwd, SKILL_PATHS.templateRoot, id, "SKILL.md");
70
+ if (existsSync(repoTemplate))
71
+ return repoTemplate;
72
+ const packaged = makePath(PACKAGE_ROOT, SKILL_ROOT, id, "SKILL.md");
73
+ if (existsSync(packaged))
74
+ return packaged;
75
+ return userSkillPath(id);
76
+ }
77
+ // listSkills 미리보기/충돌 판정을 위해 frontmatter(summary, category)와 origin 을 읽는다.
78
+ function readSkillMeta(cwd, id) {
79
+ try {
80
+ const path = sourceSkillPath(cwd, id);
81
+ if (!existsSync(path))
82
+ return { summary: "", category: "", source: "bundled" };
83
+ const { meta } = parseFrontMatter(readFileSync(path, "utf8"));
84
+ const source = existsSync(userSkillPath(id)) ? "user" : "bundled";
85
+ return {
86
+ summary: String(meta?.summary || "").trim(),
87
+ category: String(meta?.category || "").trim(),
88
+ // #694: bind = 이 스킬이 활성화되는 노드 영역(rules/phase1/phase2/doc).
89
+ // 비어있으면 영역 무관(항상). area 필터에 쓰인다.
90
+ bind: String(meta?.bind || "").trim(),
91
+ // #697: system = true면 사용자 선택 없이 init 시 자동 install+expose되는 시스템 필수 스킬.
92
+ system: meta?.system === true || String(meta?.system || "").toLowerCase() === "true",
93
+ source,
94
+ };
95
+ }
96
+ catch {
97
+ return { summary: "", category: "", source: "bundled", bind: "", system: false };
98
+ }
99
+ }
100
+ function loadSkillSource(cwd, id) {
101
+ const source = sourceSkillPath(cwd, id);
102
+ if (!existsSync(source))
103
+ throw new Error(`skill not found: ${id}`);
104
+ return readFileSync(source, "utf8");
105
+ }
106
+ // installed + exposed 모두 홈 전역 skills.json 의 SSOT.
107
+ function loadGlobalConfig() {
108
+ const g = safeReadJson(getUserSkillsConfigPath(), { installed: [], exposed: {} });
109
+ return {
110
+ installed: Array.isArray(g.installed) ? g.installed : [],
111
+ exposed: (g.exposed && typeof g.exposed === 'object') ? g.exposed : {},
112
+ };
113
+ }
114
+ function writeGlobalConfig(installed, exposed, dryRun) {
115
+ if (dryRun)
116
+ return;
117
+ const p = getUserSkillsConfigPath();
118
+ mkdirSync(dirname(p), { recursive: true });
119
+ writeFileSync(p, JSON.stringify({ version: 1, installed, exposed }, null, 2), "utf8");
120
+ }
121
+ function loadSkillConfig(cwd) {
122
+ const global = loadGlobalConfig();
123
+ // 워크스페이스 파일에 잔존하는 installed/exposed 는 마이그레이션 호환을 위해 전역으로 머지.
124
+ const ws = safeReadJson(makePath(cwd, CONFIG_FILE), { installed: [], exposed: {} });
125
+ const localInstalled = Array.isArray(ws.installed) ? ws.installed : [];
126
+ const localExposed = (ws.exposed && typeof ws.exposed === 'object') ? ws.exposed : {};
127
+ const installed = Array.from(new Set([...global.installed, ...localInstalled]));
128
+ const exposed = {};
129
+ for (const [platform, ids] of Object.entries({ ...localExposed, ...global.exposed })) {
130
+ const merged = new Set([
131
+ ...(Array.isArray(localExposed[platform]) ? localExposed[platform] : []),
132
+ ...(Array.isArray(global.exposed[platform]) ? global.exposed[platform] : []),
133
+ ]);
134
+ if (merged.size > 0)
135
+ exposed[platform] = Array.from(merged);
136
+ }
137
+ return { version: 1, installed, exposed };
138
+ }
139
+ function pointerLine(cwd, id) {
140
+ const src = sourceSkillPath(cwd, id);
141
+ let rel = relative(cwd, src).replace(/\\/g, '/');
142
+ if (rel.startsWith('..')) {
143
+ rel = src.replace(/\\/g, '/');
144
+ }
145
+ return `- ${id}: ${rel}`;
146
+ }
147
+ function detectExposedPlatforms(cwd, id) {
148
+ const platforms = [];
149
+ for (const platform of SKILL_PLATFORMS) {
150
+ if (platform.kind === "native") {
151
+ const skillFile = join(platform.nativeDir, id, "SKILL.md");
152
+ const legacyPrefixFlat = join(platform.nativeDir, `${NATIVE_SKILL_PREFIX}${id}.md`);
153
+ const legacyFlat = join(platform.nativeDir, `${id}.md`);
154
+ if (existsSync(skillFile) || existsSync(legacyPrefixFlat) || existsSync(legacyFlat))
155
+ platforms.push(platform.id);
156
+ continue;
157
+ }
158
+ const pointerPath = makePath(cwd, platform.pointerFile);
159
+ if (!existsSync(pointerPath))
160
+ continue;
161
+ try {
162
+ if (readFileSync(pointerPath, "utf8").includes(`- ${id}: `))
163
+ platforms.push(platform.id);
164
+ }
165
+ catch {
166
+ // Ignore pointer read failures and fall back to config-based exposure only.
167
+ }
168
+ }
169
+ return platforms;
170
+ }
171
+ function writeSkillConfig(cwd, config, dryRun) {
172
+ // installed + exposed 모두 전역 파일로 저장. 워크스페이스 파일에는 기록하지 않는다.
173
+ writeGlobalConfig(Array.isArray(config.installed) ? config.installed : [], (config.exposed && typeof config.exposed === 'object') ? config.exposed : {}, dryRun);
174
+ // 오토 마이그레이션: 워크스페이스 파일에 잔존하는 exposed/installed 키 제거.
175
+ if (!dryRun) {
176
+ const wsPath = makePath(cwd, CONFIG_FILE);
177
+ if (existsSync(wsPath)) {
178
+ try {
179
+ const ws = safeReadJson(wsPath, {});
180
+ if ('exposed' in ws || 'installed' in ws) {
181
+ const { exposed: _e, installed: _i, ...rest } = ws;
182
+ writeFileSync(wsPath, JSON.stringify({ version: 1, ...rest }, null, 2), "utf8");
183
+ }
184
+ }
185
+ catch {
186
+ // 마이그레이션 실패는 무시 — 전역 저장은 이미 완료.
187
+ }
188
+ }
189
+ }
190
+ }
191
+ function ensureKnownSkill(cwd, id) {
192
+ if (!getAvailableSkills(cwd).includes(id))
193
+ throw new Error(`unknown skill: ${id}`);
194
+ }
195
+ export function listSkills(cwd = process.cwd()) {
196
+ const config = loadSkillConfig(cwd);
197
+ const available = getAvailableSkills(cwd);
198
+ return available.map(id => {
199
+ const meta = readSkillMeta(cwd, id);
200
+ return {
201
+ id,
202
+ installed: config.installed.includes(id),
203
+ exposed: Array.from(new Set([
204
+ ...Object.entries(config.exposed || {})
205
+ .filter(([, ids]) => Array.isArray(ids) && ids.includes(id))
206
+ .map(([platform]) => platform),
207
+ ...detectExposedPlatforms(cwd, id)
208
+ ])),
209
+ summary: meta.summary,
210
+ category: meta.category,
211
+ source: meta.source,
212
+ };
213
+ });
214
+ }
215
+ // 번들/리포 스킬을 전역 사용자 디렉토리로 복사(fork)해 편집 가능하게 만든다.
216
+ // 이미 사용자본이 있으면 그대로 두고 경로만 반환(덮어쓰지 않음).
217
+ // fork 후 exposed native 플랫폼 파일도 갱신한다.
218
+ export function forkSkill(opts = {}) {
219
+ const cwd = opts.cwd || process.cwd();
220
+ const id = opts.skill;
221
+ if (!id)
222
+ throw new Error("skill fork requires --skill <id>");
223
+ ensureKnownSkill(cwd, id);
224
+ const target = userSkillPath(id);
225
+ if (existsSync(target)) {
226
+ // 이미 fork된 경우도 native sync — 편집 후 재반영 목적으로 호출될 수 있음.
227
+ const config = loadSkillConfig(cwd);
228
+ syncNativePlatforms(cwd, id, config, opts.dryRun);
229
+ return { id, target, created: false };
230
+ }
231
+ const body = readFileSync(sourceSkillPath(cwd, id), "utf8");
232
+ if (!opts.dryRun) {
233
+ mkdirSync(dirname(target), { recursive: true });
234
+ writeFileSync(target, body, "utf8");
235
+ }
236
+ const config = loadSkillConfig(cwd);
237
+ syncNativePlatforms(cwd, id, config, opts.dryRun);
238
+ return { id, target, created: true };
239
+ }
240
+ const SKILL_BODY_LIMIT = 600;
241
+ // native 플랫폼 — 스킬 파일이 플랫폼 자체 컨텍스트에 직접 로드되므로 rules embed 불필요.
242
+ const NATIVE_EMBED_SKIP_PLATFORMS = new Set(["claude"]);
243
+ export function detectCurrentPlatform() {
244
+ // Claude Code: CLAUDECODE=1 또는 CLAUDE_CODE_ENTRYPOINT/CLAUDE_CODE_SESSION_ID 존재
245
+ if (process.env.CLAUDECODE || process.env.CLAUDE_CODE_ENTRYPOINT || process.env.CLAUDE_CODE_SESSION_ID)
246
+ return "claude";
247
+ if (process.env.CLAUDE_VERSION || process.env.ANTHROPIC_API_KEY)
248
+ return "claude";
249
+ if (process.env.CURSOR_VERSION || process.env.EDITOR === "cursor")
250
+ return "cursor";
251
+ if (process.env.COPILOT_API_KEY || process.env.GITHUB_COPILOT)
252
+ return "copilot";
253
+ if (process.env.GEMINI_API_KEY || process.env.ANTIGRAVITY_WORKFLOW)
254
+ return "antigravity";
255
+ return null;
256
+ }
257
+ // #694: area = 현재 노드 영역(rules/phase1/phase2/doc). 꽂힌 스킬 중 bind가 이 영역과
258
+ // 일치하는 것만 노출한다(LangGraph 노드별 도구 바인딩). bind가 없는 스킬은 영역 무관으로
259
+ // 항상 노출. area를 안 주면(null) 영역 필터를 적용하지 않는다(하위 호환).
260
+ export function getActiveSkillsSurface(cwd = process.cwd(), platform = null, area = null) {
261
+ const config = loadSkillConfig(cwd);
262
+ const detectedPlatform = platform || detectCurrentPlatform();
263
+ let ids = config.installed || [];
264
+ if (detectedPlatform) {
265
+ ids = ids.filter(id => {
266
+ const exposed = config.exposed?.[detectedPlatform] || [];
267
+ return exposed.includes(id) || detectExposedPlatforms(cwd, id).includes(detectedPlatform);
268
+ });
269
+ }
270
+ if (area) {
271
+ ids = ids.filter(id => {
272
+ const bind = readSkillMeta(cwd, id).bind;
273
+ return !bind || bind === area;
274
+ });
275
+ }
276
+ if (ids.length === 0)
277
+ return "";
278
+ // native 플랫폼(claude 등)은 스킬이 이미 플랫폼 컨텍스트에 로드됨 — summary 목록만 출력.
279
+ if (detectedPlatform && NATIVE_EMBED_SKIP_PLATFORMS.has(detectedPlatform)) {
280
+ const lines = ids.map(id => {
281
+ const meta = readSkillMeta(cwd, id);
282
+ return meta.summary ? `- **${id}**: ${meta.summary}` : `- **${id}**`;
283
+ });
284
+ return `## Active Skills\n\n${lines.join("\n")}`;
285
+ }
286
+ const contents = ids.map(id => {
287
+ try {
288
+ const raw = readFileSync(sourceSkillPath(cwd, id), "utf8").trimEnd();
289
+ // 역할 레이어(category: role)는 행동 계약이라 잘리면 의미가 없다 — truncate 면제.
290
+ const isRole = readSkillMeta(cwd, id).category === "role";
291
+ const truncated = (!isRole && raw.length > SKILL_BODY_LIMIT)
292
+ ? raw.slice(0, SKILL_BODY_LIMIT) + "\n… (truncated)"
293
+ : raw;
294
+ return `### Skill: ${id}\n\n${truncated}`;
295
+ }
296
+ catch {
297
+ return "";
298
+ }
299
+ }).filter(Boolean);
300
+ if (contents.length === 0)
301
+ return "";
302
+ return `## Agent System Hooks (Active Skills)\n\n` + contents.join("\n\n");
303
+ }
304
+ // 이미 exposed된 native 플랫폼에 스킬 파일을 싱크한다.
305
+ function syncNativePlatforms(cwd, id, config, dryRun) {
306
+ const exposedPlatformIds = new Set([
307
+ ...Object.entries(config.exposed || {})
308
+ .filter(([, ids]) => Array.isArray(ids) && ids.includes(id))
309
+ .map(([p]) => p),
310
+ ...detectExposedPlatforms(cwd, id),
311
+ ]);
312
+ for (const platform of SKILL_PLATFORMS) {
313
+ if (platform.kind === "native" && exposedPlatformIds.has(platform.id)) {
314
+ upsertNativeDir(platform.nativeDir, [id], cwd, dryRun);
315
+ }
316
+ }
317
+ }
318
+ // #622-D: idempotent auto-sync entry point, called once on ticket-command entry
319
+ // (cli.mjs) and init. For every skill exposed to a native platform (Claude), it
320
+ // re-copies from the SSOT only if the content changed (hash compare in
321
+ // upsertNativeDir). This makes package-template updates propagate to ~/.claude/
322
+ // skills without a manual re-expose. Best-effort: never throws into the caller.
323
+ export function syncExposedNativeSkills(cwd = process.cwd(), opts = {}) {
324
+ try {
325
+ const config = loadSkillConfig(cwd);
326
+ let changed = 0;
327
+ for (const platform of SKILL_PLATFORMS) {
328
+ if (platform.kind !== "native")
329
+ continue;
330
+ const ids = currentlyExposed(config, platform.id);
331
+ if (ids.length === 0)
332
+ continue;
333
+ changed += upsertNativeDir(platform.nativeDir, ids, cwd, opts.dryRun) || 0;
334
+ }
335
+ return { changed };
336
+ }
337
+ catch {
338
+ return { changed: 0 };
339
+ }
340
+ }
341
+ export function addSkill(opts = {}) {
342
+ const cwd = opts.cwd || process.cwd();
343
+ const id = opts.skill;
344
+ ensureKnownSkill(cwd, id);
345
+ const config = loadSkillConfig(cwd);
346
+ if (!config.installed.includes(id))
347
+ config.installed.push(id);
348
+ writeSkillConfig(cwd, config, opts.dryRun);
349
+ syncNativePlatforms(cwd, id, config, opts.dryRun);
350
+ return { id, target: sourceSkillPath(cwd, id) };
351
+ }
352
+ // 포인터 섹션 텍스트(마커로 감싸 멱등 갱신). ids가 비면 섹션을 제거.
353
+ function buildPointerSection(cwd, platform, ids) {
354
+ if (ids.length === 0)
355
+ return "";
356
+ const lines = [
357
+ SKILL_POINTER_BEGIN,
358
+ "# DeukAgentFlow Skills",
359
+ "",
360
+ "These are thin behavior playbooks. They do not override `core-rules/AGENTS.md`, TDW, APC, Phase Gate, or PROJECT_RULE.md.",
361
+ "",
362
+ ];
363
+ // #622-C: pointer platforms get ONLY a pointer line per skill — never the full
364
+ // skill body. The full content lives in the SSOT (package templates) and, for
365
+ // Claude, in the native ~/.claude/skills dir. Inlining the body here was a leak
366
+ // that copied 6KB skill text into workspace files (e.g. GEMINI.md).
367
+ lines.push(...ids.map(id => pointerLine(cwd, id)));
368
+ lines.push(SKILL_POINTER_END);
369
+ if (platform.format === "mdc") {
370
+ return [
371
+ "---",
372
+ "description: \"DeukAgentFlow skill pointers\"",
373
+ "globs: [\"**/*\"]",
374
+ "alwaysApply: false",
375
+ "---",
376
+ ...lines,
377
+ "",
378
+ ].join("\n");
379
+ }
380
+ return lines.join("\n") + "\n";
381
+ }
382
+ // native 플랫폼: nativeDir/<id>/SKILL.md 정식 디렉토리 구조로 저장.
383
+ // Claude Code 등 native 스킬 시스템은 <id>/SKILL.md 만 인식하므로 flat 파일은 못 알아본다.
384
+ // 구식 flat(daf-<id>.md, <id>.md)이 있으면 제거한다.
385
+ function upsertNativeDir(nativeDir, ids, cwd, dryRun) {
386
+ let changed = 0;
387
+ for (const id of ids) {
388
+ const content = ensureSkillDescription(loadSkillSource(cwd, id));
389
+ const destDir = join(nativeDir, id);
390
+ const dest = join(destDir, "SKILL.md");
391
+ // #622-D: hash compare — only rewrite when the SSOT content actually differs,
392
+ // so a synced skill stays current but unchanged ones are skipped (cheap on the
393
+ // per-ticket-action auto-sync path).
394
+ const current = existsSync(dest) ? readFileSync(dest, "utf8") : null;
395
+ if (current === content)
396
+ continue;
397
+ changed++;
398
+ if (!dryRun) {
399
+ mkdirSync(destDir, { recursive: true });
400
+ writeFileSync(dest, content, "utf8");
401
+ const legacyPrefixFlat = join(nativeDir, `${NATIVE_SKILL_PREFIX}${id}.md`);
402
+ if (existsSync(legacyPrefixFlat))
403
+ rmSync(legacyPrefixFlat);
404
+ const legacyFlat = join(nativeDir, `${id}.md`);
405
+ if (existsSync(legacyFlat))
406
+ rmSync(legacyFlat);
407
+ }
408
+ }
409
+ return changed;
410
+ }
411
+ // Claude Code 정식 스킬은 frontmatter에 description 필드를 요구한다.
412
+ // 소스 SKILL.md가 summary만 가진 경우 summary 값을 description으로 보강한다.
413
+ function ensureSkillDescription(content) {
414
+ const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
415
+ if (!fmMatch)
416
+ return content;
417
+ const fm = fmMatch[1];
418
+ if (/^description:/m.test(fm))
419
+ return content;
420
+ const summaryMatch = fm.match(/^summary:[ \t]*(.+?)\r?$/m);
421
+ if (!summaryMatch)
422
+ return content;
423
+ const eol = fm.includes("\r\n") ? "\r\n" : "\n";
424
+ const newFm = `${fm}${eol}description: ${summaryMatch[1]}`;
425
+ return content.replace(fmMatch[0], `---${eol}${newFm}${eol}---`);
426
+ }
427
+ // native 플랫폼: <id>/SKILL.md 디렉토리 삭제 (없으면 silent skip).
428
+ // 구식 flat(daf-<id>.md, prefix 없는 <id>.md)도 함께 정리한다.
429
+ function removeNativeFiles(nativeDir, ids, dryRun) {
430
+ for (const id of ids) {
431
+ const dir = join(nativeDir, id);
432
+ if (!dryRun && existsSync(dir))
433
+ rmSync(dir, { recursive: true, force: true });
434
+ const prefixFlat = join(nativeDir, `${NATIVE_SKILL_PREFIX}${id}.md`);
435
+ if (!dryRun && existsSync(prefixFlat))
436
+ rmSync(prefixFlat);
437
+ const legacyFlat = join(nativeDir, `${id}.md`);
438
+ if (!dryRun && existsSync(legacyFlat))
439
+ rmSync(legacyFlat);
440
+ }
441
+ }
442
+ // 기존 파일에서 마커 구간만 교체/제거하고 나머지 내용은 보존.
443
+ function upsertPointerFile(cwd, platform, ids, dryRun) {
444
+ const target = makePath(cwd, platform.pointerFile);
445
+ const existing = existsSync(target) ? readFileSync(target, "utf8") : "";
446
+ const section = buildPointerSection(cwd, platform, ids);
447
+ const markerRe = new RegExp(`${SKILL_POINTER_BEGIN}[\\s\\S]*?${SKILL_POINTER_END}\\n?`, "m");
448
+ let next;
449
+ if (platform.format === "mdc" || !existing.trim()) {
450
+ // mdc 포인터 파일은 스킬 전용이므로 통째로 재작성. 빈 파일도 동일.
451
+ next = section;
452
+ }
453
+ else if (markerRe.test(existing)) {
454
+ next = section ? existing.replace(markerRe, section.trimEnd() + "\n") : existing.replace(markerRe, "").trimEnd() + "\n";
455
+ }
456
+ else if (section) {
457
+ next = existing.trimEnd() + "\n\n" + section;
458
+ }
459
+ else {
460
+ next = existing;
461
+ }
462
+ if (!dryRun) {
463
+ if (!next.trim()) {
464
+ if (existsSync(target))
465
+ rmSync(target);
466
+ }
467
+ else {
468
+ mkdirSync(dirname(target), { recursive: true });
469
+ writeFileSync(target, next, "utf8");
470
+ }
471
+ }
472
+ }
473
+ // platform에 현재 노출된 스킬 id 집합을 config 기준으로 계산.
474
+ function currentlyExposed(config, platformId) {
475
+ return Array.isArray(config.exposed?.[platformId]) ? config.exposed[platformId] : [];
476
+ }
477
+ // opts.skill 가 있으면 해당 스킬만, 없으면 설치된 전체를 대상으로 함.
478
+ function targetIds(config, opts) {
479
+ if (opts.skill)
480
+ return [opts.skill];
481
+ return config.installed || [];
482
+ }
483
+ const TOTAL_MAX_SKILLS = 6;
484
+ const CATEGORY_LIMITS = {
485
+ persona: 1,
486
+ coding: 3,
487
+ data: 2
488
+ };
489
+ function validateSkillConstraints(cwd, platformId, nextExposedIds) {
490
+ // #697: system:true 스킬(ticket-status-surface 등)은 사용자 선택 슬롯이 아니라 시스템
491
+ // 필수다 — 총량/카테고리 제한 카운트에서 제외한다. 사용자가 고르는 스킬만 6개로 제한.
492
+ const userIds = nextExposedIds.filter(id => !readSkillMeta(cwd, id).system);
493
+ if (userIds.length > TOTAL_MAX_SKILLS) {
494
+ throw new Error(`총 장착 가능한 스킬 개수(${TOTAL_MAX_SKILLS}개)를 초과할 수 없습니다.`);
495
+ }
496
+ const counts = {};
497
+ for (const id of userIds) {
498
+ const meta = readSkillMeta(cwd, id);
499
+ const cat = meta.category || "uncategorized";
500
+ counts[cat] = (counts[cat] || 0) + 1;
501
+ if (CATEGORY_LIMITS[cat] !== undefined && counts[cat] > CATEGORY_LIMITS[cat]) {
502
+ throw new Error(`'${cat}' 카테고리는 최대 ${CATEGORY_LIMITS[cat]}개까지만 장착할 수 있습니다.`);
503
+ }
504
+ }
505
+ }
506
+ export function exposeSkills(opts = {}) {
507
+ const cwd = opts.cwd || process.cwd();
508
+ const platform = getPlatform(String(opts.platform || "").toLowerCase());
509
+ const config = loadSkillConfig(cwd);
510
+ const ids = targetIds(config, opts);
511
+ if (ids.length === 0)
512
+ throw new Error("no skills installed; run skill add first");
513
+ for (const id of ids) {
514
+ ensureKnownSkill(cwd, id);
515
+ if (!config.installed.includes(id))
516
+ addSkill({ cwd, skill: id, dryRun: opts.dryRun });
517
+ }
518
+ config.exposed = config.exposed || {};
519
+ const nextExposed = Array.from(new Set([...currentlyExposed(config, platform.id), ...ids]));
520
+ validateSkillConstraints(cwd, platform.id, nextExposed);
521
+ // #622-C: only native platforms (Claude → ~/.claude/skills) get files written.
522
+ // Pointer platforms (codex/gemini/copilot/cursor) are covered by the per-agent
523
+ // HOME global spoke (getGlobalAgentInstructionTargets), so we NEVER write skill
524
+ // files into the workspace git tree. expose just records config + syncs native.
525
+ if (platform.kind === "native") {
526
+ upsertNativeDir(platform.nativeDir, ids, cwd, opts.dryRun);
527
+ }
528
+ config.exposed[platform.id] = nextExposed;
529
+ writeSkillConfig(cwd, config, opts.dryRun);
530
+ return { platform: platform.id, ids };
531
+ }
532
+ // #697: system:true 스킬은 사용자 선택 없이 항상 보장한다. init/dev:install 시 호출돼
533
+ // 번들 스킬 중 system 마커가 붙은 것을 모든 플랫폼에 install+expose한다(이미 돼있으면 no-op).
534
+ // ticket-status-surface처럼 상태 표현 규칙은 시스템 필수라 누락되면 안 된다.
535
+ export function ensureSystemSkills(cwd = process.cwd(), opts = {}) {
536
+ const available = getAvailableSkills(cwd);
537
+ const systemIds = available.filter(id => readSkillMeta(cwd, id).system);
538
+ if (systemIds.length === 0)
539
+ return [];
540
+ const ensured = [];
541
+ for (const platformId of SKILL_PLATFORM_IDS) {
542
+ const config = loadSkillConfig(cwd);
543
+ const already = currentlyExposed(config, platformId);
544
+ const missing = systemIds.filter(id => !already.includes(id));
545
+ if (missing.length === 0)
546
+ continue;
547
+ for (const id of missing) {
548
+ try {
549
+ // targetIds는 opts.skill만 보므로 스킬을 하나씩 expose한다(install은 자동 동반).
550
+ exposeSkills({ cwd, platform: platformId, skill: id, dryRun: opts.dryRun });
551
+ ensured.push(`${platformId}:${id}`);
552
+ }
553
+ catch { /* best-effort: 한 스킬/플랫폼 실패가 init을 막지 않음 */ }
554
+ }
555
+ }
556
+ return ensured;
557
+ }
558
+ export function unexposeSkills(opts = {}) {
559
+ const cwd = opts.cwd || process.cwd();
560
+ const platform = getPlatform(String(opts.platform || "").toLowerCase());
561
+ const config = loadSkillConfig(cwd);
562
+ config.exposed = config.exposed || {};
563
+ // 제거 대상: 특정 스킬이면 그것만, 아니면 해당 플랫폼 전체 해제.
564
+ const removeIds = opts.skill ? [opts.skill] : currentlyExposed(config, platform.id);
565
+ const nextExposed = currentlyExposed(config, platform.id).filter(id => !removeIds.includes(id));
566
+ // #622-C: native → remove from ~/.claude/skills. Pointer platforms no longer have
567
+ // workspace files (HOME global spoke covers them); if a legacy pointer file still
568
+ // exists from before this change, remove its managed section so nothing lingers.
569
+ if (platform.kind === "native") {
570
+ removeNativeFiles(platform.nativeDir, removeIds, opts.dryRun);
571
+ }
572
+ else {
573
+ removePointerFileSection(cwd, platform, opts.dryRun);
574
+ }
575
+ config.exposed[platform.id] = nextExposed;
576
+ writeSkillConfig(cwd, config, opts.dryRun);
577
+ return { platform: platform.id, ids: removeIds };
578
+ }
579
+ // Removes the deuk-managed skill section from a workspace pointer file (legacy
580
+ // cleanup). If the file becomes empty, deletes it. Never touches non-managed text.
581
+ function removePointerFileSection(cwd, platform, dryRun) {
582
+ const target = makePath(cwd, platform.pointerFile);
583
+ if (!existsSync(target))
584
+ return;
585
+ const existing = readFileSync(target, "utf8");
586
+ const markerRe = new RegExp(`${SKILL_POINTER_BEGIN}[\\s\\S]*?${SKILL_POINTER_END}\\n?`, "m");
587
+ if (!markerRe.test(existing))
588
+ return;
589
+ const next = existing.replace(markerRe, "").trimEnd() + "\n";
590
+ if (dryRun)
591
+ return;
592
+ if (!next.trim())
593
+ rmSync(target);
594
+ else
595
+ writeFileSync(target, next, "utf8");
596
+ }
597
+ // 스킬 설치 제거: 설치 파일 삭제 + 모든 플랫폼에서 노출 해제 + config 정리.
598
+ export function removeSkill(opts = {}) {
599
+ const cwd = opts.cwd || process.cwd();
600
+ const id = opts.skill;
601
+ if (!id)
602
+ throw new Error("skill remove requires --skill <id>");
603
+ const config = loadSkillConfig(cwd);
604
+ for (const platform of SKILL_PLATFORMS) {
605
+ if (detectExposedPlatforms(cwd, id).includes(platform.id) || currentlyExposed(config, platform.id).includes(id)) {
606
+ unexposeSkills({ cwd, platform: platform.id, skill: id, dryRun: opts.dryRun });
607
+ }
608
+ }
609
+ const target = makePath(cwd, SKILL_PATHS.installedRoot, id);
610
+ if (!opts.dryRun && existsSync(target))
611
+ rmSync(target, { recursive: true, force: true });
612
+ const fresh = loadSkillConfig(cwd);
613
+ fresh.installed = (fresh.installed || []).filter(x => x !== id);
614
+ writeSkillConfig(cwd, fresh, opts.dryRun);
615
+ return { id };
616
+ }
617
+ export function lintSkills(cwd = process.cwd()) {
618
+ const paths = [];
619
+ for (const root of [makePath(cwd, SKILL_ROOT), makePath(cwd, SKILL_PATHS.installedRoot)]) {
620
+ if (!existsSync(root))
621
+ continue;
622
+ for (const id of readdirSync(root)) {
623
+ const path = makePath(root, id, "SKILL.md");
624
+ if (existsSync(path))
625
+ paths.push(path);
626
+ }
627
+ }
628
+ const violations = [];
629
+ const forbidden = [/ignore ticket/i, /skip verification/i, /override (tdw|apc|phase gate)/i, /edit generated output directly/i];
630
+ for (const path of paths) {
631
+ const body = readFileSync(path, "utf8");
632
+ if (!/Authority:.*core-rules\/AGENTS\.md/i.test(body))
633
+ violations.push(`${path}: missing authority line`);
634
+ for (const pattern of forbidden) {
635
+ if (pattern.test(body))
636
+ violations.push(`${path}: forbidden phrase ${pattern}`);
637
+ }
638
+ }
639
+ return { ok: violations.length === 0, paths, violations };
640
+ }
641
+ export async function runSkill(action, opts = {}) {
642
+ if (action === "list") {
643
+ const rows = listSkills(opts.cwd);
644
+ if (opts.json)
645
+ console.log(JSON.stringify(rows, null, 2));
646
+ else
647
+ rows.forEach(row => console.log(`${row.id} installed=${row.installed ? "yes" : "no"} exposed=${row.exposed.join(",") || "-"}`));
648
+ return;
649
+ }
650
+ if (action === "add") {
651
+ const result = addSkill(opts);
652
+ console.log(`skill added: ${result.id}`);
653
+ return;
654
+ }
655
+ if (action === "expose") {
656
+ const result = exposeSkills(opts);
657
+ console.log(`skills exposed: ${result.platform} ${result.ids.join(",")}`);
658
+ return;
659
+ }
660
+ if (action === "unexpose") {
661
+ const result = unexposeSkills(opts);
662
+ console.log(`skills unexposed: ${result.platform} ${result.ids.join(",") || "-"}`);
663
+ return;
664
+ }
665
+ if (action === "remove") {
666
+ const result = removeSkill(opts);
667
+ console.log(`skill removed: ${result.id}`);
668
+ return;
669
+ }
670
+ if (action === "fork") {
671
+ const result = forkSkill(opts);
672
+ if (opts.json)
673
+ console.log(JSON.stringify(result, null, 2));
674
+ else
675
+ console.log(result.created ? `skill forked: ${result.id} -> ${result.target}` : `skill already user-owned: ${result.id} -> ${result.target}`);
676
+ return;
677
+ }
678
+ if (action === "lint") {
679
+ const result = lintSkills(opts.cwd);
680
+ if (opts.json)
681
+ console.log(JSON.stringify(result, null, 2));
682
+ else
683
+ console.log(result.ok ? "skill:lint ok" : `skill:lint failed ${result.violations.length}`);
684
+ if (!result.ok)
685
+ throw new Error(result.violations.join("\n"));
686
+ return;
687
+ }
688
+ if (action === "edit") {
689
+ const id = opts.skill;
690
+ if (!id)
691
+ throw new Error("skill edit requires --skill <id>");
692
+ ensureKnownSkill(opts.cwd || process.cwd(), id);
693
+ const { target, created } = forkSkill({ ...opts });
694
+ if (created)
695
+ console.log(`skill forked: ${id} -> ${target}`);
696
+ const editorEnv = process.env.VISUAL || process.env.EDITOR;
697
+ if (editorEnv) {
698
+ const { spawnSync } = await import("child_process");
699
+ spawnSync(editorEnv, [target], { stdio: "inherit", shell: true });
700
+ }
701
+ else {
702
+ console.log(`edit: ${target}`);
703
+ }
704
+ return;
705
+ }
706
+ throw new Error("Unknown skill action: " + action);
707
+ }
708
+ //# sourceMappingURL=cli-skill-commands.js.map