@crewx/wbs 0.1.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/dist/cli.js ADDED
@@ -0,0 +1,700 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ const child_process_1 = require("child_process");
38
+ const path = __importStar(require("path"));
39
+ const fs = __importStar(require("fs"));
40
+ const engine_1 = require("./src/engine");
41
+ function loadTracer() {
42
+ const candidates = [
43
+ path.join(__dirname, '../../../../skills/lib/skill-tracer.js'),
44
+ path.join(__dirname, '../../../skills/lib/skill-tracer.js'),
45
+ ];
46
+ for (const p of candidates) {
47
+ try {
48
+ if (fs.existsSync(p))
49
+ return require(p);
50
+ }
51
+ catch { }
52
+ }
53
+ return null;
54
+ }
55
+ const STATUS_ICONS = {
56
+ pending: '⬜️',
57
+ running: '🟑',
58
+ completed: 'βœ…',
59
+ failed: '❌',
60
+ on_hold: '⏸️',
61
+ planning: 'πŸ“',
62
+ partial: '⚠️',
63
+ };
64
+ function icon(status) {
65
+ return STATUS_ICONS[status] ?? '❓';
66
+ }
67
+ function getDisplayWidth(str) {
68
+ let width = 0;
69
+ for (const char of String(str)) {
70
+ const code = char.codePointAt(0) ?? 0;
71
+ if ((code >= 0x1100 && code <= 0x11FF) || (code >= 0x3000 && code <= 0x303F) ||
72
+ (code >= 0x3040 && code <= 0x309F) || (code >= 0x30A0 && code <= 0x30FF) ||
73
+ (code >= 0x3130 && code <= 0x318F) || (code >= 0x3200 && code <= 0x32FF) ||
74
+ (code >= 0x4E00 && code <= 0x9FFF) || (code >= 0xAC00 && code <= 0xD7AF) ||
75
+ (code >= 0xF900 && code <= 0xFAFF) || (code >= 0xFF00 && code <= 0xFFEF) ||
76
+ (code >= 0x1F300 && code <= 0x1F9FF) || (code >= 0x2600 && code <= 0x26FF) ||
77
+ (code >= 0x2700 && code <= 0x27BF)) {
78
+ width += 2;
79
+ }
80
+ else {
81
+ width += 1;
82
+ }
83
+ }
84
+ return width;
85
+ }
86
+ function pad(str, targetWidth) {
87
+ const s = String(str);
88
+ const diff = targetWidth - getDisplayWidth(s);
89
+ return diff > 0 ? s + ' '.repeat(diff) : s;
90
+ }
91
+ function colWidths(headers, rows) {
92
+ const widths = headers.map(h => getDisplayWidth(h));
93
+ rows.forEach(row => row.forEach((cell, i) => {
94
+ const w = getDisplayWidth(String(cell));
95
+ if (w > widths[i])
96
+ widths[i] = w;
97
+ }));
98
+ return widths;
99
+ }
100
+ function fmtRow(cells, widths) {
101
+ return '| ' + cells.map((c, i) => pad(String(c), widths[i])).join(' | ') + ' |';
102
+ }
103
+ function fmtSep(widths) {
104
+ return '|' + widths.map(w => '-'.repeat(w + 2)).join('|') + '|';
105
+ }
106
+ function fmtTable(headers, rows) {
107
+ const w = colWidths(headers, rows);
108
+ return [fmtRow(headers, w), fmtSep(w), ...rows.map(r => fmtRow(r, w))].join('\n');
109
+ }
110
+ function fmtProjectList(projects) {
111
+ if (!projects || projects.length === 0)
112
+ return 'πŸ“‹ WBS Projects (0)\n\nNo projects found.';
113
+ const rows = projects.map(p => {
114
+ const jobs = engine_1.engine.listJobs(p.id);
115
+ const done = jobs.filter(j => j.status === 'completed').length;
116
+ const total = jobs.length;
117
+ const progress = total > 0 ? Math.round((done / total) * 100) : 0;
118
+ const statusIcon = p.status === 'completed' ? icon('completed')
119
+ : (p.status === 'running' || jobs.some(j => j.status === 'running')) ? icon('running')
120
+ : done > 0 ? icon('running') : icon('pending');
121
+ return [statusIcon, p.id, p.title, String(total), `${progress}%`];
122
+ });
123
+ return `πŸ“‹ WBS Projects (${projects.length})\n\n${fmtTable(['μƒνƒœ', 'ID', 'ν”„λ‘œμ νŠΈλͺ…', 'Jobs', 'Progress'], rows)}`;
124
+ }
125
+ function fmtStatus(result) {
126
+ if (!result.project)
127
+ return '❌ Project not found';
128
+ const { project, jobs, summary } = result;
129
+ const lines = [];
130
+ lines.push(`πŸ“‹ ${project.title} (${project.id})`);
131
+ if (summary)
132
+ lines.push(` Status: ${project.status} | Progress: ${summary.progress}% (${summary.completed}/${summary.total})`);
133
+ if (project.detail_path)
134
+ lines.push(` Detail: ${project.detail_path}`);
135
+ lines.push('');
136
+ if (jobs.length === 0) {
137
+ lines.push(' Jobs: None');
138
+ }
139
+ else {
140
+ lines.push(' Jobs:');
141
+ const table = fmtTable(['μƒνƒœ', '#', 'Slug', 'ID', 'μž‘μ—…λͺ…', 'λ‹΄λ‹Ή'], jobs.map((j, i) => [icon(j.status), String(i + 1), (0, engine_1.getJobSlug)(j), j.id, j.title, j.agent]));
142
+ table.split('\n').forEach(l => lines.push(' ' + l));
143
+ }
144
+ return lines.join('\n');
145
+ }
146
+ function fmtJobList(jobs, wbsId) {
147
+ if (!jobs || jobs.length === 0)
148
+ return `πŸ“‹ Jobs for ${wbsId} (0)\n\nNo jobs found.`;
149
+ const done = jobs.filter(j => j.status === 'completed').length;
150
+ const progress = jobs.length > 0 ? Math.round((done / jobs.length) * 100) : 0;
151
+ const table = fmtTable(['μƒνƒœ', '#', 'Slug', 'ID', 'μž‘μ—…λͺ…', 'λ‹΄λ‹Ή', 'Status'], jobs.map((j, i) => [icon(j.status), String(i + 1), (0, engine_1.getJobSlug)(j), j.id, j.title, j.agent, j.status]));
152
+ return `πŸ“‹ Jobs for ${wbsId} (${jobs.length})\n Progress: ${progress}% (${done}/${jobs.length})\n\n${table}`;
153
+ }
154
+ function fmtExecList(execs, jobId) {
155
+ if (execs.length === 0)
156
+ return `No executions found for job: ${jobId}`;
157
+ const headers = ['μƒνƒœ', 'ID', 'PID', 'Task ID', 'μ‹œμž‘', 'μ’…λ£Œ', 'μ½”λ“œ'];
158
+ const rows = execs.map(e => [
159
+ icon(e.status), e.id, String(e.pid ?? '-'), e.task_id ?? '-',
160
+ e.started_at ? new Date(e.started_at).toLocaleString('ko-KR') : '-',
161
+ e.ended_at ? new Date(e.ended_at).toLocaleString('ko-KR') : '-',
162
+ e.exit_code !== null ? String(e.exit_code) : '-',
163
+ ]);
164
+ return `πŸ“‹ Executions for ${jobId}\n\n${fmtTable(headers, rows)}`;
165
+ }
166
+ function fmtExecStatus(exec) {
167
+ return `\nπŸ“‹ Execution: ${exec.id}\n\nStatus: ${icon(exec.status)} ${exec.status}\nJob ID: ${exec.job_id}\nPID: ${exec.pid ?? '-'}\nTask ID: ${exec.task_id ?? '-'}\nStarted: ${exec.started_at ? new Date(exec.started_at).toLocaleString('ko-KR') : '-'}\nEnded: ${exec.ended_at ? new Date(exec.ended_at).toLocaleString('ko-KR') : '-'}\nExit Code: ${exec.exit_code !== null ? exec.exit_code : '-'}\nError: ${exec.error ?? '-'}\n`;
168
+ }
169
+ function fmtRunning(execs) {
170
+ if (execs.length === 0)
171
+ return 'No running executions';
172
+ const headers = ['μƒνƒœ', 'Exec ID', 'Job ID', 'PID', 'Task ID', 'μ‹œμž‘'];
173
+ const rows = execs.map(e => [
174
+ icon(e.status), e.id, e.job_id, String(e.pid ?? '-'), e.task_id ?? '-',
175
+ e.started_at ? new Date(e.started_at).toLocaleString('ko-KR') : '-',
176
+ ]);
177
+ return `🟑 Running Executions (${execs.length})\n\n${fmtTable(headers, rows)}`;
178
+ }
179
+ function hasJson(args) { return args.includes('--json'); }
180
+ function hasFlag(args, name) { return args.includes(`--${name}`); }
181
+ function getBooleanOpt(args, opts, name) {
182
+ const inlinePrefix = `--${name}=`;
183
+ const inlineArg = args.find(arg => arg.startsWith(inlinePrefix));
184
+ if (inlineArg) {
185
+ const rawInline = inlineArg.slice(inlinePrefix.length);
186
+ if (rawInline === 'true' || rawInline === '1')
187
+ return true;
188
+ if (rawInline === 'false' || rawInline === '0')
189
+ return false;
190
+ console.error(`Invalid boolean for --${name}: ${rawInline} (use true/false)`);
191
+ process.exit(1);
192
+ }
193
+ if (hasFlag(args, name))
194
+ return true;
195
+ const raw = opts[name];
196
+ if (raw === undefined)
197
+ return undefined;
198
+ if (raw === 'true' || raw === '1')
199
+ return true;
200
+ if (raw === 'false' || raw === '0')
201
+ return false;
202
+ console.error(`Invalid boolean for --${name}: ${raw} (use true/false)`);
203
+ process.exit(1);
204
+ }
205
+ function parseOpts(args, start = 0) {
206
+ const opts = {};
207
+ for (let i = start; i < args.length; i++) {
208
+ if (args[i].startsWith('--') && args[i + 1] && !args[i + 1].startsWith('--')) {
209
+ opts[args[i].slice(2)] = args[++i];
210
+ }
211
+ }
212
+ return opts;
213
+ }
214
+ function handleJob(args) {
215
+ const sub = args[0];
216
+ switch (sub) {
217
+ case 'list': {
218
+ const wbsId = args[1];
219
+ if (!wbsId) {
220
+ console.error('Usage: wbs job list <wbs-id> [--json]');
221
+ process.exit(1);
222
+ }
223
+ const jobs = engine_1.engine.listJobs(wbsId);
224
+ console.log(hasJson(args) ? JSON.stringify(jobs, null, 2) : fmtJobList(jobs, wbsId));
225
+ break;
226
+ }
227
+ case 'add': {
228
+ const wbsId = args[1];
229
+ const opts = parseOpts(args, 2);
230
+ const { title, agent } = opts;
231
+ const description = opts.desc || opts.description || null;
232
+ const seq = opts.seq ? parseInt(opts.seq, 10) : 0;
233
+ const issue_number = opts.issue ? parseInt(opts.issue, 10) : null;
234
+ const no_git = getBooleanOpt(args, opts, 'no-git') ?? false;
235
+ if (!wbsId || !title || !agent) {
236
+ console.error('Usage: wbs job add <wbs-id> --title "Job Title" --agent "@agent" [--desc "..."] [--seq N] [--issue N] [--no-git]');
237
+ process.exit(1);
238
+ }
239
+ const jobId = engine_1.engine.addJob(wbsId, { title, description, agent, seq, issue_number, no_git });
240
+ (0, engine_1.generateDetailFile)(wbsId);
241
+ console.log(JSON.stringify({ jobId, wbsId, title, description, agent, seq, issue_number, no_git }, null, 2));
242
+ break;
243
+ }
244
+ case 'update': {
245
+ const jobIdArg = args[1];
246
+ if (!jobIdArg) {
247
+ console.error('Usage: wbs job update <job-id|slug> [--status ...] [--title ...] [--agent ...] [--seq N] [--desc ...] [--issue N] [--no-git[=true|false]]');
248
+ process.exit(1);
249
+ }
250
+ const opts = parseOpts(args, 2);
251
+ const job = (0, engine_1.resolveJobId)(jobIdArg);
252
+ if (!job) {
253
+ console.error(`Job not found: ${jobIdArg}`);
254
+ process.exit(1);
255
+ }
256
+ const jobId = job.id;
257
+ const fields = {};
258
+ if (opts.status) {
259
+ if (!['pending', 'running', 'completed', 'failed'].includes(opts.status)) {
260
+ console.error('Invalid status. Must be: pending, running, completed, or failed');
261
+ process.exit(1);
262
+ }
263
+ fields.status = opts.status;
264
+ }
265
+ if (opts.title)
266
+ fields.title = opts.title;
267
+ if (opts.agent)
268
+ fields.agent = opts.agent;
269
+ if (opts.seq !== undefined)
270
+ fields.seq = parseInt(opts.seq, 10);
271
+ if (opts.desc || opts.description)
272
+ fields.description = opts.desc || opts.description;
273
+ if (opts.issue !== undefined)
274
+ fields.issue_number = opts.issue ? parseInt(opts.issue, 10) : null;
275
+ const noGitOpt = getBooleanOpt(args, opts, 'no-git');
276
+ if (noGitOpt !== undefined)
277
+ fields.no_git = noGitOpt;
278
+ if (Object.keys(fields).length === 0) {
279
+ console.error('No update options provided.');
280
+ process.exit(1);
281
+ }
282
+ engine_1.engine.updateJob(jobId, fields);
283
+ const updated = engine_1.engine.getJob(jobId);
284
+ console.log(JSON.stringify(updated, null, 2));
285
+ break;
286
+ }
287
+ case 'next': {
288
+ const wbsId = args[1];
289
+ if (!wbsId) {
290
+ console.error('Usage: wbs job next <wbs-id>');
291
+ process.exit(1);
292
+ }
293
+ const job = engine_1.engine.getNextJob(wbsId);
294
+ if (job) {
295
+ engine_1.engine.runWorker(job).then(r => console.log(JSON.stringify(r, null, 2)));
296
+ }
297
+ else {
298
+ console.log('No pending jobs');
299
+ }
300
+ break;
301
+ }
302
+ case 'run': {
303
+ const wbsId = args[1];
304
+ if (!wbsId) {
305
+ console.error('Usage: wbs job run <wbs-id>');
306
+ process.exit(1);
307
+ }
308
+ engine_1.engine.run(wbsId);
309
+ break;
310
+ }
311
+ case 'retry': {
312
+ const jobIdArg = args[1];
313
+ if (!jobIdArg) {
314
+ console.error('Usage: wbs job retry <job-id|slug>');
315
+ process.exit(1);
316
+ }
317
+ const job = (0, engine_1.resolveJobId)(jobIdArg);
318
+ if (!job) {
319
+ console.error(`Job not found: ${jobIdArg}`);
320
+ process.exit(1);
321
+ }
322
+ engine_1.engine.updateJob(job.id, { status: 'pending' });
323
+ console.log(`[WBS] Retrying job: ${job.title}`);
324
+ engine_1.engine.runWorker(job).then(r => console.log(JSON.stringify(r, null, 2)));
325
+ break;
326
+ }
327
+ default:
328
+ console.log(`
329
+ Job Commands - Manage jobs within a WBS project
330
+
331
+ Usage:
332
+ wbs job list <wbs-id> [--json] List all jobs in a project
333
+ wbs job add <wbs-id> [options] Add a new job to the project
334
+ wbs job update <job-id> [options] Update an existing job
335
+ wbs job next <wbs-id> Run the next pending job
336
+ wbs job run <wbs-id> [--parallel] Run all pending jobs (default: sequential)
337
+ wbs job retry <job-id> Retry a failed/completed job
338
+
339
+ Options for 'job add':
340
+ --title "Job Title" Job title (required)
341
+ --agent "@agent" Worker agent (required)
342
+ --seq N Execution sequence (default: 0)
343
+ --desc "..." Detailed instructions
344
+ --issue N Related GitHub issue number
345
+ --no-git Skip git worktree/PR template for non-code jobs
346
+
347
+ Options for 'job update':
348
+ --no-git[=true|false] Set/clear no_git flag
349
+
350
+ Options for 'job run':
351
+ --parallel Run same-seq pending jobs concurrently (job run only)
352
+ `);
353
+ }
354
+ }
355
+ function handleExec(args) {
356
+ const sub = args[0];
357
+ switch (sub) {
358
+ case 'list': {
359
+ const jobIdArg = args[1];
360
+ if (!jobIdArg) {
361
+ console.error('Usage: wbs exec list <job-id|slug> [--json]');
362
+ process.exit(1);
363
+ }
364
+ const resolvedJob = (0, engine_1.resolveJobId)(jobIdArg);
365
+ const jobId = resolvedJob?.id ?? jobIdArg;
366
+ const execs = engine_1.engine.listExecutions(jobId);
367
+ console.log(hasJson(args) ? JSON.stringify(execs, null, 2) : fmtExecList(execs, jobId));
368
+ break;
369
+ }
370
+ case 'status': {
371
+ const execId = args[1];
372
+ if (!execId) {
373
+ console.error('Usage: wbs exec status <exec-id> [--json]');
374
+ process.exit(1);
375
+ }
376
+ const exec = engine_1.engine.getExecution(execId);
377
+ if (!exec) {
378
+ console.error(`Execution not found: ${execId}`);
379
+ process.exit(1);
380
+ }
381
+ console.log(hasJson(args) ? JSON.stringify(exec, null, 2) : fmtExecStatus(exec));
382
+ break;
383
+ }
384
+ case 'kill': {
385
+ const execId = args[1];
386
+ if (!execId) {
387
+ console.error('Usage: wbs exec kill <exec-id>');
388
+ process.exit(1);
389
+ }
390
+ const ok = engine_1.engine.killExecution(execId);
391
+ if (ok) {
392
+ console.log(`Killed execution: ${execId}`);
393
+ }
394
+ else {
395
+ console.error(`Could not kill execution: ${execId} (not running or not found)`);
396
+ process.exit(1);
397
+ }
398
+ break;
399
+ }
400
+ case 'running': {
401
+ const execs = engine_1.engine.getRunningExecutions();
402
+ console.log(hasJson(args) ? JSON.stringify(execs, null, 2) : fmtRunning(execs));
403
+ break;
404
+ }
405
+ default:
406
+ console.log(`
407
+ Execution Commands - Manage job executions
408
+
409
+ Usage:
410
+ wbs exec list <job-id> [--json] List execution history for a job
411
+ wbs exec status <exec-id> [--json] Get execution details
412
+ wbs exec kill <exec-id> Kill a running execution
413
+ wbs exec running [--json] List all running executions
414
+ `);
415
+ }
416
+ }
417
+ function getLocalTimestamp() {
418
+ const now = new Date();
419
+ return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;
420
+ }
421
+ function daemonTick() {
422
+ const ts = getLocalTimestamp();
423
+ console.log(`[${ts}] Daemon tick`);
424
+ const zombies = engine_1.engine.checkAndFixZombies();
425
+ if (zombies.length > 0) {
426
+ console.log(`[${ts}] Fixed ${zombies.length} zombie(s):`);
427
+ zombies.forEach(z => console.log(` - ${z.jobId}: ${z.reason}`));
428
+ }
429
+ const activeProjects = engine_1.engine.getActiveProjects();
430
+ if (activeProjects.length === 0) {
431
+ console.log(`[${ts}] No active projects`);
432
+ return;
433
+ }
434
+ for (const project of activeProjects) {
435
+ const nextSeq = engine_1.engine.getNextSeq(project.id);
436
+ if (nextSeq === null) {
437
+ console.log(`[${ts}] ${project.id}: No pending jobs`);
438
+ continue;
439
+ }
440
+ const running = engine_1.engine.getJobsBySeq(project.id, nextSeq, 'running');
441
+ const pending = engine_1.engine.getJobsBySeq(project.id, nextSeq, 'pending');
442
+ if (running.length > 0) {
443
+ console.log(`[${ts}] ${project.id}: ${running.length} job(s) running in seq ${nextSeq}`);
444
+ continue;
445
+ }
446
+ if (pending.length > 0) {
447
+ console.log(`[${ts}] ${project.id}: Starting next seq ${nextSeq} (${pending.length} pending)`);
448
+ try {
449
+ (0, child_process_1.execSync)(`crewx x "@wbs_coordinator ${project.id} λ‹€μŒ μž‘μ—… μ§„ν–‰ν•΄μ€˜"`, { stdio: 'inherit', cwd: process.cwd() });
450
+ }
451
+ catch (e) {
452
+ console.error(`[${ts}] Failed to call coordinator: ${e.message}`);
453
+ }
454
+ }
455
+ }
456
+ }
457
+ function startDaemon() {
458
+ const pidFile = engine_1.engine.daemonPidFile;
459
+ if (fs.existsSync(pidFile)) {
460
+ const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10);
461
+ if (engine_1.engine.isProcessAlive(pid)) {
462
+ console.error(`[WBS Daemon] Already running (PID: ${pid})`);
463
+ return false;
464
+ }
465
+ fs.unlinkSync(pidFile);
466
+ }
467
+ const logFile = engine_1.engine.daemonLogFile;
468
+ const out = fs.openSync(logFile, 'a');
469
+ const child = (0, child_process_1.spawn)(process.execPath, [__filename, 'daemon', '--foreground'], {
470
+ detached: true,
471
+ stdio: ['ignore', out, out],
472
+ cwd: process.cwd(),
473
+ });
474
+ fs.writeFileSync(pidFile, String(child.pid));
475
+ child.unref();
476
+ console.log(`[WBS Daemon] Started (PID: ${child.pid})`);
477
+ console.log(`[WBS Daemon] Log: ${logFile}`);
478
+ return true;
479
+ }
480
+ function stopDaemon() {
481
+ const pidFile = engine_1.engine.daemonPidFile;
482
+ if (!fs.existsSync(pidFile)) {
483
+ console.error('[WBS Daemon] Not running (no PID file)');
484
+ return false;
485
+ }
486
+ const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10);
487
+ if (!engine_1.engine.isProcessAlive(pid)) {
488
+ console.log('[WBS Daemon] Process not found, cleaning up PID file');
489
+ fs.unlinkSync(pidFile);
490
+ return true;
491
+ }
492
+ try {
493
+ process.kill(pid, 'SIGTERM');
494
+ fs.unlinkSync(pidFile);
495
+ console.log(`[WBS Daemon] Stopped (PID: ${pid})`);
496
+ return true;
497
+ }
498
+ catch (e) {
499
+ console.error(`[WBS Daemon] Failed to stop: ${e.message}`);
500
+ return false;
501
+ }
502
+ }
503
+ function handleDaemon(args) {
504
+ const sub = args[0];
505
+ switch (sub) {
506
+ case 'start':
507
+ startDaemon();
508
+ break;
509
+ case 'stop':
510
+ stopDaemon();
511
+ break;
512
+ case 'status': {
513
+ const pidFile = engine_1.engine.daemonPidFile;
514
+ if (!fs.existsSync(pidFile)) {
515
+ const msg = { running: false, message: 'Not running' };
516
+ console.log(hasJson(args) ? JSON.stringify(msg) : `[WBS Daemon] Not running`);
517
+ }
518
+ else {
519
+ const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10);
520
+ const alive = engine_1.engine.isProcessAlive(pid);
521
+ const msg = alive ? { running: true, pid, message: `Running (PID: ${pid})` } : { running: false, pid, message: 'Stale PID file (process dead)' };
522
+ console.log(hasJson(args) ? JSON.stringify(msg, null, 2) : `[WBS Daemon] ${msg.message}`);
523
+ }
524
+ break;
525
+ }
526
+ case 'restart':
527
+ stopDaemon();
528
+ setTimeout(() => startDaemon(), 1000);
529
+ break;
530
+ case '--foreground': {
531
+ console.log(`[WBS Daemon] Starting in foreground mode`);
532
+ console.log(`[WBS Daemon] Interval: ${engine_1.DAEMON_INTERVAL_MS / 1000 / 60} minutes`);
533
+ console.log(`[WBS Daemon] Zombie timeout: ${engine_1.ZOMBIE_TIMEOUT_MS / 1000 / 60} minutes`);
534
+ console.log(`[WBS Daemon] Press Ctrl+C to stop\n`);
535
+ daemonTick();
536
+ setInterval(daemonTick, engine_1.DAEMON_INTERVAL_MS);
537
+ break;
538
+ }
539
+ case 'tick':
540
+ daemonTick();
541
+ break;
542
+ default:
543
+ console.log(`
544
+ Daemon Commands - Background scheduler for WBS
545
+
546
+ Usage:
547
+ wbs daemon start Start daemon in background
548
+ wbs daemon stop Stop running daemon
549
+ wbs daemon status Check daemon status
550
+ wbs daemon restart Restart daemon
551
+ wbs daemon tick Manual tick (for testing)
552
+ `);
553
+ }
554
+ }
555
+ function showSkillHelp() {
556
+ const skillMd = path.join(__dirname, '..', 'SKILL.md');
557
+ if (fs.existsSync(skillMd)) {
558
+ process.stdout.write(fs.readFileSync(skillMd, 'utf-8'));
559
+ }
560
+ else {
561
+ showHelp();
562
+ }
563
+ }
564
+ function showHelp() {
565
+ console.log(`
566
+ WBS Coordinator - Work Breakdown Structure Management
567
+
568
+ Project Commands:
569
+ wbs create "Title" [--detail "path"] [--source "branch"] [--target "branch"] Create a new WBS project
570
+ wbs list [--json] List all projects
571
+ wbs status <wbs-id> [--json] Get project status with jobs summary
572
+ wbs delete <wbs-id> Delete a project and its jobs
573
+
574
+ Job Commands:
575
+ wbs job list <wbs-id> [--json] List all jobs in a project
576
+ wbs job add <wbs-id> [opts] Add a job to the project
577
+ wbs job update <job-id> [opts] Update a job
578
+ wbs job next <wbs-id> Run the next pending job
579
+ wbs job run <wbs-id> Run all pending jobs
580
+ wbs job retry <job-id> Retry a failed/completed job
581
+
582
+ Execution Commands:
583
+ wbs exec list <job-id> List execution history for a job
584
+ wbs exec status <exec-id> Get execution details
585
+ wbs exec kill <exec-id> Kill a running execution
586
+ wbs exec running List all running executions
587
+
588
+ Daemon Commands:
589
+ wbs daemon start Start background scheduler
590
+ wbs daemon stop Stop daemon
591
+ wbs daemon status Check daemon status
592
+ wbs daemon tick Manual tick (for testing)
593
+
594
+ Output Options:
595
+ --json Output raw JSON instead of human-readable table
596
+
597
+ Status Icons:
598
+ ⬜️ pending 🟑 running βœ… completed ❌ failed
599
+
600
+ Examples:
601
+ wbs create "User Authentication"
602
+ wbs job add wbs-1 --title "Design User model" --agent "@claude:sonnet" --seq 1
603
+ wbs status wbs-1
604
+ wbs job run wbs-1
605
+ wbs exec list job-1
606
+ `);
607
+ }
608
+ const args = process.argv.slice(2);
609
+ const command = args[0];
610
+ const isDaemonForeground = command === 'daemon' && args.includes('--foreground');
611
+ async function runMain() {
612
+ switch (command) {
613
+ case 'create': {
614
+ const title = args[1];
615
+ if (!title) {
616
+ console.error('Usage: wbs create "Project Title" [--detail "path"] [--source "branch"] [--target "branch"]');
617
+ process.exit(1);
618
+ }
619
+ const opts = parseOpts(args, 2);
620
+ const detailOpt = opts.detail ?? null;
621
+ const sourceBranchOpt = opts.source || undefined;
622
+ const targetBranchOpt = opts.target || undefined;
623
+ const id = engine_1.engine.create(title, detailOpt, sourceBranchOpt, targetBranchOpt);
624
+ let detailPath = detailOpt;
625
+ if (!detailPath) {
626
+ const detailsDir = path.join(process.cwd(), 'wbs-details');
627
+ if (!fs.existsSync(detailsDir))
628
+ fs.mkdirSync(detailsDir, { recursive: true });
629
+ detailPath = `wbs-details/${id}-detail.md`;
630
+ const detailFile = path.join(process.cwd(), detailPath);
631
+ fs.writeFileSync(detailFile, `# ${title}\n\n## κ°œμš”\n\n(ν”„λ‘œμ νŠΈ κ°œμš”λ₯Ό μž‘μ„±ν•˜μ„Έμš”)\n\n## μš”κ΅¬μ‚¬ν•­\n\n(μš”κ΅¬μ‚¬ν•­μ„ μž‘μ„±ν•˜μ„Έμš”)\n\n## μ°Έκ³  자료\n\n(μ°Έκ³  자료 λ§ν¬λ‚˜ μ„€λͺ…을 μΆ”κ°€ν•˜μ„Έμš”)\n`);
632
+ engine_1.engine.updateDetailPath(id, detailPath);
633
+ }
634
+ console.log(JSON.stringify({ id, title, detailPath }, null, 2));
635
+ break;
636
+ }
637
+ case 'list': {
638
+ const projects = engine_1.engine.list();
639
+ console.log(hasJson(args) ? JSON.stringify(projects, null, 2) : fmtProjectList(projects));
640
+ break;
641
+ }
642
+ case 'status': {
643
+ const wbsId = args[1];
644
+ if (!wbsId) {
645
+ console.error('Usage: wbs status <wbs-id> [--json]');
646
+ process.exit(1);
647
+ }
648
+ const result = engine_1.engine.status(wbsId);
649
+ if (result.project) {
650
+ const pending = result.jobs.filter(j => j.status === 'pending').length;
651
+ const running = result.jobs.filter(j => j.status === 'running').length;
652
+ const completed = result.jobs.filter(j => j.status === 'completed').length;
653
+ const failed = result.jobs.filter(j => j.status === 'failed').length;
654
+ result.summary = {
655
+ total: result.jobs.length, pending, running, completed, failed,
656
+ progress: result.jobs.length > 0 ? Math.round((completed / result.jobs.length) * 100) : 0,
657
+ };
658
+ }
659
+ console.log(hasJson(args) ? JSON.stringify(result, null, 2) : fmtStatus(result));
660
+ break;
661
+ }
662
+ case 'delete': {
663
+ const wbsId = args[1];
664
+ if (!wbsId) {
665
+ console.error('Usage: wbs delete <wbs-id>');
666
+ process.exit(1);
667
+ }
668
+ engine_1.engine.delete(wbsId);
669
+ console.log(`Deleted: ${wbsId}`);
670
+ break;
671
+ }
672
+ case 'help':
673
+ showSkillHelp();
674
+ break;
675
+ case 'job':
676
+ handleJob(args.slice(1));
677
+ break;
678
+ case 'exec':
679
+ handleExec(args.slice(1));
680
+ break;
681
+ case 'daemon':
682
+ handleDaemon(args.slice(1));
683
+ break;
684
+ default: showHelp();
685
+ }
686
+ }
687
+ const tracer = isDaemonForeground ? null : loadTracer();
688
+ if (tracer) {
689
+ tracer.run('crewx/wbs', runMain).catch((e) => {
690
+ console.error(e.message);
691
+ process.exit(1);
692
+ });
693
+ }
694
+ else {
695
+ runMain().catch((e) => {
696
+ console.error(e.message);
697
+ process.exit(1);
698
+ });
699
+ }
700
+ //# sourceMappingURL=cli.js.map