therookie 0.4.38 → 0.4.39

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "therookie",
3
- "version": "0.4.38",
3
+ "version": "0.4.39",
4
4
  "description": "Persistent memory for Claude Code and Codex CLI — teach your AI once and it remembers across sessions, tools, and machines. The AI new hire that remembers what you teach it.",
5
5
  "license": "MIT",
6
6
  "keywords": [
package/src/cli.js CHANGED
@@ -20,7 +20,7 @@ const HELP = `rookie — the Rookie Skill/Hook 설치 관리자
20
20
  사용:
21
21
  rookie init [--local] [--force] [--yes] [--deep-scan] [--claude-md | --no-claude-md]
22
22
  rookie login [--local] [--claude-md | --no-claude-md]
23
- rookie update [--force] [--claude-md | --no-claude-md]
23
+ rookie update [--force] [--claude-md | --no-claude-md] [--codex-md | --no-codex-md]
24
24
  rookie link [--local]
25
25
  rookie save [--file <body.json>] [--json] # 또는 stdin. --json = 서버 raw 응답 출력
26
26
  rookie status
@@ -28,7 +28,7 @@ const HELP = `rookie — the Rookie Skill/Hook 설치 관리자
28
28
  rookie unlink [--local | --global]
29
29
  rookie token list | revoke <id> | rotate
30
30
  rookie sweep-recovery [--force]
31
- rookie codex-setup [--yes]
31
+ rookie codex-setup [--yes] [--codex-md | --no-codex-md]
32
32
  rookie done-scan [--force]
33
33
  rookie context-diet [--json] [--force]
34
34
  rookie orphan-extract [--batch=5]
@@ -40,6 +40,8 @@ const HELP = `rookie — the Rookie Skill/Hook 설치 관리자
40
40
  --deep-scan 홈 디렉토리에서 로컬 Skill 전수 스캔
41
41
  --claude-md 글로벌 ~/.claude/CLAUDE.md 에 더루키 관리 블록을 넣는다(심링크·이미 같은 규칙이 있어도, update 에서도)
42
42
  --no-claude-md 글로벌 CLAUDE.md 를 건드리지 않는다 (환경 변수 ROOKIE_NO_CLAUDE_MD=1 과 같다)
43
+ --codex-md Codex 전역 지시 파일(AGENTS.override.md 가 있으면 그것, 없으면 AGENTS.md)에 더루키 관리 블록을 넣는다
44
+ --no-codex-md Codex 전역 지시 파일을 건드리지 않는다 (환경 변수 ROOKIE_NO_CODEX_MD=1 과 같다)
43
45
 
44
46
  명령:
45
47
  init 첫 설치. 비밀번호 입력 받아 PAT 발급 + Skill/Hook 배포.
@@ -4,6 +4,7 @@ import { existsSync } from 'node:fs'
4
4
  import { confirm } from '../prompt.js'
5
5
  import { loadConfig } from '../config.js'
6
6
  import { applyCodexSetup } from '../lib/deploy-artifacts.js'
7
+ import { runCodexMdStep } from '../lib/claude-md-block.js'
7
8
 
8
9
  // Codex config.toml 에 더루키 SessionStart hook 을 동의 기반으로 등록한다(managed 블록만).
9
10
  // 명시적 Codex-only 재설정 진입점 — 내부 write 는 applyCodexSetup(3층 adapter) 공유(spec §4 item 6).
@@ -33,4 +34,13 @@ export async function runCodexSetup(flags) {
33
34
  process.exit(1)
34
35
  }
35
36
  console.log('✓ 등록 완료. 새 Codex 세션부터 자동 적용됩니다.')
37
+
38
+ // spec 2026-09-29 codex-agents-md — Codex 전역 지시 파일에 "더루키 먼저" 관리 블록(동의 기반).
39
+ const { lines } = await runCodexMdStep({
40
+ mode: 'connect',
41
+ flags,
42
+ interactive: !!process.stdin.isTTY && !flags.yes,
43
+ confirmFn: confirm,
44
+ })
45
+ for (const line of lines) console.log(line)
36
46
  }
@@ -1,5 +1,5 @@
1
1
  import { existsSync, readFileSync, readdirSync } from 'node:fs'
2
- import { claudeMdStatusLine } from '../lib/claude-md-block.js'
2
+ import { claudeMdStatusLine, codexMdStatusLine } from '../lib/claude-md-block.js'
3
3
  import { homedir } from 'node:os'
4
4
  import { join } from 'node:path'
5
5
  import { execSync } from 'node:child_process'
@@ -45,6 +45,8 @@ export async function runDoctor(flags) {
45
45
  console.log(hasRookieHook(lSettings) ? '✓ 로컬 hook 등록됨' : ' 로컬 hook 없음')
46
46
  const claudeMdLine = claudeMdStatusLine()
47
47
  if (claudeMdLine) console.log(claudeMdLine)
48
+ const codexMdLine = codexMdStatusLine()
49
+ if (codexMdLine) console.log(codexMdLine)
48
50
 
49
51
  if (g && l && g.version !== l.version) {
50
52
  console.log(`⚠ 버전 불일치: 글로벌 v${g.version} vs 로컬 v${l.version}`)
@@ -13,7 +13,7 @@ import { semverGt, fetchReleaseManifest, selfUpdateAndRerun, isNpmManagedInstall
13
13
  import { installSkill } from '../lib/skill-installer.js'
14
14
  import { claudeHookCommand, escapeJsonStringValue } from '../lib/hook-command.js'
15
15
  import { mergeHooks } from '../lib/merge-hooks.js'
16
- import { runClaudeMdStep } from '../lib/claude-md-block.js'
16
+ import { runClaudeMdStep, runCodexMdStep } from '../lib/claude-md-block.js'
17
17
  import { confirm } from '../prompt.js'
18
18
 
19
19
  const __filename = fileURLToPath(import.meta.url)
@@ -58,13 +58,11 @@ export async function runUpdate(flags) {
58
58
  // manifest "최신" 조기 종료·서버 실패보다 앞에 둔다. 표식이 있으면 갱신, 없으면 대화형 확인 또는
59
59
  // --claude-md 일 때만 붙인다 — update 는 모델이 비대화로 돌리는 경로가 있다.
60
60
  {
61
- const { lines } = await runClaudeMdStep({
62
- mode: 'update',
63
- flags,
64
- interactive: !!process.stdin.isTTY && !flags.yes,
65
- confirmFn: confirm,
66
- })
67
- for (const line of lines) console.log(line)
61
+ const interactive = !!process.stdin.isTTY && !flags.yes
62
+ for (const step of [runClaudeMdStep, runCodexMdStep]) {
63
+ const { lines } = await step({ mode: 'update', flags, interactive, confirmFn: confirm })
64
+ for (const line of lines) console.log(line)
65
+ }
68
66
  }
69
67
 
70
68
  let serverManifest
@@ -21,6 +21,7 @@ import { fileURLToPath } from 'node:url'
21
21
 
22
22
  const __dirname = dirname(fileURLToPath(import.meta.url))
23
23
  export const DEFAULT_TEMPLATE_PATH = join(__dirname, '..', '..', 'templates', 'claude-md', 'rookie-block.md')
24
+ export const CODEX_TEMPLATE_PATH = join(__dirname, '..', '..', 'templates', 'codex-md', 'rookie-block.md')
24
25
 
25
26
  const BEGIN_RE = /<!-- rookie:begin v(\d+) -->/g
26
27
  const END_MARK = '<!-- rookie:end -->'
@@ -58,28 +59,61 @@ export function upsertRookieBlock(content, blockText) {
58
59
  return content + sep + blockText + '\n'
59
60
  }
60
61
 
61
- // 무엇을 할지 정한다(쓰지 않는다). force = --claude-md.
62
+ // 대상 서술자 — 어느 폴더의 어느 파일을 관리하는가(spec 2026-09-29 codex-agents-md §3).
63
+ // candidates: 앞에서부터 먼저 존재하는 파일이 Codex/Claude 가 실제로 읽는 파일이다.
64
+ export function claudeTarget({ home = homedir() } = {}) {
65
+ return {
66
+ dir: join(home, '.claude'),
67
+ candidates: ['CLAUDE.md'],
68
+ createName: 'CLAUDE.md',
69
+ needles: [SAME_INTENT_NEEDLE],
70
+ noDirAction: 'no_claude_dir',
71
+ templatePath: DEFAULT_TEMPLATE_PATH,
72
+ }
73
+ }
74
+
75
+ // Codex 는 $CODEX_HOME(없으면 ~/.codex)의 AGENTS.override.md 가 있으면 그것만 읽고 AGENTS.md 는 무시한다
76
+ // (2026-09-29 codex 0.153.4 바이너리 strings + codex exec 실측).
77
+ export function codexTarget({ home = homedir(), env = process.env } = {}) {
78
+ return {
79
+ dir: env.CODEX_HOME ? env.CODEX_HOME : join(home, '.codex'),
80
+ candidates: ['AGENTS.override.md', 'AGENTS.md'],
81
+ createName: 'AGENTS.md',
82
+ needles: [SAME_INTENT_NEEDLE, 'rookie_recall'],
83
+ noDirAction: 'no_codex_dir',
84
+ templatePath: CODEX_TEMPLATE_PATH,
85
+ }
86
+ }
87
+
88
+ function pickTargetFile(dir, candidates) {
89
+ for (const name of candidates) {
90
+ const p = join(dir, name)
91
+ try { return { path: p, lst: lstatSync(p) } } catch { /* 다음 후보 */ }
92
+ }
93
+ return null
94
+ }
95
+
96
+ // 무엇을 할지 정한다(쓰지 않는다). force = --claude-md / --codex-md.
62
97
  // action: create | append | replace | noop | noop_edited | skip_same_intent |
63
- // skip_symlink_unmanaged | skip_broken_symlink | skip_newer | malformed | no_claude_dir
64
- export function planClaudeMd({ home = homedir(), force = false, template } = {}) {
65
- const tpl = template ?? readTemplateBlock()
66
- const claudeDir = join(home, '.claude')
67
- const path = join(claudeDir, 'CLAUDE.md')
68
- const base = { path, target: path, isSymlink: false, template: tpl }
69
- if (!existsSync(claudeDir)) return { ...base, action: 'no_claude_dir' }
70
-
71
- let lst = null
72
- try { lst = lstatSync(path) } catch { lst = null }
73
- if (!lst) return { ...base, action: 'create' }
74
-
75
- let target = path
76
- const isSymlink = lst.isSymbolicLink()
98
+ // skip_symlink_unmanaged | skip_broken_symlink | skip_newer | malformed | no_claude_dir | no_codex_dir
99
+ export function planManagedMd({ target, force = false, template } = {}) {
100
+ const tpl = template ?? readTemplateBlock(target.templatePath)
101
+ const createPath = join(target.dir, target.createName)
102
+ const base = { path: createPath, target: createPath, isSymlink: false, template: tpl, needles: target.needles }
103
+ if (!existsSync(target.dir)) return { ...base, action: target.noDirAction }
104
+
105
+ const found = pickTargetFile(target.dir, target.candidates)
106
+ if (!found) return { ...base, action: 'create' }
107
+
108
+ const path = found.path
109
+ let real = path
110
+ const isSymlink = found.lst.isSymbolicLink()
77
111
  if (isSymlink) {
78
- try { target = realpathSync(path) } catch { return { ...base, isSymlink, action: 'skip_broken_symlink' } }
112
+ try { real = realpathSync(path) } catch { return { ...base, path, target: path, isSymlink, action: 'skip_broken_symlink' } }
79
113
  }
80
- const content = readFileSync(target, 'utf8')
114
+ const content = readFileSync(real, 'utf8')
81
115
  const p = parseRookieBlock(content)
82
- const ctx = { ...base, target, isSymlink, content, existing: p }
116
+ const ctx = { ...base, path, target: real, isSymlink, content, existing: p }
83
117
 
84
118
  if (p.state === 'malformed') return { ...ctx, action: 'malformed' }
85
119
  if (p.state === 'ok') {
@@ -90,10 +124,18 @@ export function planClaudeMd({ home = homedir(), force = false, template } = {})
90
124
  }
91
125
  // 표식 없음
92
126
  if (isSymlink && !force) return { ...ctx, action: 'skip_symlink_unmanaged' }
93
- if (content.includes(SAME_INTENT_NEEDLE) && !force) return { ...ctx, action: 'skip_same_intent' }
127
+ if (target.needles.some((n) => content.includes(n)) && !force) return { ...ctx, action: 'skip_same_intent' }
94
128
  return { ...ctx, action: 'append' }
95
129
  }
96
130
 
131
+ export function planClaudeMd({ home = homedir(), force = false, template } = {}) {
132
+ return planManagedMd({ target: claudeTarget({ home }), force, template })
133
+ }
134
+
135
+ export function planCodexMd({ home = homedir(), env = process.env, force = false, template } = {}) {
136
+ return planManagedMd({ target: codexTarget({ home, env }), force, template })
137
+ }
138
+
97
139
  export const WRITING_ACTIONS = new Set(['create', 'append', 'replace'])
98
140
 
99
141
  // plan 대로 쓴다. 대상 파일 폴더에 임시 파일을 만들고 rename — 심링크 경로 자체는 건드리지 않는다.
@@ -121,9 +163,29 @@ export function applyClaudeMd(plan) {
121
163
  return { written: true, action: plan.action, target, backup: mode !== null ? `${target}.rookie-bak` : null }
122
164
  }
123
165
 
124
- // 명령(init·login·update)이 공유하는 한 단계. 출력 줄 목록을 돌려준다(테스트 가능하게).
125
- // mode 'connect'(login·init): 기본 적용. 'update': 표식이 있을 때만 갱신, 없으면 대화형 확인 또는 --claude-md.
126
- export async function runClaudeMdStep({
166
+ // 대상별 문구·플래그. CLAUDE.md 문구는 0.4.38 과 글자 그대로다(테스트가 고정한다).
167
+ export const KINDS = {
168
+ claude: {
169
+ label: () => '글로벌 CLAUDE.md',
170
+ forceFlag: 'claude-md',
171
+ optOutFlag: 'no-claude-md',
172
+ optOutEnv: 'ROOKIE_NO_CLAUDE_MD',
173
+ plan: ({ home, force, template }) => planClaudeMd({ home, force, template }),
174
+ },
175
+ codex: {
176
+ // Codex 는 override 가 있으면 그 파일을 읽으므로 실제 파일 이름을 그대로 보인다.
177
+ label: (plan) => `Codex ${plan ? basename(plan.path) : 'AGENTS.md'}`,
178
+ forceFlag: 'codex-md',
179
+ optOutFlag: 'no-codex-md',
180
+ optOutEnv: 'ROOKIE_NO_CODEX_MD',
181
+ plan: ({ home, env, force, template }) => planCodexMd({ home, env, force, template }),
182
+ },
183
+ }
184
+
185
+ // 명령(init·login·update·codex-setup)이 공유하는 한 단계. 출력 줄 목록을 돌려준다(테스트 가능하게).
186
+ // mode 'connect'(login·init·codex-setup): 기본 적용. 'update': 표식이 있을 때만 갱신, 없으면 대화형 확인 또는 강제 플래그.
187
+ export async function runManagedMdStep({
188
+ kind = 'claude',
127
189
  mode,
128
190
  flags = {},
129
191
  env = process.env,
@@ -132,40 +194,44 @@ export async function runClaudeMdStep({
132
194
  home = homedir(),
133
195
  template,
134
196
  } = {}) {
197
+ const K = KINDS[kind]
135
198
  const lines = []
136
- if (flags['no-claude-md'] || env.ROOKIE_NO_CLAUDE_MD === '1') return { lines, result: { written: false, action: 'opted_out' } }
199
+ if (flags[K.optOutFlag] || env[K.optOutEnv] === '1') return { lines, result: { written: false, action: 'opted_out' } }
137
200
 
138
- const force = !!flags['claude-md']
201
+ const force = !!flags[K.forceFlag]
202
+ const hintCmd = `rookie update --${K.forceFlag}`
139
203
  let plan
140
204
  try {
141
- plan = planClaudeMd({ home, force, template })
205
+ plan = K.plan({ home, env, force, template })
142
206
  } catch (e) {
143
- lines.push(`⚠ 글로벌 CLAUDE.md 점검 실패: ${e.message}`)
207
+ lines.push(`⚠ ${K.label()} 점검 실패: ${e.message}`)
144
208
  return { lines, result: { written: false, action: 'error' } }
145
209
  }
210
+ const label = K.label(plan)
146
211
  const where = plan.isSymlink ? `${plan.path} → ${plan.target}` : plan.path
147
212
 
148
213
  switch (plan.action) {
149
214
  case 'no_claude_dir':
215
+ case 'no_codex_dir':
150
216
  case 'noop':
151
217
  return { lines, result: { written: false, action: plan.action } }
152
218
  case 'noop_edited':
153
- lines.push(` 글로벌 CLAUDE.md 의 더루키 블록이 수정돼 있어 그대로 둡니다 — 템플릿으로 되돌리려면 rookie update --claude-md`)
219
+ lines.push(` ${label} 의 더루키 블록이 수정돼 있어 그대로 둡니다 — 템플릿으로 되돌리려면 ${hintCmd}`)
154
220
  return { lines, result: { written: false, action: plan.action } }
155
221
  case 'skip_same_intent':
156
- lines.push(` 글로벌 CLAUDE.md 에 더루키 회상 규칙이 이미 있어 블록을 넣지 않았습니다 — 관리 블록으로 두려면 --claude-md`)
222
+ lines.push(` ${label} 에 더루키 회상 규칙이 이미 있어 블록을 넣지 않았습니다 — 관리 블록으로 두려면 --${K.forceFlag}`)
157
223
  return { lines, result: { written: false, action: plan.action } }
158
224
  case 'skip_symlink_unmanaged':
159
- lines.push(` 글로벌 CLAUDE.md 가 심링크라 블록을 넣지 않았습니다(${where}) — 넣으려면 --claude-md`)
225
+ lines.push(` ${label} 가 심링크라 블록을 넣지 않았습니다(${where}) — 넣으려면 --${K.forceFlag}`)
160
226
  return { lines, result: { written: false, action: plan.action } }
161
227
  case 'skip_broken_symlink':
162
- lines.push(`⚠ 글로벌 CLAUDE.md 심링크의 대상이 없습니다(${plan.path}) — 건드리지 않았습니다`)
228
+ lines.push(`⚠ ${label} 심링크의 대상이 없습니다(${plan.path}) — 건드리지 않았습니다`)
163
229
  return { lines, result: { written: false, action: plan.action } }
164
230
  case 'skip_newer':
165
- lines.push(` 글로벌 CLAUDE.md 의 더루키 블록(v${plan.existing.version})이 이 CLI(v${plan.template.version})보다 새 버전이라 그대로 둡니다`)
231
+ lines.push(` ${label} 의 더루키 블록(v${plan.existing.version})이 이 CLI(v${plan.template.version})보다 새 버전이라 그대로 둡니다`)
166
232
  return { lines, result: { written: false, action: plan.action } }
167
233
  case 'malformed':
168
- lines.push(`⚠ 글로벌 CLAUDE.md 의 더루키 블록 표식이 손상돼 건드리지 않았습니다 — 수동 점검: ${plan.target}`)
234
+ lines.push(`⚠ ${label} 의 더루키 블록 표식이 손상돼 건드리지 않았습니다 — 수동 점검: ${plan.target}`)
169
235
  return { lines, result: { written: false, action: plan.action } }
170
236
  default:
171
237
  break
@@ -175,18 +241,18 @@ export async function runClaudeMdStep({
175
241
  const isNewBlock = plan.action === 'create' || plan.action === 'append'
176
242
  if (mode === 'update' && isNewBlock && !force) {
177
243
  if (!interactive) {
178
- lines.push(` 글로벌 CLAUDE.md 에 더루키 블록이 없습니다 — 넣으려면 rookie update --claude-md (rookie doctor 로 확인)`)
244
+ lines.push(` ${label} 에 더루키 블록이 없습니다 — 넣으려면 ${hintCmd} (rookie doctor 로 확인)`)
179
245
  return { lines, result: { written: false, action: 'needs_consent' } }
180
246
  }
181
- console.log(describePlan(plan))
182
- if (!(await confirmFn('글로벌 CLAUDE.md 에 이 블록을 넣을까요?', { defaultYes: false }))) {
183
- lines.push(' 글로벌 CLAUDE.md 블록 추가를 건너뛰었습니다')
247
+ console.log(describePlan(plan, label))
248
+ if (!(await confirmFn(`${label} 에 이 블록을 넣을까요?`, { defaultYes: false }))) {
249
+ lines.push(` ${label} 블록 추가를 건너뛰었습니다`)
184
250
  return { lines, result: { written: false, action: 'declined' } }
185
251
  }
186
252
  } else if (mode === 'connect' && isNewBlock && interactive) {
187
- console.log(describePlan(plan))
188
- if (!(await confirmFn('글로벌 CLAUDE.md 에 이 블록을 넣을까요?', { defaultYes: true }))) {
189
- lines.push(' 글로벌 CLAUDE.md 블록 추가를 건너뛰었습니다 (나중에: rookie update --claude-md)')
253
+ console.log(describePlan(plan, label))
254
+ if (!(await confirmFn(`${label} 에 이 블록을 넣을까요?`, { defaultYes: true }))) {
255
+ lines.push(` ${label} 블록 추가를 건너뛰었습니다 (나중에: ${hintCmd})`)
190
256
  return { lines, result: { written: false, action: 'declined' } }
191
257
  }
192
258
  }
@@ -195,49 +261,69 @@ export async function runClaudeMdStep({
195
261
  try {
196
262
  result = applyClaudeMd(plan)
197
263
  } catch (e) {
198
- lines.push(`⚠ 글로벌 CLAUDE.md 쓰기 실패(원본 보존): ${e.message}`)
264
+ lines.push(`⚠ ${label} 쓰기 실패(원본 보존): ${e.message}`)
199
265
  return { lines, result: { written: false, action: 'error' } }
200
266
  }
201
267
  if (result.written) {
202
268
  const verb = plan.action === 'replace' ? `갱신(v${plan.existing.version} → v${plan.template.version})` : `추가(v${plan.template.version})`
203
- lines.push(`✓ 글로벌 CLAUDE.md 더루키 블록 ${verb}: ${where}`)
269
+ lines.push(`✓ ${label} 더루키 블록 ${verb}: ${where}`)
204
270
  if (result.backup) lines.push(` · 이전 본문 backup → ${result.backup}`)
205
- if (mode === 'connect' && !interactive && isNewBlock) lines.push(' (넣지 않으려면 표식 사이를 지우고 ROOKIE_NO_CLAUDE_MD=1 로 두세요)')
271
+ if (mode === 'connect' && !interactive && isNewBlock) lines.push(` (넣지 않으려면 표식 사이를 지우고 ${K.optOutEnv}=1 로 두세요)`)
206
272
  }
207
273
  return { lines, result }
208
274
  }
209
275
 
276
+ export async function runClaudeMdStep(opts = {}) {
277
+ return runManagedMdStep({ ...opts, kind: 'claude' })
278
+ }
279
+
280
+ export async function runCodexMdStep(opts = {}) {
281
+ return runManagedMdStep({ ...opts, kind: 'codex' })
282
+ }
283
+
210
284
  // 쓰기 전에 보여 줄 설명.
211
- export function describePlan(plan) {
285
+ export function describePlan(plan, label = '글로벌 CLAUDE.md') {
212
286
  const where = plan.isSymlink ? `${plan.path} → ${plan.target}` : plan.path
213
287
  const what = plan.action === 'create' ? '새 파일로 만들고' : plan.action === 'append' ? '파일 끝에' : '기존 블록을 바꿔'
214
288
  return [
215
- `\n글로벌 CLAUDE.md 에 더루키 관리 블록을 둡니다 — ${where}`,
289
+ `\n${label} 에 더루키 관리 블록을 둡니다 — ${where}`,
216
290
  ` ${what} 아래 블록을 넣습니다(표식 밖의 글은 건드리지 않습니다):`,
217
291
  ...plan.template.text.split('\n').map((l) => ` | ${l}`),
218
292
  ].join('\n')
219
293
  }
220
294
 
221
295
  // doctor 용 한 줄(읽기만).
222
- export function claudeMdStatusLine({ home = homedir(), template } = {}) {
296
+ export function managedMdStatusLine({ kind = 'claude', home = homedir(), env = process.env, template } = {}) {
297
+ const K = KINDS[kind]
298
+ const hintCmd = `rookie update --${K.forceFlag}`
223
299
  let plan
224
- try { plan = planClaudeMd({ home, force: false, template }) } catch (e) { return `⚠ 글로벌 CLAUDE.md 점검 실패: ${e.message}` }
300
+ try { plan = K.plan({ home, env, force: false, template }) } catch (e) { return `⚠ ${K.label()} 점검 실패: ${e.message}` }
301
+ const label = K.label(plan)
225
302
  const v = plan.template.version
226
303
  switch (plan.action) {
227
- case 'no_claude_dir': return null
228
- case 'noop': return `✓ 글로벌 CLAUDE.md 더루키 블록 v${v}`
229
- case 'replace': return `⚠ 글로벌 CLAUDE.md 더루키 블록 v${plan.existing.version} → v${v} — rookie update`
230
- case 'noop_edited': return '⚠ 글로벌 CLAUDE.md 더루키 블록이 수정됨 — 되돌리려면 rookie update --claude-md'
231
- case 'skip_newer': return `✓ 글로벌 CLAUDE.md 더루키 블록 v${plan.existing.version} (이 CLI 보다 새 버전)`
232
- case 'skip_same_intent': return '✓ 글로벌 CLAUDE.md 에 더루키 회상 규칙 있음(관리 블록 아님)'
304
+ case 'no_claude_dir':
305
+ case 'no_codex_dir': return null
306
+ case 'noop': return `✓ ${label} 더루키 블록 v${v}`
307
+ case 'replace': return `⚠ ${label} 더루키 블록 v${plan.existing.version} → v${v} — rookie update`
308
+ case 'noop_edited': return `⚠ ${label} 더루키 블록이 수정됨 — 되돌리려면 ${hintCmd}`
309
+ case 'skip_newer': return `✓ ${label} 더루키 블록 v${plan.existing.version} (이 CLI 보다 새 버전)`
310
+ case 'skip_same_intent': return `✓ ${label} 에 더루키 회상 규칙 있음(관리 블록 아님)`
233
311
  case 'skip_symlink_unmanaged':
234
- return plan.content.includes(SAME_INTENT_NEEDLE)
235
- ? '✓ 글로벌 CLAUDE.md(심링크)에 더루키 회상 규칙 있음(관리 블록 아님)'
236
- : '⚠ 글로벌 CLAUDE.md(심링크)에 더루키 블록 없음 — rookie update --claude-md'
237
- case 'skip_broken_symlink': return `⚠ 글로벌 CLAUDE.md 심링크 대상 없음 — ${plan.path}`
238
- case 'malformed': return `✗ 글로벌 CLAUDE.md 더루키 블록 손상(begin/end 불일치) — 수동 점검: ${plan.target}`
239
- case 'create': return '⚠ 글로벌 CLAUDE.md 없음 — rookie update --claude-md'
240
- case 'append': return '⚠ 글로벌 CLAUDE.md 에 더루키 블록 없음 — rookie update --claude-md'
312
+ return plan.needles.some((n) => plan.content.includes(n))
313
+ ? `✓ ${label}(심링크)에 더루키 회상 규칙 있음(관리 블록 아님)`
314
+ : `⚠ ${label}(심링크)에 더루키 블록 없음 — ${hintCmd}`
315
+ case 'skip_broken_symlink': return `⚠ ${label} 심링크 대상 없음 — ${plan.path}`
316
+ case 'malformed': return `✗ ${label} 더루키 블록 손상(begin/end 불일치) — 수동 점검: ${plan.target}`
317
+ case 'create': return `⚠ ${label} 없음 — ${hintCmd}`
318
+ case 'append': return `⚠ ${label} 에 더루키 블록 없음 — ${hintCmd}`
241
319
  default: return null
242
320
  }
243
321
  }
322
+
323
+ export function claudeMdStatusLine({ home = homedir(), template } = {}) {
324
+ return managedMdStatusLine({ kind: 'claude', home, template })
325
+ }
326
+
327
+ export function codexMdStatusLine({ home = homedir(), env = process.env, template } = {}) {
328
+ return managedMdStatusLine({ kind: 'codex', home, env, template })
329
+ }
@@ -0,0 +1,7 @@
1
+ <!-- rookie:begin v1 -->
2
+ ## 더루키(the Rookie) 기억 — 로컬보다 먼저
3
+ - 전에 한 일·결정·맥락이 필요하면(어제·전에·이어서 같은 말이 없어도) 로컬 메모·Codex 자체 기억을 뒤지기 **전에** 더루키 회상을 먼저 부른다 — rookie MCP 도구 `rookie_recall` 이 있으면 그것을, 없으면 셸에서 `GET /api/rookie/recall`(rookie CLI 설정의 endpoint·토큰). 로컬 기억은 보조다.
4
+ - 새로 기억할 것(운영 사실·결정·규칙·취향)은 **더루키에 먼저** 저장한다 — `rookie_save` 도구 또는 셸의 `rookie save`. 로컬 메모는 필요할 때만 보조로 남긴다.
5
+ - 예외: 작업 폴더에 `.rookie-ignore` 가 있거나, `ROOKIE_DISABLED=1` 이거나, `~/.rookie/config.json` 이 없으면 위 두 줄을 따르지 않는다.
6
+ - 세부 절차는 세션 시작 주입(더루키 hook)이 정본이다 — 여기에 옮겨 적지 않는다.
7
+ <!-- rookie:end -->
package/version.json CHANGED
@@ -1,4 +1,4 @@
1
1
  {
2
- "cli_version": "0.4.38",
2
+ "cli_version": "0.4.39",
3
3
  "skill_version": "1.40.1"
4
4
  }