careermate 0.0.4 → 0.0.6

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 (124) hide show
  1. package/AGENTS.md +11 -7
  2. package/INSTALL.md +18 -16
  3. package/README.md +28 -26
  4. package/THIRD_PARTY_NOTICES.md +2 -3
  5. package/bin/careermate.mjs +8 -1
  6. package/dist/career-os/README.md +8 -3
  7. package/dist/career-os/eop/career-description.md +136 -0
  8. package/dist/career-os/eop/cover-letter.md +7 -5
  9. package/dist/career-os/eop/fit-analysis.md +15 -5
  10. package/dist/career-os/eop/interview-prep.md +209 -21
  11. package/dist/career-os/eop/job-analysis.md +5 -3
  12. package/dist/career-os/eop/profile-extraction.md +5 -4
  13. package/dist/career-os/knowledge/cover-letter.md +4 -4
  14. package/dist/career-os/knowledge/fit-matching.md +8 -8
  15. package/dist/career-os/knowledge/interview-behavioral.md +76 -8
  16. package/dist/career-os/knowledge/interview-technical.md +106 -4
  17. package/dist/career-os/knowledge/linkedin-profile.md +1 -1
  18. package/dist/career-os/knowledge/networking-referrals.md +1 -1
  19. package/dist/career-os/knowledge/offer-evaluation-decision.md +10 -10
  20. package/dist/career-os/knowledge/portfolio.md +3 -3
  21. package/dist/career-os/knowledge/verifiers/ats-compat.md +8 -4
  22. package/dist/career-os/knowledge/verifiers/human-voice.md +2 -2
  23. package/dist/career-os/knowledge/verifiers/recency-staleness.md +1 -1
  24. package/dist/career-os/knowledge/verifiers/responsiveness-on-target.md +2 -2
  25. package/dist/career-os/knowledge/verifiers/truthfulness.md +1 -1
  26. package/dist/doctor.mjs +1019 -127
  27. package/dist/mcp.mjs +1036 -126
  28. package/dist/migrate.mjs +52 -1
  29. package/dist/public/app.js +42 -16
  30. package/dist/public/i18n/en/applications.js +23 -0
  31. package/dist/public/i18n/en/common.js +22 -0
  32. package/dist/public/i18n/en/documents.js +133 -0
  33. package/dist/public/i18n/en/home.js +75 -0
  34. package/dist/public/i18n/en/interview.js +69 -0
  35. package/dist/public/i18n/en/jobs.js +152 -0
  36. package/dist/public/i18n/en/nav.js +18 -0
  37. package/dist/public/i18n/en/profile.js +201 -0
  38. package/dist/public/i18n/en/settings.js +127 -0
  39. package/dist/public/i18n/en/status.js +22 -0
  40. package/dist/public/i18n/en.js +17 -0
  41. package/dist/public/i18n/ko/applications.js +23 -0
  42. package/dist/public/i18n/ko/common.js +22 -0
  43. package/dist/public/i18n/ko/documents.js +133 -0
  44. package/dist/public/i18n/ko/home.js +63 -0
  45. package/dist/public/i18n/ko/interview.js +69 -0
  46. package/dist/public/i18n/ko/jobs.js +152 -0
  47. package/dist/public/i18n/ko/nav.js +18 -0
  48. package/dist/public/i18n/ko/profile.js +201 -0
  49. package/dist/public/i18n/ko/settings.js +127 -0
  50. package/dist/public/i18n/ko/status.js +23 -0
  51. package/dist/public/i18n/ko.js +18 -0
  52. package/dist/public/i18n.js +55 -0
  53. package/dist/public/index.html +1 -0
  54. package/dist/public/lib.js +25 -17
  55. package/dist/public/pages/applications.js +17 -21
  56. package/dist/public/pages/documents.js +153 -107
  57. package/dist/public/pages/home.js +88 -53
  58. package/dist/public/pages/interview.js +105 -51
  59. package/dist/public/pages/jobs.js +330 -106
  60. package/dist/public/pages/profile.js +157 -152
  61. package/dist/public/pages/settings.js +270 -72
  62. package/dist/public/styles.css +38 -3
  63. package/dist/web.mjs +859 -103
  64. package/docs/FAQ.md +26 -5
  65. package/docs/MCP_TOOLS.md +26 -12
  66. package/docs/START_WORKFLOW.md +2 -2
  67. package/docs/SUPPORTED_AI_APPS.md +13 -0
  68. package/docs/WORKFLOWS.md +80 -23
  69. package/docs/career-os/README.md +8 -3
  70. package/docs/career-os/eop/career-description.md +136 -0
  71. package/docs/career-os/eop/cover-letter.md +7 -5
  72. package/docs/career-os/eop/fit-analysis.md +15 -5
  73. package/docs/career-os/eop/interview-prep.md +209 -21
  74. package/docs/career-os/eop/job-analysis.md +5 -3
  75. package/docs/career-os/eop/profile-extraction.md +5 -4
  76. package/docs/career-os/knowledge/cover-letter.md +4 -4
  77. package/docs/career-os/knowledge/fit-matching.md +8 -8
  78. package/docs/career-os/knowledge/interview-behavioral.md +76 -8
  79. package/docs/career-os/knowledge/interview-technical.md +106 -4
  80. package/docs/career-os/knowledge/linkedin-profile.md +1 -1
  81. package/docs/career-os/knowledge/networking-referrals.md +1 -1
  82. package/docs/career-os/knowledge/offer-evaluation-decision.md +10 -10
  83. package/docs/career-os/knowledge/portfolio.md +3 -3
  84. package/docs/career-os/knowledge/verifiers/ats-compat.md +8 -4
  85. package/docs/career-os/knowledge/verifiers/human-voice.md +2 -2
  86. package/docs/career-os/knowledge/verifiers/recency-staleness.md +1 -1
  87. package/docs/career-os/knowledge/verifiers/responsiveness-on-target.md +2 -2
  88. package/docs/career-os/knowledge/verifiers/truthfulness.md +1 -1
  89. package/package.json +2 -2
  90. package/packages/core/src/onboarding.ts +2 -1
  91. package/packages/core/src/services.ts +138 -21
  92. package/packages/core/src/summary.ts +59 -1
  93. package/packages/core/src/verify/lint.ts +4 -0
  94. package/packages/db/src/index.ts +1 -0
  95. package/packages/db/src/repositories.ts +0 -0
  96. package/packages/db/src/schema.ts +17 -0
  97. package/packages/exporters/src/index.ts +28 -17
  98. package/packages/knowledge/src/index.ts +46 -7
  99. package/packages/mcp-tools/src/bridge.ts +194 -13
  100. package/packages/mcp-tools/src/tools.ts +390 -38
  101. package/packages/mcp-tools/src/update.ts +31 -16
  102. package/packages/parsers/src/documents.ts +12 -1
  103. package/packages/prompts/src/humanize.ts +46 -0
  104. package/packages/prompts/src/index.ts +5 -1
  105. package/packages/prompts/src/install.ts +4 -4
  106. package/packages/prompts/src/onboarding.ts +4 -4
  107. package/packages/prompts/src/system.ts +17 -8
  108. package/packages/shared/src/enums.ts +14 -4
  109. package/packages/shared/src/schemas.ts +41 -0
  110. package/packages/workflows/src/definitions.ts +47 -10
  111. package/scripts/init.ts +284 -13
  112. package/dist/site/analytics.js +0 -97
  113. package/dist/site/favicon.svg +0 -1
  114. package/dist/site/fonts/PretendardVariable.woff2 +0 -0
  115. package/dist/site/icon-180.png +0 -0
  116. package/dist/site/icon-512.png +0 -0
  117. package/dist/site/index.html +0 -565
  118. package/dist/site/landing.js +0 -54
  119. package/dist/site/llms-install.txt +0 -218
  120. package/dist/site/robots.txt +0 -4
  121. package/dist/site/sitemap.xml +0 -21
  122. package/dist/site/start.html +0 -353
  123. package/dist/site/style.css +0 -559
  124. package/dist/site/vercel.json +0 -20
@@ -193,17 +193,34 @@ export function getUpdateStatus(): UpdateStatus {
193
193
  }
194
194
 
195
195
  /**
196
- * 즉시(바운드) 업데이트 확인. 캐시가 만료면 레지스트리를 한 번 await하고 캐시를 갱신한다.
197
- * doctor·check_for_update처럼 "지금 확인" 의미가 분명한 일회성 호출에서 사용.
196
+ * 즉시(바운드) 업데이트 확인. doctor·check_for_update처럼 "지금 확인" 의미가 분명한
197
+ * 일회성 호출에서 사용.
198
+ *
199
+ * `force=true`("지금 확인")면 신선한 24h 캐시여도 무시하고 레지스트리를 새로 읽는다 —
200
+ * 캐시가 직전 릴리스(예: 0.0.3) 시점에 채워진 채 새 버전(0.0.4)이 나오면, 설치본보다
201
+ * 오래된 `latest`를 돌려주던 버그를 막는다. 강제 조회가 네트워크 오류로 실패하면 멀쩡한
202
+ * 기존 캐시를 null로 덮지 않고(비차단 신호 getUpdateStatus 보호) "확인 실패"로만 보고한다.
203
+ *
204
+ * `force=false`(기본, 만료 재확인)면 기존대로 캐시를 존중하고, 실패는 짧은 TTL의 null로
205
+ * 기록해 잦은 재시도를 막는다.
198
206
  */
199
- export async function getUpdateStatusAsync(): Promise<UpdateStatus> {
207
+ export async function getUpdateStatusAsync(force = false): Promise<UpdateStatus> {
200
208
  if (checkDisabled()) return buildStatus(null);
201
- let cache = readCache();
202
- if (cacheExpired(cache)) {
203
- cache = { checkedAt: Date.now(), latest: await fetchLatest() };
204
- writeCache(cache);
209
+ const cache = readCache();
210
+ if (!force && !cacheExpired(cache)) return buildStatus(cache);
211
+
212
+ const latest = await fetchLatest();
213
+ if (latest !== null) {
214
+ const fresh = { checkedAt: Date.now(), latest };
215
+ writeCache(fresh);
216
+ return buildStatus(fresh);
205
217
  }
206
- return buildStatus(cache);
218
+ // fetch 실패. 강제("지금 확인")는 정직하게 실패로 보고하되 멀쩡한 기존 캐시는 보존한다.
219
+ if (force) return buildStatus(null);
220
+ // 비강제(만료 재확인)는 기존처럼 null을 짧은 TTL로 기록해 잦은 재시도를 막는다.
221
+ const failed = { checkedAt: Date.now(), latest: null };
222
+ writeCache(failed);
223
+ return buildStatus(failed);
207
224
  }
208
225
 
209
226
  export interface UpdateResult {
@@ -288,13 +305,11 @@ export async function runSelfUpdate(): Promise<UpdateResult> {
288
305
  manual: manualCommand(),
289
306
  };
290
307
  }
291
- // 최신 버전: 신선한 캐시가 있으면 재사용하고, 없으면 한 번만 조회한다(레지스트리 중복 호출 방지).
292
- let cache = readCache();
293
- if (cacheExpired(cache)) {
294
- cache = { checkedAt: Date.now(), latest: await fetchLatest() };
295
- writeCache(cache);
296
- }
297
- const latest = cache?.latest ?? null;
308
+ // 최신 버전: "업데이트해줘"는 명시적 사용자 행동이라 신선한 캐시여도 새로 조회한다 —
309
+ // 직전 릴리스 시점에 채워진 캐시가 'already latest'로 오판해 실제 업데이트를 건너뛰지 않게.
310
+ // 실패(null)면 아래 가드가 안내로 빠지며, 멀쩡한 기존 캐시는 덮지 않는다.
311
+ const latest = await fetchLatest();
312
+ if (latest !== null) writeCache({ checkedAt: Date.now(), latest });
298
313
  // 최신 버전을 확인하지 못함(오프라인/레지스트리 오류): 무엇을 설치할지 모른 채 실행 중인
299
314
  // 설치를 덮어쓰면 반파 위험이 있으므로 시도하지 않고 안내한다.
300
315
  if (latest === null) {
@@ -325,6 +340,6 @@ export async function runSelfUpdate(): Promise<UpdateResult> {
325
340
  ok: true,
326
341
  from,
327
342
  to: latest,
328
- message: `careermate를 ${latest ? `v${latest}` : '최신 버전'}(으)로 업데이트했습니다. 적용하려면 AI 앱(또는 MCP 연결)을 재시작하세요. 재시작 시 데이터베이스는 자동으로 마이그레이션되며 기존 데이터는 보존됩니다.`,
343
+ message: `CareerMate를 ${latest ? `v${latest}` : '최신 버전'}(으)로 업데이트했습니다. 적용하려면 AI 앱(또는 MCP 연결)을 재시작하세요. 재시작 시 데이터베이스는 자동으로 마이그레이션되며 기존 데이터는 보존됩니다.`,
329
344
  };
330
345
  }
@@ -124,7 +124,18 @@ export async function extractDocument(input: string, baseDir?: string): Promise<
124
124
  });
125
125
 
126
126
  // Decline secrets/credentials before touching the file (see SENSITIVE_PATH_RE).
127
- if (SENSITIVE_PATH_RE.test(filePath)) {
127
+ // Check the literal path AND the fully-resolved real path: a benign-named
128
+ // symlink (e.g. inbox/resume.pdf → ~/.ssh/id_rsa) would otherwise slip the
129
+ // deny-list and leak the target's contents. realpathSync follows every link
130
+ // and `..` segment to the final on-disk target. It throws for a
131
+ // missing/broken path — that's fine, statSync below reports it cleanly.
132
+ let realPath = filePath;
133
+ try {
134
+ realPath = fs.realpathSync.native(filePath);
135
+ } catch {
136
+ /* not yet resolvable — leave realPath as the literal path */
137
+ }
138
+ if (SENSITIVE_PATH_RE.test(filePath) || SENSITIVE_PATH_RE.test(realPath)) {
128
139
  return base('unsupported', {
129
140
  warnings: [
130
141
  '보안상 민감할 수 있는 파일(개인 키·자격증명 등)은 읽지 않습니다. 이력서·경력기술서·자기소개서 같은 문서 파일을 지정해 주세요.',
@@ -16,6 +16,8 @@ export const HUMANIZE_WRITING_GUIDE = `# 사람이 쓴 것처럼: 자기소개
16
16
  목표: 사실과 경험은 100% 그대로 두면서, "AI가 쓴 티"가 나는 문장 습관만 제거한다.
17
17
  검열기를 속이는 게 아니라, 채용 담당자가 읽기에 자연스럽고 사람 냄새 나는 글을 쓰는 것이 목적이다.
18
18
 
19
+ > **언어 범위:** 아래 AI-tell 목록은 한국어 글 기준이다. 영어 등 다른 언어로 쓸 때는 이 목록을 그대로 적용하지 말고, **같은 원리**(클리셰·기계적 병렬·과장 수식·공허한 일반론 제거, 구체성, 사실·수치 보존)를 그 언어의 자연스러운 관습으로 적용한다. 결과물의 언어는 대상 공고·시장을 따른다.
20
+
19
21
  ## 절대 원칙
20
22
  - 저장된 실제 경험·수치·회사명·고유명사·직접 인용은 절대 바꾸거나 지어내지 않는다. 문장 결만 다듬는다.
21
23
  - 빈칸이나 확인이 필요한 부분은 매끄럽게 채워 넣지 말고 그대로 표시해 사용자에게 묻는다.
@@ -57,3 +59,47 @@ export const HUMANIZE_WRITING_GUIDE = `# 사람이 쓴 것처럼: 자기소개
57
59
  - 소리 내어 읽었을 때 어색하거나 "사람이 이렇게 말 안 하는데" 싶은 곳을 다듬는다.
58
60
  - 사실·수치·고유명사가 원본과 100% 일치하는가?
59
61
  - 사용자의 톤과 강조 포인트가 살아 있는가?`;
62
+
63
+ /**
64
+ * STRUCTURED_DOC_WRITING_GUIDE — 경력기술서·이력서·포트폴리오처럼 채용 담당자가
65
+ * '읽지 않고 스캔'하는 구조형 문서용 보정 지침.
66
+ *
67
+ * 근거: 글쓰기 지침 A/B 실험(2026-06, 90 에이전트·블라인드 강제선택 100%)에서
68
+ * 전체지침본은 사람다움·종합점수를 크게 올렸지만 '구성(structure)' 점수만 유일하게
69
+ * 떨어졌다(Δ-1.8). 원인은 자기소개서용 산문화 압력(AI-tell #3 기계적 병렬·#8 형식 과용)이
70
+ * 불릿·번호 성과가 정석인 경력기술서에까지 적용돼 스캔성을 깎은 것. 이 보정은 구조형
71
+ * 문서에서 '구조는 살리고 문장 언어만 사람답게'로 압력을 재조정한다.
72
+ */
73
+ export const STRUCTURED_DOC_WRITING_GUIDE = `# 구조형 문서(경력기술서·이력서·포트폴리오) 보정
74
+
75
+ 경력기술서·이력서·포트폴리오는 채용 담당자가 처음부터 끝까지 읽는 글이 아니라 몇 초 안에 "스캔"하는 문서다. 불릿, 번호 매긴 성과, 직무·기간·역할을 구분한 섹션 구조는 AI가 쓴 티가 아니라 이 문서의 정석이다. 자기소개서처럼 줄글(산문)로 풀지 말고 구조를 보존한다.
76
+
77
+ ## 위 8개 AI-tell 중 구조형에서 다르게 적용하는 것
78
+ - 3번(기계적 병렬)·8번(형식 과용)은 산문 문장에만 적용한다. 성과를 나열하는 불릿·번호 목록 구조 자체에는 적용하지 않는다 — 불릿로 끊어 쓰는 것은 가독성을 위한 정상 구조다.
79
+ - 단 의미 없는 "첫째·둘째·셋째" 서수 나열, 모든 불릿을 똑같은 골격("~을 통해 ~을 함")으로 찍어내는 것, 과한 굵게·이모지·가운뎃점 떡칠은 구조형에서도 제거한다.
80
+
81
+ ## 구조는 살리고, 불릿 '안의 언어'만 사람답게
82
+ - 각 불릿은 동사나 수치로 시작하고, 검증 가능한 사실(숫자·기간·도구·역할·결과)을 한 줄에 담는다.
83
+ - 불릿 안의 번역투·클리셰·공허한 일반론·수동태는 자기소개서와 똑같이 제거한다. (예: "분석을 진행하였습니다" → "분석했다", "많은 성과를 냈습니다" → 실제 수치)
84
+ - STAR을 한두 줄로 압축한다: 무엇을 왜 했고 결과 수치가 어떻게 됐는지.
85
+
86
+ ## Do / Don't
87
+ - Do: 두괄식 불릿(결과·수치를 앞세움), 직무·기간·역할 구분, 성과를 숫자로.
88
+ - Don't: 경력기술서를 자기소개서처럼 줄글로 평탄화하지 않는다. 불릿을 없애 스캔하기 어렵게 만들지 않는다. 라벨·구조를 다 걷어내 가독성을 떨어뜨리지 않는다.
89
+
90
+ 핵심: 자기소개서는 사람의 목소리(산문)로, 경력기술서·이력서는 사람의 언어로 쓴 깔끔한 구조로. 둘 다 사실·수치·고유명사는 100% 보존한다.`;
91
+
92
+ /** get_writing_style_guide가 구조 보존 보정을 적용할 문서 유형. */
93
+ const STRUCTURED_DOC_TYPES = new Set(['career_description', 'resume', 'portfolio']);
94
+
95
+ /**
96
+ * doc_type에 맞는 글쓰기 가이드를 돌려준다.
97
+ * 자기소개서·자기 PR·지원 메일 등 산문형은 HUMANIZE_WRITING_GUIDE만,
98
+ * 경력기술서·이력서·포트폴리오 등 구조형은 거기에 구조 보존 보정을 덧붙인다.
99
+ */
100
+ export function getWritingStyleGuide(docType?: string): string {
101
+ if (docType && STRUCTURED_DOC_TYPES.has(docType)) {
102
+ return `${HUMANIZE_WRITING_GUIDE}\n\n${STRUCTURED_DOC_WRITING_GUIDE}`;
103
+ }
104
+ return HUMANIZE_WRITING_GUIDE;
105
+ }
@@ -11,7 +11,11 @@ export {
11
11
  INSTALL_PROMPT_GENERIC,
12
12
  } from './install.ts';
13
13
  export { ONBOARDING_PROMPT } from './onboarding.ts';
14
- export { HUMANIZE_WRITING_GUIDE } from './humanize.ts';
14
+ export {
15
+ HUMANIZE_WRITING_GUIDE,
16
+ STRUCTURED_DOC_WRITING_GUIDE,
17
+ getWritingStyleGuide,
18
+ } from './humanize.ts';
15
19
 
16
20
  import { CAREERMATE_SYSTEM_PROMPT } from './system.ts';
17
21
  import {
@@ -16,7 +16,7 @@ export const INSTALL_PROMPT_CLAUDE = `Claude야, 아래 CareerMate 설치 페이
16
16
  4. 그다음 나에게 이력서나 자기소개서 파일을 업로드하라고 안내해줘.
17
17
  5. 내가 올린 파일을 바탕으로 내 프로필을 구조화해서 \`save_profile\`로 CareerMate에 저장하고, 온보딩을 시작해줘.
18
18
 
19
- 기술 용어는 최대한 빼고, 매 단계마다 지금 뭘 하고 있는지 쉬운 한국어로 알려줘. 막히는 부분이 있으면 추측하지 말고 나에게 물어봐줘.`;
19
+ 기술 용어는 최대한 빼고, 매 단계마다 지금 뭘 하고 있는지 내가 쓰는 언어로 쉽게 알려줘. 막히는 부분이 있으면 추측하지 말고 나에게 물어봐줘.`;
20
20
 
21
21
  export const INSTALL_PROMPT_CHATGPT = `ChatGPT야, 아래 페이지를 읽고 내 컴퓨터에 CareerMate를 설치하고 설정해줘. 설치가 끝나면 연결 상태를 확인하고, 나에게 이력서나 자기소개서 파일을 업로드하라고 안내해줘. 그다음 내 프로필을 구조화해서 CareerMate에 저장해줘.
22
22
 
@@ -27,7 +27,7 @@ export const INSTALL_PROMPT_CHATGPT = `ChatGPT야, 아래 페이지를 읽고
27
27
  4. 이력서·자기소개서 파일을 업로드하라고 나에게 안내해줘.
28
28
  5. 올린 파일로 내 프로필을 정리해서 \`save_profile\`로 저장하고 온보딩을 시작해줘.
29
29
 
30
- 진행 상황은 항상 쉬운 한국어로 설명하고, 확실하지 않은 정보는 지어내지 말고 나에게 물어봐줘.`;
30
+ 진행 상황은 항상 내가 쓰는 언어로 쉽게 설명하고, 확실하지 않은 정보는 지어내지 말고 나에게 물어봐줘.`;
31
31
 
32
32
  export const INSTALL_PROMPT_GENERIC = `안녕, 아래 CareerMate 설치 안내(설치 페이지 또는 INSTALL.md)를 읽고 내 컴퓨터에 CareerMate를 설치하고 설정해줘.
33
33
 
@@ -36,8 +36,8 @@ CareerMate는 내 커리어 데이터(프로필·이력서·자기소개서·지
36
36
  이렇게 진행해줘.
37
37
  1. 설치 안내 문서를 끝까지 읽고 그대로 따라 설치해.
38
38
  2. 네가 쓰는 AI 도구에 CareerMate MCP 서버를 연결(설정)해줘.
39
- 3. 연결 후 \`get_onboarding_status\`를 호출해 연결 상태를 확인하고, 결과를 쉬운 한국어로 알려줘.
39
+ 3. 연결 후 \`get_onboarding_status\`를 호출해 연결 상태를 확인하고, 결과를 내가 쓰는 언어로 쉽게 알려줘.
40
40
  4. 나에게 이력서나 자기소개서 파일을 업로드하라고 안내해줘.
41
41
  5. 올린 파일로 내 프로필을 구조화해서 \`save_profile\`로 저장하고 온보딩을 시작해줘.
42
42
 
43
- 매 단계마다 쉬운 한국어로 설명하고, 불확실한 내용은 지어내지 말고 나에게 확인해줘.`;
43
+ 매 단계마다 내가 쓰는 언어로 쉽게 설명하고, 불확실한 내용은 지어내지 말고 나에게 확인해줘.`;
@@ -6,14 +6,14 @@
6
6
  * the dashboard. Korean-first.
7
7
  */
8
8
 
9
- export const ONBOARDING_PROMPT = `이제 CareerMate에 연결되었습니다. 사용자의 커리어 데이터 첫 셋업(온보딩)을 도와주세요.
9
+ export const ONBOARDING_PROMPT = `이제 CareerMate에 연결되었습니다. 사용자의 커리어 데이터 첫 셋업(온보딩)을 도와주세요. 사용자가 쓰는 언어로 안내하세요 — 이 안내문이 한국어라도 사용자가 다른 언어로 말하면 그 언어로 답합니다.
10
10
 
11
11
  # 1단계 — 현재 상태 확인
12
12
  - 먼저 \`get_onboarding_status\`를 호출하세요. 결과의 has_profile / has_resume / has_cover_letter / has_experience / has_skills / has_job, profile_completeness(완성도), next_steps(다음 단계)를 확인합니다.
13
- - 사용자에게 "지금까지 등록된 정보"와 "앞으로 채우면 좋은 정보"를 쉬운 한국어로 짧게 안내하세요. (이미 채워진 항목은 다시 묻지 마세요.)
13
+ - 사용자에게 "지금까지 등록된 정보"와 "앞으로 채우면 좋은 정보"를 사용자의 언어로 쉽고 짧게 안내하세요. (이미 채워진 항목은 다시 묻지 마세요.)
14
14
 
15
15
  # 2단계 — 프로필 수집 및 저장
16
- - 사용자가 올린 이력서/자기소개서 파일이나 사용자가 직접 알려준 내용을 바탕으로 프로필을 구조화하세요.
16
+ - 사용자가 올린 이력서/경력기술서/포트폴리오/자기소개서 파일이나 사용자가 직접 알려준 내용을 바탕으로 프로필을 구조화하세요.
17
17
  - 이름, 연락처, 한 줄 소개(headline), 자기소개 요약(summary), 희망 직무(desired_roles), 희망 근무 조건(desired_conditions)을 정리합니다.
18
18
  - 글쓰기 선호도 함께 물어보세요: 선호 문체(preferred_tone, 예: "담백하고 구체적")와 강조하고 싶은 핵심 포인트(emphasis_points). 이 정보는 이후 자기소개서 작성에 그대로 활용됩니다.
19
19
  - 정리한 내용을 \`save_profile\`로 저장하세요. 파일에서 읽어온 정보는 source를 'upload', 사용자가 말로 알려준 정보는 'manual'로 표시하세요.
@@ -32,4 +32,4 @@ export const ONBOARDING_PROMPT = `이제 CareerMate에 연결되었습니다.
32
32
  - 다시 \`get_onboarding_status\`로 완성도를 확인하고, 비어 있는 부분이 있으면 채우자고 제안하세요.
33
33
  - 마무리로 다음 행동을 제안하세요. 예: "지원하고 싶은 공고가 있으면 링크나 내용을 붙여주세요. 적합도를 분석해드릴게요."
34
34
 
35
- 진행 내내 기술 용어(MCP, job_id 등)는 빼고, 지금 무엇을 하고 있는지 쉬운 한국어로 알려주세요.`;
35
+ 진행 내내 기술 용어(MCP, job_id 등)는 빼고, 지금 무엇을 하고 있는지 사용자의 언어로 쉽게 알려주세요.`;
@@ -20,7 +20,7 @@ CareerMate는 사용자의 컴퓨터에서 동작하는 로컬 커리어 관리
20
20
  - 기억에 의존하지 말고, 매 작업 시작 시 실제 데이터를 다시 불러오세요.
21
21
 
22
22
  2. 결과는 반드시 다시 저장합니다.
23
- - 분석·작성한 내용은 머릿속이나 대화창에만 남기지 말고 해당 저장 툴로 CareerMate에 기록하세요: 공고는 \`save_job_posting\`, 적합도 분석은 \`save_fit_analysis\`, 자기소개서 버전은 \`save_cover_letter_version\`, 면접 준비는 \`save_interview_prep\`, 상태 변경은 \`update_application_status\`.
23
+ - 분석·작성한 내용은 머릿속이나 대화창에만 남기지 말고 해당 저장 툴로 CareerMate에 기록하세요: 공고는 \`save_job_posting\`, 적합도 분석은 \`save_fit_analysis\`, 자기소개서 버전은 \`save_cover_letter_version\`, 경력기술서는 \`add_resume\`(kind=career_description), 면접 준비는 \`save_interview_prep\`, 상태 변경은 \`update_application_status\`.
24
24
  - 저장 후에는 무엇이 저장되었는지 사용자에게 간단히 알려주세요.
25
25
 
26
26
  3. 데이터를 지어내지 않습니다.
@@ -32,23 +32,32 @@ CareerMate는 사용자의 컴퓨터에서 동작하는 로컬 커리어 관리
32
32
  - 민감 정보(연락처, 주소 등)는 꼭 필요한 작업에만 사용하고 불필요하게 노출하지 마세요.
33
33
 
34
34
  5. 자기소개서는 작성 전 확인을 받습니다.
35
- - 자기소개서를 새로 쓰거나 크게 고치기 전에, 어떤 공고를 대상으로 어떤 톤과 강조점으로 쓸지 사용자에게 먼저 확인하세요.
35
+ - 자기소개서나 경력기술서를 새로 쓰거나 크게 고치기 전에, 어떤 공고/직무를 대상으로 어떤 톤과 강조점으로 쓸지 사용자에게 먼저 확인하세요.
36
36
  - 작성 후에는 \`save_cover_letter_version\`으로 버전을 저장하고, 변경 요약을 \`note\`에 남기세요. 사용자가 원하면 \`export_cover_letter\`로 내보내세요.
37
37
 
38
- 6. 사용자에게는 쉬운 한국어로 알립니다.
38
+ 6. 사용자에게는 사용자의 언어로 쉽게 알립니다.
39
+ - **응답 언어 = 사용자의 언어.** 사용자가 한국어로 말하면 한국어로, 영어 등 다른 언어로 말하면 그 언어로 답합니다. 이 지침·도구 응답·플레이북이 한국어로 적혀 있어도 그대로 복사하지 말고 사용자 언어로 옮겨 전달하세요(사실·수치·고유명사는 보존).
39
40
  - "MCP", "툴 호출", "job_id" 같은 기술 용어는 사용자에게 직접 노출하지 마세요. "공고를 저장했어요", "적합도를 분석했어요"처럼 일상적인 표현으로 진행 상황을 알리세요.
41
+ - 도구 응답에 \`user_message\`가 있으면 사용자에게 보고할 때 그 내용을 우선 활용하되, 사용자가 한국어가 아닌 언어로 대화하면 한국어 원문을 그대로 복사하지 말고 사용자 언어로 옮겨 전달하세요. \`id\`, \`job_id\`, \`cover_letter_id\`, \`workflow_id\`, JSON 원문, 도구 이름은 내부 작업용으로만 쓰고 사용자에게 그대로 복사하지 마세요.
42
+ - 도구 응답에 \`suggested_next_action\`이 있으면 다음 행동을 강요하지 말고 선택지로 제안하세요.
40
43
  - 무엇을 했고, 무엇을 알게 되었고, 다음에 무엇을 할 수 있는지 짧고 명확하게 전달하세요.
41
44
 
42
45
  7. 다음 단계를 먼저 제안합니다.
43
- - 작업이 끝나면 자연스러운 다음 행동을 제안하세요. 예: 공고 분석 후 → "이 공고에 맞춘 자기소개서를 써드릴까요?", 자기소개서 저장 후 → "지원 상태를 '지원 완료'로 바꿀까요?", 상태가 '서류 합격'이 되면 → "면접 준비를 도와드릴까요?".
46
+ - 작업이 끝나면 자연스러운 다음 행동을 제안하세요. 예: 공고 분석 후 → "이 공고에 맞춘 자기소개서를 써드릴까요?", 경력기술서 저장 후 → "이 공고에 맞춰 자기소개서도 정리할까요?", 자기소개서 저장 후 → "지원 상태를 '지원 완료'로 바꿀까요?", 상태가 '서류 합격'이 되면 → "면접 준비를 도와드릴까요?".
47
+ - 공고 분석 후에는 곧장 자기소개서를 쓰지 말고, 반드시 선택지를 보여주세요: "1. 네, 이 공고에 맞춘 자기소개서를 써줘" / "2. 아니요, 분석만 저장해줘". 사용자가 1번을 고른 경우에만 자기소개서 작성 흐름으로 이어갑니다.
44
48
  - \`get_onboarding_status\`의 next_steps나 \`open_dashboard\`/\`open_application\`을 활용해 사용자를 다음 행동으로 안내하세요.
45
49
 
46
- 8. 글은 AI 티 안 나게 씁니다.
50
+ 8. 업데이트 요청은 명확하게 처리합니다.
51
+ - 사용자가 "최신이야?", "업데이트 있어?"처럼 확인만 요청하면 \`check_for_update\`를 호출하고 결과를 쉬운 말로 알려주세요.
52
+ - 사용자가 "업데이트해줘", "CareerMate 업데이트해줘", "최신으로 올려줘"처럼 적용을 명시하면 \`update_careermate\`를 바로 호출하세요. 이 도구가 최신 버전 확인도 함께 합니다.
53
+ - 업데이트 성공 후에는 적용하려면 AI 앱 또는 MCP 연결을 재시작해야 한다고 안내하세요. 사용자 커리어 데이터는 로컬에 보존되고, npm에는 공개 버전 정보 확인/설치 요청만 나갑니다.
54
+
55
+ 9. 글은 AI 티 안 나게 씁니다.
47
56
  - 자기소개서·자기 PR·지원 메일 등 사람이 쓴 듯한 글을 작성하기 직전에는 \`get_writing_style_guide\`를 호출해 글쓰기 규칙을 가져오고 그대로 적용하세요.
48
- - 번역투, "열정을 가지고/귀사의 발전에 기여" 같은 클리셰, 기계적 병렬(첫째·둘째·셋째), 상투적 연결어, 균일한 문장 리듬을 피하고 담백하고 구체적으로 씁니다. 단, 사실·수치·고유명사는 절대 바꾸거나 지어내지 않습니다.
57
+ - 번역투, "열정을 가지고/귀사의 발전에 기여" 같은 클리셰, 기계적 병렬(첫째·둘째·셋째), 상투적 연결어, 균일한 문장 리듬을 피하고 담백하고 구체적으로 씁니다. 단, 사실·수치·고유명사는 절대 바꾸거나 지어내지 않습니다. 이 AI-tell 목록은 한국어 글 기준이며, 다른 언어로 쓸 때는 같은 원리를 그 언어의 자연스러운 관습으로 적용합니다(산출물 언어·관행은 대상 공고·시장을 따름).
49
58
 
50
- 9. 전문가 절차로 만들고, 저장 전 당신이 자가검증합니다.
51
- - 공고 분석·자소서·면접 자료처럼 사용자 대면 결과를 만들기 전에는, 즉답하지 말고 \`get_workflow_guide\`(workflow_id 지정)를 먼저 호출해 그 작업의 전문가 실행 절차(EOP)와 적용할 플레이북·검증기 순서를 받으세요. 이어 \`get_playbook\`으로 도메인 지식을 받아 작성하고, save로 저장하기 직전 \`get_verifier\`로 검증 루브릭을 받아 **당신이 직접** 점검·수정한 뒤 저장합니다.
59
+ 10. 전문가 절차로 만들고, 저장 전 당신이 자가검증합니다.
60
+ - 공고 분석·자소서·경력기술서·면접 자료처럼 사용자 대면 결과를 만들기 전에는, 즉답하지 말고 \`get_workflow_guide\`(workflow_id 지정)를 먼저 호출해 그 작업의 전문가 실행 절차(EOP)와 적용할 플레이북·검증기 순서를 받으세요. 이어 \`get_playbook\`으로 도메인 지식을 받아 작성하고, save로 저장하기 직전 \`get_verifier\`로 검증 루브릭을 받아 **당신이 직접** 점검·수정한 뒤 저장합니다.
52
61
  - CareerMate는 슬롭 밀도·키워드 매칭처럼 셀 수 있는 항목만 보조로 돕고, 의미 판단(이 글이 질문에 답하는가·톤이 맞는가)은 당신이 합니다. 사실·수치·고유명사는 그대로 보존합니다.
53
62
 
54
63
  # 작업 방식 요약
@@ -45,7 +45,7 @@ export const APPLICATION_BOARD_ORDER: ApplicationStatus[] = [
45
45
  'rejected',
46
46
  ];
47
47
 
48
- /** Reaching this status unlocks interview-prep prompting in the workflow. */
48
+ /** Reaching this status makes interview prep a recommended next step. */
49
49
  export const INTERVIEW_UNLOCK_STATUSES: ApplicationStatus[] = [
50
50
  'document_passed',
51
51
  'interview',
@@ -61,9 +61,9 @@ export const INTERVIEW_UNLOCK_STATUSES: ApplicationStatus[] = [
61
61
  * backward among active stages, be parked (`on_hold`) or closed (`rejected`) from
62
62
  * anywhere, and be reopened/resumed from those terminal states. The one move we
63
63
  * forbid is regressing an *active* application back to `draft` ("untracked"),
64
- * which would silently drop its lifecycle history. The hard spec invariant
65
- * (interview prep requires `document_passed`+) is enforced separately at the
66
- * service layer via {@link INTERVIEW_UNLOCK_STATUSES}.
64
+ * which would silently drop its lifecycle history. Interview prep can be saved
65
+ * whenever the user asks for it; {@link INTERVIEW_UNLOCK_STATUSES} only controls
66
+ * when it is recommended as a timely next step.
67
67
  */
68
68
  export const ALLOWED_STATUS_TRANSITIONS: Record<ApplicationStatus, ApplicationStatus[]> = {
69
69
  draft: ['planned', 'applied', 'document_passed', 'interview', 'final_passed', 'rejected', 'on_hold'],
@@ -131,6 +131,16 @@ export const ACTIVITY_TYPES = [
131
131
  ] as const;
132
132
  export type ActivityType = (typeof ACTIVITY_TYPES)[number];
133
133
 
134
+ /** Per-job timeline entries shown on the job detail screen. */
135
+ export const APPLICATION_TIMELINE_TYPES = [
136
+ 'job_saved',
137
+ 'fit_analysis_saved',
138
+ 'cover_letter_version_saved',
139
+ 'application_status_changed',
140
+ 'interview_prep_saved',
141
+ ] as const;
142
+ export type ApplicationTimelineType = (typeof APPLICATION_TIMELINE_TYPES)[number];
143
+
134
144
  /** Entity kinds referenced by activities and deep links. */
135
145
  export const ENTITY_TYPES = [
136
146
  'profile',
@@ -11,6 +11,7 @@
11
11
  import { z } from 'zod';
12
12
  import {
13
13
  APPLICATION_STATUSES,
14
+ APPLICATION_TIMELINE_TYPES,
14
15
  ACTIVITY_TYPES,
15
16
  CONTENT_SOURCES,
16
17
  DOCUMENT_KINDS,
@@ -402,11 +403,21 @@ export const ApplicationInputSchema = z.object({
402
403
  });
403
404
  export type ApplicationInput = z.infer<typeof ApplicationInputSchema>;
404
405
 
406
+ export const ApplicationSubmissionSchema = z.object({
407
+ submitted_at: optLine.describe('실제 제출일 YYYY-MM-DD'),
408
+ channel: optLine.describe('제출 채널(원티드/사람인/회사 채용페이지 등)'),
409
+ cover_letter_id: optLine,
410
+ cover_letter_version_id: optLine,
411
+ document_ids: strList.optional().describe('제출한 이력서·경력기술서·포트폴리오 문서 ID 목록'),
412
+ });
413
+ export type ApplicationSubmission = z.infer<typeof ApplicationSubmissionSchema>;
414
+
405
415
  export const ApplicationStatusUpdateSchema = z.object({
406
416
  job_id: optLine,
407
417
  application_id: optLine,
408
418
  status: z.enum(APPLICATION_STATUSES),
409
419
  note: optNote.describe('상태 변경 사유/메모'),
420
+ submission: ApplicationSubmissionSchema.optional().describe('지원 완료 기록에 남길 제출 자료 스냅샷'),
410
421
  });
411
422
  export type ApplicationStatusUpdate = z.infer<typeof ApplicationStatusUpdateSchema>;
412
423
 
@@ -432,8 +443,16 @@ export const StarGuideSchema = z.object({
432
443
  });
433
444
  export type StarGuide = z.infer<typeof StarGuideSchema>;
434
445
 
446
+ /** Interview round/track a question belongs to (drives the 기술/인성·컬처핏 tabs). */
447
+ export const INTERVIEW_CATEGORIES = ['technical', 'behavioral'] as const;
448
+ export type InterviewCategory = (typeof INTERVIEW_CATEGORIES)[number];
449
+
435
450
  export const InterviewQuestionSchema = z.object({
436
451
  question: z.string().max(MAX_NOTE),
452
+ category: z
453
+ .enum(INTERVIEW_CATEGORIES)
454
+ .optional()
455
+ .describe("질문 갈래: 'technical'(기술·직무 1차) | 'behavioral'(인성·컬처핏·임원 2차, 자기소개서 기반). 생략 시 기술로 간주."),
437
456
  intent: z.string().max(MAX_NOTE).optional().describe('질문 의도'),
438
457
  followups: strList.optional().describe('예상 꼬리 질문'),
439
458
  answer_outline: optBody.describe('답변 가이드/핵심 포인트'),
@@ -471,6 +490,28 @@ export const ActivityRecordSchema = z.object({
471
490
  });
472
491
  export type ActivityRecord = z.infer<typeof ActivityRecordSchema>;
473
492
 
493
+ export const ApplicationTimelineInputSchema = z.object({
494
+ job_id: reqLine,
495
+ type: z.enum(APPLICATION_TIMELINE_TYPES),
496
+ title: reqLine,
497
+ summary: optNote,
498
+ payload: z.record(z.string(), z.unknown()).optional(),
499
+ occurred_at: optLine,
500
+ });
501
+ export type ApplicationTimelineInput = z.infer<typeof ApplicationTimelineInputSchema>;
502
+
503
+ export const ApplicationTimelineRecordSchema = z.object({
504
+ id,
505
+ job_id: z.string(),
506
+ type: z.enum(APPLICATION_TIMELINE_TYPES),
507
+ title: z.string(),
508
+ summary: z.string().nullable(),
509
+ payload: z.record(z.string(), z.unknown()),
510
+ occurred_at: isoDateTime,
511
+ created_at: isoDateTime,
512
+ });
513
+ export type ApplicationTimelineRecord = z.infer<typeof ApplicationTimelineRecordSchema>;
514
+
474
515
  /* ----------------------------------------------------- Aggregates / context */
475
516
 
476
517
  export const OnboardingStatusSchema = z.object({
@@ -3,7 +3,8 @@
3
3
  *
4
4
  * Step-by-step workflows the AI should follow, encoded as DATA + helper text.
5
5
  * Each step references concrete MCP tool names so the AI knows exactly which
6
- * tool to call. Korean-first (read by the AI and surfaced to users).
6
+ * tool to call. Authored in Korean, but language-adaptive: the AI surfaces
7
+ * these to users in the user's own language (see AGENTS.md output-language rule).
7
8
  */
8
9
 
9
10
  export interface WorkflowDefinition {
@@ -29,7 +30,7 @@ export const WORKFLOWS: WorkflowDefinition[] = [
29
30
  '사용자가 처음 CareerMate에 연결했거나, "시작하자 / 셋업해줘 / 프로필 등록"이라고 요청할 때.',
30
31
  steps: [
31
32
  '`get_onboarding_status`를 호출해 현재 등록 상태(has_profile, has_resume, has_cover_letter, has_experience, has_skills, profile_completeness, next_steps)를 확인한다.',
32
- '비어 있는 항목을 사용자에게 쉬운 한국어로 안내하고, 먼저 "기존에 작성해 둔 이력서·경력기술서 파일이 있는지" 묻는다(hwp/docx/pdf 모두 가능).',
33
+ '비어 있는 항목을 사용자의 언어로 쉽게 안내하고, 먼저 "기존에 작성해 둔 이력서·경력기술서·자기소개서 파일이 있는지" 묻는다(hwp/docx/pdf 모두 가능).',
33
34
  '파일 입력 분기: ① 사용자가 "파일이 있다"고 하면 `open_inbox`로 인입 폴더를 열어 파일을 넣게 하고, "다 넣었다"고 하면 `read_inbox`로 본문을 읽는다(pdf·이미지는 read_inbox가 경로만 주므로 클라이언트의 파일 읽기로 직접 읽는다). ② 사용자가 파일 경로를 직접 알려주면 `read_document`로 읽는다. ③ "없다/직접 입력/텍스트로 붙여넣기"면 폴더를 열지 말고 받은 텍스트를 그대로 쓴다. (묻지도 않고 open_inbox로 폴더를 자동으로 열지 않는다.)',
34
35
  '수집한 정보를 구조화해 `save_profile`로 저장한다(이름·연락처·headline·summary·desired_roles·desired_conditions, 그리고 글쓰기 선호인 preferred_tone과 emphasis_points 포함). 파일 출처는 source=upload, 직접 입력은 manual.',
35
36
  '업로드한 이력서/경력기술서/포트폴리오 본문을 `add_resume`로 저장한다(kind·title 지정, 대표 문서는 is_primary=true).',
@@ -52,9 +53,9 @@ export const WORKFLOWS: WorkflowDefinition[] = [
52
53
  '공고 요구사항과 사용자 경력/스킬/프로젝트를 비교한다: 일치하는 강점(strengths)과 부족한 부분(gaps), 매칭 키워드(matched_keywords)와 누락 키워드(missing_keywords)를 도출하고, 종합 적합도 점수(score 0~100)와 요약(summary), 지원 전략 제안(recommendations)을 정리한다. 없는 경험을 지어내지 않는다.',
53
54
  '아직 저장되지 않은 공고라면 `save_job_posting`으로 공고를 저장하고 job_id를 확보한다.',
54
55
  '도출한 분석 결과를 `save_fit_analysis`로 저장한다(job_id, score, summary, strengths, gaps, matched_keywords, missing_keywords, recommendations).',
55
- '분석 결과를 사용자에게 쉬운 한국어로 설명한다(잘 맞는 점, 보완할 점, 추천 전략). 기술 용어는 빼고 전달한다.',
56
- '"이 공고에 맞춘 자기소개서를 써드릴까요?"라고 자기소개서 작성을 제안한다.',
57
- '사용자가 동의하면 자기소개서를 작성하기 직전에 `get_writing_style_guide`를 호출해 "AI 티 안 나는 글쓰기 규칙"을 가져온다.',
56
+ '분석 결과를 사용자의 언어로 쉽게 설명한다(잘 맞는 점, 보완할 점, 추천 전략). 기술 용어는 빼고 전달한다.',
57
+ '분석 뒤 곧장 자기소개서를 쓰지 말고, "이 공고에 맞춘 자기소개서를 써드릴까요?"라고 묻고 선택지를 제시한다: ① "네, 이 공고에 맞춘 자기소개서를 써줘" ② "아니요, 분석만 저장해줘".',
58
+ '사용자가 ①을 선택하면 자기소개서를 작성하기 직전에 `get_writing_style_guide`를 호출해 "AI 티 안 나는 글쓰기 규칙"을 가져온다. ②를 선택하면 자소서 작성 없이 멈추고 저장 결과와 대시보드 확인만 안내한다.',
58
59
  '적합도 분석·글쓰기 선호와 글쓰기 규칙을 함께 반영해 자기소개서를 작성하고(번역투·클리셰·기계적 병렬 제거, 사실·수치는 그대로), `save_cover_letter_version`으로 저장한다(job_id 연결, note에 작성 의도 요약).',
59
60
  '`open_application`으로 해당 지원 건을 열어 사용자가 결과를 확인하게 하고, 지원 상태 변경(예: 지원 예정/지원 완료)을 제안한다.',
60
61
  ],
@@ -73,7 +74,26 @@ export const WORKFLOWS: WorkflowDefinition[] = [
73
74
  '저장된 실제 경험·성과만으로 자기소개서를 작성하되, 위 글쓰기 규칙을 적용해 사람이 쓴 듯 자연스럽게 쓴다(사실·수치·고유명사는 그대로). 빈칸이나 확인이 필요한 부분은 추측하지 말고 표시해 사용자에게 묻는다.',
74
75
  '완성본을 `save_cover_letter_version`으로 저장한다(기존 자기소개서면 cover_letter_id, 새로 만들면 title, 공고용이면 job_id, 변경 요약은 note, source=ai).',
75
76
  '사용자가 원하면 `export_cover_letter`로 파일로 내보내고, 추가 수정 요청이 있으면 새 버전으로 다시 저장한다.',
76
- '저장 결과를 쉬운 한국어로 알리고, 지원 상태 업데이트(`update_application_status`)나 면접 준비 등 다음 단계를 제안한다.',
77
+ '저장 결과를 사용자의 언어로 알리고, 지원 상태 업데이트(`update_application_status`)나 면접 준비 등 다음 단계를 제안한다.',
78
+ ],
79
+ },
80
+ {
81
+ id: 'write_career_description',
82
+ title: '경력기술서 작성',
83
+ description:
84
+ '프로필·이력서·경력·프로젝트·스킬을 근거로 실제 제출 가능한 경력기술서를 작성하고 문서함에 저장하는 흐름.',
85
+ trigger:
86
+ '사용자가 "경력기술서 써줘 / 경력기술서 만들어줘 / 내 경력 정리해줘"라고 요청할 때.',
87
+ steps: [
88
+ '`get_onboarding_status`로 준비 상태를 확인하고, `get_profile`·`get_resumes`·`get_experiences`·`get_projects`·`get_skills`를 호출해 기존 프로필, 이력서, 경력기술서(kind=career_description), 경력, 프로젝트, 기술스택을 모두 확인한다.',
89
+ '사용자가 파일 경로를 주면 `read_document`로 읽고, 인입 폴더를 사용했다면 `read_inbox`로 본문을 읽는다. 이미 저장된 자료와 새 파일을 비교해 중복·상충되는 사실은 표시하고 추측하지 않는다.',
90
+ '`get_workflow_guide({ workflow_id: "write_career_description" })`가 안내하는 전문가 절차를 따른다. 작성 직전 `get_playbook({ domain: "resume" })`와 `get_playbook({ domain: "ats" })`를 받아 실제 경력기술서 형식과 ATS 친화성을 적용한다.',
91
+ '작성 전에 목적을 확인한다: 범용 마스터본인지, 특정 직무/공고용인지, 신입/무경력이라 프로젝트·교육·활동 중심 역량기술서로 쓸지, 분량과 톤은 어떻게 할지 묻는다.',
92
+ '경력기술서는 자기소개서처럼 서사로 쓰지 않는다. 제목, 핵심 요약, 핵심 역량, 경력/프로젝트별 역할·기간·도구·문제·행동·성과를 표나 불릿 중심으로 정리한다. 수치·회사명·기간·직책은 저장된 사실만 쓰고, 없는 성과 수치는 만들지 않는다.',
93
+ '무경력/비개발자라면 경력을 날조하지 말고 교육·프로젝트·운영 경험·업무 도구·협업 경험·성과 증거 중심의 "역량기술서"로 구성한다.',
94
+ '저장 직전 `get_verifier({ id: "truthfulness" })`, `get_verifier({ id: "consistency" })`, `get_verifier({ id: "ats-compat" })`를 받아 당신이 직접 점검하고, 허위·상충·키워드 누락을 고친다. 확인이 필요한 사실은 본문에 표시하고 사용자에게 묻는다.',
95
+ '완성본을 `add_resume`으로 저장한다(kind=career_description, source=ai, title은 사용 목적이 드러나게 작성, tags에 직무/용도 키워드 포함). 대표 문서로 삼아도 되는지 확인된 경우에만 is_primary=true를 쓴다.',
96
+ '저장 결과를 사용자의 언어로 알리고, 문서 탭에서 확인할 수 있음을 안내한다. 특정 공고용으로 작성했다면 이어서 자기소개서나 면접 준비를 제안한다.',
77
97
  ],
78
98
  },
79
99
  {
@@ -86,7 +106,7 @@ export const WORKFLOWS: WorkflowDefinition[] = [
86
106
  steps: [
87
107
  '필요하면 `get_application_context` 또는 `get_job_posting`으로 대상 지원 건과 공고를 확인한다.',
88
108
  '적절한 상태로 `update_application_status`를 호출한다. 8단계: draft(작성 중), planned(지원 예정), applied(지원 완료), document_passed(서류 합격), interview(면접 진행), final_passed(최종 합격), rejected(불합격), on_hold(보류). 변경 사유는 note에 남긴다.',
89
- '변경 결과를 사용자에게 쉬운 한국어로 알린다.',
109
+ '변경 결과를 사용자의 언어로 쉽게 알린다.',
90
110
  '상태에 맞는 다음 단계를 제안한다: applied → 결과 기다리기 안내, document_passed/interview → 면접 준비(prepare_interview) 제안, final_passed → 축하 및 마무리, rejected/on_hold → 회고 또는 다른 공고 탐색 제안.',
91
111
  '필요하면 `open_application`으로 해당 지원 건을 열어 보여준다.',
92
112
  ],
@@ -95,15 +115,32 @@ export const WORKFLOWS: WorkflowDefinition[] = [
95
115
  id: 'prepare_interview',
96
116
  title: '면접 준비',
97
117
  description:
98
- '서류 합격 이후 예상 질문·STAR 답변·1분 자기소개를 준비해 저장하는 흐름.',
118
+ '공고 기준 예상 질문·STAR 답변·1분 자기소개를 준비해 저장하는 흐름.',
99
119
  trigger:
100
- '지원 상태가 서류 합격(document_passed) 또는 면접 진행(interview)으로 바뀌었을 때, 또는 사용자가 "면접 준비 도와줘"라고 요청할 때.',
120
+ '사용자가 "면접 준비 도와줘"라고 요청하거나, 지원 상태가 서류 합격(document_passed) 또는 면접 진행(interview)으로 바뀌었을 때.',
101
121
  steps: [
102
122
  '`get_application_context`(job_id 포함)를 호출해 공고, 프로필, 경력/프로젝트, 적합도 분석(강점·갭), 자기소개서를 가져온다.',
103
123
  '공고와 사용자 경험을 바탕으로 예상 면접 질문(question, intent, followups, answer_outline)을 도출한다.',
104
124
  '핵심 경험에 대한 STAR 가이드(question, situation, task, action, result)와 1분 자기소개(self_introduction) 초안을 작성한다. 실제 경험만 사용한다.',
105
125
  '준비 내용을 `save_interview_prep`로 저장한다(job_id, questions, star_guides, self_introduction, notes).',
106
- '준비 내용을 사용자에게 쉬운 한국어로 요약해 전달하고, 모의 면접/추가 질문 연습이나 상태 업데이트(`update_application_status`로 interview/final_passed)를 제안한다.',
126
+ '준비 내용을 사용자의 언어로 쉽게 요약해 전달하고, 모의 면접/추가 질문 연습이나 상태 업데이트(`update_application_status`로 interview/final_passed)를 제안한다.',
127
+ ],
128
+ },
129
+ {
130
+ id: 'build_personal_brand',
131
+ title: '퍼스널 브랜드 (LinkedIn·포트폴리오)',
132
+ description:
133
+ 'LinkedIn 프로필과 포트폴리오 같은 자기표현 자산을 직무에 맞춰 작성·최적화하는 흐름. 이력서 임포트(온보딩)와 분리된 별도 작업.',
134
+ trigger:
135
+ '사용자가 "링크드인 프로필 봐줘 / 포트폴리오 정리해줘 / 퍼스널 브랜딩 도와줘"라고 요청할 때.',
136
+ steps: [
137
+ '`get_application_context`(가능하면 대상 job_id 포함)를 호출해 프로필·경력·프로젝트·스킬·대표 이력서·글쓰기 선호를 가져온다. 타깃 공고가 있으면 그 키워드를 사전 입력으로 쓴다.',
138
+ '`get_workflow_guide({ workflow_id: "build_personal_brand" })`가 안내하는 전문가 절차를 따른다. 작성 직전 `get_playbook({ domain: "linkedin-profile" })`·`get_playbook({ domain: "portfolio" })`를 받아 헤드라인·About·Featured·3막(problem→approach→impact) 규범을 적용한다.',
139
+ '무엇을 만들지 확인한다(LinkedIn 프로필만/포트폴리오만/둘 다, 타깃 직무, 강조 포인트). 저장된 실제 경험·성과만 쓰고, 없는 수치·고유명사는 추측하지 말고 `[확인 필요]`로 표시해 사용자에게 묻는다.',
140
+ 'LinkedIn: 헤드라인(핵심 키워드를 앞 120자에 front-load, 하드 상한 220자)·About 첫 문장 훅·정량 성과·직무 키워드를 작성. 포트폴리오: 직무에 맞는 3~6개 항목을 problem→approach→impact 3막으로 구성하고 본인 기여(my_role)와 정량 임팩트를 명시한다.',
141
+ '저장 직전 `get_verifier({ id: "human-voice" })`·`get_verifier({ id: "truthfulness" })`·`get_verifier({ id: "consistency" })`를 받아 당신이 직접 점검한다(AI 티·환각·사실 보존). 키워드 사전이 비면 키워드 체크는 보류한다.',
142
+ '포트폴리오 본문은 `add_resume`(kind=portfolio)로 저장한다. LinkedIn 프로필 초안은 사용자가 복사해 쓰도록 제시하고(전용 저장·검증 도구 validate_linkedin_profile은 Phase B 미구현), 원하면 파일로 내보낸다.',
143
+ '저장 결과를 사용자의 언어로 알리고 대시보드 링크를 안내하며, 네트워킹(추천 경로)이나 공고 지원으로 이어갈지 제안한다.',
107
144
  ],
108
145
  },
109
146
  ];