diff --git a/README.md b/README.md
index 4310a37..5d82f94 100644
--- a/README.md
+++ b/README.md
@@ -7,6 +7,15 @@ Windows 전용 시맨틱 런처 & 워크스페이스 매니저
개발 참고: Claw Code 동등성 작업 추적 문서
`docs/claw-code-parity-plan.md`
+- 업데이트: 2026-04-14 21:25 (KST)
+- 문서 생성 고도화 2차를 반영했습니다. [ArtifactQualityReviewService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ArtifactQualityReviewService.cs)를 추가해 HTML/DOCX/XLSX 결과물에 공통 품질 점수와 보완 포인트를 부여하고, [HtmlSkill.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/HtmlSkill.cs), [DocxSkill.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/DocxSkill.cs), [ExcelSkill.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ExcelSkill.cs)에 연결했습니다.
+- Word 쪽은 템플릿·커버·목차를 강화했습니다. `docx_create`는 `template_path`, `cover_subtitle`, `cover_meta`, `toc`를 받아 템플릿 기반 문서와 커버 페이지, TOC 필드를 함께 만들 수 있게 되었고, 결과 요약에도 문서 품질 리뷰가 같이 남습니다.
+- Excel 쪽은 요약 시트 품질을 높였습니다. summary sheet가 detail sheet 링크를 자동으로 넣고, KPI/하이라이트/액션 구조를 가진 워크북에 대해 품질 점수를 함께 반환합니다.
+- HTML 쪽은 결과 요약에 업무 문서 기준의 로컬 품질 리뷰를 붙였습니다. 커버, 목차, 인쇄 최적화, 구조화 블록 사용 여부를 함께 평가해 후속 보정 근거를 더 쉽게 확인할 수 있습니다.
+- 번들 스킬도 확장했습니다. [executive-brief.skill.md](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/skills/executive-brief.skill.md), [kpi-workbook.skill.md](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/skills/kpi-workbook.skill.md), [board-report-html.skill.md](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/skills/board-report-html.skill.md)을 추가해 임원 보고 문서, KPI 워크북, HTML 이사회 보고서 생성을 바로 유도할 수 있게 했습니다.
+- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_doc_phase2\\ -p:IntermediateOutputPath=obj\\verify_doc_phase2\\` 경고 0 / 오류 0
+- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "ArtifactQualityReviewServiceTests|DocxSkillTemplateFeaturesTests|ExcelSkillExecutiveSummaryLinkTests|DocumentAssemblerSemanticTests|DocumentPlannerBusinessDocumentTests|HtmlSkillConsultingSectionsTests|ExcelSkillSummarySheetTests" -p:OutputPath=bin\\verify_doc_phase2_tests\\ -p:IntermediateOutputPath=obj\\verify_doc_phase2_tests\\` 통과 9
+
- 업데이트: 2026-04-14 19:13 (KST)
- `claude-code` 기준 Phase 4 범위를 이어서 반영했습니다. MCP 서버 메타데이터를 `mcp` 스코프의 synthetic skill로 노출하는 [McpSkillCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/McpSkillCatalog.cs)를 추가했고, 스킬 source 정책은 `managed/user/additional/project/plugin/mcp/legacy` 단위로 켜고 끌 수 있게 확장했습니다.
- 슬래시 명령 합성도 정리했습니다. [SlashCommandCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/SlashCommandCatalog.cs)와 [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)가 이제 builtin command와 skill을 우선순위 기반으로 dedupe해 같은 `/명령`이 겹칠 때 더 안정적으로 하나만 노출합니다.
diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md
index 2c6b880..abe884b 100644
--- a/docs/DEVELOPMENT.md
+++ b/docs/DEVELOPMENT.md
@@ -1,4 +1,14 @@
업데이트: 2026-04-14 19:50 (KST)
+업데이트: 2026-04-14 21:25 (KST)
+- 문서 생성 고도화 2차를 반영했습니다. `src/AxCopilot/Services/Agent/ArtifactQualityReviewService.cs`를 추가해 HTML/DOCX/XLSX 산출물에 대해 로컬 품질 점수와 보완 포인트를 계산하고, `HtmlSkill`, `DocxSkill`, `ExcelSkill`이 같은 리뷰 모델을 공유하도록 맞췄습니다.
+- `src/AxCopilot/Services/Agent/DocxSkill.cs`는 `template_path`, `cover_subtitle`, `cover_meta`, `toc`를 지원하도록 확장했습니다. DOCX 템플릿 복제 후 본문을 재구성하고, 커버 페이지와 TOC 필드를 삽입한 뒤 structured review 결과를 함께 반환합니다.
+- `src/AxCopilot/Services/Agent/ExcelSkill.cs`는 executive summary sheet에 detail sheet 링크를 자동 생성하고, KPI/highlights/actions 구조를 workbook quality review와 연결하도록 보강했습니다.
+- `src/AxCopilot/Services/Agent/HtmlSkill.cs`는 로컬 품질 리뷰를 결과 요약에 포함하도록 바꿨고, 문서형 번들 스킬로 `executive-brief`, `kpi-workbook`, `board-report-html`을 추가했습니다.
+- 테스트: `ArtifactQualityReviewServiceTests`, `DocxSkillTemplateFeaturesTests`, `ExcelSkillExecutiveSummaryLinkTests` 추가
+- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_doc_phase2\\ -p:IntermediateOutputPath=obj\\verify_doc_phase2\\` 경고 0 / 오류 0
+- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "ArtifactQualityReviewServiceTests|DocxSkillTemplateFeaturesTests|ExcelSkillExecutiveSummaryLinkTests|DocumentAssemblerSemanticTests|DocumentPlannerBusinessDocumentTests|HtmlSkillConsultingSectionsTests|ExcelSkillSummarySheetTests" -p:OutputPath=bin\\verify_doc_phase2_tests\\ -p:IntermediateOutputPath=obj\\verify_doc_phase2_tests\\` 통과 9
+
+업데이트: 2026-04-14 19:50 (KST)
- Agent loop/queue/context 품질을 보강했습니다. `src/AxCopilot/Services/Agent/AgentCommandQueue.cs`로 실행 중 추가 입력을 우선순위와 interrupt 여부까지 포함해 관리하고, `AgentLoopService`는 이를 안전하게 반영합니다.
- `AgentToolResultBudget`, `AgentQueryContextBuilder`, `ChatModels`는 tool result preview를 메시지에 캐시해 긴 세션과 재질문에서도 같은 축약 결과를 재사용하도록 정리했습니다.
- 코드 탭의 내장 언어 지원을 `src/AxCopilot/Services/CodeLanguageCatalog.cs`로 통합했고, 설정의 코드 탭에 지원 언어(LSP)와 코드 탭 기본 지원 언어를 명시적으로 표시합니다.
diff --git a/src/AxCopilot.Tests/Services/ArtifactQualityReviewServiceTests.cs b/src/AxCopilot.Tests/Services/ArtifactQualityReviewServiceTests.cs
new file mode 100644
index 0000000..c9c99e6
--- /dev/null
+++ b/src/AxCopilot.Tests/Services/ArtifactQualityReviewServiceTests.cs
@@ -0,0 +1,73 @@
+using AxCopilot.Services.Agent;
+using FluentAssertions;
+using Xunit;
+
+namespace AxCopilot.Tests.Services;
+
+public class ArtifactQualityReviewServiceTests
+{
+ [Fact]
+ public void ReviewHtml_ShouldDetectRichBusinessStructure()
+ {
+ var html =
+ """
+
Executive Summary
Summary text.
Another supporting paragraph.
+ Current State
Current state detail.
Evidence paragraph.
+ Important message
+
+
+
+ Recommendation
Recommendation text.
Action support text.
+ Appendix
Reference detail.
More detail.
+ """;
+
+ var review = ArtifactQualityReviewService.ReviewHtml("Board Report", html, hasCover: true, hasTableOfContents: true, printReady: true);
+
+ review.Score.Should().BeGreaterThan(75);
+ review.Strengths.Should().Contain(s => s.Contains("커버") || s.Contains("목차"));
+ review.Issues.Should().NotContain(i => i.Severity == ArtifactReviewSeverity.Critical);
+ }
+
+ [Fact]
+ public void ReviewStructuredDocument_ShouldFlagMissingExecutiveSections()
+ {
+ var review = ArtifactQualityReviewService.ReviewStructuredDocument(new StructuredDocumentReviewInput(
+ "Ops Memo",
+ 2,
+ 600,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false));
+
+ review.Issues.Should().Contain(i => i.Message.Contains("Executive Summary"));
+ review.Issues.Should().Contain(i => i.Message.Contains("권고안"));
+ }
+
+ [Fact]
+ public void ReviewWorkbook_ShouldFlagMissingSummaryForMultiSheet()
+ {
+ var review = ArtifactQualityReviewService.ReviewWorkbook(new WorkbookReviewInput(
+ "PMO Tracker",
+ 3,
+ 3,
+ 40,
+ 2,
+ 0,
+ 0,
+ false,
+ false,
+ false));
+
+ review.Issues.Should().Contain(i => i.Message.Contains("요약 시트"));
+ review.Score.Should().BeLessThan(80);
+ }
+}
diff --git a/src/AxCopilot.Tests/Services/DocxSkillTemplateFeaturesTests.cs b/src/AxCopilot.Tests/Services/DocxSkillTemplateFeaturesTests.cs
new file mode 100644
index 0000000..880ec14
--- /dev/null
+++ b/src/AxCopilot.Tests/Services/DocxSkillTemplateFeaturesTests.cs
@@ -0,0 +1,76 @@
+using System.IO;
+using System.Text.Json;
+using AxCopilot.Services.Agent;
+using DocumentFormat.OpenXml.Packaging;
+using DocumentFormat.OpenXml.Wordprocessing;
+using FluentAssertions;
+using Xunit;
+
+namespace AxCopilot.Tests.Services;
+
+public class DocxSkillTemplateFeaturesTests
+{
+ [Fact]
+ public async Task ExecuteAsync_ShouldCreateDocx_WithTemplateCoverAndToc()
+ {
+ var workDir = Path.Combine(Path.GetTempPath(), "ax-docx-template-" + Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(workDir);
+
+ try
+ {
+ var templatePath = Path.Combine(workDir, "template.docx");
+ using (var template = WordprocessingDocument.Create(templatePath, DocumentFormat.OpenXml.WordprocessingDocumentType.Document))
+ {
+ var mainPart = template.AddMainDocumentPart();
+ mainPart.Document = new Document(new Body(new Paragraph(new Run(new Text("Template Root")))));
+ mainPart.Document.Save();
+ }
+
+ var tool = new DocxSkill();
+ var context = new AgentContext
+ {
+ WorkFolder = workDir,
+ Permission = "Auto",
+ OperationMode = "external",
+ };
+
+ var args = JsonDocument.Parse(
+ """
+ {
+ "path": "executive-brief.docx",
+ "title": "Executive Brief",
+ "template_path": "template.docx",
+ "cover_subtitle": "Q2 Operating Review",
+ "cover_meta": ["Prepared for Steering Committee", "Confidential"],
+ "toc": true,
+ "sections": [
+ { "heading": "Executive Summary", "body": "Summary line one.\nSummary line two.", "level": 1 },
+ { "heading": "Recommendation", "body": "Recommendation details.", "level": 1 }
+ ]
+ }
+ """).RootElement;
+
+ var result = await tool.ExecuteAsync(args, context, CancellationToken.None);
+
+ result.Success.Should().BeTrue();
+ var outputPath = Path.Combine(workDir, "executive-brief.docx");
+ File.Exists(outputPath).Should().BeTrue();
+
+ using var doc = WordprocessingDocument.Open(outputPath, false);
+ doc.MainDocumentPart!.Document.Body!.InnerText.Should().Contain("Q2 Operating Review");
+ doc.MainDocumentPart.Document.Body.InnerText.Should().Contain("Prepared for Steering Committee");
+ doc.MainDocumentPart.Document.Body.Descendants().Should().Contain(f => f.Text.Contains("TOC"));
+ }
+ finally
+ {
+ try
+ {
+ if (Directory.Exists(workDir))
+ Directory.Delete(workDir, true);
+ }
+ catch
+ {
+ }
+ }
+ }
+}
diff --git a/src/AxCopilot.Tests/Services/ExcelSkillExecutiveSummaryLinkTests.cs b/src/AxCopilot.Tests/Services/ExcelSkillExecutiveSummaryLinkTests.cs
new file mode 100644
index 0000000..2bf2afe
--- /dev/null
+++ b/src/AxCopilot.Tests/Services/ExcelSkillExecutiveSummaryLinkTests.cs
@@ -0,0 +1,80 @@
+using System.IO;
+using System.Text.Json;
+using AxCopilot.Services.Agent;
+using DocumentFormat.OpenXml.Packaging;
+using DocumentFormat.OpenXml.Spreadsheet;
+using FluentAssertions;
+using Xunit;
+
+namespace AxCopilot.Tests.Services;
+
+public class ExcelSkillExecutiveSummaryLinkTests
+{
+ [Fact]
+ public async Task ExecuteAsync_ShouldCreateSummarySheetWithDetailLinks()
+ {
+ var workDir = Path.Combine(Path.GetTempPath(), "ax-xlsx-summary-" + Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(workDir);
+
+ try
+ {
+ var tool = new ExcelSkill();
+ var context = new AgentContext
+ {
+ WorkFolder = workDir,
+ Permission = "Auto",
+ OperationMode = "external",
+ };
+
+ var args = JsonDocument.Parse(
+ """
+ {
+ "path": "ops-review.xlsx",
+ "summary_sheet": {
+ "name": "Summary",
+ "title": "Ops Review",
+ "highlights": ["Margin improved", "Backlog stable"],
+ "actions": ["Expand automation", "Tighten forecast"]
+ },
+ "sheets": [
+ {
+ "name": "Revenue",
+ "headers": ["Metric", "Value"],
+ "rows": [["Revenue", 120], ["Margin", 18], ["Delta", "=B2-B3"]]
+ },
+ {
+ "name": "Pipeline",
+ "headers": ["Stage", "Count"],
+ "rows": [["Qualified", 14], ["Proposal", 8]]
+ }
+ ]
+ }
+ """).RootElement;
+
+ var result = await tool.ExecuteAsync(args, context, CancellationToken.None);
+
+ result.Success.Should().BeTrue();
+ var outputPath = Path.Combine(workDir, "ops-review.xlsx");
+ File.Exists(outputPath).Should().BeTrue();
+
+ using var doc = SpreadsheetDocument.Open(outputPath, false);
+ var workbookPart = doc.WorkbookPart;
+ workbookPart.Should().NotBeNull();
+ var summarySheet = workbookPart!.Workbook.Sheets!.Elements().First(s => s.Name == "Summary");
+ var summaryPart = (WorksheetPart)workbookPart.GetPartById(summarySheet.Id!);
+ summaryPart.Worksheet.Elements().Should().ContainSingle();
+ summaryPart.Worksheet.Descendants().Count().Should().BeGreaterOrEqualTo(2);
+ }
+ finally
+ {
+ try
+ {
+ if (Directory.Exists(workDir))
+ Directory.Delete(workDir, true);
+ }
+ catch
+ {
+ }
+ }
+ }
+}
diff --git a/src/AxCopilot/Services/Agent/ArtifactQualityReviewService.cs b/src/AxCopilot/Services/Agent/ArtifactQualityReviewService.cs
new file mode 100644
index 0000000..f7e8fa9
--- /dev/null
+++ b/src/AxCopilot/Services/Agent/ArtifactQualityReviewService.cs
@@ -0,0 +1,216 @@
+using System.Text.RegularExpressions;
+
+namespace AxCopilot.Services.Agent;
+
+public enum ArtifactReviewSeverity
+{
+ Info,
+ Warning,
+ Critical,
+}
+
+public sealed record ArtifactReviewIssue(string Message, ArtifactReviewSeverity Severity);
+
+public sealed record ArtifactQualityReport(
+ string ArtifactType,
+ int Score,
+ IReadOnlyList Strengths,
+ IReadOnlyList Issues)
+{
+ public string ToToolSummary()
+ {
+ var strengths = Strengths.Take(3).ToList();
+ var issues = Issues.OrderByDescending(i => i.Severity).Take(3).Select(i => i.Message).ToList();
+
+ var parts = new List { $"품질 점수 {Score}/100" };
+ if (strengths.Count > 0)
+ parts.Add("강점: " + string.Join(", ", strengths));
+ if (issues.Count > 0)
+ parts.Add("보완: " + string.Join(", ", issues));
+ else
+ parts.Add("보완 필요 사항 없음");
+
+ return string.Join(" | ", parts);
+ }
+}
+
+public sealed record StructuredDocumentReviewInput(
+ string Title,
+ int SectionCount,
+ int BodyCharacterCount,
+ int TableCount,
+ int ListCount,
+ int CalloutCount,
+ int HighlightCount,
+ int IconCount,
+ bool HasCoverPage,
+ bool HasTableOfContents,
+ bool HasTemplate,
+ bool HasHeaderFooter,
+ bool HasExecutiveSummarySection,
+ bool HasRecommendationSection,
+ bool HasAppendixSection);
+
+public sealed record WorkbookReviewInput(
+ string Title,
+ int SheetCount,
+ int DetailSheetCount,
+ int DataRowCount,
+ int FormulaCount,
+ int HyperlinkCount,
+ int DataValidationCount,
+ bool HasSummarySheet,
+ bool HasHighlightSection,
+ bool HasActionSection);
+
+public static class ArtifactQualityReviewService
+{
+ private static readonly string[] ExecutiveKeywords = ["executive summary", "요약", "핵심 요약", "summary"];
+ private static readonly string[] RecommendationKeywords = ["recommendation", "권고", "제안", "next steps", "실행 과제"];
+ private static readonly string[] AppendixKeywords = ["appendix", "부록", "참고", "reference"];
+
+ public static ArtifactQualityReport ReviewHtml(string title, string html, bool hasCover, bool hasTableOfContents, bool printReady)
+ {
+ title ??= string.Empty;
+ html ??= string.Empty;
+
+ var strengths = new List();
+ var issues = new List();
+
+ var sectionCount = Regex.Matches(html, @"= 5) strengths.Add($"핵심 섹션 {sectionCount}개 구성");
+ if (tableCount > 0 || comparisonCount > 0 || roadmapCount > 0 || matrixCount > 0 || kpiCount > 0)
+ strengths.Add("표/비교/로드맵/KPI 등 구조화 블록 활용");
+ if (calloutCount > 0) strengths.Add("핵심 메시지 강조 블록 포함");
+
+ if (html.Length < 1800)
+ issues.Add(new("문서 본문이 짧아 메시지와 근거 밀도가 낮을 수 있습니다", ArtifactReviewSeverity.Warning));
+ if (sectionCount < 4)
+ issues.Add(new("업무 문서 기준 핵심 섹션 수가 부족합니다", ArtifactReviewSeverity.Warning));
+ if (paragraphCount < sectionCount * 2)
+ issues.Add(new("일부 섹션의 서술이 충분하지 않습니다", ArtifactReviewSeverity.Warning));
+ if (tableCount + comparisonCount + roadmapCount + matrixCount + kpiCount == 0)
+ issues.Add(new("구조화 시각 요소가 없어 보고서 완성도가 낮을 수 있습니다", ArtifactReviewSeverity.Warning));
+ if (placeholderCount > 0)
+ issues.Add(new($"플레이스홀더/미완성 표현 {placeholderCount}건이 남아 있습니다", ArtifactReviewSeverity.Critical));
+ if (!ContainsAny(html, ExecutiveKeywords))
+ issues.Add(new("요약 또는 핵심 메시지 섹션이 명확하지 않습니다", ArtifactReviewSeverity.Warning));
+
+ return BuildReport("html", strengths, issues);
+ }
+
+ public static ArtifactQualityReport ReviewStructuredDocument(StructuredDocumentReviewInput input)
+ {
+ var strengths = new List();
+ var issues = new List();
+
+ if (input.HasTemplate) strengths.Add("템플릿 기반 스타일 상속");
+ if (input.HasCoverPage) strengths.Add("커버 페이지 포함");
+ if (input.HasTableOfContents) strengths.Add("목차 포함");
+ if (input.HasHeaderFooter) strengths.Add("머리글/바닥글 적용");
+ if (input.SectionCount >= 5) strengths.Add($"핵심 섹션 {input.SectionCount}개 구성");
+ if (input.TableCount > 0) strengths.Add($"테이블 {input.TableCount}개 포함");
+ if (input.CalloutCount + input.HighlightCount > 0) strengths.Add("강조 블록 활용");
+ if (input.ListCount > 0) strengths.Add("실행 항목/목록 구조 포함");
+
+ if (input.BodyCharacterCount < 1400)
+ issues.Add(new("문서 분량이 짧아 경영 보고용 밀도가 부족할 수 있습니다", ArtifactReviewSeverity.Warning));
+ if (input.SectionCount < 4)
+ issues.Add(new("업무 문서 기준 핵심 섹션 수가 부족합니다", ArtifactReviewSeverity.Warning));
+ if (!input.HasExecutiveSummarySection)
+ issues.Add(new("Executive Summary 또는 요약 섹션이 없습니다", ArtifactReviewSeverity.Warning));
+ if (!input.HasRecommendationSection)
+ issues.Add(new("권고안 또는 다음 단계 섹션이 없습니다", ArtifactReviewSeverity.Warning));
+ if (input.SectionCount >= 6 && !input.HasAppendixSection)
+ issues.Add(new("긴 문서인데 부록/참고 섹션이 없습니다", ArtifactReviewSeverity.Info));
+ if (input.TableCount + input.ListCount + input.CalloutCount + input.HighlightCount == 0)
+ issues.Add(new("표, 목록, 강조 블록이 없어 문서가 단조로울 수 있습니다", ArtifactReviewSeverity.Warning));
+
+ return BuildReport("docx", strengths, issues);
+ }
+
+ public static ArtifactQualityReport ReviewWorkbook(WorkbookReviewInput input)
+ {
+ var strengths = new List();
+ var issues = new List();
+
+ if (input.HasSummarySheet) strengths.Add("요약 시트 포함");
+ if (input.DetailSheetCount > 1) strengths.Add($"상세 시트 {input.DetailSheetCount}개 구성");
+ if (input.FormulaCount > 0) strengths.Add($"수식 {input.FormulaCount}개 포함");
+ if (input.HyperlinkCount > 0) strengths.Add("시트 간 빠른 이동 링크 포함");
+ if (input.DataValidationCount > 0) strengths.Add("데이터 검증 규칙 포함");
+ if (input.HasHighlightSection) strengths.Add("핵심 인사이트 영역 포함");
+ if (input.HasActionSection) strengths.Add("후속 액션 영역 포함");
+
+ if (input.SheetCount > 1 && !input.HasSummarySheet)
+ issues.Add(new("멀티시트 워크북인데 요약 시트가 없습니다", ArtifactReviewSeverity.Warning));
+ if (input.DataRowCount >= 10 && input.FormulaCount == 0)
+ issues.Add(new("데이터 행 수 대비 수식/집계가 부족합니다", ArtifactReviewSeverity.Warning));
+ if (input.HasSummarySheet && input.HyperlinkCount == 0)
+ issues.Add(new("요약 시트에서 상세 시트로 이동하는 링크가 없습니다", ArtifactReviewSeverity.Warning));
+ if (input.DataValidationCount == 0 && input.DataRowCount >= 5)
+ issues.Add(new("입력 통제를 위한 데이터 검증 규칙이 없습니다", ArtifactReviewSeverity.Info));
+
+ return BuildReport("xlsx", strengths, issues);
+ }
+
+ public static bool ContainsBusinessKeyword(string text, params string[] keywords)
+ {
+ return ContainsAny(text, keywords);
+ }
+
+ private static ArtifactQualityReport BuildReport(
+ string artifactType,
+ List strengths,
+ List issues)
+ {
+ var score = 72;
+ score += Math.Min(18, strengths.Count * 4);
+ score -= issues.Sum(issue => issue.Severity switch
+ {
+ ArtifactReviewSeverity.Critical => 12,
+ ArtifactReviewSeverity.Warning => 6,
+ _ => 2,
+ });
+ score = Math.Clamp(score, 35, 98);
+
+ return new ArtifactQualityReport(artifactType, score, strengths, issues);
+ }
+
+ private static int CountPlaceholders(string text)
+ {
+ var count = 0;
+ if (string.IsNullOrWhiteSpace(text))
+ return count;
+
+ var patterns = new[]
+ {
+ @"\[(todo|placeholder|내용.*작성|fill me)\]",
+ @"lorem ipsum",
+ @"tbd",
+ };
+
+ foreach (var pattern in patterns)
+ count += Regex.Matches(text, pattern, RegexOptions.IgnoreCase).Count;
+
+ return count;
+ }
+
+ private static bool ContainsAny(string text, IEnumerable keywords)
+ {
+ return keywords.Any(keyword => text.Contains(keyword, StringComparison.OrdinalIgnoreCase));
+ }
+}
diff --git a/src/AxCopilot/Services/Agent/DocxSkill.cs b/src/AxCopilot/Services/Agent/DocxSkill.cs
index 8b6ef12..6aaee0c 100644
--- a/src/AxCopilot/Services/Agent/DocxSkill.cs
+++ b/src/AxCopilot/Services/Agent/DocxSkill.cs
@@ -28,6 +28,15 @@ public class DocxSkill : IAgentTool
["path"] = new() { Type = "string", Description = "Output file path (.docx). Relative to work folder." },
["title"] = new() { Type = "string", Description = "Document title (optional)." },
["theme"] = new() { Type = "string", Description = "Visual theme: professional (default), modern, dark, minimal, creative." },
+ ["template_path"] = new() { Type = "string", Description = "Optional .docx template path. Reuses template styles and section setup." },
+ ["cover_subtitle"] = new() { Type = "string", Description = "Optional subtitle shown on the cover page." },
+ ["cover_meta"] = new()
+ {
+ Type = "array",
+ Description = "Optional cover page meta lines such as author, organization, review window, or distribution note.",
+ Items = new() { Type = "string" }
+ },
+ ["toc"] = new() { Type = "boolean", Description = "Include a table of contents field. Default: true when there are 4+ sections." },
["sections"] = new()
{
Type = "array",
@@ -114,6 +123,13 @@ public class DocxSkill : IAgentTool
var footerText = args.SafeTryGetProperty("footer", out var ftr) ? ftr.SafeGetString() : null;
var showPageNumbers = args.SafeTryGetProperty("page_numbers", out var pn) ? pn.GetBoolean() :
(headerText != null || footerText != null);
+ var coverSubtitle = args.SafeTryGetProperty("cover_subtitle", out var subtitleEl) ? subtitleEl.SafeGetString() : null;
+ var templatePath = args.SafeTryGetProperty("template_path", out var templateEl) ? templateEl.SafeGetString() : null;
+ var coverMeta = ReadStringArray(args, "cover_meta");
+ var sections = args.GetProperty("sections");
+ var useToc = args.SafeTryGetProperty("toc", out var tocEl)
+ ? tocEl.ValueKind == JsonValueKind.True
+ : sections.GetArrayLength() >= 4;
var themeName = args.SafeTryGetProperty("theme", out var th) ? th.SafeGetString() ?? "professional" : "professional";
var theme = Themes.TryGetValue(themeName, out var tc) ? tc : Themes["professional"];
@@ -131,26 +147,48 @@ public class DocxSkill : IAgentTool
try
{
- var sections = args.GetProperty("sections");
-
var dir = Path.GetDirectoryName(fullPath);
if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir);
+ string? templateFullPath = null;
+ var templateApplied = false;
- using var doc = WordprocessingDocument.Create(fullPath, WordprocessingDocumentType.Document);
- var mainPart = doc.AddMainDocumentPart();
+ if (!string.IsNullOrWhiteSpace(templatePath))
+ {
+ templateFullPath = FileReadTool.ResolvePath(templatePath, context.WorkFolder);
+ if (!templateFullPath.EndsWith(".docx", StringComparison.OrdinalIgnoreCase))
+ templateFullPath += ".docx";
+
+ if (string.Equals(Path.GetFullPath(templateFullPath), Path.GetFullPath(fullPath), StringComparison.OrdinalIgnoreCase))
+ return ToolResult.Fail("template_path와 path는 같은 파일일 수 없습니다.");
+ if (!context.IsPathAllowed(templateFullPath))
+ return ToolResult.Fail($"템플릿 경로 접근 차단: {templateFullPath}");
+ if (!File.Exists(templateFullPath))
+ return ToolResult.Fail($"DOCX 템플릿을 찾을 수 없습니다: {templateFullPath}");
+
+ File.Copy(templateFullPath, fullPath, true);
+ templateApplied = true;
+ }
+
+ using var doc = templateApplied
+ ? WordprocessingDocument.Open(fullPath, true)
+ : WordprocessingDocument.Create(fullPath, WordprocessingDocumentType.Document);
+ var mainPart = doc.MainDocumentPart ?? doc.AddMainDocumentPart();
// 기본 스타일 파트 추가 (styles.xml)
- var stylesPart = mainPart.AddNewPart();
- stylesPart.Styles = CreateDefaultDocxStyles();
- stylesPart.Styles.Save();
+ EnsureStyles(mainPart, templateApplied);
- mainPart.Document = new Document();
- var body = mainPart.Document.AppendChild(new Body());
+ var body = InitializeDocumentBody(mainPart, templateApplied);
// 머리글/바닥글 설정
if (headerText != null || footerText != null || showPageNumbers)
AddHeaderFooter(mainPart, body, headerText, footerText, showPageNumbers);
+ if (!string.IsNullOrWhiteSpace(title) || !string.IsNullOrWhiteSpace(coverSubtitle) || coverMeta.Count > 0)
+ AppendCoverPage(body, title, coverSubtitle, coverMeta, theme);
+
+ if (useToc)
+ AppendTableOfContents(body);
+
// 제목
if (!string.IsNullOrEmpty(title))
{
@@ -167,6 +205,11 @@ public class DocxSkill : IAgentTool
int sectionCount = 0;
int tableCount = 0;
+ int listCount = 0;
+ int calloutCount = 0;
+ int highlightCount = 0;
+ int iconCount = 0;
+ var headings = new List();
foreach (var section in sections.EnumerateArray())
{
var blockType = section.SafeTryGetProperty("type", out var bt) ? bt.SafeGetString()?.ToLower() : null;
@@ -187,24 +230,28 @@ public class DocxSkill : IAgentTool
if (blockType == "list")
{
AppendList(body, section);
+ listCount++;
continue;
}
if (blockType == "callout")
{
AppendCallout(body, section);
+ calloutCount++;
continue;
}
if (blockType == "highlight_box")
{
body.Append(CreateHighlightBox(section));
+ highlightCount++;
continue;
}
if (blockType == "icon")
{
body.Append(CreateIconParagraph(section));
+ iconCount++;
continue;
}
@@ -214,7 +261,10 @@ public class DocxSkill : IAgentTool
var level = section.SafeTryGetProperty("level", out var lv) ? lv.GetInt32() : 1;
if (!string.IsNullOrEmpty(heading))
+ {
body.Append(CreateHeadingParagraph(heading, level, theme));
+ headings.Add(heading);
+ }
if (!string.IsNullOrEmpty(bodyText))
{
@@ -231,6 +281,9 @@ public class DocxSkill : IAgentTool
mainPart.Document.Save();
var parts = new List();
+ if (templateApplied) parts.Add("템플릿");
+ if (useToc) parts.Add("목차");
+ if (!string.IsNullOrWhiteSpace(coverSubtitle) || coverMeta.Count > 0) parts.Add("커버");
if (!string.IsNullOrEmpty(title)) parts.Add($"제목: {title}");
if (sectionCount > 0) parts.Add($"섹션: {sectionCount}개");
if (tableCount > 0) parts.Add($"테이블: {tableCount}개");
@@ -238,6 +291,46 @@ public class DocxSkill : IAgentTool
if (showPageNumbers) parts.Add("페이지번호");
parts.Add($"테마: {themeName}");
+ #if false
+ var review = ArtifactQualityReviewService.ReviewStructuredDocument(new StructuredDocumentReviewInput(
+ title,
+ sectionCount,
+ sections.ToString().Length,
+ tableCount,
+ listCount,
+ calloutCount,
+ highlightCount,
+ iconCount,
+ !string.IsNullOrWhiteSpace(coverSubtitle) || coverMeta.Count > 0,
+ useToc,
+ templateApplied,
+ headerText != null || footerText != null || showPageNumbers,
+ headings.Any(h => ArtifactQualityReviewService.ContainsBusinessKeyword(h, "executive summary", "summary", "요약")),
+ headings.Any(h => ArtifactQualityReviewService.ContainsBusinessKeyword(h, "recommendation", "권고", "제안", "next step", "실행")),
+ headings.Any(h => ArtifactQualityReviewService.ContainsBusinessKeyword(h, "appendix", "부록", "참고"))
+ ));
+
+ parts.Add(review.ToToolSummary());
+ #endif
+ var review = ArtifactQualityReviewService.ReviewStructuredDocument(new StructuredDocumentReviewInput(
+ title,
+ sectionCount,
+ sections.ToString().Length,
+ tableCount,
+ listCount,
+ calloutCount,
+ highlightCount,
+ iconCount,
+ !string.IsNullOrWhiteSpace(coverSubtitle) || coverMeta.Count > 0,
+ useToc,
+ templateApplied,
+ headerText != null || footerText != null || showPageNumbers,
+ headings.Any(h => ArtifactQualityReviewService.ContainsBusinessKeyword(h, "executive summary", "summary")),
+ headings.Any(h => ArtifactQualityReviewService.ContainsBusinessKeyword(h, "recommendation", "proposal", "next step", "action")),
+ headings.Any(h => ArtifactQualityReviewService.ContainsBusinessKeyword(h, "appendix", "reference", "supplement"))
+ ));
+
+ parts.Add(review.ToToolSummary());
return ToolResult.Ok(
$"Word 문서 생성 완료: {fullPath}\n{string.Join(", ", parts)}",
fullPath);
@@ -252,6 +345,118 @@ public class DocxSkill : IAgentTool
// 제목/소제목/본문 단락 생성
// ═══════════════════════════════════════════════════
+ private static List ReadStringArray(JsonElement args, string propertyName)
+ {
+ var items = new List();
+ if (!args.SafeTryGetProperty(propertyName, out var arr) || arr.ValueKind != JsonValueKind.Array)
+ return items;
+
+ foreach (var item in arr.EnumerateArray())
+ {
+ var text = item.SafeGetString();
+ if (!string.IsNullOrWhiteSpace(text))
+ items.Add(text);
+ }
+
+ return items;
+ }
+
+ private static Body InitializeDocumentBody(MainDocumentPart mainPart, bool preserveSectionSetup)
+ {
+ mainPart.Document ??= new Document();
+
+ SectionProperties? preservedSection = null;
+ if (preserveSectionSetup)
+ preservedSection = mainPart.Document.Body?.Elements().LastOrDefault()?.CloneNode(true) as SectionProperties;
+
+ mainPart.Document.Body = new Body();
+ var body = mainPart.Document.Body;
+
+ if (preservedSection != null)
+ body.Append(preservedSection);
+
+ return body;
+ }
+
+ private static void EnsureStyles(MainDocumentPart mainPart, bool preserveExisting)
+ {
+ var stylesPart = mainPart.StyleDefinitionsPart;
+ if (stylesPart == null)
+ {
+ stylesPart = mainPart.AddNewPart();
+ stylesPart.Styles = CreateDefaultDocxStyles();
+ stylesPart.Styles.Save();
+ return;
+ }
+
+ if (!preserveExisting || stylesPart.Styles == null)
+ {
+ stylesPart.Styles = CreateDefaultDocxStyles();
+ stylesPart.Styles.Save();
+ }
+ }
+
+ private static void AppendCoverPage(Body body, string title, string? subtitle, IReadOnlyList metaLines, ThemeColors theme)
+ {
+ if (!string.IsNullOrWhiteSpace(title))
+ body.Append(CreateTitleParagraph(title, theme));
+
+ if (!string.IsNullOrWhiteSpace(subtitle))
+ {
+ body.Append(new Paragraph(
+ new ParagraphProperties
+ {
+ Justification = new Justification { Val = JustificationValues.Center },
+ SpacingBetweenLines = new SpacingBetweenLines { After = "220" },
+ },
+ new Run(
+ new RunProperties(
+ new FontSize { Val = "26" },
+ new Color { Val = theme.H2 },
+ new RunFonts { Ascii = "Noto Sans KR", HighAnsi = "Noto Sans KR", EastAsia = "Noto Sans KR" }),
+ new Text(subtitle))));
+ }
+
+ foreach (var line in metaLines)
+ {
+ body.Append(new Paragraph(
+ new ParagraphProperties
+ {
+ Justification = new Justification { Val = JustificationValues.Center },
+ SpacingBetweenLines = new SpacingBetweenLines { After = "80" },
+ },
+ new Run(
+ new RunProperties(
+ new FontSize { Val = "20" },
+ new Color { Val = "666666" },
+ new RunFonts { Ascii = "Noto Sans KR", HighAnsi = "Noto Sans KR", EastAsia = "Noto Sans KR" }),
+ new Text(line))));
+ }
+
+ body.Append(CreatePageBreak());
+ }
+
+ private static void AppendTableOfContents(Body body)
+ {
+ body.Append(new Paragraph(
+ new ParagraphProperties(new SpacingBetweenLines { Before = "120", After = "120" }),
+ new Run(
+ new RunProperties(
+ new Bold(),
+ new FontSize { Val = "28" },
+ new RunFonts { Ascii = "Noto Sans KR", HighAnsi = "Noto Sans KR", EastAsia = "Noto Sans KR" }),
+ new Text("목차"))));
+
+ var paragraph = new Paragraph();
+ paragraph.Append(new Run(new FieldChar { FieldCharType = FieldCharValues.Begin }));
+ paragraph.Append(new Run(new FieldCode(" TOC \\o \"1-3\" \\h \\z \\u ") { Space = SpaceProcessingModeValues.Preserve }));
+ paragraph.Append(new Run(new FieldChar { FieldCharType = FieldCharValues.Separate }));
+ paragraph.Append(new Run(new Text("목차를 업데이트하려면 Word에서 필드를 새로 고치세요.")));
+ paragraph.Append(new Run(new FieldChar { FieldCharType = FieldCharValues.End }));
+ body.Append(paragraph);
+ body.Append(CreatePageBreak());
+ }
+
private static Paragraph CreateTitleParagraph(string text, ThemeColors theme)
{
var para = new Paragraph();
@@ -779,6 +984,7 @@ public class DocxSkill : IAgentTool
// SectionProperties에 머리글 연결
var secProps = body.GetFirstChild() ?? body.AppendChild(new SectionProperties());
+ secProps.Elements().ToList().ForEach(r => r.Remove());
secProps.Append(new HeaderReference
{
Type = HeaderFooterValues.Default,
@@ -825,6 +1031,7 @@ public class DocxSkill : IAgentTool
footerPart.Footer = footer;
var secProps = body.GetFirstChild() ?? body.AppendChild(new SectionProperties());
+ secProps.Elements().ToList().ForEach(r => r.Remove());
secProps.Append(new FooterReference
{
Type = HeaderFooterValues.Default,
diff --git a/src/AxCopilot/Services/Agent/ExcelSkill.cs b/src/AxCopilot/Services/Agent/ExcelSkill.cs
index 240c0c7..67644ca 100644
--- a/src/AxCopilot/Services/Agent/ExcelSkill.cs
+++ b/src/AxCopilot/Services/Agent/ExcelSkill.cs
@@ -202,6 +202,19 @@ public class ExcelSkill : IAgentTool
summaryArg.ValueKind == JsonValueKind.Object,
numFmts.Count > 0, alignments.Count > 0, themeName);
+ var review = ArtifactQualityReviewService.ReviewWorkbook(new WorkbookReviewInput(
+ sheetName,
+ 1,
+ 1,
+ rowCount,
+ CountFormulaCells(rows),
+ 0,
+ 0,
+ false,
+ false,
+ summaryArg.ValueKind == JsonValueKind.Object));
+ features += $"\n{review.ToToolSummary()}";
+
return ToolResult.Ok(
$"Excel 파일 생성 완료: {fullPath}\n시트: {sheetName}, 열: {colCount}, 행: {rowCount}{features}",
fullPath);
@@ -274,6 +287,18 @@ public class ExcelSkill : IAgentTool
mergesArg.ValueKind == JsonValueKind.Array,
summaryArg.ValueKind == JsonValueKind.Object,
numFmts.Count > 0, alignments.Count > 0, themeName);
+ var review = ArtifactQualityReviewService.ReviewWorkbook(new WorkbookReviewInput(
+ summaryName,
+ 2,
+ 1,
+ rowCount,
+ CountFormulaCells(rows),
+ 1,
+ 0,
+ true,
+ HasSummaryItems(summarySheet, "highlights"),
+ HasSummaryItems(summarySheet, "actions")));
+ features += $"\n{review.ToToolSummary()}";
return ToolResult.Ok(
$"Excel ?뚯씪 ?앹꽦 ?꾨즺: {fullPath}\n?쒗듃: {summaryName}, {sheetName}, ?? {colCount}, ?? {rowCount}{features} [?붿빟 ?쒗듃]",
@@ -313,6 +338,7 @@ public class ExcelSkill : IAgentTool
uint sheetId = 1;
var totalSheets = 0;
var totalRows = 0;
+ var totalFormulaCount = 0;
var detailSheetNames = new List();
foreach (var sheetDef in sheetsArr.EnumerateArray())
@@ -373,10 +399,10 @@ public class ExcelSkill : IAgentTool
sheetId++;
totalSheets++;
totalRows += rowCount;
+ totalFormulaCount += CountFormulaCells(rows);
}
workbookPart.Workbook.Save();
-
return ToolResult.Ok(
$"Excel 파일 생성 완료: {fullPath}\n시트: {totalSheets}개, 총 데이터 행: {totalRows}",
fullPath);
@@ -403,6 +429,7 @@ public class ExcelSkill : IAgentTool
worksheetPart.Worksheet.Append(sheetData);
var merges = new MergeCells();
+ var hyperlinks = new Hyperlinks();
uint rowIndex = 1;
var title = summarySheet.SafeTryGetProperty("title", out var titleEl)
@@ -454,8 +481,27 @@ public class ExcelSkill : IAgentTool
AppendMergedTextRow(sheetData, merges, rowIndex++, "A", "F", $"• {detailSheetName}", 0);
}
+ if (detailSheetNames.Count > 0)
+ {
+ var hyperlinkStartRow = rowIndex - (uint)detailSheetNames.Count;
+ for (var i = 0; i < detailSheetNames.Count; i++)
+ {
+ hyperlinks.Append(new Hyperlink
+ {
+ Reference = $"A{hyperlinkStartRow + (uint)i}",
+ Location = $"'{detailSheetNames[i]}'!A1",
+ Display = detailSheetNames[i]
+ });
+ }
+ }
+
if (merges.HasChildren)
worksheetPart.Worksheet.InsertAfter(merges, sheetData);
+ if (hyperlinks.HasChildren)
+ {
+ var anchor = merges.HasChildren ? (OpenXmlElement)merges : sheetData;
+ worksheetPart.Worksheet.InsertAfter(hyperlinks, anchor);
+ }
}
private static void AppendSummaryTextSection(JsonElement summarySheet, string key, string heading, SheetData sheetData, MergeCells merges, ref uint rowIndex)
@@ -979,6 +1025,36 @@ public class ExcelSkill : IAgentTool
return result;
}
+ private static int CountFormulaCells(JsonElement rows)
+ {
+ if (rows.ValueKind != JsonValueKind.Array)
+ return 0;
+
+ var count = 0;
+ foreach (var row in rows.EnumerateArray())
+ {
+ if (row.ValueKind != JsonValueKind.Array)
+ continue;
+
+ foreach (var cell in row.EnumerateArray())
+ {
+ var text = cell.ToString();
+ if (!string.IsNullOrWhiteSpace(text) && text.StartsWith('='))
+ count++;
+ }
+ }
+
+ return count;
+ }
+
+ private static bool HasSummaryItems(JsonElement summarySheet, string propertyName)
+ {
+ return summarySheet.ValueKind == JsonValueKind.Object
+ && summarySheet.SafeTryGetProperty(propertyName, out var items)
+ && items.ValueKind == JsonValueKind.Array
+ && items.GetArrayLength() > 0;
+ }
+
private static string BuildFeatureList(bool isStyled, bool freezeHeader,
bool hasMerges, bool hasSummary, bool hasNumFmts, bool hasAlignments, string? theme)
{
diff --git a/src/AxCopilot/Services/Agent/HtmlSkill.cs b/src/AxCopilot/Services/Agent/HtmlSkill.cs
index d8231a2..6237cf8 100644
--- a/src/AxCopilot/Services/Agent/HtmlSkill.cs
+++ b/src/AxCopilot/Services/Agent/HtmlSkill.cs
@@ -288,6 +288,8 @@ public class HtmlSkill : IAgentTool
if (!string.IsNullOrEmpty(accentColor)) features.Add($"색상:{accentColor}");
if (usePrint) features.Add("인쇄최적화");
var featureStr = features.Count > 0 ? $" [{string.Join(", ", features)}]" : "";
+ var review = ArtifactQualityReviewService.ReviewHtml(title, sb.ToString(), hasCover, useToc, usePrint);
+ featureStr += $"\n{review.ToToolSummary()}";
return ToolResult.Ok(
$"HTML 문서 생성 완료: {fullPath} (디자인: {mood}{featureStr})",
diff --git a/src/AxCopilot/skills/board-report-html.skill.md b/src/AxCopilot/skills/board-report-html.skill.md
new file mode 100644
index 0000000..0138d03
--- /dev/null
+++ b/src/AxCopilot/skills/board-report-html.skill.md
@@ -0,0 +1,26 @@
+---
+name: board-report-html
+label: HTML 보고서
+description: 이사회/운영위원회용 HTML 보고서를 구조화된 섹션과 인쇄형 레이아웃으로 생성합니다.
+icon: \uE8A7
+when_to_use: 웹 공유와 인쇄를 모두 고려한 HTML 경영 보고서가 필요할 때
+argument-hint: <보고 목적과 청중>
+allowed-tools:
+ - folder_map
+ - file_read
+ - document_plan
+ - document_assemble
+ - html_create
+tabs: cowork
+---
+
+HTML 업무 보고서를 작성하세요.
+
+작성 원칙:
+- `html_create`를 우선 사용하고 `toc`, `print`, `cover`, `sections`를 적극 활용하세요.
+- 문서는 `Executive Summary -> Current State -> Key Issues -> Options -> Recommendation -> Roadmap -> Appendix` 순서를 기본으로 잡으세요.
+- comparison, roadmap, matrix, KPI, callout 블록을 내용에 맞게 섞어 쓰세요.
+- 섹션마다 최소 두 개 이상의 실질 문단 또는 구조화 블록을 포함하세요.
+- 단순 꾸밈보다 의사결정에 필요한 근거, 비교, 후속 조치를 분명히 드러내세요.
+
+결과물은 작업 폴더에 저장하고, 인쇄 가능 여부와 핵심 결론을 함께 요약하세요.
diff --git a/src/AxCopilot/skills/executive-brief.skill.md b/src/AxCopilot/skills/executive-brief.skill.md
new file mode 100644
index 0000000..ca2d826
--- /dev/null
+++ b/src/AxCopilot/skills/executive-brief.skill.md
@@ -0,0 +1,26 @@
+---
+name: executive-brief
+label: 임원 보고 문서
+description: 경영진 보고용 Word 문서를 AX 네이티브 도구 중심으로 구성합니다.
+icon: \uE9D9
+when_to_use: 경영진, 임원진, steering committee, 의사결정 회의용 Word 보고 문서를 빠르게 만들 때
+argument-hint: <보고 주제와 의사결정 포인트>
+allowed-tools:
+ - folder_map
+ - file_read
+ - document_plan
+ - document_assemble
+ - docx_create
+tabs: cowork
+---
+
+임원 보고 문서를 작성하세요.
+
+핵심 원칙:
+- Python 우회 대신 `document_plan`, `document_assemble`, `docx_create`를 우선 사용하세요.
+- 문서는 `Executive Summary -> Situation -> Key Findings -> Recommendation -> Next Steps -> Appendix` 구조를 기본으로 잡으세요.
+- 각 섹션 제목은 메시지형으로 쓰고, 표와 강조 블록을 적극 활용하세요.
+- 긴 문서는 `document_plan`으로 구조를 먼저 만들고, 최종 산출은 DOCX로 조립하세요.
+- 사내 템플릿이 있다면 `template_path`, `cover_subtitle`, `toc`를 포함해 완성도를 높이세요.
+
+결과물은 작업 폴더에 저장하고, 최종 응답에는 파일 경로와 핵심 섹션을 요약하세요.
diff --git a/src/AxCopilot/skills/kpi-workbook.skill.md b/src/AxCopilot/skills/kpi-workbook.skill.md
new file mode 100644
index 0000000..4213718
--- /dev/null
+++ b/src/AxCopilot/skills/kpi-workbook.skill.md
@@ -0,0 +1,25 @@
+---
+name: kpi-workbook
+label: KPI 워크북
+description: 요약 시트와 상세 시트를 갖춘 KPI 분석용 Excel 워크북을 생성합니다.
+icon: \uE9D2
+when_to_use: KPI 대시보드, 운영 리뷰, 예산 대비 실적, PMO 추적용 Excel 워크북이 필요할 때
+argument-hint: <분석 주제와 포함할 핵심 지표>
+allowed-tools:
+ - folder_map
+ - file_read
+ - data_pivot
+ - excel_create
+tabs: cowork
+---
+
+요약 시트가 있는 KPI 워크북을 만드세요.
+
+작성 원칙:
+- 단일 표보다 `Summary + Detail` 구조의 워크북을 우선 구성하세요.
+- 요약 시트에는 KPI, highlights, actions를 넣고 상세 시트로 이어지는 구조를 만드세요.
+- 수치 데이터가 있으면 가능한 범위에서 수식과 집계 행을 포함하세요.
+- 시트 이름은 이해하기 쉽게 짓고, 운영 회의에서 바로 쓰일 수 있도록 핵심 지표와 후속 액션을 함께 정리하세요.
+- 원본 데이터가 여러 개면 먼저 `data_pivot`으로 정리한 뒤 `excel_create`를 호출하세요.
+
+결과물은 작업 폴더에 저장하고, 시트 구성과 핵심 KPI를 함께 요약하세요.