@team-semicolon/semo-cli 4.12.0 → 4.15.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.
@@ -86,6 +86,9 @@ function registerCommitmentsCommands(program) {
86
86
  JSON.stringify(steps),
87
87
  ]);
88
88
  console.log(chalk_1.default.green(`✔ commitment created: ${id}`));
89
+ if (!options.sourceType) {
90
+ console.log(chalk_1.default.yellow(`⚠ source-type 미지정: 자동 검증 불가. --source-type 권장.`));
91
+ }
89
92
  console.log(JSON.stringify({ id, bot_id: options.botId, title: options.title, deadline_at: deadlineAt }));
90
93
  }
91
94
  catch (err) {
@@ -239,7 +242,8 @@ function registerCommitmentsCommands(program) {
239
242
  }
240
243
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
241
244
  params.push(parseInt(options.limit));
242
- const result = await pool.query(`SELECT id, bot_id, status, title, deadline_at::text, steps,
245
+ const result = await pool.query(`SELECT id, bot_id, status, title, description, source_type, source_ref,
246
+ deadline_at::text, steps,
243
247
  last_heartbeat_at::text, created_at::text, completed_at::text, metadata
244
248
  FROM semo.bot_commitments
245
249
  ${where}
@@ -290,6 +294,8 @@ function registerCommitmentsCommands(program) {
290
294
  .command("watch")
291
295
  .description("워치독 — 활성 약속의 health 상태 조회")
292
296
  .option("--format <type>", "출력 형식 (table|json)", "table")
297
+ .option("--bot-id <id>", "특정 봇만 필터")
298
+ .option("--exclude-bot <id>", "특정 봇 제외")
293
299
  .action(async (options) => {
294
300
  const connected = await (0, database_1.isDbConnected)();
295
301
  if (!connected) {
@@ -299,17 +305,33 @@ function registerCommitmentsCommands(program) {
299
305
  }
300
306
  try {
301
307
  const pool = (0, database_1.getPool)();
302
- const result = await pool.query(`SELECT id, bot_id, status, title, deadline_at::text,
308
+ const conditions = [];
309
+ const params = [];
310
+ let paramIdx = 1;
311
+ if (options.botId) {
312
+ conditions.push(`bot_id = $${paramIdx++}`);
313
+ params.push(options.botId);
314
+ }
315
+ if (options.excludeBot) {
316
+ conditions.push(`bot_id != $${paramIdx++}`);
317
+ params.push(options.excludeBot);
318
+ }
319
+ const whereClause = conditions.length > 0
320
+ ? `WHERE ${conditions.join(" AND ")}`
321
+ : "";
322
+ const result = await pool.query(`SELECT id, bot_id, status, title, description, source_type, source_ref,
323
+ deadline_at::text,
303
324
  health, minutes_since_heartbeat, minutes_overdue,
304
325
  steps, last_heartbeat_at::text, created_at::text, metadata
305
326
  FROM semo.v_active_commitments
327
+ ${whereClause}
306
328
  ORDER BY
307
329
  CASE health
308
330
  WHEN 'overdue' THEN 0
309
331
  WHEN 'stale' THEN 1
310
332
  ELSE 2
311
333
  END,
312
- deadline_at ASC NULLS LAST`);
334
+ deadline_at ASC NULLS LAST`, params);
313
335
  if (options.format === "json") {
314
336
  const summary = {
315
337
  total: result.rows.length,
@@ -0,0 +1,9 @@
1
+ /**
2
+ * commitments 테스트 — bot_commitments CRUD + 워치독 뷰 검증
3
+ *
4
+ * 실행: npx ts-node packages/cli/src/commands/commitments.test.ts
5
+ *
6
+ * 실제 DB 연결 필요 (semo.bot_commitments 테이블).
7
+ * 테스트 데이터는 bot_id='__test__' 접두사로 생성하며 종료 시 정리.
8
+ */
9
+ export {};
@@ -0,0 +1,223 @@
1
+ "use strict";
2
+ /**
3
+ * commitments 테스트 — bot_commitments CRUD + 워치독 뷰 검증
4
+ *
5
+ * 실행: npx ts-node packages/cli/src/commands/commitments.test.ts
6
+ *
7
+ * 실제 DB 연결 필요 (semo.bot_commitments 테이블).
8
+ * 테스트 데이터는 bot_id='__test__' 접두사로 생성하며 종료 시 정리.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ const database_1 = require("../database");
12
+ const TEST_BOT = "__test__";
13
+ let passed = 0;
14
+ let failed = 0;
15
+ const createdIds = [];
16
+ function assert(condition, message) {
17
+ if (condition) {
18
+ passed++;
19
+ console.log(` ✅ ${message}`);
20
+ }
21
+ else {
22
+ failed++;
23
+ console.log(` ❌ ${message}`);
24
+ }
25
+ }
26
+ function makeId() {
27
+ const rand = Math.random().toString(36).slice(2, 6);
28
+ return `cmt-${TEST_BOT}-${Date.now()}-${rand}`;
29
+ }
30
+ async function cleanup() {
31
+ try {
32
+ const pool = (0, database_1.getPool)();
33
+ await pool.query(`DELETE FROM semo.bot_commitments WHERE bot_id = $1`, [TEST_BOT]);
34
+ }
35
+ catch { /* ignore */ }
36
+ }
37
+ async function run() {
38
+ console.log("\n🧪 commitments.test.ts\n");
39
+ const connected = await (0, database_1.isDbConnected)();
40
+ if (!connected) {
41
+ console.log("❌ DB 연결 실패 — 테스트 스킵");
42
+ process.exit(1);
43
+ }
44
+ const pool = (0, database_1.getPool)();
45
+ // 사전 정리
46
+ await cleanup();
47
+ // ── Case 1: CREATE — 기본 생성 ─────────────────────────────────────────────
48
+ console.log("Case 1: 기본 생성");
49
+ const id1 = makeId();
50
+ createdIds.push(id1);
51
+ {
52
+ await pool.query(`INSERT INTO semo.bot_commitments (id, bot_id, status, title, deadline_at)
53
+ VALUES ($1, $2, 'pending', '테스트 약속 1', NOW() + INTERVAL '15 minutes')`, [id1, TEST_BOT]);
54
+ const r = await pool.query(`SELECT * FROM semo.bot_commitments WHERE id = $1`, [id1]);
55
+ assert(r.rows.length === 1, "INSERT 성공");
56
+ assert(r.rows[0].status === "pending", "초기 상태 = pending");
57
+ assert(r.rows[0].bot_id === TEST_BOT, `bot_id = ${TEST_BOT}`);
58
+ assert(r.rows[0].completed_at === null, "completed_at = null (아직 완료 전)");
59
+ }
60
+ // ── Case 2: CREATE — steps JSON ────────────────────────────────────────────
61
+ console.log("Case 2: steps JSON 저장");
62
+ const id2 = makeId();
63
+ createdIds.push(id2);
64
+ {
65
+ const steps = [
66
+ { label: "Typography", done: false },
67
+ { label: "Layout", done: false },
68
+ { label: "Motion", done: false },
69
+ ];
70
+ await pool.query(`INSERT INTO semo.bot_commitments (id, bot_id, status, title, steps)
71
+ VALUES ($1, $2, 'pending', 'Steps 테스트', $3)`, [id2, TEST_BOT, JSON.stringify(steps)]);
72
+ const r = await pool.query(`SELECT steps FROM semo.bot_commitments WHERE id = $1`, [id2]);
73
+ const savedSteps = r.rows[0].steps;
74
+ assert(Array.isArray(savedSteps), "steps는 배열");
75
+ assert(savedSteps.length === 3, `steps 3개 (got ${savedSteps.length})`);
76
+ assert(savedSteps[0].label === "Typography", "첫 step label 일치");
77
+ assert(savedSteps.every((s) => !s.done), "모든 step done=false");
78
+ }
79
+ // ── Case 3: UPDATE — heartbeat ─────────────────────────────────────────────
80
+ console.log("Case 3: heartbeat 갱신");
81
+ {
82
+ await pool.query(`UPDATE semo.bot_commitments
83
+ SET last_heartbeat_at = NOW(),
84
+ status = CASE WHEN status = 'pending' THEN 'active' ELSE status END
85
+ WHERE id = $1`, [id1]);
86
+ const r = await pool.query(`SELECT status, last_heartbeat_at FROM semo.bot_commitments WHERE id = $1`, [id1]);
87
+ assert(r.rows[0].status === "active", "heartbeat → status = active");
88
+ assert(r.rows[0].last_heartbeat_at !== null, "last_heartbeat_at 채워짐");
89
+ }
90
+ // ── Case 4: UPDATE — step-done ─────────────────────────────────────────────
91
+ console.log("Case 4: step 완료 처리");
92
+ {
93
+ await pool.query(`UPDATE semo.bot_commitments
94
+ SET steps = (
95
+ SELECT jsonb_agg(
96
+ CASE
97
+ WHEN elem->>'label' = $2 THEN jsonb_set(elem, '{done}', 'true')
98
+ ELSE elem
99
+ END
100
+ )
101
+ FROM jsonb_array_elements(steps) AS elem
102
+ )
103
+ WHERE id = $1`, [id2, "Typography"]);
104
+ const r = await pool.query(`SELECT steps FROM semo.bot_commitments WHERE id = $1`, [id2]);
105
+ const steps = r.rows[0].steps;
106
+ const typo = steps.find((s) => s.label === "Typography");
107
+ const layout = steps.find((s) => s.label === "Layout");
108
+ assert(typo.done === true, "Typography done=true");
109
+ assert(layout.done === false, "Layout 여전히 done=false");
110
+ }
111
+ // ── Case 5: DONE — completed_at 트리거 ────────────────────────────────────
112
+ console.log("Case 5: done → completed_at 자동 채움");
113
+ {
114
+ await pool.query(`UPDATE semo.bot_commitments SET status = 'done' WHERE id = $1`, [id1]);
115
+ const r = await pool.query(`SELECT status, completed_at FROM semo.bot_commitments WHERE id = $1`, [id1]);
116
+ assert(r.rows[0].status === "done", "status = done");
117
+ assert(r.rows[0].completed_at !== null, "completed_at 자동 채워짐 (트리거)");
118
+ }
119
+ // ── Case 6: FAIL — metadata.fail_reason ────────────────────────────────────
120
+ console.log("Case 6: fail + reason 저장");
121
+ const id3 = makeId();
122
+ createdIds.push(id3);
123
+ {
124
+ await pool.query(`INSERT INTO semo.bot_commitments (id, bot_id, status, title)
125
+ VALUES ($1, $2, 'active', '실패 테스트')`, [id3, TEST_BOT]);
126
+ await pool.query(`UPDATE semo.bot_commitments
127
+ SET status = 'failed', metadata = metadata || jsonb_build_object('fail_reason', $2::text)
128
+ WHERE id = $1`, [id3, "빌드 실패"]);
129
+ const r = await pool.query(`SELECT status, completed_at, metadata FROM semo.bot_commitments WHERE id = $1`, [id3]);
130
+ assert(r.rows[0].status === "failed", "status = failed");
131
+ assert(r.rows[0].completed_at !== null, "completed_at 자동 채워짐");
132
+ assert(r.rows[0].metadata.fail_reason === "빌드 실패", "metadata.fail_reason 저장됨");
133
+ }
134
+ // ── Case 7: updated_at 트리거 ──────────────────────────────────────────────
135
+ console.log("Case 7: updated_at 자동 갱신");
136
+ {
137
+ const before = await pool.query(`SELECT updated_at FROM semo.bot_commitments WHERE id = $1`, [id2]);
138
+ // 최소 1ms 대기
139
+ await new Promise((r) => setTimeout(r, 50));
140
+ await pool.query(`UPDATE semo.bot_commitments SET title = 'Updated title' WHERE id = $1`, [id2]);
141
+ const after = await pool.query(`SELECT updated_at FROM semo.bot_commitments WHERE id = $1`, [id2]);
142
+ const t1 = new Date(before.rows[0].updated_at).getTime();
143
+ const t2 = new Date(after.rows[0].updated_at).getTime();
144
+ assert(t2 > t1, `updated_at 갱신됨 (${t2 - t1}ms 차이)`);
145
+ }
146
+ // ── Case 8: v_active_commitments 뷰 ───────────────────────────────────────
147
+ console.log("Case 8: v_active_commitments 뷰");
148
+ const id4 = makeId();
149
+ createdIds.push(id4);
150
+ {
151
+ // overdue commitment 생성 (deadline 과거)
152
+ await pool.query(`INSERT INTO semo.bot_commitments (id, bot_id, status, title, deadline_at)
153
+ VALUES ($1, $2, 'active', 'Overdue 테스트', NOW() - INTERVAL '10 minutes')`, [id4, TEST_BOT]);
154
+ const r = await pool.query(`SELECT health, minutes_overdue FROM semo.v_active_commitments WHERE id = $1`, [id4]);
155
+ assert(r.rows.length === 1, "뷰에서 조회 가능");
156
+ assert(r.rows[0].health === "overdue", `health = overdue (got ${r.rows[0].health})`);
157
+ assert(r.rows[0].minutes_overdue > 0, `minutes_overdue > 0 (got ${r.rows[0].minutes_overdue})`);
158
+ }
159
+ // ── Case 9: stale 감지 ─────────────────────────────────────────────────────
160
+ console.log("Case 9: stale heartbeat 감지");
161
+ const id5 = makeId();
162
+ createdIds.push(id5);
163
+ {
164
+ await pool.query(`INSERT INTO semo.bot_commitments (id, bot_id, status, title, last_heartbeat_at)
165
+ VALUES ($1, $2, 'active', 'Stale 테스트', NOW() - INTERVAL '45 minutes')`, [id5, TEST_BOT]);
166
+ const r = await pool.query(`SELECT health, minutes_since_heartbeat FROM semo.v_active_commitments WHERE id = $1`, [id5]);
167
+ assert(r.rows[0].health === "stale", `health = stale (got ${r.rows[0].health})`);
168
+ assert(r.rows[0].minutes_since_heartbeat >= 44, `heartbeat 45분+ 전`);
169
+ }
170
+ // ── Case 10: on-track ──────────────────────────────────────────────────────
171
+ console.log("Case 10: on-track (정상)");
172
+ const id6 = makeId();
173
+ createdIds.push(id6);
174
+ {
175
+ await pool.query(`INSERT INTO semo.bot_commitments (id, bot_id, status, title, deadline_at, last_heartbeat_at)
176
+ VALUES ($1, $2, 'active', 'On-track 테스트', NOW() + INTERVAL '30 minutes', NOW())`, [id6, TEST_BOT]);
177
+ const r = await pool.query(`SELECT health FROM semo.v_active_commitments WHERE id = $1`, [id6]);
178
+ assert(r.rows[0].health === "on-track", `health = on-track (got ${r.rows[0].health})`);
179
+ }
180
+ // ── Case 11: 완료된 건은 뷰에서 제외 ──────────────────────────────────────
181
+ console.log("Case 11: 완료된 commitment는 뷰에서 제외");
182
+ {
183
+ const r = await pool.query(`SELECT * FROM semo.v_active_commitments WHERE id = $1`, [id1] // Case 5에서 done 처리됨
184
+ );
185
+ assert(r.rows.length === 0, "done 상태는 v_active_commitments에서 제외");
186
+ }
187
+ // ── Case 12: 인덱스 사용 확인 ─────────────────────────────────────────────
188
+ console.log("Case 12: 인덱스 존재 확인");
189
+ {
190
+ const r = await pool.query(`SELECT indexname FROM pg_indexes WHERE tablename = 'bot_commitments' AND schemaname = 'semo' ORDER BY indexname`);
191
+ const names = r.rows.map((row) => row.indexname);
192
+ assert(names.includes("idx_commitments_active"), "idx_commitments_active 존재");
193
+ assert(names.includes("idx_commitments_bot"), "idx_commitments_bot 존재");
194
+ }
195
+ // ── Case 13: watch --bot-id 필터 ─────────────────────────────────────────
196
+ console.log("Case 13: watch --bot-id 필터");
197
+ {
198
+ // __test__ 봇의 활성 커밋먼트만 조회 (id4=overdue, id5=stale, id6=on-track)
199
+ const r = await pool.query(`SELECT id FROM semo.v_active_commitments WHERE bot_id = $1`, [TEST_BOT]);
200
+ assert(r.rows.length >= 3, `--bot-id 필터: __test__ 항목 3건+ (got ${r.rows.length})`);
201
+ assert(r.rows.every((row) => row.id.startsWith("cmt-__test__")), "모두 __test__ 봇 소유");
202
+ }
203
+ // ── Case 14: watch --exclude-bot 필터 ──────────────────────────────────
204
+ console.log("Case 14: watch --exclude-bot 필터");
205
+ {
206
+ const r = await pool.query(`SELECT id, bot_id FROM semo.v_active_commitments WHERE bot_id != $1`, [TEST_BOT]);
207
+ const hasTestBot = r.rows.some((row) => row.bot_id === TEST_BOT);
208
+ assert(!hasTestBot, "--exclude-bot 필터: __test__ 봇 제외됨");
209
+ }
210
+ // ── 정리 ──────────────────────────────────────────────────────────────────
211
+ await cleanup();
212
+ // ── 결과 ──────────────────────────────────────────────────────────────────
213
+ console.log(`\n${"─".repeat(40)}`);
214
+ console.log(`총 ${passed + failed}개 테스트: ✅ ${passed} passed, ❌ ${failed} failed\n`);
215
+ await (0, database_1.closeConnection)();
216
+ process.exit(failed > 0 ? 1 : 0);
217
+ }
218
+ run().catch(async (err) => {
219
+ console.error("테스트 런타임 에러:", err);
220
+ await cleanup();
221
+ await (0, database_1.closeConnection)();
222
+ process.exit(1);
223
+ });
@@ -53,7 +53,8 @@ const path = __importStar(require("path"));
53
53
  const os = __importStar(require("os"));
54
54
  const database_1 = require("../database");
55
55
  const kb_1 = require("../kb");
56
- // [v4.4.0] syncSkillsToDB 제거semo-system/ 폐기됨, 스킬 SoT는 DB 직접 관리
56
+ // [v4.7.0] syncSkillsToDB 복원워크스페이스 DB 동기화 경로 재활성화
57
+ const skill_sync_1 = require("./skill-sync");
57
58
  const global_cache_1 = require("../global-cache");
58
59
  const semo_workspace_1 = require("../semo-workspace");
59
60
  // ============================================================
@@ -211,8 +212,27 @@ function registerContextCommands(program) {
211
212
  // [v4.2.0] KB→md 파일 생성 제거 — semo CLI kb 명령어로 대체
212
213
  // 기존 memory/*.md (team, projects, decisions, infra, process, bots, ontology) 파일은
213
214
  // semo CLI가 실시간 DB 조회로 대체합니다.
214
- // [v4.4.0] semo-system/ → DB 스킬 동기화 제거 (semo-system 폐기됨)
215
- // 스킬 SoT는 DB(skill_definitions). 수정은 직접 DB UPDATE 또는 마이그레이션.
215
+ // [v4.7.0] 워크스페이스 → DB 스킬 동기화 복원
216
+ // v4.4.0에서 제거했으나, 워크스페이스 스킬이 DB 미반영되는 문제 발생.
217
+ // --no-skills 플래그로 스킵 가능.
218
+ if (options.skills !== false) {
219
+ spinner.text = "스킬 동기화 (워크스페이스 → DB)...";
220
+ try {
221
+ const client = await pool.connect();
222
+ try {
223
+ const skillResult = await (0, skill_sync_1.syncSkillsToDB)(client, pool);
224
+ if (skillResult.total > 0) {
225
+ console.log(chalk_1.default.green(` ✓ 스킬 DB 동기화: ${skillResult.total}개 스킬 upsert`));
226
+ }
227
+ }
228
+ finally {
229
+ client.release();
230
+ }
231
+ }
232
+ catch (skillErr) {
233
+ console.log(chalk_1.default.yellow(` ⚠ 스킬 DB 동기화 실패 (비치명적): ${skillErr}`));
234
+ }
235
+ }
216
236
  // DB → 글로벌 캐시 (skills/commands/agents → ~/.claude/)
217
237
  if (options.globalCache !== false) {
218
238
  spinner.text = "글로벌 캐시 동기화 (skills/commands/agents)...";
@@ -0,0 +1,8 @@
1
+ /**
2
+ * semo harness — 프로젝트 하네스 관리
3
+ *
4
+ * 결정론적 하네스(Husky, ESLint, Prettier, commitlint)를
5
+ * 어떤 레포에서든 일관되게 적용·점검·갱신하는 CLI.
6
+ */
7
+ import { Command } from 'commander';
8
+ export declare function registerHarnessCommands(program: Command): void;