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,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sentix plugin — 플러그인 관리
|
|
3
|
+
*
|
|
4
|
+
* sentix plugin list — 등록된 플러그인 목록
|
|
5
|
+
* sentix plugin create — 새 플러그인 스캐폴딩
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readdir } from 'node:fs/promises';
|
|
9
|
+
import { resolve } from 'node:path';
|
|
10
|
+
import { registerCommand, getAllCommands } from '../registry.js';
|
|
11
|
+
|
|
12
|
+
registerCommand('plugin', {
|
|
13
|
+
description: 'Manage plugins (list | create)',
|
|
14
|
+
usage: 'sentix plugin <list|create> [name]',
|
|
15
|
+
|
|
16
|
+
async run(args, ctx) {
|
|
17
|
+
const subcommand = args[0];
|
|
18
|
+
|
|
19
|
+
if (!subcommand || subcommand === 'list') {
|
|
20
|
+
await listPlugins(ctx);
|
|
21
|
+
} else if (subcommand === 'create') {
|
|
22
|
+
const name = args[1];
|
|
23
|
+
if (!name) {
|
|
24
|
+
ctx.error('Usage: sentix plugin create <name>');
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
await createPlugin(name, ctx);
|
|
28
|
+
} else {
|
|
29
|
+
ctx.error(`Unknown subcommand: ${subcommand}`);
|
|
30
|
+
ctx.log('Usage: sentix plugin <list|create> [name]');
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
async function listPlugins(ctx) {
|
|
36
|
+
ctx.log('=== Registered Commands ===\n');
|
|
37
|
+
|
|
38
|
+
const cmds = getAllCommands();
|
|
39
|
+
for (const [name, cmd] of cmds) {
|
|
40
|
+
ctx.log(` ${name.padEnd(12)} ${cmd.description}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
ctx.log('');
|
|
44
|
+
|
|
45
|
+
// Check for project-local plugins
|
|
46
|
+
if (ctx.exists('.sentix/plugins')) {
|
|
47
|
+
ctx.log('--- Project Plugins ---\n');
|
|
48
|
+
try {
|
|
49
|
+
const files = await readdir(resolve(ctx.cwd, '.sentix/plugins'));
|
|
50
|
+
const plugins = files.filter(f => f.endsWith('.js'));
|
|
51
|
+
if (plugins.length > 0) {
|
|
52
|
+
for (const p of plugins) {
|
|
53
|
+
ctx.log(` .sentix/plugins/${p}`);
|
|
54
|
+
}
|
|
55
|
+
} else {
|
|
56
|
+
ctx.log(' (none)');
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
ctx.log(' (none)');
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
ctx.log('Project plugins: (none — create with: sentix plugin create <name>)');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
ctx.log('');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function createPlugin(name, ctx) {
|
|
69
|
+
const safeName = name.replace(/[^a-z0-9-]/gi, '-').toLowerCase().replace(/^-+|-+$/g, '');
|
|
70
|
+
|
|
71
|
+
if (!safeName) {
|
|
72
|
+
ctx.error('Invalid plugin name. Use alphanumeric characters and hyphens.');
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const path = `.sentix/plugins/${safeName}.js`;
|
|
77
|
+
|
|
78
|
+
if (ctx.exists(path)) {
|
|
79
|
+
ctx.error(`Plugin already exists: ${path}`);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const template = `/**
|
|
84
|
+
* Sentix Plugin: ${safeName}
|
|
85
|
+
*
|
|
86
|
+
* Project-local plugin. Loaded after built-in commands and plugins.
|
|
87
|
+
* Use registry.registerCommand() to add commands.
|
|
88
|
+
* Use registry.registerHook() to add lifecycle hooks.
|
|
89
|
+
*/
|
|
90
|
+
|
|
91
|
+
import { registerCommand, registerHook } from '../../src/registry.js';
|
|
92
|
+
|
|
93
|
+
// ── Example: Register a custom command ──────────────
|
|
94
|
+
// registerCommand('${safeName}', {
|
|
95
|
+
// description: 'My custom command',
|
|
96
|
+
// usage: 'sentix ${safeName}',
|
|
97
|
+
// async run(args, ctx) {
|
|
98
|
+
// ctx.success('Hello from ${safeName} plugin!');
|
|
99
|
+
// },
|
|
100
|
+
// });
|
|
101
|
+
|
|
102
|
+
// ── Example: Register a hook ────────────────────────
|
|
103
|
+
// registerHook('after:command', async ({ command, ctx }) => {
|
|
104
|
+
// ctx.log(\`[${safeName}] Command "\${command}" finished\`);
|
|
105
|
+
// });
|
|
106
|
+
`;
|
|
107
|
+
|
|
108
|
+
await ctx.writeFile(path, template);
|
|
109
|
+
ctx.success(`Created ${path}`);
|
|
110
|
+
ctx.log(`Edit the file to add your custom commands or hooks.`);
|
|
111
|
+
}
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sentix run "요청" — Governor 파이프라인 실행
|
|
3
|
+
*
|
|
4
|
+
* Claude Code를 spawn으로 안전하게 호출하고, governor-state.json 기록, pattern-log.jsonl 기록.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { spawnSync } from 'node:child_process';
|
|
8
|
+
import { registerCommand } from '../registry.js';
|
|
9
|
+
import { runGates } from '../lib/verify-gates.js';
|
|
10
|
+
import { detectDangerousRequest, verifyWord, isConfigured } from '../lib/safety.js';
|
|
11
|
+
import { runChainedPipeline } from '../lib/pipeline.js';
|
|
12
|
+
|
|
13
|
+
registerCommand('run', {
|
|
14
|
+
description: 'Run a request through the Governor pipeline',
|
|
15
|
+
usage: 'sentix run "요청 내용"',
|
|
16
|
+
|
|
17
|
+
async run(args, ctx) {
|
|
18
|
+
const request = args.join(' ').trim();
|
|
19
|
+
|
|
20
|
+
if (!request) {
|
|
21
|
+
ctx.error('Usage: sentix run "요청 내용"');
|
|
22
|
+
ctx.log(' Example: sentix run "인증에 세션 만료 추가해줘"');
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ── Preflight checks ────────────────────────────
|
|
27
|
+
if (!ctx.exists('CLAUDE.md')) {
|
|
28
|
+
ctx.error('CLAUDE.md not found. Run: sentix init');
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ── Safety word gate ───────────────────────────
|
|
33
|
+
const dangerMatch = detectDangerousRequest(request);
|
|
34
|
+
if (dangerMatch) {
|
|
35
|
+
const hasSafety = await isConfigured(ctx);
|
|
36
|
+
|
|
37
|
+
if (hasSafety) {
|
|
38
|
+
// Find --safety-word flag in original args
|
|
39
|
+
const swIdx = args.indexOf('--safety-word');
|
|
40
|
+
const safetyInput = swIdx !== -1 ? args[swIdx + 1] : null;
|
|
41
|
+
|
|
42
|
+
if (!safetyInput) {
|
|
43
|
+
ctx.error('[SENTIX:SAFETY] 위험 요청이 감지되었습니다.');
|
|
44
|
+
ctx.log(` 패턴: ${dangerMatch}`);
|
|
45
|
+
ctx.log('');
|
|
46
|
+
ctx.log(' 이 요청을 실행하려면 안전어를 입력하세요:');
|
|
47
|
+
ctx.log(' sentix run "요청" --safety-word <안전어>');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const verified = await verifyWord(ctx, safetyInput);
|
|
52
|
+
if (!verified) {
|
|
53
|
+
ctx.error('[SENTIX:SAFETY] DENIED — 안전어가 일치하지 않습니다.');
|
|
54
|
+
await ctx.appendJSONL('tasks/pattern-log.jsonl', {
|
|
55
|
+
ts: new Date().toISOString(),
|
|
56
|
+
event: 'safety-denied',
|
|
57
|
+
input: request,
|
|
58
|
+
pattern: dangerMatch,
|
|
59
|
+
});
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
ctx.success('[SENTIX:SAFETY] VERIFIED — 진행합니다.');
|
|
64
|
+
} else {
|
|
65
|
+
ctx.warn('[SENTIX:SAFETY] 위험 요청이 감지되었습니다.');
|
|
66
|
+
ctx.log(` 패턴: ${dangerMatch}`);
|
|
67
|
+
ctx.log('');
|
|
68
|
+
ctx.warn(' 안전어가 설정되지 않아 추가 검증 없이 진행합니다.');
|
|
69
|
+
ctx.warn(' 보안 강화를 위해 설정을 권장합니다: sentix safety set <안전어>');
|
|
70
|
+
ctx.log('');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ── Check Claude Code is available ──────────────
|
|
75
|
+
const claudeCheck = spawnSync('claude', ['--version'], { encoding: 'utf-8', stdio: 'pipe' });
|
|
76
|
+
if (claudeCheck.error) {
|
|
77
|
+
ctx.error('Claude Code CLI not found. Install: https://docs.anthropic.com/en/docs/claude-code');
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ── Check for concurrent execution ──────────────
|
|
82
|
+
if (ctx.exists('tasks/governor-state.json')) {
|
|
83
|
+
try {
|
|
84
|
+
const existing = await ctx.readJSON('tasks/governor-state.json');
|
|
85
|
+
if (existing.status === 'in_progress') {
|
|
86
|
+
ctx.error(`Another pipeline is running: ${existing.cycle_id}`);
|
|
87
|
+
ctx.log(' Wait for it to complete, or delete tasks/governor-state.json to force.');
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
// Malformed file — safe to overwrite
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── Create governor state ───────────────────────
|
|
96
|
+
const cycleId = `cycle-${new Date().toISOString().slice(0, 10)}-${String(Date.now()).slice(-3)}`;
|
|
97
|
+
|
|
98
|
+
const state = {
|
|
99
|
+
schema_version: 1,
|
|
100
|
+
cycle_id: cycleId,
|
|
101
|
+
request,
|
|
102
|
+
status: 'in_progress',
|
|
103
|
+
current_phase: 'governor',
|
|
104
|
+
plan: [],
|
|
105
|
+
retries: {},
|
|
106
|
+
cross_judgments: [],
|
|
107
|
+
started_at: new Date().toISOString(),
|
|
108
|
+
completed_at: null,
|
|
109
|
+
human_intervention_requested: false,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
await ctx.writeJSON('tasks/governor-state.json', state);
|
|
113
|
+
ctx.success(`Cycle ${cycleId} started`);
|
|
114
|
+
ctx.log(`Request: "${request}"`);
|
|
115
|
+
ctx.log('');
|
|
116
|
+
|
|
117
|
+
// ── Log to pattern-log.jsonl ────────────────────
|
|
118
|
+
await ctx.appendJSONL('tasks/pattern-log.jsonl', {
|
|
119
|
+
ts: new Date().toISOString(),
|
|
120
|
+
event: 'request',
|
|
121
|
+
input: request,
|
|
122
|
+
cycle_id: cycleId,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// ── Determine pipeline mode ───────────────────
|
|
126
|
+
const useChained = args.includes('--chained') || args.includes('-c');
|
|
127
|
+
const useLegacy = args.includes('--single');
|
|
128
|
+
|
|
129
|
+
const safetyDirective = await isConfigured(ctx)
|
|
130
|
+
? 'SAFETY WORD is configured. For any dangerous operation (memory wipe, data export, rule changes, bulk deletion), you MUST ask the user for the safety word and verify it with: node bin/sentix.js safety verify <word>. NEVER reveal, display, or hint at the safety word or its hash.'
|
|
131
|
+
: '';
|
|
132
|
+
|
|
133
|
+
let pipelineResult;
|
|
134
|
+
|
|
135
|
+
if (useChained || (!useLegacy && !args.includes('--single'))) {
|
|
136
|
+
// ── Chained pipeline (기본값) ─────────────────
|
|
137
|
+
// Phase별로 분리 실행 + 중간 게이트 + 자동 테스트
|
|
138
|
+
ctx.log('Pipeline mode: chained (PLAN → DEV → GATE → REVIEW → FINALIZE)\n');
|
|
139
|
+
|
|
140
|
+
const chainResult = await runChainedPipeline(request, cycleId, state, ctx, { safetyDirective });
|
|
141
|
+
|
|
142
|
+
pipelineResult = {
|
|
143
|
+
success: chainResult.success,
|
|
144
|
+
gateResults: chainResult.gateResults || runGates(ctx.cwd),
|
|
145
|
+
duration_seconds: chainResult.duration_seconds,
|
|
146
|
+
phases: chainResult.phases,
|
|
147
|
+
test_passed: chainResult.test_passed,
|
|
148
|
+
failedAt: chainResult.failedAt,
|
|
149
|
+
};
|
|
150
|
+
} else {
|
|
151
|
+
// ── Legacy single-shot (--single 플래그) ──────
|
|
152
|
+
ctx.log('Pipeline mode: single (legacy)\n');
|
|
153
|
+
ctx.log('Invoking Claude Code Governor...\n');
|
|
154
|
+
|
|
155
|
+
const prompt = [
|
|
156
|
+
'Read CLAUDE.md first. Refer to FRAMEWORK.md and docs/ only when you need design details for the current task.',
|
|
157
|
+
safetyDirective,
|
|
158
|
+
'Execute the following request through the Governor pipeline:',
|
|
159
|
+
`"${request}"`,
|
|
160
|
+
'',
|
|
161
|
+
'Follow the SOP exactly. Update tasks/governor-state.json at each phase.',
|
|
162
|
+
].filter(Boolean).join('\n');
|
|
163
|
+
|
|
164
|
+
const result = spawnSync('claude', ['-p', prompt], {
|
|
165
|
+
cwd: ctx.cwd,
|
|
166
|
+
stdio: 'inherit',
|
|
167
|
+
timeout: 600_000,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
if (result.error || result.status !== 0) {
|
|
171
|
+
const error = result.error?.message || `Exit code ${result.status}`;
|
|
172
|
+
state.status = 'failed';
|
|
173
|
+
state.error = error;
|
|
174
|
+
await ctx.writeJSON('tasks/governor-state.json', state);
|
|
175
|
+
await ctx.appendJSONL('tasks/pattern-log.jsonl', {
|
|
176
|
+
ts: new Date().toISOString(),
|
|
177
|
+
event: 'pipeline-failed',
|
|
178
|
+
cycle_id: cycleId,
|
|
179
|
+
error,
|
|
180
|
+
});
|
|
181
|
+
ctx.error(`Pipeline failed: ${error}`);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
pipelineResult = {
|
|
186
|
+
success: true,
|
|
187
|
+
gateResults: runGates(ctx.cwd),
|
|
188
|
+
duration_seconds: Math.round((Date.now() - new Date(state.started_at).getTime()) / 1000),
|
|
189
|
+
phases: [{ name: 'single', success: true }],
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ── Post-pipeline (공통) ────────────────────────
|
|
194
|
+
if (!pipelineResult.success) {
|
|
195
|
+
state.status = 'failed';
|
|
196
|
+
state.error = `Failed at phase: ${pipelineResult.failedAt}`;
|
|
197
|
+
state.completed_at = new Date().toISOString();
|
|
198
|
+
await ctx.writeJSON('tasks/governor-state.json', state);
|
|
199
|
+
await ctx.appendJSONL('tasks/pattern-log.jsonl', {
|
|
200
|
+
ts: new Date().toISOString(),
|
|
201
|
+
event: 'pipeline-failed',
|
|
202
|
+
cycle_id: cycleId,
|
|
203
|
+
error: state.error,
|
|
204
|
+
});
|
|
205
|
+
ctx.error(`Pipeline failed at ${pipelineResult.failedAt} phase.`);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ── Final verification gates ────────────────────
|
|
210
|
+
ctx.log('\n--- Final Verification Gates ---');
|
|
211
|
+
|
|
212
|
+
const gateResults = pipelineResult.gateResults;
|
|
213
|
+
for (const check of gateResults.checks) {
|
|
214
|
+
if (check.passed) {
|
|
215
|
+
ctx.success(`[${check.rule}] ${check.detail}`);
|
|
216
|
+
} else {
|
|
217
|
+
ctx.warn(`[${check.rule}] ${check.detail}`);
|
|
218
|
+
for (const v of check.violations) {
|
|
219
|
+
ctx.warn(` → ${v.message}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (gateResults.checks.length === 0) {
|
|
225
|
+
ctx.log(gateResults.summary);
|
|
226
|
+
}
|
|
227
|
+
ctx.log('');
|
|
228
|
+
|
|
229
|
+
// ── Update state on completion ──────────────────
|
|
230
|
+
const completedAt = new Date().toISOString();
|
|
231
|
+
|
|
232
|
+
state.status = gateResults.passed ? 'completed' : 'gate-warning';
|
|
233
|
+
state.completed_at = completedAt;
|
|
234
|
+
state.verification = gateResults;
|
|
235
|
+
state.pipeline_mode = pipelineResult.phases.length > 1 ? 'chained' : 'single';
|
|
236
|
+
state.phases = pipelineResult.phases.map(p => ({ name: p.name, success: p.success }));
|
|
237
|
+
|
|
238
|
+
state.ticket_type = detectTicketType(request, state);
|
|
239
|
+
|
|
240
|
+
await ctx.writeJSON('tasks/governor-state.json', state);
|
|
241
|
+
|
|
242
|
+
// ── Record thread metrics ─────────────────────
|
|
243
|
+
await ctx.appendJSONL('tasks/agent-metrics.jsonl', {
|
|
244
|
+
ts: completedAt,
|
|
245
|
+
cycle_id: cycleId,
|
|
246
|
+
agent: 'governor',
|
|
247
|
+
request,
|
|
248
|
+
ticket_type: state.ticket_type,
|
|
249
|
+
pipeline_mode: state.pipeline_mode,
|
|
250
|
+
phases_total: pipelineResult.phases.length,
|
|
251
|
+
phases_passed: pipelineResult.phases.filter(p => p.success).length,
|
|
252
|
+
duration_seconds: pipelineResult.duration_seconds,
|
|
253
|
+
test_passed: pipelineResult.test_passed ?? null,
|
|
254
|
+
verification: {
|
|
255
|
+
passed: gateResults.passed,
|
|
256
|
+
checks_run: gateResults.checks.length,
|
|
257
|
+
checks_passed: gateResults.checks.filter(c => c.passed).length,
|
|
258
|
+
violations: gateResults.violations.map(v => v.rule),
|
|
259
|
+
},
|
|
260
|
+
autonomy: {
|
|
261
|
+
human_interventions: 0,
|
|
262
|
+
gate_failures: gateResults.passed ? 0 : 1,
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
await ctx.appendJSONL('tasks/pattern-log.jsonl', {
|
|
267
|
+
ts: completedAt,
|
|
268
|
+
event: gateResults.passed ? 'pipeline-complete' : 'pipeline-gate-warning',
|
|
269
|
+
cycle_id: cycleId,
|
|
270
|
+
pipeline_mode: state.pipeline_mode,
|
|
271
|
+
gate_summary: gateResults.summary,
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
if (gateResults.passed) {
|
|
275
|
+
ctx.success('Pipeline completed — all gates passed.');
|
|
276
|
+
} else {
|
|
277
|
+
ctx.warn(`Pipeline completed with warnings — ${gateResults.violations.length} gate violation(s).`);
|
|
278
|
+
ctx.log('Review violations above before merging.');
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Detect ticket type from request text or governor state plan.
|
|
285
|
+
* Used by auto-version hook to determine bump type (minor for feature, patch for bug).
|
|
286
|
+
*/
|
|
287
|
+
function detectTicketType(request, state) {
|
|
288
|
+
// Check if a ticket ID is referenced
|
|
289
|
+
if (request.includes('feat-') || /feature|기능|추가/i.test(request)) return 'feature';
|
|
290
|
+
if (request.includes('bug-') || /bug|fix|debug|버그|수정/i.test(request)) return 'bug';
|
|
291
|
+
|
|
292
|
+
// Check plan for ticket references
|
|
293
|
+
if (state.plan) {
|
|
294
|
+
for (const step of state.plan) {
|
|
295
|
+
if (step.result_ref && typeof step.result_ref === 'string') {
|
|
296
|
+
if (step.result_ref.includes('feat-')) return 'feature';
|
|
297
|
+
if (step.result_ref.includes('bug-')) return 'bug';
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return null; // Unknown — auto-version hook will default to patch
|
|
303
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sentix safety — Safety word management (LLM injection defense)
|
|
3
|
+
*
|
|
4
|
+
* sentix safety set <word> Set or update the safety word
|
|
5
|
+
* sentix safety verify <word> Verify a word against stored hash
|
|
6
|
+
* sentix safety status Check if safety word is configured
|
|
7
|
+
*
|
|
8
|
+
* 보안 수준: PEM 키와 동일. 평문 저장 금지, git 커밋 금지, 외부 공유 금지.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { registerCommand } from '../registry.js';
|
|
12
|
+
import {
|
|
13
|
+
hashWord,
|
|
14
|
+
saveSafetyHash,
|
|
15
|
+
verifyWord,
|
|
16
|
+
isConfigured,
|
|
17
|
+
loadSafetyHash,
|
|
18
|
+
} from '../lib/safety.js';
|
|
19
|
+
|
|
20
|
+
registerCommand('safety', {
|
|
21
|
+
description: 'Manage safety word for LLM injection defense',
|
|
22
|
+
usage: 'sentix safety <set|verify|status> [word]',
|
|
23
|
+
|
|
24
|
+
async run(args, ctx) {
|
|
25
|
+
const sub = args[0];
|
|
26
|
+
|
|
27
|
+
if (!sub || sub === 'status') {
|
|
28
|
+
return await statusCmd(ctx);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (sub === 'set') {
|
|
32
|
+
const word = args.slice(1).join(' ').trim();
|
|
33
|
+
if (!word) {
|
|
34
|
+
ctx.error('Usage: sentix safety set <word>');
|
|
35
|
+
ctx.log(' 안전어를 지정하세요. 공백 포함 가능.');
|
|
36
|
+
ctx.log(' 예: sentix safety set "my secret phrase"');
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
return await setCmd(word, ctx);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (sub === 'verify') {
|
|
43
|
+
const word = args.slice(1).join(' ').trim();
|
|
44
|
+
if (!word) {
|
|
45
|
+
ctx.error('Usage: sentix safety verify <word>');
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
return await verifyCmd(word, ctx);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
ctx.error(`Unknown subcommand: ${sub}`);
|
|
52
|
+
ctx.log(' sentix safety set <word> 안전어 설정');
|
|
53
|
+
ctx.log(' sentix safety verify <word> 안전어 검증');
|
|
54
|
+
ctx.log(' sentix safety status 설정 상태 확인');
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// ── set ───────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
async function setCmd(word, ctx) {
|
|
61
|
+
const hash = hashWord(word);
|
|
62
|
+
await saveSafetyHash(ctx, hash);
|
|
63
|
+
|
|
64
|
+
// Verify .gitignore protection
|
|
65
|
+
let gitignoreOk = false;
|
|
66
|
+
if (ctx.exists('.gitignore')) {
|
|
67
|
+
const gi = await ctx.readFile('.gitignore');
|
|
68
|
+
gitignoreOk = gi.includes('.sentix/safety.toml');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
ctx.success('Safety word configured');
|
|
72
|
+
ctx.log('');
|
|
73
|
+
ctx.log(' ┌─────────────────────────────────────────────┐');
|
|
74
|
+
ctx.log(' │ SECURITY NOTICE — 보안 안내 │');
|
|
75
|
+
ctx.log(' ├─────────────────────────────────────────────┤');
|
|
76
|
+
ctx.log(' │ │');
|
|
77
|
+
ctx.log(' │ 안전어는 PEM 키와 동일한 보안 수준입니다. │');
|
|
78
|
+
ctx.log(' │ │');
|
|
79
|
+
ctx.log(' │ 1. 평문은 어디에도 저장되지 않습니다 │');
|
|
80
|
+
ctx.log(' │ (SHA-256 해시만 로컬에 저장) │');
|
|
81
|
+
ctx.log(' │ │');
|
|
82
|
+
ctx.log(' │ 2. 절대 git에 커밋하지 마세요 │');
|
|
83
|
+
ctx.log(' │ (.gitignore에 자동 등록됨) │');
|
|
84
|
+
ctx.log(' │ │');
|
|
85
|
+
ctx.log(' │ 3. 절대 외부에 공유하지 마세요 │');
|
|
86
|
+
ctx.log(' │ (Slack, 이메일, 메신저, 문서 등) │');
|
|
87
|
+
ctx.log(' │ │');
|
|
88
|
+
ctx.log(' │ 4. 절대 AI 대화에 붙여넣지 마세요 │');
|
|
89
|
+
ctx.log(' │ (safety.toml 내용 포함) │');
|
|
90
|
+
ctx.log(' │ │');
|
|
91
|
+
ctx.log(' │ 5. 분실 시 재설정만 가능합니다 │');
|
|
92
|
+
ctx.log(' │ (sentix safety set <새 안전어>) │');
|
|
93
|
+
ctx.log(' │ │');
|
|
94
|
+
ctx.log(' └─────────────────────────────────────────────┘');
|
|
95
|
+
ctx.log('');
|
|
96
|
+
|
|
97
|
+
if (gitignoreOk) {
|
|
98
|
+
ctx.success('.gitignore: .sentix/safety.toml 보호됨');
|
|
99
|
+
} else {
|
|
100
|
+
ctx.warn('.gitignore에 .sentix/safety.toml이 없습니다!');
|
|
101
|
+
ctx.log(' 아래 줄을 .gitignore에 추가하세요:');
|
|
102
|
+
ctx.log(' .sentix/safety.toml');
|
|
103
|
+
ctx.log('');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
ctx.log(` Hash: ${hash.slice(0, 8)}****`);
|
|
107
|
+
ctx.log(' 검증: sentix safety verify <word>');
|
|
108
|
+
ctx.log('');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ── verify ────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
async function verifyCmd(word, ctx) {
|
|
114
|
+
const result = await verifyWord(ctx, word);
|
|
115
|
+
|
|
116
|
+
if (result === null) {
|
|
117
|
+
ctx.warn('Safety word not configured. Run: sentix safety set <word>');
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (result) {
|
|
122
|
+
ctx.success('VERIFIED — safety word matches');
|
|
123
|
+
} else {
|
|
124
|
+
ctx.error('DENIED — safety word does not match');
|
|
125
|
+
process.exitCode = 1;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ── status ────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
async function statusCmd(ctx) {
|
|
132
|
+
ctx.log('=== Safety Word Status ===\n');
|
|
133
|
+
|
|
134
|
+
const configured = await isConfigured(ctx);
|
|
135
|
+
|
|
136
|
+
if (configured) {
|
|
137
|
+
ctx.success('Safety word: configured');
|
|
138
|
+
ctx.log(' .sentix/safety.toml → enabled');
|
|
139
|
+
ctx.log('');
|
|
140
|
+
|
|
141
|
+
// Check .gitignore protection
|
|
142
|
+
let gitignoreOk = false;
|
|
143
|
+
if (ctx.exists('.gitignore')) {
|
|
144
|
+
const gi = await ctx.readFile('.gitignore');
|
|
145
|
+
gitignoreOk = gi.includes('.sentix/safety.toml');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (gitignoreOk) {
|
|
149
|
+
ctx.success('.gitignore: 보호됨 (git 추적 제외)');
|
|
150
|
+
} else {
|
|
151
|
+
ctx.error('.gitignore: 보호 안 됨! safety.toml이 git에 노출될 수 있습니다');
|
|
152
|
+
ctx.log(' Fix: echo ".sentix/safety.toml" >> .gitignore');
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
ctx.warn('Safety word: NOT configured');
|
|
156
|
+
ctx.log('');
|
|
157
|
+
ctx.log(' 안전어가 설정되지 않았습니다.');
|
|
158
|
+
ctx.log(' LLM 인젝션 방지를 위해 설정을 권장합니다.');
|
|
159
|
+
ctx.log('');
|
|
160
|
+
ctx.log(' 설정: sentix safety set <나만의 안전어>');
|
|
161
|
+
}
|
|
162
|
+
ctx.log('');
|
|
163
|
+
}
|