jutell 0.3.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.
Files changed (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +11 -0
  3. package/assets/default-config.json +26 -0
  4. package/assets/local-admin/assets/index-CVml-p-C.css +1 -0
  5. package/assets/local-admin/assets/index-Gxd8X8ii.js +60 -0
  6. package/assets/local-admin/index.html +14 -0
  7. package/assets/local-admin-server.js +1391 -0
  8. package/assets/mcp-server/config/bridge-config.js +72 -0
  9. package/assets/mcp-server/index.js +42 -0
  10. package/assets/mcp-server/tools/bridge-tools.js +64 -0
  11. package/assets/mcp-server/tools/catalog.js +25 -0
  12. package/assets/mcp-server/tools/usage-counters.js +97 -0
  13. package/assets/skill/SKILL.md +135 -0
  14. package/assets/skill/references/explained-diff-format.md +117 -0
  15. package/assets/skill/references/feature-registry.md +29 -0
  16. package/assets/skill/references/glossary-ko.md +302 -0
  17. package/assets/skill/references/report-format.md +176 -0
  18. package/assets/skill/references/risk-level-guide.md +85 -0
  19. package/assets/templates/request-builder/BUG_REPORT_REQUEST.md +100 -0
  20. package/assets/templates/request-builder/CODE_REVIEW_REQUEST.md +93 -0
  21. package/assets/templates/request-builder/DESIGN_REQUEST.md +111 -0
  22. package/assets/templates/request-builder/FEATURE_REQUEST.md +93 -0
  23. package/assets/templates/request-builder/MANUAL_EDIT_GUIDE.md +95 -0
  24. package/assets/templates/request-builder/NEXT_AGENT_HANDOFF.md +106 -0
  25. package/assets/templates/request-builder/PROJECT_PLANNING_REQUEST.md +106 -0
  26. package/assets/templates/request-builder/README.md +48 -0
  27. package/assets/version.json +6 -0
  28. package/dist/cli.js +82 -0
  29. package/dist/commands/dashboard.js +81 -0
  30. package/dist/commands/default.js +103 -0
  31. package/dist/commands/lifecycle.js +166 -0
  32. package/dist/commands/migrate.js +159 -0
  33. package/dist/commands/provider.js +135 -0
  34. package/dist/commands/session/add-work.js +43 -0
  35. package/dist/commands/session/create-page.js +53 -0
  36. package/dist/commands/session/finish-session.js +28 -0
  37. package/dist/commands/session/index.js +76 -0
  38. package/dist/commands/session/move-page.js +37 -0
  39. package/dist/commands/session/new-session.js +24 -0
  40. package/dist/commands/session/operator-storage.js +126 -0
  41. package/dist/commands/session/prompt.js +77 -0
  42. package/dist/commands/session/storage-command.js +74 -0
  43. package/dist/commands/session/storage.js +212 -0
  44. package/dist/commands/session/types.js +1 -0
  45. package/dist/commands/status.js +208 -0
  46. package/dist/commands/upgrade.js +113 -0
  47. package/dist/commands/use.js +180 -0
  48. package/dist/compat.js +5 -0
  49. package/dist/config/managed.js +257 -0
  50. package/dist/config/paths.js +100 -0
  51. package/dist/index.js +4 -0
  52. package/dist/installer/agents.js +42 -0
  53. package/dist/installer/claude.js +160 -0
  54. package/dist/installer/config.js +45 -0
  55. package/dist/installer/opencode.js +237 -0
  56. package/dist/installer/providers.js +15 -0
  57. package/dist/installer/skill.js +94 -0
  58. package/dist/output/format.js +187 -0
  59. package/dist/process/mcpProbe.js +122 -0
  60. package/dist/process/system.js +34 -0
  61. package/dist/types.js +1 -0
  62. package/package.json +55 -0
@@ -0,0 +1,72 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+ export const FEATURE_IDS = ['changeSummary', 'userVisibleChanges', 'internalChanges', 'mainFiles', 'explainedDiff', 'glossary', 'validationResults', 'riskAssessment', 'userActions', 'nextActionSuggestions', 'requestClarificationGuide', 'manualEditGuidance', 'requestBuilder'];
4
+ export const PROFILE_FEATURES = {
5
+ minimal: { changeSummary: true, userVisibleChanges: true, internalChanges: false, mainFiles: false, explainedDiff: false, glossary: false, validationResults: true, riskAssessment: false, userActions: true, nextActionSuggestions: false, requestClarificationGuide: false, manualEditGuidance: false, requestBuilder: true },
6
+ balanced: { changeSummary: true, userVisibleChanges: true, internalChanges: true, mainFiles: true, explainedDiff: true, glossary: true, validationResults: true, riskAssessment: true, userActions: true, nextActionSuggestions: true, requestClarificationGuide: true, manualEditGuidance: true, requestBuilder: true },
7
+ learning: { changeSummary: true, userVisibleChanges: true, internalChanges: true, mainFiles: true, explainedDiff: true, glossary: true, validationResults: true, riskAssessment: true, userActions: true, nextActionSuggestions: true, requestClarificationGuide: true, manualEditGuidance: true, requestBuilder: true },
8
+ detailed: { changeSummary: true, userVisibleChanges: true, internalChanges: true, mainFiles: true, explainedDiff: true, glossary: true, validationResults: true, riskAssessment: true, userActions: true, nextActionSuggestions: true, requestClarificationGuide: true, manualEditGuidance: true, requestBuilder: true },
9
+ };
10
+ export const DEFAULT_CONFIG = {
11
+ version: 1,
12
+ profile: 'balanced',
13
+ features: { ...PROFILE_FEATURES.balanced },
14
+ limits: { maxMainFiles: 5, maxGlossaryTerms: 3, compactReportMaxSentences: 12 },
15
+ mcp: { enabled: false },
16
+ usageMeasurement: { localCountersEnabled: false },
17
+ };
18
+ const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
19
+ const hasOnlyKeys = (value, keys) => Object.keys(value).every((key) => keys.includes(key));
20
+ export function normalizeConfig(value) {
21
+ if (!isRecord(value) || value.version !== 1 || !['minimal', 'balanced', 'learning', 'detailed'].includes(String(value.profile)))
22
+ return structuredClone(DEFAULT_CONFIG);
23
+ const featuresInput = isRecord(value.features) ? value.features : {};
24
+ if (!hasOnlyKeys(featuresInput, FEATURE_IDS) || FEATURE_IDS.some((id) => featuresInput[id] !== undefined && typeof featuresInput[id] !== 'boolean'))
25
+ return structuredClone(DEFAULT_CONFIG);
26
+ if (!isRecord(value.limits) || !hasOnlyKeys(value.limits, ['maxMainFiles', 'maxGlossaryTerms', 'compactReportMaxSentences']))
27
+ return structuredClone(DEFAULT_CONFIG);
28
+ const ranges = { maxMainFiles: [1, 10], maxGlossaryTerms: [0, 10], compactReportMaxSentences: [4, 30] };
29
+ for (const [key, [min, max]] of Object.entries(ranges)) {
30
+ const number = value.limits[key];
31
+ if (typeof number !== 'number' || !Number.isInteger(number) || number < min || number > max)
32
+ return structuredClone(DEFAULT_CONFIG);
33
+ }
34
+ if ('mcp' in value && (!isRecord(value.mcp) || typeof value.mcp.enabled !== 'boolean'))
35
+ return structuredClone(DEFAULT_CONFIG);
36
+ if ('usageMeasurement' in value && (!isRecord(value.usageMeasurement) || !hasOnlyKeys(value.usageMeasurement, ['localCountersEnabled']) || typeof value.usageMeasurement.localCountersEnabled !== 'boolean'))
37
+ return structuredClone(DEFAULT_CONFIG);
38
+ const features = Object.fromEntries(FEATURE_IDS.map((id) => [id, typeof featuresInput[id] === 'boolean' ? featuresInput[id] : PROFILE_FEATURES[value.profile][id]]));
39
+ return {
40
+ version: 1,
41
+ profile: value.profile,
42
+ features,
43
+ limits: { ...value.limits },
44
+ mcp: isRecord(value.mcp) ? { enabled: value.mcp.enabled } : { ...DEFAULT_CONFIG.mcp },
45
+ usageMeasurement: isRecord(value.usageMeasurement) ? { localCountersEnabled: value.usageMeasurement.localCountersEnabled } : { ...DEFAULT_CONFIG.usageMeasurement },
46
+ };
47
+ }
48
+ async function readText(file) {
49
+ try {
50
+ return await fs.readFile(file, 'utf8');
51
+ }
52
+ catch {
53
+ return '';
54
+ }
55
+ }
56
+ export async function readBridgeContext(projectRoot = process.cwd()) {
57
+ const preferredConfig = await readText(path.join(projectRoot, '.jutell.json'));
58
+ const configText = preferredConfig || await readText(path.join(projectRoot, '.beginner-bridge.json'));
59
+ let parsed;
60
+ try {
61
+ parsed = JSON.parse(configText);
62
+ }
63
+ catch {
64
+ parsed = undefined;
65
+ }
66
+ const config = normalizeConfig(parsed);
67
+ const skillText = await readText(path.join(projectRoot, '.agents', 'skills', 'beginner-bridge', 'SKILL.md'));
68
+ const agentsExists = Boolean(await readText(path.join(projectRoot, 'AGENTS.md')));
69
+ const configExists = Boolean(configText);
70
+ const skillExists = Boolean(skillText);
71
+ return { projectRoot, config, configExists, skillExists, agentsExists, skillText };
72
+ }
@@ -0,0 +1,42 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { readBridgeContext } from './config/bridge-config.js';
4
+ import { activeFeatures, beginnerReportRules, bridgeStatus, reportPreferences, safeReportRequirements } from './tools/bridge-tools.js';
5
+ import { recordToolCall } from './tools/usage-counters.js';
6
+ const server = new McpServer({
7
+ name: 'JuTell',
8
+ version: '0.3.0',
9
+ }, {
10
+ instructions: 'JuTell by Ju0 is a local read-only report helper. Read only project configuration and approved report rules. Never access project code, Git diff, prompts, AI answers, secrets, or external networks. Skill mode remains available if this MCP server is disabled or unavailable. When both jutell and beginner_bridge servers are visible, prefer the canonical jutell server; use beginner_bridge only for compatibility. For owner-facing reports, apply the JuTell reporting guidance before composing the final answer.',
11
+ });
12
+ const noInput = { inputSchema: {} };
13
+ async function counted(toolName, run) {
14
+ const result = await run();
15
+ const characters = result.content.reduce((sum, item) => {
16
+ const text = item.text;
17
+ return sum + (typeof text === 'string' ? text.length : 0);
18
+ }, 0);
19
+ await recordToolCall(toolName, characters);
20
+ return result;
21
+ }
22
+ server.registerTool('get_bridge_status', { ...noInput, description: 'Return local JuTell configuration and availability status.' }, async () => counted('get_bridge_status', async () => {
23
+ const context = await readBridgeContext();
24
+ return { content: [{ type: 'text', text: JSON.stringify(bridgeStatus(context)) }] };
25
+ }));
26
+ server.registerTool('get_active_features', { ...noInput, description: 'Return active JuTell features and their safe omissions.' }, async () => counted('get_active_features', async () => {
27
+ const context = await readBridgeContext();
28
+ return { content: [{ type: 'text', text: JSON.stringify({ features: activeFeatures(context.config) }) }] };
29
+ }));
30
+ server.registerTool('get_report_preferences', { ...noInput, description: 'Return the current report Profile and limits.' }, async () => counted('get_report_preferences', async () => {
31
+ const context = await readBridgeContext();
32
+ return { content: [{ type: 'text', text: JSON.stringify(reportPreferences(context.config)) }] };
33
+ }));
34
+ server.registerTool('get_beginner_report_rules', { ...noInput, description: 'Return only the active report rules needed for the current project configuration.' }, async () => counted('get_beginner_report_rules', async () => {
35
+ const context = await readBridgeContext();
36
+ return { content: [{ type: 'text', text: JSON.stringify(beginnerReportRules(context.config)) }] };
37
+ }));
38
+ server.registerTool('get_safe_report_requirements', { ...noInput, description: 'Return information that must never be hidden from a final report.' }, async () => counted('get_safe_report_requirements', async () => {
39
+ return { content: [{ type: 'text', text: JSON.stringify(safeReportRequirements()) }] };
40
+ }));
41
+ const transport = new StdioServerTransport();
42
+ await server.connect(transport);
@@ -0,0 +1,64 @@
1
+ import { FEATURE_CATALOG, SAFETY_REQUIREMENTS } from './catalog.js';
2
+ const profileLabel = { minimal: '최소 보고', balanced: '균형 보고', learning: '학습 보고', detailed: '상세 보고' };
3
+ const reportLength = (value) => value <= 8 ? '짧음' : value <= 12 ? '보통' : '자세함';
4
+ export function parseSkillVersion(skillText) {
5
+ if (!skillText)
6
+ return undefined;
7
+ const match = skillText.match(/jutellSkillVersion\s*:\s*["']?([0-9A-Za-z.\-]+)/);
8
+ return match ? match[1] : undefined;
9
+ }
10
+ export function bridgeStatus(context) {
11
+ return {
12
+ configFileExists: context.configExists,
13
+ skillFileExists: context.skillExists,
14
+ agentsFileExists: context.agentsExists,
15
+ profile: context.config.profile,
16
+ activeFeatureCount: Object.values(context.config.features).filter(Boolean).length,
17
+ configVersion: context.config.version,
18
+ skillVersion: parseSkillVersion(context.skillText),
19
+ externalTransmission: false,
20
+ telemetryEnabled: false,
21
+ mcpEnabled: context.config.mcp.enabled,
22
+ };
23
+ }
24
+ export function activeFeatures(config) {
25
+ return Object.keys(FEATURE_CATALOG).map((id) => ({ id, name: FEATURE_CATALOG[id].label, active: config.features[id], omittedWhenOff: FEATURE_CATALOG[id].omitted, alwaysReported: FEATURE_CATALOG[id].forced }));
26
+ }
27
+ export function reportPreferences(config) {
28
+ return {
29
+ profile: config.profile,
30
+ profileName: profileLabel[config.profile],
31
+ mainFilesLevel: config.limits.maxMainFiles <= 3 ? '최소' : config.limits.maxMainFiles <= 5 ? '보통' : '많이',
32
+ glossaryLevel: config.limits.maxGlossaryTerms === 0 ? '사용 안 함' : config.limits.maxGlossaryTerms <= 1 ? '조금' : config.limits.maxGlossaryTerms <= 3 ? '보통' : '많이',
33
+ reportLengthLevel: reportLength(config.limits.compactReportMaxSentences),
34
+ limits: config.limits,
35
+ };
36
+ }
37
+ export const EXPLAINED_DIFF_SECTIONS = ['무엇을 바꿨나요?', '왜 바꿨나요?', '어디를 바꿨나요?', '실제 중요한 변경', '내가 직접 다듬고 싶다면?'];
38
+ const explainedDiffRule = {
39
+ when: '의미 있는 변경(기능 동작·화면 변화·데이터 처리 변화)을 보고할 때',
40
+ sections: EXPLAINED_DIFF_SECTIONS,
41
+ groupingRule: '관련된 파일과 변경은 기능 단위로 묶어 설명하고 전체 Diff 원문을 반복하지 않습니다.',
42
+ noEvidenceRules: {
43
+ why: '변경 이유의 근거가 없으면 추측하지 않고 "변경 이유는 Agent 결과에서 확인되지 않았습니다."로 표시합니다.',
44
+ customization: '문구·색상·여백·이동 경로처럼 직접 다듬을 수 있는 위치의 코드 근거가 없으면 "내가 직접 다듬고 싶다면?" 섹션을 만들지 않습니다.',
45
+ riskyArea: '인증·권한·결제·데이터베이스처럼 위험한 영역은 간단한 화면 다듬기 대상으로 제시하지 않고 주의가 필요하다고 표시합니다.',
46
+ },
47
+ };
48
+ export function beginnerReportRules(config) {
49
+ const active = Object.entries(config.features).filter(([, enabled]) => enabled).map(([id]) => FEATURE_CATALOG[id].label);
50
+ return {
51
+ language: '비개발자가 이해할 수 있는 쉬운 한국어',
52
+ activeReportSections: active,
53
+ limits: config.limits,
54
+ evidenceRule: '실제로 확인한 사실, 코드만 보고 예상한 내용, 확인하지 못한 내용을 구분합니다.',
55
+ statusRule: '검증 결과와 보고서 상태를 일치시킵니다.',
56
+ diffRule: '코드 또는 Diff를 사용자에게 보여줄 경우, 바로 뒤에 무엇을 수정했고 사용자에게 어떤 영향이 있는지 기능 단위로 설명합니다.',
57
+ ...(config.features.explainedDiff ? { explainedDiffRule } : {}),
58
+ safetyRequirements: SAFETY_REQUIREMENTS,
59
+ notCollected: ['프로젝트 코드', 'Git diff', 'Prompt', 'AI 답변 원문', '파일 경로', '비밀정보'],
60
+ };
61
+ }
62
+ export function safeReportRequirements() {
63
+ return { alwaysReport: SAFETY_REQUIREMENTS, note: '활성 Feature가 꺼져 있어도 위 정보는 숨기지 않습니다.' };
64
+ }
@@ -0,0 +1,25 @@
1
+ export const FEATURE_CATALOG = {
2
+ changeSummary: { label: '변경 요약', description: '무엇이 바뀌었는지 짧게 알려줍니다.', omitted: '일반 변경 요약', forced: '작업 실패와 범위 밖 변경' },
3
+ userVisibleChanges: { label: '사용자에게 보이는 변화', description: '화면이나 사용 방법이 어떻게 달라지는지 설명합니다.', omitted: '일반 화면 변화', forced: '중요한 안전 영향' },
4
+ internalChanges: { label: '프로그램 내부 변화', description: '화면 뒤에서 어떤 동작이 바뀌었는지 쉽게 설명합니다.', omitted: '일반 내부 동작 설명', forced: '데이터 손실·보안 영향' },
5
+ mainFiles: { label: '주요 파일 설명', description: '변경에 중요한 파일을 몇 개만 골라 역할을 설명합니다.', omitted: '일반 주요 파일 설명', forced: '사용자가 요청한 파일 설명' },
6
+ explainedDiff: { label: '설명형 변경 요약', description: '의미 있는 변경을 무엇을·왜·어디를·중요한 변경 순으로 묶어 설명합니다. 근거 없는 이유와 다듬기 위치는 만들지 않습니다.', omitted: '일반 변경 의미 설명', forced: '데이터 손실·보안 관련 중요 변경' },
7
+ glossary: { label: '개발 용어 설명', description: '필요한 개발 용어를 처음 나올 때 쉬운 말로 풀이합니다.', omitted: '선택적 용어 설명', forced: '안전 판단에 필요한 의미' },
8
+ validationResults: { label: '검증 결과', description: '실행한 테스트와 검사 결과를 알려줍니다.', omitted: '통과한 검증의 일반 설명', forced: '핵심 검증 실패와 보류 사유' },
9
+ riskAssessment: { label: '위험도 안내', description: '변경이 기존 기능에 미칠 수 있는 영향의 크기를 설명합니다.', omitted: '일반 위험도 설명', forced: '높은 위험·판정 불가' },
10
+ userActions: { label: '사용자 확인 안내', description: '사용자가 직접 확인하거나 결정할 일을 알려줍니다.', omitted: '일반 확인 안내', forced: '안전·데이터 손실 관련 행동' },
11
+ nextActionSuggestions: { label: '다음 행동 제안', description: '보고서 끝에 다음 행동 제안을 최대 3개 추가합니다.', omitted: '일반 다음 행동 제안', forced: '안전·데이터 손실 관련 행동' },
12
+ requestClarificationGuide: { label: '요청 명확화 질문', description: '요청이 모호할 때 결과가 크게 달라지는 항목을 작업 전에 확인합니다.', omitted: '모호한 요청에 대한 일반 확인', forced: '데이터 손실·보안 관련 확인' },
13
+ manualEditGuidance: { label: '직접 수정 안내', description: '사용자가 직접 파일을 고칠 때 위치와 주의점을 안내합니다.', omitted: '일반 직접 수정 안내', forced: '데이터 손실·보안 관련 수정 안내' },
14
+ requestBuilder: { label: '요청 만들기', description: '요청 템플릿 제공 안내를 포함합니다.', omitted: '템플릿 제공 안내', forced: '없음' },
15
+ };
16
+ export const SAFETY_REQUIREMENTS = [
17
+ '작업 실패',
18
+ '핵심 검증 실패',
19
+ '중요한 미확인 사항',
20
+ '높은 위험 또는 위험도 판정 불가',
21
+ '비밀정보 노출 위험',
22
+ '데이터 손실 가능성',
23
+ '요청 범위 밖 변경',
24
+ '작업 보류 사유',
25
+ ];
@@ -0,0 +1,97 @@
1
+ import { promises as fs, existsSync } from 'node:fs';
2
+ import { randomUUID } from 'node:crypto';
3
+ import path from 'node:path';
4
+ import { readBridgeContext } from '../config/bridge-config.js';
5
+ function stderrSummary(message) {
6
+ try {
7
+ process.stderr.write(`JuTell usage counters: ${message}\n`);
8
+ }
9
+ catch { /* 안전한 요약 실패도 무시 */ }
10
+ }
11
+ async function localDir(projectRoot) {
12
+ const preferred = path.join(projectRoot, '.jutell-local');
13
+ const legacy = path.join(projectRoot, '.beginner-bridge-local');
14
+ return existsSync(preferred) || !existsSync(legacy) ? preferred : legacy;
15
+ }
16
+ export async function resolveCountersFile(projectRoot) {
17
+ return path.join(await localDir(projectRoot), 'usage-counters.json');
18
+ }
19
+ async function exists(file) {
20
+ try {
21
+ await fs.access(file);
22
+ return true;
23
+ }
24
+ catch {
25
+ return false;
26
+ }
27
+ }
28
+ async function readCounters(file) {
29
+ if (!await exists(file))
30
+ return { state: 'missing', value: null };
31
+ try {
32
+ const parsed = JSON.parse(await fs.readFile(file, 'utf8'));
33
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
34
+ return { state: 'corrupt', value: null };
35
+ const value = parsed;
36
+ if (value.schemaVersion !== 1 || typeof value.enabled !== 'boolean' || typeof value.totalToolCalls !== 'number' || typeof value.tools !== 'object' || value.tools === null || Array.isArray(value.tools))
37
+ return { state: 'corrupt', value: null };
38
+ return { state: 'ok', value: parsed };
39
+ }
40
+ catch {
41
+ return { state: 'corrupt', value: null };
42
+ }
43
+ }
44
+ async function writeCounters(file, value) {
45
+ const directory = path.dirname(file);
46
+ for (let attempt = 0; attempt < 2; attempt += 1) {
47
+ try {
48
+ await fs.mkdir(directory, { recursive: true });
49
+ const tempFile = path.join(directory, `.usage-counters.${process.pid}.${randomUUID()}.tmp`);
50
+ await fs.writeFile(tempFile, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
51
+ try {
52
+ await fs.rename(tempFile, file);
53
+ }
54
+ catch (error) {
55
+ await fs.rm(tempFile, { force: true });
56
+ throw error;
57
+ }
58
+ return true;
59
+ }
60
+ catch { /* 재시도 */ }
61
+ }
62
+ return false;
63
+ }
64
+ const writeQueue = [];
65
+ export async function recordToolCall(toolName, responseCharacters, projectRoot = process.cwd()) {
66
+ try {
67
+ const context = await readBridgeContext(projectRoot);
68
+ if (!context.config.usageMeasurement.localCountersEnabled)
69
+ return;
70
+ const file = await resolveCountersFile(projectRoot);
71
+ const run = (writeQueue.at(-1) ?? Promise.resolve()).then(async () => {
72
+ const current = await readCounters(file);
73
+ if (current.state === 'corrupt')
74
+ throw new Error('corrupt counters file kept untouched');
75
+ const base = current.state === 'ok' ? structuredClone(current.value) : { schemaVersion: 1, enabled: true, updatedAt: null, totalToolCalls: 0, tools: {}, templateCopies: {} };
76
+ const tool = base.tools[toolName] ?? { calls: 0, responseCharacters: 0, lastCalledAt: null };
77
+ tool.calls += 1;
78
+ tool.responseCharacters += responseCharacters;
79
+ tool.lastCalledAt = new Date().toISOString();
80
+ base.tools[toolName] = tool;
81
+ base.totalToolCalls += 1;
82
+ base.updatedAt = new Date().toISOString();
83
+ if (!await writeCounters(file, base))
84
+ throw new Error('counters write failed');
85
+ });
86
+ writeQueue.push(run.catch(() => undefined));
87
+ try {
88
+ await run;
89
+ }
90
+ catch (error) {
91
+ stderrSummary(error instanceof Error ? error.message : 'record failed without affecting the tool response');
92
+ }
93
+ }
94
+ catch (error) {
95
+ stderrSummary(error instanceof Error ? error.message : 'record failed without affecting the tool response');
96
+ }
97
+ }
@@ -0,0 +1,135 @@
1
+ ---
2
+ name: beginner-bridge
3
+ jutellSkillVersion: "0.3.0"
4
+ schemaVersion: 1
5
+ description: JuTell by Ju0 creates concise, evidence-based work reports for non-developers, separating observed facts, code-based expectations, verification results, risks, and user actions. The legacy Skill ID is retained for compatibility.
6
+ ---
7
+
8
+ # JuTell by Ju0
9
+
10
+ Codex 작업 결과를 비개발자가 이해할 수 있는 하나의 보고서로 정리한다. 확인하지 않은 내용을 완료된 사실처럼 표현하지 않는다. 제품 목적은 AI가 만든 결과를 사람이 이해하고 운영하도록 돕는 것이다.
11
+
12
+ ## 적용 전 읽기
13
+
14
+ 필요한 문서만 progressive disclosure 방식으로 읽는다.
15
+
16
+ 1. `AGENTS.md`가 있으면 먼저 읽고 시스템·개발자·사용자 지침을 따른다.
17
+ 2. 제품 범위나 보고서 규칙이 필요하면 `docs/PRODUCT_SCOPE.md`와 `docs/BEGINNER_REPORT_SPEC.md`를 읽는다.
18
+ 3. 용어가 등장하면 `docs/GLOSSARY_POLICY.md`와 `references/glossary-ko.md`를 필요한 부분만 읽는다.
19
+ 4. 보고서 형식이 필요하면 `references/report-format.md`를 읽는다.
20
+ 5. 위험도 판단이 필요하면 `references/risk-level-guide.md`를 읽는다.
21
+ 6. V0.1 시나리오나 평가를 수행할 때만 `docs/TEST_SCENARIOS.md`를 읽는다.
22
+
23
+ 문서가 없거나 읽을 수 없으면 추측으로 보완하지 말고 그 사실을 보고한다.
24
+
25
+ ## 적용하지 않을 상황
26
+
27
+ 다음 경우에는 전체 작업 보고서 형식을 강제로 적용하지 않는다.
28
+
29
+ * 일반 개발 개념 질문
30
+ * 프로젝트와 관계없는 질문
31
+ * 단순 용어 뜻 질문
32
+ * 사용자가 다른 출력 형식을 명확히 요청한 경우
33
+
34
+ 사용자가 상세 보고를 생략해달라고 하면 최소 보고만 제공한다. 작업 완료 여부, 주요 수정 파일, 중요한 실패 또는 미확인 항목은 생략하지 않는다.
35
+
36
+ ## 설정 해석 절차
37
+
38
+ 실행 절차에 들어가기 전에 다음 순서로 로컬 설정을 해석한다.
39
+
40
+ 1. 프로젝트 루트에서 `.jutell.json`을 먼저 확인하고, 없으면 `.beginner-bridge.json`을 호환 경로로 확인한다.
41
+ 2. JSON, `version`, Profile, Feature ID, boolean 값과 limits를 검증한다.
42
+ 3. 선택한 Profile의 기본값을 적용한다. 파일이 없으면 `balanced`를 사용한다.
43
+ 4. 명시적으로 적힌 `features`와 `limits`를 Profile 기본값보다 우선 적용한다.
44
+ 5. 안전상 강제 보고 항목과 사용자가 현재 요청에서 요구한 형식을 적용한다.
45
+ 6. 이번 보고서에서 사용할 최종 활성 Feature와 limits를 확정한다.
46
+ 7. 활성 Feature만 사용해 보고서를 작성하되, 강제 보고 항목은 생략하지 않는다.
47
+
48
+ 설정 오류가 있으면 전체 설정을 추측해 고치지 않고 `balanced`로 진행한다. 오류가 있는 경우에만 설정 문제를 짧게 알리며 설정 파일 전체는 출력하지 않는다.
49
+
50
+ ## MCP 서버 선택
51
+
52
+ JuTell MCP가 보이면 canonical `jutell` 서버를 사용한다. `jutell`과 `beginner_bridge`가 모두 보이면 `jutell`을 우선하고 `beginner_bridge`는 호환용으로만 사용한다.
53
+
54
+ ## 실행 절차
55
+
56
+ 1. 소유자 대상 구현·보고 작업이면 최종 답변을 작성하기 전에 JuTell 보고 규칙(`get_beginner_report_rules` 등)을 먼저 확인해 적용한다.
57
+ 2. 사용자 요청, 작업 유형, 허용 범위와 금지 범위를 확인한다.
58
+ 3. 코드 변경 작업이면 가능한 범위에서 작업 시작 기준 상태를 기록한다.
59
+ * Git 저장소와 브랜치
60
+ * 기존 수정 파일과 추적되지 않은 파일
61
+ * 실행 가능한 테스트·빌드·검사 명령
62
+ * 브라우저 또는 실제 실행 가능 여부
63
+ 4. 기준 상태를 기록하지 못하면 기존 변경과 이번 변경을 임의로 섞지 않는다. Codex가 직접 수정한 사실이 명확한 파일만 이번 변경으로 표시하고 나머지는 출처 구분 불가로 표시한다.
64
+ 5. 실제 변경 파일과 내용을 확인한다. 파일명만으로 기능 역할이나 변경 의미를 확정하지 않는다.
65
+ 6. 주요 파일을 작업 규모에 맞게 선택한다. 단순 작업은 최대 3개, 일반 작업은 최대 5개를 우선 설명한다.
66
+ 7. 공식 문서나 프로젝트 설정에서 확인 가능한 검증 명령을 찾는다. 명령을 임의로 만들어 실행하지 않는다.
67
+ 8. 안전한 검증만 실행한다.
68
+ * 사용자가 실행을 금지한 명령은 실행하지 않는다.
69
+ * 비밀정보를 출력할 가능성이 있는 명령은 실행하지 않는다.
70
+ * 전체 환경변수, `.env`, 인증 헤더, 쿠키, 토큰, 연결 문자열을 그대로 출력하지 않는다.
71
+ * 도구 출력과 오류 로그를 사용자에게 보여주기 전에 민감한 값을 제거한다.
72
+ * 브라우저는 현재 Codex 환경에 이미 제공되고 사용자가 허용한 경우에만 선택적으로 사용한다.
73
+ 9. 검증 결과를 실제 실행 범위와 함께 기록한다. 실행하지 못한 검증을 통과했다고 쓰지 않는다.
74
+ 10. 네 가지 정보 체계를 분리한다.
75
+ * 근거 출처: 파일, Git, 명령, 브라우저 또는 실제 실행, 코드 예상, 사용자 제공 정보
76
+ * 확인 상태: 확인됨, 일부 확인, 확인하지 못함
77
+ * 사용자 행동: 사용자 확인 필요, 추가 테스트 권장, 설정 필요, 사용자 결정 필요
78
+ * 보고서 상태: 확인 완료, 추가 확인 필요, 일부 확인, 작업 보류, 범위 밖
79
+ 11. 위험도는 변경 영향도를 기준으로 판단한다. 검증 도구가 없다는 이유만으로 위험도를 판정 불가로 만들지 않는다. 여러 조건이 겹치면 가장 높은 위험도를 적용한다.
80
+ 12. 필요한 용어만 처음 등장할 때 설명한다. 기술 전용 용어는 해당 기술이 사용되는 것을 확인한 경우에만 설명한다. 기본 사전에 없는 용어는 추측하지 않는다.
81
+ 13. 활성 Feature와 `references/report-format.md`에 따라 하나의 비개발자용 최종 보고를 작성한다. `explainedDiff`가 활성이면 의미 있는 변경에 같은 근거로 설명형 변경 요약을 덧붙인다.
82
+ 14. `nextActionSuggestions`가 활성일 때 보고서 끝에 다음 행동 제안을 **최대 3개**만 추가한다. 다음 경우에만 제안한다.
83
+ * 사용자가 직접 확인해야 할 항목이 남은 경우
84
+ * 검증되지 않아 보류된 항목이 있는 경우
85
+ * 설정이 필요한 경우
86
+ * 데이터 손실·보안 위험이 확인된 경우
87
+ 제안은 보고서에서 이미 확인된 항목 중에서만 고르고, 추측이나 새 작업을 제안하지 않는다. 해당하지 않으면 생략한다.
88
+ 15. 제출 전 다음을 점검한다.
89
+ * 화면 변화와 내부 변화를 분리했는가
90
+ * 예상과 실제 확인을 구분했는가
91
+ * 근거가 중요한 주장과 일치하는가
92
+ * 검증 결과와 보고서 상태가 일치하는가
93
+ * 위험도 근거가 실제 변경과 일치하는가
94
+ * 주요 파일 수를 지켰는가
95
+ * 비밀정보가 보고서와 사용자에게 보인 출력에 없는가
96
+ * 중요한 미확인 사항과 사용자 행동을 표시했는가
97
+ * 보고서가 작업 규모에 비해 길지 않은가
98
+ * 최종 문장을 다시 읽고 깨진 글자, 의미 없는 문자열, 미완성 자리표시자, 문장 중간에 섞인 비정상 문자열이 없는지 확인했는가 (정상적인 코드·경로·명령어와 외국어 기술 용어는 오타로 판단하지 않으며, 자동 수정으로 기술 내용을 바꾸지 않는다)
99
+
100
+ 설정 상세와 Feature별 예외는 `docs/FEATURE_CONFIGURATION.md`와 `references/feature-registry.md`에서 확인한다.
101
+
102
+ ## 작업 유형별 출력
103
+
104
+ * 코드 변경 완료: 기본 6개 항목의 최종 보고를 작성한다.
105
+ * 변경 내용 설명: 코드를 수정하지 않고 현재 변경만 설명한다.
106
+ * 특정 코드·파일 설명: 전체 완료 보고서가 아니라 요청한 범위의 기능 블록과 파일 역할을 설명한다.
107
+ * 계획 요청: 예상 범위와 검증 계획을 설명하고 완료된 것처럼 쓰지 않는다.
108
+
109
+ ## 코드 또는 Diff 설명
110
+
111
+ 코드나 Diff 원문을 요청받았거나 실제로 사용자에게 보여준 경우에만 다음을 따른다.
112
+
113
+ * 코드·Diff 원문이 없으면 이 규칙을 강제하지 않는다.
114
+ * 모든 줄을 한 줄씩 해설하지 않고 기능 단위로 묶어 설명한다.
115
+ * 파일 역할을 한 문장으로 설명한다.
116
+ * 변경 전 문제, 변경한 내용, 사용자에게 보이는 변화를 구분해 설명한다.
117
+ * 수정 시 주의할 영향과 확인된 사실·예상을 구분한다.
118
+ * Diff가 매우 길면 핵심 구간만 설명하고 전체 원문을 반복하지 않는다.
119
+
120
+ ## 참고 문서
121
+
122
+ 세부 내용은 필요할 때만 직접 읽는다.
123
+
124
+ * `docs/PRODUCT_SCOPE.md` — 제품 목적과 V0.1 범위
125
+ * `docs/BEGINNER_REPORT_SPEC.md` — 공식 정보 체계, 검증과 보고서 상태 연결
126
+ * `docs/GLOSSARY_POLICY.md` — 용어 설명 정책
127
+ * `docs/TEST_SCENARIOS.md` — V0.1 시나리오와 평가 기준
128
+ * `docs/FEATURE_CONFIGURATION.md` — 로컬 Feature 설정, Profile과 우선순위
129
+ * `references/glossary-ko.md` — 핵심 용어와 문맥 주의사항
130
+ * `references/explained-diff-format.md` — 의미 있는 변경의 설명형 요약 형식
131
+ * `references/feature-registry.md` — Feature ID와 강제 보고 예외
132
+ * `references/report-format.md` — 보고서 형식
133
+ * `references/risk-level-guide.md` — 위험도 예시와 우선순위
134
+
135
+ V0.1 시나리오를 실제로 실행하지 않았다면 V0.1 통과를 선언하지 않는다.
@@ -0,0 +1,117 @@
1
+ # JuTell — Explained Diff Format Reference
2
+
3
+ `explainedDiff` Feature가 활성일 때, 의미 있는 변경을 비개발자가 이해할 수 있는 형식으로 설명하는 규칙이다. 기존 6개 보고 항목을 대체하지 않는다. 변경 요약·내부 변화·주요 파일 설명에 이미 모인 근거를 다시 사용해 설명을 덧붙이며, 별도의 보고 체계나 용어 체계를 만들지 않는다.
4
+
5
+ ## 1. 적용 조건
6
+
7
+ * `.jutell.json`의 `explainedDiff`가 켜져 있을 때만 적용한다.
8
+ * 모든 작업에 적용하지 않는다. 다음 중 하나에 해당하는 의미 있는 변경에만 사용한다.
9
+ * 기능 동작이 달라진 경우
10
+ * 사용자 화면이나 사용 방법이 달라진 경우
11
+ * 데이터 저장·처리 방식이 달라진 경우
12
+ * 문구 한 개, 색상 한 곳처럼 단순 작업은 기존 보고 형식만으로 충분하다.
13
+ * 코드나 Diff 원문 자체를 요청받아 설명할 때는 `report-format.md`의 코드·Diff 설명 규칙과 함께 이 형식을 사용한다.
14
+
15
+ ## 2. 출력 형식
16
+
17
+ ```md
18
+ [무엇을 바꿨나요?]
19
+ <비개발자 언어로 한두 문장>
20
+
21
+ [왜 바꿨나요?]
22
+ <확인된 변경 이유 또는 아래 3번 문구>
23
+
24
+ [어디를 바꿨나요?]
25
+ - `<파일>` — <역할 한 문장>
26
+
27
+ [실제 중요한 변경]
28
+ - <기능 단위로 묶은 중요한 변경>
29
+ ```
30
+
31
+ `내가 직접 다듬고 싶다면?` 항목은 4번 조건을 만족할 때만 마지막에 추가한다.
32
+
33
+ ## 3. 환각 방지 규칙
34
+
35
+ 근거 없는 내용을 확인된 사실처럼 쓰지 않는다. 이것이 이 형식의 최우선 규칙이다.
36
+
37
+ ### 3.1 왜 바꿨나요?
38
+
39
+ 변경 이유는 다음 근거로만 작성한다.
40
+
41
+ * Agent 작업 결과에 기록된 변경 목적
42
+ * 사용자 요청 문장
43
+ * 코드 비교로 직접 확인한 변경 전후 차이
44
+
45
+ 위 근거가 없으면 추측하지 않고 다음처럼 표시한다.
46
+
47
+ > 변경 이유는 Agent 결과에서 확인되지 않았습니다.
48
+
49
+ 파일 이름만으로 변경 이유를 만들어내지 않는다.
50
+
51
+ ### 3.2 무엇을 바꿨나요?와 실제 중요한 변경
52
+
53
+ * 실제 읽거나 비교한 변경 범위 안에서만 작성한다.
54
+ * 파일명만으로 파일 역할이나 기능 효과를 단정하지 않는다.
55
+ * 실행하지 않은 동작은 "예상"으로 구분한다.
56
+
57
+ ### 3.3 어디를 바꿨나요?
58
+
59
+ * `mainFiles`와 같은 주요 파일 선정 규칙을 따른다. 단순 작업 최대 3개, 일반 작업 최대 5개.
60
+ * 관련된 파일은 하나의 논리적 변경으로 묶어 함께 표시한다.
61
+
62
+ ## 4. 내가 직접 다듬고 싶다면?
63
+
64
+ 일반 코딩 안내가 아니라, 이번 변경과 연결된 위치만 알려준다. 다음처럼 코드에서 직접 확인한 단순 수정 지점에만 제시한다.
65
+
66
+ | 대상 | 예시 |
67
+ |---|---|
68
+ | 버튼·표시 문구 | 해당 컴포넌트의 라벨 문자열 |
69
+ | 색상 | 해당 스타일 값 또는 디자인 변수 |
70
+ | 여백·간격 | 해당 화면의 간격 값 |
71
+ | 이동 경로 | 연결된 페이지 경로 또는 링크 |
72
+ | 간단한 화면 동작 | 해당 컴포넌트의 표시 조건 |
73
+
74
+ 규칙:
75
+
76
+ * 코드 근거가 없으면 이 항목을 만들지 않는다. 억지로 채우지 않는다.
77
+ * 인증, 권한, 결제, 데이터베이스 처리 같은 영역은 간단한 다듬기 대상으로 제시하지 않는다. 대신 "이 부분은 간단한 화면 수정 대상이 아니라 주의가 필요한 변경입니다."처럼 표시한다.
78
+ * TypeScript 문법 강의, React 줄별 해설처럼 구현 세부를 가르치지 않는다.
79
+
80
+ ## 5. 묶기와 길이
81
+
82
+ * 관련된 변경은 기능 단위로 묶는다. 파일마다 다섯 항목을 반복하지 않는다.
83
+ * 전체 Diff 원문을 다시 출력하지 않는다. 중요한 변경 블록만 요약한다.
84
+ * [실제 중요한 변경]은 확인된 핵심 동작 위주로 작성하고, 같은 내용을 다른 보고 항목에서 반복하지 않는다.
85
+ * 기존 보고서 길이 규칙(`compactReportMaxSentences`, 작업 규모별 길이)이 우선한다. 설명을 붙인다고 길이 제한을 무시하지 않는다.
86
+
87
+ ## 6. 예시
88
+
89
+ 합성 예시이며 실제 프로젝트가 아니다.
90
+
91
+ ```md
92
+ [무엇을 바꿨나요?]
93
+ 검색 버튼이 빈 입력 상태에서도 눌리던 문제를 고쳤습니다.
94
+
95
+ [왜 바꿨나요?]
96
+ 빈 검색어로 실행되면 결과 없는 화면만 보여줬기 때문입니다.
97
+
98
+ [어디를 바꿨나요?]
99
+ - `src/features/search/SearchBar.tsx` — 검색창 입력과 버튼을 담당하는 파일
100
+
101
+ [실제 중요한 변경]
102
+ - 검색어가 비어 있으면 검색 버튼이 실행되지 않음
103
+ - 검색어를 다시 입력하면 버튼이 원래대로 활성화됨
104
+ ```
105
+
106
+ 코드에서 버튼 문구 위치를 직접 확인했다면 마지막에 다음을 추가한다.
107
+
108
+ ```md
109
+ [내가 직접 다듬고 싶다면?]
110
+ 버튼 문구는 `SearchBar.tsx`의 버튼 라벨 부분에서 바꿀 수 있습니다.
111
+ ```
112
+
113
+ 변경 이유를 확인할 근거가 없었다면 `[왜 바꿨나요?]`에는 "변경 이유는 Agent 결과에서 확인되지 않았습니다."라고만 쓴다.
114
+
115
+ ## 7. 용어 설명
116
+
117
+ 설명에 필요한 개발 용어는 `glossary` Feature와 `glossary-ko.md` 규칙을 그대로 사용한다. 이 형식을 위해 별도의 용어 체계를 만들지 않는다.
@@ -0,0 +1,29 @@
1
+ # JuTell Feature Registry
2
+
3
+ 로컬 설정을 해석할 때 빠르게 확인하는 짧은 reference다. 기본값은 `balanced` 기준으로 모두 켬이다.
4
+
5
+ | ID | 기본값 | 꺼졌을 때 생략하는 정보 | 꺼져도 보고할 예외 | 관련 limits |
6
+ |---|---|---|---|---|
7
+ | `changeSummary` | 켬 | 일반 변경 요약 | 작업 실패·범위 밖 변경 | 없음 |
8
+ | `userVisibleChanges` | 켬 | 일반 화면·사용 방법 변화 | 중요한 안전 영향 | 없음 |
9
+ | `internalChanges` | 켬 | 일반 내부 동작 설명 | 데이터 손실·보안·범위 밖 영향 | 없음 |
10
+ | `mainFiles` | 켬 | 주요 파일 역할 설명 | 사용자가 요청한 파일 설명 | `maxMainFiles` |
11
+ | `explainedDiff` | 켬 | 일반 변경 의미 설명 | 데이터 손실·보안 관련 중요 변경 | 없음 |
12
+ | `glossary` | 켬 | 선택적 용어 괄호 설명 | 안전 판단에 필요한 의미 | `maxGlossaryTerms` |
13
+ | `validationResults` | 켬 | 통과한 검증의 일반 설명 | 핵심 검증 실패·작업 보류 | 없음 |
14
+ | `riskAssessment` | 켬 | 일반 위험도 설명 | 높은 위험·판정 불가·비밀정보 위험 | 없음 |
15
+ | `userActions` | 켬 | 일반 확인·추가 테스트 안내 | 안전·데이터 손실 관련 행동 | 없음 |
16
+ | `nextActionSuggestions` | 켬 | 일반 다음 행동 제안 | 안전·데이터 손실 관련 행동 | 없음 |
17
+ | `requestClarificationGuide` | 켬 | 모호한 요청에 대한 일반 확인 | 데이터 손실·보안 관련 확인 | 없음 |
18
+ | `manualEditGuidance` | 켬 | 일반 직접 수정 안내 | 데이터 손실·보안 관련 수정 안내 | 없음 |
19
+ | `requestBuilder` | 켬 | 템플릿 제공 안내 | 없음 | 없음 |
20
+
21
+ ## 적용 순서
22
+
23
+ 1. 안전상 강제되는 정보 확인
24
+ 2. 사용자 요청 형식 확인
25
+ 3. 명시적 Feature와 limits 적용
26
+ 4. 선택한 Profile 기본값 적용
27
+ 5. 나머지는 `balanced` 기본값 적용
28
+
29
+ 설정 오류는 추측으로 보정하지 않고 `balanced`로 진행한다. 설정 파일 전체를 최종 보고서에 출력하지 않는다.