sentix 2.0.1
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/LICENSE +21 -0
- package/README.md +627 -0
- package/bin/sentix.js +116 -0
- package/package.json +37 -0
- package/src/CLAUDE.md +26 -0
- package/src/commands/CLAUDE.md +29 -0
- package/src/commands/context.js +227 -0
- package/src/commands/doctor.js +213 -0
- package/src/commands/evolve.js +203 -0
- package/src/commands/feature.js +327 -0
- package/src/commands/init.js +467 -0
- package/src/commands/metrics.js +170 -0
- package/src/commands/plugin.js +111 -0
- package/src/commands/run.js +303 -0
- package/src/commands/safety.js +163 -0
- package/src/commands/status.js +149 -0
- package/src/commands/ticket.js +362 -0
- package/src/commands/update.js +143 -0
- package/src/commands/version.js +218 -0
- package/src/context.js +104 -0
- package/src/dev-server.js +154 -0
- package/src/lib/agent-loop.js +110 -0
- package/src/lib/api-client.js +213 -0
- package/src/lib/changelog.js +110 -0
- package/src/lib/pipeline.js +218 -0
- package/src/lib/provider.js +129 -0
- package/src/lib/safety.js +146 -0
- package/src/lib/semver.js +40 -0
- package/src/lib/similarity.js +58 -0
- package/src/lib/ticket-index.js +137 -0
- package/src/lib/tools.js +142 -0
- package/src/lib/verify-gates.js +254 -0
- package/src/plugins/auto-version.js +89 -0
- package/src/plugins/logger.js +55 -0
- package/src/registry.js +63 -0
- package/src/version.js +15 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sentix status — Governor 상태 + Memory Layer 요약 + 진화 단계 표시
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { registerCommand } from '../registry.js';
|
|
6
|
+
import { loadIndex } from '../lib/ticket-index.js';
|
|
7
|
+
|
|
8
|
+
registerCommand('status', {
|
|
9
|
+
description: 'Show Governor state and Memory Layer summary',
|
|
10
|
+
usage: 'sentix status',
|
|
11
|
+
|
|
12
|
+
async run(_args, ctx) {
|
|
13
|
+
ctx.log('=== Sentix Status ===\n');
|
|
14
|
+
|
|
15
|
+
// ── Governor State ──────────────────────────────
|
|
16
|
+
if (ctx.exists('tasks/governor-state.json')) {
|
|
17
|
+
try {
|
|
18
|
+
const state = await ctx.readJSON('tasks/governor-state.json');
|
|
19
|
+
ctx.log(`Governor: ${state.status || 'unknown'}`);
|
|
20
|
+
ctx.log(`Cycle: ${state.cycle_id || 'none'}`);
|
|
21
|
+
ctx.log(`Phase: ${state.current_phase || 'idle'}`);
|
|
22
|
+
ctx.log(`Request: "${state.request || ''}"`);
|
|
23
|
+
|
|
24
|
+
if (state.plan && state.plan.length > 0) {
|
|
25
|
+
ctx.log('\nPipeline:');
|
|
26
|
+
for (const step of state.plan) {
|
|
27
|
+
const icon = step.status === 'done' ? '✓'
|
|
28
|
+
: step.status === 'running' ? '▶'
|
|
29
|
+
: '○';
|
|
30
|
+
ctx.log(` ${icon} ${step.agent} — ${step.status}${step.result ? ` (${step.result})` : ''}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (state.retries && Object.keys(state.retries).length > 0) {
|
|
35
|
+
ctx.log(`\nRetries: ${JSON.stringify(state.retries)}`);
|
|
36
|
+
}
|
|
37
|
+
} catch {
|
|
38
|
+
ctx.warn('Could not read governor-state.json');
|
|
39
|
+
}
|
|
40
|
+
} else {
|
|
41
|
+
ctx.log('Governor: idle (no active cycle)');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ── Memory Layer ────────────────────────────────
|
|
45
|
+
ctx.log('\n--- Memory Layer ---\n');
|
|
46
|
+
|
|
47
|
+
// lessons.md
|
|
48
|
+
if (ctx.exists('tasks/lessons.md')) {
|
|
49
|
+
const lessons = await ctx.readFile('tasks/lessons.md');
|
|
50
|
+
const lines = lessons.split('\n').filter(l => l.startsWith('- '));
|
|
51
|
+
ctx.log(`Lessons: ${lines.length} entries`);
|
|
52
|
+
} else {
|
|
53
|
+
ctx.log('Lessons: (not initialized)');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// patterns.md
|
|
57
|
+
if (ctx.exists('tasks/patterns.md')) {
|
|
58
|
+
const patterns = await ctx.readFile('tasks/patterns.md');
|
|
59
|
+
const lines = patterns.split('\n').filter(l => l.startsWith('- '));
|
|
60
|
+
ctx.log(`Patterns: ${lines.length} entries`);
|
|
61
|
+
} else {
|
|
62
|
+
ctx.log('Patterns: (not initialized)');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// pattern-log.jsonl
|
|
66
|
+
if (ctx.exists('tasks/pattern-log.jsonl')) {
|
|
67
|
+
const log = await ctx.readFile('tasks/pattern-log.jsonl');
|
|
68
|
+
const entries = log.trim().split('\n').filter(Boolean);
|
|
69
|
+
ctx.log(`Pattern Log: ${entries.length} events`);
|
|
70
|
+
} else {
|
|
71
|
+
ctx.log('Pattern Log: (empty)');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// agent-metrics.jsonl
|
|
75
|
+
if (ctx.exists('tasks/agent-metrics.jsonl')) {
|
|
76
|
+
const metrics = await ctx.readFile('tasks/agent-metrics.jsonl');
|
|
77
|
+
const entries = metrics.trim().split('\n').filter(Boolean);
|
|
78
|
+
ctx.log(`Metrics: ${entries.length} records`);
|
|
79
|
+
} else {
|
|
80
|
+
ctx.log('Metrics: (empty)');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── Tickets ───────────────────────────────────────
|
|
84
|
+
ctx.log('\n--- Tickets ---\n');
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const tickets = await loadIndex(ctx);
|
|
88
|
+
if (tickets.length > 0) {
|
|
89
|
+
const byStatus = {};
|
|
90
|
+
for (const t of tickets) {
|
|
91
|
+
byStatus[t.status] = (byStatus[t.status] || 0) + 1;
|
|
92
|
+
}
|
|
93
|
+
const critical = tickets.filter(t => t.severity === 'critical' && t.status !== 'closed').length;
|
|
94
|
+
const parts = Object.entries(byStatus).map(([k, v]) => `${k}: ${v}`);
|
|
95
|
+
ctx.log(`Tickets: ${tickets.length} total (${parts.join(', ')})`);
|
|
96
|
+
if (critical > 0) {
|
|
97
|
+
ctx.warn(`Critical: ${critical} open critical ticket(s)`);
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
ctx.log('Tickets: (none)');
|
|
101
|
+
}
|
|
102
|
+
} catch {
|
|
103
|
+
ctx.log('Tickets: (not initialized)');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── Evolution Stage ─────────────────────────────
|
|
107
|
+
ctx.log('\n--- Evolution ---\n');
|
|
108
|
+
|
|
109
|
+
if (ctx.exists('.sentix/config.toml')) {
|
|
110
|
+
const config = await ctx.readFile('.sentix/config.toml');
|
|
111
|
+
|
|
112
|
+
const layers = [
|
|
113
|
+
{ name: 'Core (Governor + Agents)', key: 'layers.core', required: true },
|
|
114
|
+
{ name: 'Learning Pipeline', key: 'layers.learning' },
|
|
115
|
+
{ name: 'Pattern Engine', key: 'layers.pattern_engine' },
|
|
116
|
+
{ name: 'Visual Perception', key: 'layers.visual' },
|
|
117
|
+
{ name: 'Self-Evolution', key: 'layers.evolution' },
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
for (const layer of layers) {
|
|
121
|
+
const enabled = layer.required || isLayerEnabled(config, layer.key);
|
|
122
|
+
const icon = enabled ? '●' : '○';
|
|
123
|
+
ctx.log(` ${enabled ? icon : icon} ${layer.name}${enabled ? '' : ' (disabled)'}`);
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
ctx.warn('.sentix/config.toml not found. Run: sentix init');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
ctx.log('');
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Parse TOML config to check if a specific layer section has enabled = true.
|
|
135
|
+
* Handles per-section parsing instead of global string search.
|
|
136
|
+
*/
|
|
137
|
+
function isLayerEnabled(config, sectionKey) {
|
|
138
|
+
const sectionHeader = `[${sectionKey}]`;
|
|
139
|
+
const idx = config.indexOf(sectionHeader);
|
|
140
|
+
if (idx === -1) return false;
|
|
141
|
+
|
|
142
|
+
// Extract content between this section header and the next section header
|
|
143
|
+
const afterSection = config.slice(idx + sectionHeader.length);
|
|
144
|
+
const nextSection = afterSection.indexOf('\n[');
|
|
145
|
+
const sectionContent = nextSection === -1 ? afterSection : afterSection.slice(0, nextSection);
|
|
146
|
+
|
|
147
|
+
// Look for enabled = true within this section only
|
|
148
|
+
return /enabled\s*=\s*true/.test(sectionContent);
|
|
149
|
+
}
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sentix ticket — 버그/이슈 티켓 관리
|
|
3
|
+
*
|
|
4
|
+
* sentix ticket create "설명" [--severity critical|warning|suggestion]
|
|
5
|
+
* sentix ticket list [--status open] [--severity critical]
|
|
6
|
+
* sentix ticket debug <ticket-id>
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { spawnSync } from 'node:child_process';
|
|
10
|
+
import { registerCommand } from '../registry.js';
|
|
11
|
+
import {
|
|
12
|
+
loadIndex, addTicket, updateTicket, findTicket,
|
|
13
|
+
nextTicketId, classifySeverity, sortBySeverity, createTicketEntry,
|
|
14
|
+
} from '../lib/ticket-index.js';
|
|
15
|
+
import { findBestMatch } from '../lib/similarity.js';
|
|
16
|
+
|
|
17
|
+
registerCommand('ticket', {
|
|
18
|
+
description: 'Manage bug/issue tickets (create | list | debug)',
|
|
19
|
+
usage: 'sentix ticket <create|list|debug> [args...] [--severity critical|warning|suggestion]',
|
|
20
|
+
|
|
21
|
+
async run(args, ctx) {
|
|
22
|
+
const subcommand = args[0];
|
|
23
|
+
|
|
24
|
+
if (subcommand === 'create') {
|
|
25
|
+
await createTicket(args.slice(1), ctx);
|
|
26
|
+
} else if (!subcommand || subcommand === 'list') {
|
|
27
|
+
await listTickets(args.slice(1), ctx);
|
|
28
|
+
} else if (subcommand === 'debug') {
|
|
29
|
+
const ticketId = args[1];
|
|
30
|
+
if (!ticketId) {
|
|
31
|
+
ctx.error('Usage: sentix ticket debug <ticket-id>');
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
await debugTicket(ticketId, ctx);
|
|
35
|
+
} else {
|
|
36
|
+
ctx.error(`Unknown subcommand: ${subcommand}`);
|
|
37
|
+
ctx.log('Usage: sentix ticket <create|list|debug> [args...]');
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// ── sentix ticket create ──────────────────────────────
|
|
43
|
+
|
|
44
|
+
async function createTicket(args, ctx) {
|
|
45
|
+
// Parse --severity flag
|
|
46
|
+
let severity = null;
|
|
47
|
+
const flagIdx = args.indexOf('--severity');
|
|
48
|
+
if (flagIdx !== -1) {
|
|
49
|
+
severity = args[flagIdx + 1];
|
|
50
|
+
if (!['critical', 'warning', 'suggestion'].includes(severity)) {
|
|
51
|
+
ctx.error(`Invalid severity: ${severity} (use critical|warning|suggestion)`);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
args.splice(flagIdx, 2);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const description = args.join(' ').trim();
|
|
58
|
+
if (!description) {
|
|
59
|
+
ctx.error('Usage: sentix ticket create "bug description" [--severity critical|warning|suggestion]');
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Auto-classify severity if not specified
|
|
64
|
+
if (!severity) {
|
|
65
|
+
severity = classifySeverity(description);
|
|
66
|
+
ctx.log(`Auto-classified severity: ${severity}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Duplicate detection
|
|
70
|
+
await checkDuplicates(description, ctx);
|
|
71
|
+
|
|
72
|
+
// Create ticket
|
|
73
|
+
const id = await nextTicketId(ctx, 'bug');
|
|
74
|
+
const title = description.length > 80 ? description.slice(0, 77) + '...' : description;
|
|
75
|
+
|
|
76
|
+
const entry = createTicketEntry({
|
|
77
|
+
id,
|
|
78
|
+
type: 'bug',
|
|
79
|
+
title,
|
|
80
|
+
severity,
|
|
81
|
+
description,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Generate markdown file
|
|
85
|
+
const md = `# ${id}: ${title}
|
|
86
|
+
|
|
87
|
+
- **Status:** open
|
|
88
|
+
- **Severity:** ${severity}
|
|
89
|
+
- **Created:** ${entry.created_at}
|
|
90
|
+
- **Related lessons:** ${await findRelatedLessons(description, ctx)}
|
|
91
|
+
|
|
92
|
+
## Description
|
|
93
|
+
|
|
94
|
+
${description}
|
|
95
|
+
|
|
96
|
+
## Root Cause Analysis
|
|
97
|
+
|
|
98
|
+
<!-- Populated after sentix ticket debug -->
|
|
99
|
+
|
|
100
|
+
## Resolution
|
|
101
|
+
|
|
102
|
+
<!-- Populated after fix -->
|
|
103
|
+
`;
|
|
104
|
+
|
|
105
|
+
await ctx.writeFile(entry.file_path, md);
|
|
106
|
+
await addTicket(ctx, entry);
|
|
107
|
+
|
|
108
|
+
// Log event
|
|
109
|
+
await ctx.appendJSONL('tasks/pattern-log.jsonl', {
|
|
110
|
+
ts: new Date().toISOString(),
|
|
111
|
+
event: 'ticket:create',
|
|
112
|
+
id,
|
|
113
|
+
severity,
|
|
114
|
+
title,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
ctx.success(`Created ${id}: ${title}`);
|
|
118
|
+
ctx.log(` Severity: ${severity}`);
|
|
119
|
+
ctx.log(` File: ${entry.file_path}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ── sentix ticket list ────────────────────────────────
|
|
123
|
+
|
|
124
|
+
async function listTickets(args, ctx) {
|
|
125
|
+
ctx.log('=== Tickets ===\n');
|
|
126
|
+
|
|
127
|
+
let entries = await loadIndex(ctx);
|
|
128
|
+
|
|
129
|
+
if (entries.length === 0) {
|
|
130
|
+
ctx.log(' (no tickets)');
|
|
131
|
+
ctx.log('\n Create one: sentix ticket create "description"');
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Parse filters
|
|
136
|
+
const statusIdx = args.indexOf('--status');
|
|
137
|
+
if (statusIdx !== -1 && args[statusIdx + 1]) {
|
|
138
|
+
const status = args[statusIdx + 1];
|
|
139
|
+
entries = entries.filter(e => e.status === status);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const sevIdx = args.indexOf('--severity');
|
|
143
|
+
if (sevIdx !== -1 && args[sevIdx + 1]) {
|
|
144
|
+
const sev = args[sevIdx + 1];
|
|
145
|
+
entries = entries.filter(e => e.severity === sev);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
entries = sortBySeverity(entries);
|
|
149
|
+
|
|
150
|
+
// Table header
|
|
151
|
+
ctx.log(` ${'ID'.padEnd(12)} ${'SEVERITY'.padEnd(12)} ${'STATUS'.padEnd(14)} TITLE`);
|
|
152
|
+
ctx.log(` ${'─'.repeat(12)} ${'─'.repeat(12)} ${'─'.repeat(14)} ${'─'.repeat(30)}`);
|
|
153
|
+
|
|
154
|
+
for (const e of entries) {
|
|
155
|
+
const sev = e.severity ? e.severity.padEnd(12) : '-'.padEnd(12);
|
|
156
|
+
ctx.log(` ${e.id.padEnd(12)} ${sev} ${e.status.padEnd(14)} ${e.title}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
ctx.log(`\n Total: ${entries.length} ticket(s)`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ── sentix ticket debug ───────────────────────────────
|
|
163
|
+
|
|
164
|
+
async function debugTicket(ticketId, ctx) {
|
|
165
|
+
// 1. Find ticket
|
|
166
|
+
const ticket = await findTicket(ctx, ticketId);
|
|
167
|
+
if (!ticket) {
|
|
168
|
+
ctx.error(`Ticket not found: ${ticketId}`);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (ticket.status !== 'open' && ticket.status !== 'in_progress') {
|
|
173
|
+
ctx.error(`Ticket ${ticketId} is ${ticket.status} — only open or in_progress tickets can be debugged`);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// 2. Check Claude Code
|
|
178
|
+
const claudeCheck = spawnSync('claude', ['--version'], { encoding: 'utf-8', stdio: 'pipe' });
|
|
179
|
+
if (claudeCheck.error) {
|
|
180
|
+
ctx.error('Claude Code CLI not found. Install: https://docs.anthropic.com/en/docs/claude-code');
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// 3. Check for concurrent pipeline
|
|
185
|
+
if (ctx.exists('tasks/governor-state.json')) {
|
|
186
|
+
try {
|
|
187
|
+
const existing = await ctx.readJSON('tasks/governor-state.json');
|
|
188
|
+
if (existing.status === 'in_progress') {
|
|
189
|
+
ctx.error(`Another pipeline is running: ${existing.cycle_id}`);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
} catch { /* safe to proceed */ }
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 4. Update ticket status
|
|
196
|
+
await updateTicket(ctx, ticketId, { status: 'in_progress' });
|
|
197
|
+
ctx.log(`Debugging ${ticketId}: ${ticket.title}`);
|
|
198
|
+
ctx.log(`Severity: ${ticket.severity}\n`);
|
|
199
|
+
|
|
200
|
+
// 5. Read ticket markdown
|
|
201
|
+
let ticketContent = '';
|
|
202
|
+
if (ctx.exists(ticket.file_path)) {
|
|
203
|
+
ticketContent = await ctx.readFile(ticket.file_path);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// 6. Read lessons for context
|
|
207
|
+
let lessons = '';
|
|
208
|
+
if (ctx.exists('tasks/lessons.md')) {
|
|
209
|
+
lessons = await ctx.readFile('tasks/lessons.md');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// 7. Determine retry limit by severity
|
|
213
|
+
const retryLimits = { critical: 3, warning: 10, suggestion: 0 };
|
|
214
|
+
const retryLimit = retryLimits[ticket.severity] || 3;
|
|
215
|
+
|
|
216
|
+
// 8. Create governor state
|
|
217
|
+
const cycleId = `debug-${ticketId}-${String(Date.now()).slice(-3)}`;
|
|
218
|
+
const state = {
|
|
219
|
+
schema_version: 1,
|
|
220
|
+
cycle_id: cycleId,
|
|
221
|
+
request: `DEBUG: ${ticket.title}`,
|
|
222
|
+
status: 'in_progress',
|
|
223
|
+
current_phase: 'dev-fix',
|
|
224
|
+
plan: [{ agent: 'dev-fix', status: 'running', result_ref: null }],
|
|
225
|
+
retries: { 'dev-fix': 0 },
|
|
226
|
+
cross_judgments: [],
|
|
227
|
+
started_at: new Date().toISOString(),
|
|
228
|
+
completed_at: null,
|
|
229
|
+
human_intervention_requested: false,
|
|
230
|
+
ticket_id: ticketId,
|
|
231
|
+
ticket_type: 'bug',
|
|
232
|
+
};
|
|
233
|
+
await ctx.writeJSON('tasks/governor-state.json', state);
|
|
234
|
+
|
|
235
|
+
// 9. Log event
|
|
236
|
+
await ctx.appendJSONL('tasks/pattern-log.jsonl', {
|
|
237
|
+
ts: new Date().toISOString(),
|
|
238
|
+
event: 'ticket:debug',
|
|
239
|
+
id: ticketId,
|
|
240
|
+
severity: ticket.severity,
|
|
241
|
+
cycle_id: cycleId,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// 10. Invoke Claude Code with debug prompt
|
|
245
|
+
const prompt = [
|
|
246
|
+
'Read CLAUDE.md and FRAMEWORK.md first.',
|
|
247
|
+
'',
|
|
248
|
+
`DEBUG MODE — Ticket: ${ticketId}`,
|
|
249
|
+
'',
|
|
250
|
+
'## Ticket Content',
|
|
251
|
+
ticketContent,
|
|
252
|
+
'',
|
|
253
|
+
'## Known Lessons',
|
|
254
|
+
lessons.slice(0, 2000),
|
|
255
|
+
'',
|
|
256
|
+
'## Instructions',
|
|
257
|
+
'1. Analyze the bug described in the ticket',
|
|
258
|
+
'2. Identify root cause',
|
|
259
|
+
'3. Implement fix within SCOPE (respect hard rules)',
|
|
260
|
+
'4. Run tests to verify fix',
|
|
261
|
+
'5. Generate LESSON_LEARNED and append to tasks/lessons.md',
|
|
262
|
+
`6. Update ${ticket.file_path} with root cause analysis`,
|
|
263
|
+
'7. Update tasks/governor-state.json at each phase',
|
|
264
|
+
'',
|
|
265
|
+
`Severity: ${ticket.severity} (retry limit: ${retryLimit})`,
|
|
266
|
+
].join('\n');
|
|
267
|
+
|
|
268
|
+
ctx.log('Invoking Claude Code for debugging...\n');
|
|
269
|
+
|
|
270
|
+
const result = spawnSync('claude', ['-p', prompt], {
|
|
271
|
+
cwd: ctx.cwd,
|
|
272
|
+
stdio: 'inherit',
|
|
273
|
+
timeout: 600_000,
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// 11. Handle result
|
|
277
|
+
if (result.error || result.status !== 0) {
|
|
278
|
+
state.status = 'failed';
|
|
279
|
+
state.error = result.error?.message || `Exit code ${result.status}`;
|
|
280
|
+
await ctx.writeJSON('tasks/governor-state.json', state);
|
|
281
|
+
|
|
282
|
+
await ctx.appendJSONL('tasks/pattern-log.jsonl', {
|
|
283
|
+
ts: new Date().toISOString(),
|
|
284
|
+
event: 'ticket:debug:failed',
|
|
285
|
+
id: ticketId,
|
|
286
|
+
cycle_id: cycleId,
|
|
287
|
+
error: state.error,
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// Escalate critical failures to roadmap
|
|
291
|
+
if (ticket.severity === 'critical') {
|
|
292
|
+
ctx.warn('Critical ticket debug failed — escalating to roadmap');
|
|
293
|
+
if (ctx.exists('tasks/roadmap.md')) {
|
|
294
|
+
const roadmap = await ctx.readFile('tasks/roadmap.md');
|
|
295
|
+
const escalation = `\n- **[ESCALATED]** ${ticketId}: ${ticket.title} (debug failed, needs manual review)\n`;
|
|
296
|
+
await ctx.writeFile('tasks/roadmap.md', roadmap + escalation);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
await updateTicket(ctx, ticketId, { status: 'open' });
|
|
301
|
+
ctx.error(`Debug failed for ${ticketId}`);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// 12. Success
|
|
306
|
+
state.status = 'completed';
|
|
307
|
+
state.completed_at = new Date().toISOString();
|
|
308
|
+
state.plan[0].status = 'done';
|
|
309
|
+
await ctx.writeJSON('tasks/governor-state.json', state);
|
|
310
|
+
|
|
311
|
+
await updateTicket(ctx, ticketId, {
|
|
312
|
+
status: 'review',
|
|
313
|
+
related_cycle: cycleId,
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
await ctx.appendJSONL('tasks/pattern-log.jsonl', {
|
|
317
|
+
ts: new Date().toISOString(),
|
|
318
|
+
event: 'ticket:debug:complete',
|
|
319
|
+
id: ticketId,
|
|
320
|
+
cycle_id: cycleId,
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
ctx.success(`Debug completed for ${ticketId} — status: review`);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// ── Helpers ───────────────────────────────────────────
|
|
327
|
+
|
|
328
|
+
async function checkDuplicates(description, ctx) {
|
|
329
|
+
const candidates = [];
|
|
330
|
+
|
|
331
|
+
// Collect from lessons.md
|
|
332
|
+
if (ctx.exists('tasks/lessons.md')) {
|
|
333
|
+
const lessons = await ctx.readFile('tasks/lessons.md');
|
|
334
|
+
const lines = lessons.split('\n').filter(l => l.startsWith('- '));
|
|
335
|
+
for (const line of lines) {
|
|
336
|
+
candidates.push(line.replace(/^-\s*/, ''));
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Collect from existing tickets
|
|
341
|
+
const entries = await loadIndex(ctx);
|
|
342
|
+
for (const e of entries) {
|
|
343
|
+
candidates.push(e.title);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const match = findBestMatch(description, candidates);
|
|
347
|
+
if (match) {
|
|
348
|
+
ctx.warn(`Possible duplicate (${(match.score * 100).toFixed(0)}% similar):`);
|
|
349
|
+
ctx.warn(` "${match.text}"`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async function findRelatedLessons(description, ctx) {
|
|
354
|
+
if (!ctx.exists('tasks/lessons.md')) return '(none)';
|
|
355
|
+
|
|
356
|
+
const lessons = await ctx.readFile('tasks/lessons.md');
|
|
357
|
+
const lines = lessons.split('\n').filter(l => l.startsWith('- '));
|
|
358
|
+
|
|
359
|
+
const match = findBestMatch(description, lines.map(l => l.replace(/^-\s*/, '')));
|
|
360
|
+
if (match) return match.text;
|
|
361
|
+
return '(none)';
|
|
362
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sentix update — 프레임워크 파일을 최신 버전으로 동기화
|
|
3
|
+
*
|
|
4
|
+
* sentix 원본 패키지에서 프레임워크 공통 파일을 가져와 로컬 프로젝트를 업데이트한다.
|
|
5
|
+
* 프로젝트 고유 파일(CLAUDE.md, .sentix/config.toml, providers/, env-profiles/)은 건드리지 않는다.
|
|
6
|
+
*
|
|
7
|
+
* 사용법:
|
|
8
|
+
* sentix update # 실제 업데이트
|
|
9
|
+
* sentix update --dry # 변경 사항만 미리 확인
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { registerCommand } from '../registry.js';
|
|
13
|
+
import { VERSION } from '../version.js';
|
|
14
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
15
|
+
import { resolve, dirname } from 'node:path';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
|
|
18
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const sentixRoot = resolve(__dirname, '..', '..');
|
|
20
|
+
|
|
21
|
+
// 동기화 대상: 프레임워크 공통 파일 (모든 프로젝트가 동일해야 하는 것)
|
|
22
|
+
const SYNC_FILES = [
|
|
23
|
+
{ src: '.github/workflows/deploy.yml', dst: '.github/workflows/deploy.yml' },
|
|
24
|
+
{ src: '.github/workflows/security-scan.yml', dst: '.github/workflows/security-scan.yml' },
|
|
25
|
+
{ src: '.sentix/rules/hard-rules.md', dst: '.sentix/rules/hard-rules.md' },
|
|
26
|
+
{ src: 'FRAMEWORK.md', dst: 'FRAMEWORK.md' },
|
|
27
|
+
{ src: 'docs/governor-sop.md', dst: 'docs/governor-sop.md' },
|
|
28
|
+
{ src: 'docs/agent-scopes.md', dst: 'docs/agent-scopes.md' },
|
|
29
|
+
{ src: 'docs/severity.md', dst: 'docs/severity.md' },
|
|
30
|
+
{ src: 'docs/architecture.md', dst: 'docs/architecture.md' },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
registerCommand('update', {
|
|
34
|
+
description: 'Update framework files to the latest sentix version',
|
|
35
|
+
usage: 'sentix update [--dry]',
|
|
36
|
+
|
|
37
|
+
async run(args, ctx) {
|
|
38
|
+
const dryRun = args.includes('--dry');
|
|
39
|
+
|
|
40
|
+
ctx.log(`sentix update v${VERSION}`);
|
|
41
|
+
ctx.log(`source: ${sentixRoot}`);
|
|
42
|
+
ctx.log(`target: ${ctx.cwd}`);
|
|
43
|
+
if (dryRun) ctx.warn('DRY RUN — no files will be changed\n');
|
|
44
|
+
else ctx.log('');
|
|
45
|
+
|
|
46
|
+
// sentix 원본에서 실행 중인지 확인
|
|
47
|
+
if (resolve(ctx.cwd) === resolve(sentixRoot)) {
|
|
48
|
+
ctx.error('Cannot update sentix itself. Run this from a downstream project.');
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// sentix가 초기화된 프로젝트인지 확인
|
|
53
|
+
if (!ctx.exists('.sentix/config.toml') && !ctx.exists('CLAUDE.md')) {
|
|
54
|
+
ctx.error('This project has not been initialized with sentix.');
|
|
55
|
+
ctx.log('Run: sentix init');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const results = { updated: [], created: [], skipped: [], unchanged: [] };
|
|
60
|
+
|
|
61
|
+
for (const { src, dst } of SYNC_FILES) {
|
|
62
|
+
const srcPath = resolve(sentixRoot, src);
|
|
63
|
+
|
|
64
|
+
// 원본 파일이 없으면 스킵
|
|
65
|
+
if (!existsSync(srcPath)) {
|
|
66
|
+
results.skipped.push({ file: dst, reason: 'source not found' });
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const srcContent = readFileSync(srcPath, 'utf-8');
|
|
71
|
+
|
|
72
|
+
if (ctx.exists(dst)) {
|
|
73
|
+
const dstContent = await ctx.readFile(dst);
|
|
74
|
+
|
|
75
|
+
if (srcContent === dstContent) {
|
|
76
|
+
results.unchanged.push(dst);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// diff 요약 생성
|
|
81
|
+
const srcLines = srcContent.split('\n');
|
|
82
|
+
const dstLines = dstContent.split('\n');
|
|
83
|
+
const added = srcLines.length - dstLines.length;
|
|
84
|
+
|
|
85
|
+
ctx.log(`${dryRun ? '[DRY] ' : ''}Updating: ${dst}`);
|
|
86
|
+
ctx.log(` ${dstLines.length} lines → ${srcLines.length} lines (${added >= 0 ? '+' : ''}${added})`);
|
|
87
|
+
|
|
88
|
+
// 주요 변경 내용 표시 (새로 추가된 라인 중 의미 있는 것)
|
|
89
|
+
const dstSet = new Set(dstLines.map(l => l.trim()));
|
|
90
|
+
const newLines = srcLines
|
|
91
|
+
.filter(l => l.trim() && !l.trim().startsWith('#') && !dstSet.has(l.trim()))
|
|
92
|
+
.slice(0, 5);
|
|
93
|
+
if (newLines.length > 0) {
|
|
94
|
+
ctx.log(' New:');
|
|
95
|
+
for (const line of newLines) {
|
|
96
|
+
ctx.log(` + ${line.trim().substring(0, 80)}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!dryRun) {
|
|
101
|
+
await ctx.writeFile(dst, srcContent);
|
|
102
|
+
ctx.success(`Updated: ${dst}`);
|
|
103
|
+
}
|
|
104
|
+
results.updated.push(dst);
|
|
105
|
+
} else {
|
|
106
|
+
ctx.log(`${dryRun ? '[DRY] ' : ''}Creating: ${dst}`);
|
|
107
|
+
if (!dryRun) {
|
|
108
|
+
await ctx.writeFile(dst, srcContent);
|
|
109
|
+
ctx.success(`Created: ${dst}`);
|
|
110
|
+
}
|
|
111
|
+
results.created.push(dst);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 요약
|
|
116
|
+
ctx.log('\n=== Update Summary ===');
|
|
117
|
+
if (results.updated.length > 0) {
|
|
118
|
+
ctx.log(`Updated: ${results.updated.length} file(s)`);
|
|
119
|
+
for (const f of results.updated) ctx.log(` ${f}`);
|
|
120
|
+
}
|
|
121
|
+
if (results.created.length > 0) {
|
|
122
|
+
ctx.log(`Created: ${results.created.length} file(s)`);
|
|
123
|
+
for (const f of results.created) ctx.log(` ${f}`);
|
|
124
|
+
}
|
|
125
|
+
if (results.unchanged.length > 0) {
|
|
126
|
+
ctx.log(`Unchanged: ${results.unchanged.length} file(s)`);
|
|
127
|
+
}
|
|
128
|
+
if (results.skipped.length > 0) {
|
|
129
|
+
ctx.warn(`Skipped: ${results.skipped.length} file(s)`);
|
|
130
|
+
for (const s of results.skipped) ctx.log(` ${s.file} (${s.reason})`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const totalChanges = results.updated.length + results.created.length;
|
|
134
|
+
if (totalChanges === 0) {
|
|
135
|
+
ctx.success('\nAlready up to date.');
|
|
136
|
+
} else if (dryRun) {
|
|
137
|
+
ctx.warn(`\n${totalChanges} file(s) would be changed. Run without --dry to apply.`);
|
|
138
|
+
} else {
|
|
139
|
+
ctx.success(`\n${totalChanges} file(s) updated to sentix v${VERSION}.`);
|
|
140
|
+
ctx.log('Run: sentix doctor — to verify project health');
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
});
|