@team-semicolon/semo-cli 4.1.5 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -112,7 +112,6 @@ semo update --skip-cli # CLI 업데이트 건너뛰기
112
112
  | 패키지 | 설명 |
113
113
  |--------|------|
114
114
  | `semo-core` | 원칙, 오케스트레이터, 공통 커맨드 |
115
- | `semo-skills` | 13개 통합 스킬 (coder, tester, planner, deployer 등) |
116
115
 
117
116
  ### Extensions (선택)
118
117
 
@@ -166,8 +165,8 @@ your-project/
166
165
  │ └── commands/SEMO/ # SEMO 커맨드
167
166
 
168
167
  └── semo-system/ # White Box (읽기 전용)
169
- ├── semo-core/ # Layer 0: 원칙, 오케스트레이션
170
- ├── semo-skills/ # Layer 1: 통합 스킬
168
+ ├── semo-core/ # 원칙, 오케스트레이션
169
+ ├── bot-workspaces/ # 전용 스킬/컨텍스트
171
170
  ├── biz/ # Business Layer (선택)
172
171
  ├── eng/ # Engineering Layer (선택)
173
172
  └── ops/ # Operations Layer (선택)
@@ -203,7 +202,7 @@ MCP 연동을 위해 다음 환경변수를 설정하세요:
203
202
  | `next.config.js`, `next.config.mjs`, `next.config.ts` | `eng/nextjs` |
204
203
  | `pom.xml`, `build.gradle` | `eng/spring` |
205
204
  | `Dockerfile`, `docker-compose.yml` | `eng/infra` |
206
- | `semo-core`, `semo-skills` | `meta` |
205
+ | `semo-core` | `meta` |
207
206
 
208
207
  ## 레거시 명령어 호환
209
208
 
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Bot Workspace Audit — 표준 구조 compliance 감사
3
+ *
4
+ * 12가지 체크를 수행하고 점수/등급을 산정한다.
5
+ * - 파일 9개 (root 7 + memory/slim + skills/)
6
+ * - KB 3개 (team, process, decision 도메인 존재)
7
+ * --fix 옵션으로 누락 파일/디렉토리 자동 생성 가능.
8
+ */
9
+ import { Pool, PoolClient } from "pg";
10
+ export interface AuditCheck {
11
+ name: string;
12
+ passed: boolean;
13
+ detail: string;
14
+ }
15
+ export interface BotAuditResult {
16
+ botId: string;
17
+ rating: "GOOD" | "NEEDS-WORK" | "POOR";
18
+ score: number;
19
+ checks: AuditCheck[];
20
+ }
21
+ export declare function auditBot(botDir: string, botId: string): BotAuditResult;
22
+ export declare function fixBot(botDir: string, botId: string, checks: AuditCheck[]): number;
23
+ export declare function auditBotKb(pool: Pool): Promise<AuditCheck[]>;
24
+ export declare function auditBotDb(botId: string, pool: Pool): Promise<AuditCheck[]>;
25
+ export declare function mergeDbChecks(result: BotAuditResult, dbChecks: AuditCheck[]): BotAuditResult;
26
+ export declare function formatAuditSlack(results: BotAuditResult[]): string;
27
+ export declare function storeAuditResults(results: BotAuditResult[], client: PoolClient): Promise<void>;
@@ -0,0 +1,338 @@
1
+ "use strict";
2
+ /**
3
+ * Bot Workspace Audit — 표준 구조 compliance 감사
4
+ *
5
+ * 12가지 체크를 수행하고 점수/등급을 산정한다.
6
+ * - 파일 9개 (root 7 + memory/slim + skills/)
7
+ * - KB 3개 (team, process, decision 도메인 존재)
8
+ * --fix 옵션으로 누락 파일/디렉토리 자동 생성 가능.
9
+ */
10
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ var desc = Object.getOwnPropertyDescriptor(m, k);
13
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
14
+ desc = { enumerable: true, get: function() { return m[k]; } };
15
+ }
16
+ Object.defineProperty(o, k2, desc);
17
+ }) : (function(o, m, k, k2) {
18
+ if (k2 === undefined) k2 = k;
19
+ o[k2] = m[k];
20
+ }));
21
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
22
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
23
+ }) : function(o, v) {
24
+ o["default"] = v;
25
+ });
26
+ var __importStar = (this && this.__importStar) || (function () {
27
+ var ownKeys = function(o) {
28
+ ownKeys = Object.getOwnPropertyNames || function (o) {
29
+ var ar = [];
30
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
31
+ return ar;
32
+ };
33
+ return ownKeys(o);
34
+ };
35
+ return function (mod) {
36
+ if (mod && mod.__esModule) return mod;
37
+ var result = {};
38
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
39
+ __setModuleDefault(result, mod);
40
+ return result;
41
+ };
42
+ })();
43
+ Object.defineProperty(exports, "__esModule", { value: true });
44
+ exports.auditBot = auditBot;
45
+ exports.fixBot = fixBot;
46
+ exports.auditBotKb = auditBotKb;
47
+ exports.auditBotDb = auditBotDb;
48
+ exports.mergeDbChecks = mergeDbChecks;
49
+ exports.formatAuditSlack = formatAuditSlack;
50
+ exports.storeAuditResults = storeAuditResults;
51
+ const fs = __importStar(require("fs"));
52
+ const path = __importStar(require("path"));
53
+ const crypto_1 = require("crypto");
54
+ function fileExistsCheck(name, relativePath) {
55
+ return {
56
+ name,
57
+ check: (botDir) => {
58
+ const fullPath = path.join(botDir, relativePath);
59
+ const exists = fs.existsSync(fullPath);
60
+ return {
61
+ name,
62
+ passed: exists,
63
+ detail: exists ? `${relativePath} exists` : `${relativePath} missing`,
64
+ };
65
+ },
66
+ };
67
+ }
68
+ function dirExistsCheck(name, relativePath) {
69
+ return {
70
+ name,
71
+ check: (botDir) => {
72
+ const fullPath = path.join(botDir, relativePath);
73
+ const exists = fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory();
74
+ return {
75
+ name,
76
+ passed: exists,
77
+ detail: exists
78
+ ? `${relativePath}/ exists`
79
+ : `${relativePath}/ missing`,
80
+ };
81
+ },
82
+ };
83
+ }
84
+ function memorySlimCheck() {
85
+ return {
86
+ name: "memory/slim",
87
+ check: (botDir) => {
88
+ const memoryPath = path.join(botDir, "MEMORY.md");
89
+ if (!fs.existsSync(memoryPath)) {
90
+ return { name: "memory/slim", passed: true, detail: "MEMORY.md not present (N/A)" };
91
+ }
92
+ const lines = fs.readFileSync(memoryPath, "utf-8").split("\n").length;
93
+ const passed = lines < 50;
94
+ return {
95
+ name: "memory/slim",
96
+ passed,
97
+ detail: passed
98
+ ? `MEMORY.md is ${lines} lines (< 50)`
99
+ : `MEMORY.md is ${lines} lines (>= 50, bloated)`,
100
+ };
101
+ },
102
+ };
103
+ }
104
+ const CHECK_DEFS = [
105
+ // 1-7: Root files
106
+ fileExistsCheck("root/SOUL.md", "SOUL.md"),
107
+ fileExistsCheck("root/IDENTITY.md", "IDENTITY.md"),
108
+ fileExistsCheck("root/AGENTS.md", "AGENTS.md"),
109
+ fileExistsCheck("root/USER.md", "USER.md"),
110
+ fileExistsCheck("root/TOOLS.md", "TOOLS.md"),
111
+ fileExistsCheck("root/RULES.md", "RULES.md"),
112
+ fileExistsCheck("root/MEMORY.md", "MEMORY.md"),
113
+ // 8: Memory slim
114
+ memorySlimCheck(),
115
+ // 9: skills/
116
+ dirExistsCheck("skills/", "skills"),
117
+ ];
118
+ // ============================================================
119
+ // Core audit function
120
+ // ============================================================
121
+ function auditBot(botDir, botId) {
122
+ const checks = CHECK_DEFS.map((def) => def.check(botDir, botId));
123
+ const passed = checks.filter((c) => c.passed).length;
124
+ const total = checks.length;
125
+ const score = Math.round((passed / total) * 100);
126
+ const memorySlim = checks.find((c) => c.name === "memory/slim");
127
+ const isMemorySlim = memorySlim ? memorySlim.passed : true;
128
+ let rating;
129
+ if (score >= 80 && isMemorySlim) {
130
+ rating = "GOOD";
131
+ }
132
+ else if (score >= 50) {
133
+ rating = "NEEDS-WORK";
134
+ }
135
+ else {
136
+ rating = "POOR";
137
+ }
138
+ return { botId, rating, score, checks };
139
+ }
140
+ // ============================================================
141
+ // Auto-fix
142
+ // ============================================================
143
+ const FIXABLE_FILES = {
144
+ "root/SOUL.md": "SOUL.md",
145
+ "root/IDENTITY.md": "IDENTITY.md",
146
+ "root/AGENTS.md": "AGENTS.md",
147
+ "root/USER.md": "USER.md",
148
+ "root/TOOLS.md": "TOOLS.md",
149
+ "root/RULES.md": "RULES.md",
150
+ "root/MEMORY.md": "MEMORY.md",
151
+ };
152
+ const FIXABLE_DIRS = {
153
+ "skills/": "skills",
154
+ };
155
+ const FILE_TEMPLATES = {
156
+ "RULES.md": (botId) => `# RULES.md — ${botId} 행동 규칙\n\n> 공식 표준: kb_get(domain='process', key='bot-workspace-standard')\n\n## NON-NEGOTIABLE\n\n## 행동 원칙\n\n## 금지 사항\n`,
157
+ };
158
+ function fixBot(botDir, botId, checks) {
159
+ let fixed = 0;
160
+ for (const check of checks) {
161
+ if (check.passed)
162
+ continue;
163
+ // Fix missing files
164
+ if (FIXABLE_FILES[check.name]) {
165
+ const relPath = FIXABLE_FILES[check.name];
166
+ const fullPath = path.join(botDir, relPath);
167
+ const dir = path.dirname(fullPath);
168
+ if (!fs.existsSync(dir)) {
169
+ fs.mkdirSync(dir, { recursive: true });
170
+ }
171
+ const filename = path.basename(relPath);
172
+ const templateFn = FILE_TEMPLATES[relPath];
173
+ const content = templateFn
174
+ ? templateFn(botId)
175
+ : `# ${filename}\n\n> TODO: ${botId}\n`;
176
+ fs.writeFileSync(fullPath, content, "utf-8");
177
+ fixed++;
178
+ }
179
+ // Fix missing directories
180
+ if (FIXABLE_DIRS[check.name]) {
181
+ const relPath = FIXABLE_DIRS[check.name];
182
+ const fullPath = path.join(botDir, relPath);
183
+ if (!fs.existsSync(fullPath)) {
184
+ fs.mkdirSync(fullPath, { recursive: true });
185
+ fixed++;
186
+ }
187
+ }
188
+ }
189
+ return fixed;
190
+ }
191
+ // ============================================================
192
+ // DB storage
193
+ // ============================================================
194
+ // ============================================================
195
+ // KB domain checks (async, requires pool)
196
+ // ============================================================
197
+ const KB_REQUIRED_DOMAINS = ["team", "process", "decision"];
198
+ async function auditBotKb(pool) {
199
+ const checks = [];
200
+ try {
201
+ const result = await pool.query(`SELECT domain, COUNT(*)::int as cnt
202
+ FROM semo.knowledge_base
203
+ WHERE domain = ANY($1)
204
+ GROUP BY domain`, [KB_REQUIRED_DOMAINS]);
205
+ const counts = new Map(result.rows.map((r) => [r.domain, r.cnt]));
206
+ for (const domain of KB_REQUIRED_DOMAINS) {
207
+ const cnt = counts.get(domain) ?? 0;
208
+ checks.push({
209
+ name: `kb/${domain}`,
210
+ passed: cnt > 0,
211
+ detail: cnt > 0
212
+ ? `KB ${domain} 도메인: ${cnt}개 엔트리`
213
+ : `KB ${domain} 도메인: 엔트리 없음`,
214
+ });
215
+ }
216
+ }
217
+ catch (err) {
218
+ for (const domain of KB_REQUIRED_DOMAINS) {
219
+ checks.push({
220
+ name: `kb/${domain}`,
221
+ passed: false,
222
+ detail: `KB 조회 실패: ${err}`,
223
+ });
224
+ }
225
+ }
226
+ return checks;
227
+ }
228
+ // ============================================================
229
+ // DB sync checks (async, requires pool)
230
+ // ============================================================
231
+ const SYNC_STALE_HOURS = 24;
232
+ async function auditBotDb(botId, pool) {
233
+ const checks = [];
234
+ try {
235
+ const result = await pool.query("SELECT synced_at FROM semo.bot_status WHERE bot_id = $1", [botId]);
236
+ const registered = result.rows.length > 0;
237
+ checks.push({
238
+ name: "db/registered",
239
+ passed: registered,
240
+ detail: registered ? "bot_status에 등록됨" : "bot_status에 미등록",
241
+ });
242
+ if (registered) {
243
+ const syncedAt = result.rows[0].synced_at;
244
+ const hoursAgo = syncedAt
245
+ ? (Date.now() - new Date(syncedAt).getTime()) / 3600000
246
+ : Infinity;
247
+ const recent = hoursAgo < SYNC_STALE_HOURS;
248
+ checks.push({
249
+ name: "db/synced_recent",
250
+ passed: recent,
251
+ detail: recent
252
+ ? `${Math.round(hoursAgo)}시간 전 동기화`
253
+ : syncedAt
254
+ ? `${Math.round(hoursAgo)}시간 전 (>${SYNC_STALE_HOURS}h stale)`
255
+ : "synced_at 없음",
256
+ });
257
+ }
258
+ else {
259
+ checks.push({
260
+ name: "db/synced_recent",
261
+ passed: false,
262
+ detail: "DB 미등록 — 동기화 불가",
263
+ });
264
+ }
265
+ }
266
+ catch (err) {
267
+ checks.push({
268
+ name: "db/registered",
269
+ passed: false,
270
+ detail: `DB 조회 실패: ${err}`,
271
+ });
272
+ checks.push({
273
+ name: "db/synced_recent",
274
+ passed: false,
275
+ detail: "DB 조회 실패",
276
+ });
277
+ }
278
+ return checks;
279
+ }
280
+ function mergeDbChecks(result, dbChecks) {
281
+ const allChecks = [...result.checks, ...dbChecks];
282
+ const passed = allChecks.filter((c) => c.passed).length;
283
+ const total = allChecks.length;
284
+ const score = Math.round((passed / total) * 100);
285
+ const memorySlim = allChecks.find((c) => c.name === "memory/slim");
286
+ const isMemorySlim = memorySlim ? memorySlim.passed : true;
287
+ let rating;
288
+ if (score >= 80 && isMemorySlim) {
289
+ rating = "GOOD";
290
+ }
291
+ else if (score >= 50) {
292
+ rating = "NEEDS-WORK";
293
+ }
294
+ else {
295
+ rating = "POOR";
296
+ }
297
+ return { botId: result.botId, rating, score, checks: allChecks };
298
+ }
299
+ // ============================================================
300
+ // Slack formatter
301
+ // ============================================================
302
+ const RATING_EMOJI = {
303
+ GOOD: "🟢",
304
+ "NEEDS-WORK": "🟡",
305
+ POOR: "🔴",
306
+ };
307
+ function formatAuditSlack(results) {
308
+ const date = new Date().toISOString().slice(0, 10);
309
+ const good = results.filter((r) => r.rating === "GOOD").length;
310
+ const needsWork = results.filter((r) => r.rating === "NEEDS-WORK").length;
311
+ const poor = results.filter((r) => r.rating === "POOR").length;
312
+ const avgScore = Math.round(results.reduce((s, r) => s + r.score, 0) / results.length);
313
+ const lines = [
314
+ `📋 *봇 워크스페이스 Audit* — ${date}`,
315
+ "",
316
+ ];
317
+ for (const r of results) {
318
+ const emoji = RATING_EMOJI[r.rating] ?? "⚪";
319
+ const failed = r.checks.filter((c) => !c.passed);
320
+ const failInfo = failed.length > 0
321
+ ? ` — missing: ${failed.map((c) => c.name).join(", ")}`
322
+ : "";
323
+ lines.push(`${emoji} *${r.botId}* ${r.score}% ${r.rating}${failInfo}`);
324
+ }
325
+ lines.push("");
326
+ lines.push(`${results.length}개 봇 | 평균 ${avgScore}% | GOOD: ${good} / NEEDS-WORK: ${needsWork} / POOR: ${poor}`);
327
+ return lines.join("\n");
328
+ }
329
+ // ============================================================
330
+ // DB storage
331
+ // ============================================================
332
+ async function storeAuditResults(results, client) {
333
+ const runId = (0, crypto_1.randomUUID)();
334
+ for (const r of results) {
335
+ await client.query(`INSERT INTO semo.bot_workspace_audits (bot_id, run_id, rating, score, checks)
336
+ VALUES ($1, $2, $3, $4, $5)`, [r.botId, runId, r.rating, r.score, JSON.stringify(r.checks)]);
337
+ }
338
+ }