therookie 0.4.5 → 0.4.12

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,7 +1,7 @@
1
1
  {
2
2
  "name": "therookie",
3
- "version": "0.4.5",
4
- "description": "the Rookie \u2014 the AI new hire that remembers what you teach it. A CLI that connects a personal AI memory to AI tools like Claude and Codex.",
3
+ "version": "0.4.12",
4
+ "description": "the Rookie — the AI new hire that remembers what you teach it. A CLI that connects a personal AI memory to AI tools like Claude and Codex.",
5
5
  "license": "MIT",
6
6
  "keywords": [
7
7
  "ai",
@@ -129,6 +129,13 @@ export async function runUpdate(flags) {
129
129
  )
130
130
  chmodSync(join(pretoolBinDir, 'posttool-recall-nudge.sh'), 0o755)
131
131
 
132
+ // spec 2026-07-22 §D3 — 회상 없이 파일 수정 착수 시 보정 넛지.
133
+ cpSync(
134
+ join(TEMPLATES, 'hook', 'bin', 'work-recall-gate.sh'),
135
+ join(pretoolBinDir, 'work-recall-gate.sh')
136
+ )
137
+ chmodSync(join(pretoolBinDir, 'work-recall-gate.sh'), 0o755)
138
+
132
139
  const libDir = join(binDir, 'lib')
133
140
  mkdirSync(libDir, { recursive: true })
134
141
  cpSync(join(TEMPLATES, 'hook', 'lib', 'queue.sh'), join(libDir, 'queue.sh'))
@@ -160,6 +167,7 @@ export async function runUpdate(flags) {
160
167
  posttoolRecallNudgeCmd: claudeHookCommand(join(pretoolBinDir, 'posttool-recall-nudge.sh')),
161
168
  guardCmd: claudeHookCommand(join(binDir, 'guard-destructive-db.sh')),
162
169
  skillGateCmd: claudeHookCommand(join(binDir, 'user-prompt-skill-gate.sh')),
170
+ workRecallGateCmd: claudeHookCommand(join(pretoolBinDir, 'work-recall-gate.sh')),
163
171
  })
164
172
 
165
173
  // v6 P0b — 글로벌 SKILL.md 갱신 (backup-and-overwrite). 같은 본문이면 no-op.
@@ -171,6 +179,8 @@ export async function runUpdate(flags) {
171
179
  if (skillResult.installed) {
172
180
  console.log(`✓ Skill 갱신 → ${skillResult.path}`)
173
181
  if (skillResult.backup) console.log(` · 이전 본문 backup → ${skillResult.backup}`)
182
+ } else if (skillResult.assets_synced > 0) {
183
+ console.log(`✓ Skill 본문 변경 없음 · 부속 문서 ${skillResult.assets_synced}건 동기화`)
174
184
  } else {
175
185
  console.log(`✓ Skill 본문 변경 없음 (manifest 동기화만)`)
176
186
  }
@@ -203,6 +213,7 @@ async function mergeSettings(settingsPath, partialPath, subs) {
203
213
  .replaceAll('__POSTTOOL_RECALL_NUDGE_CMD__', escapeJsonStringValue(subs.posttoolRecallNudgeCmd))
204
214
  .replaceAll('__GUARD_DESTRUCTIVE_DB_CMD__', escapeJsonStringValue(subs.guardCmd))
205
215
  .replaceAll('__USER_PROMPT_SKILL_GATE_CMD__', escapeJsonStringValue(subs.skillGateCmd))
216
+ .replaceAll('__WORK_RECALL_GATE_CMD__', escapeJsonStringValue(subs.workRecallGateCmd))
206
217
  const partial = JSON.parse(partialRaw)
207
218
 
208
219
  let current = {}
@@ -7,6 +7,7 @@ import { saveClientManifest } from './manifest.js'
7
7
  import { upsertCodexHookBlock } from './codex-config.js'
8
8
  import { claudeHookCommand, escapeJsonStringValue } from './hook-command.js'
9
9
  import { mergeHooks } from './merge-hooks.js'
10
+ import { syncSkillAssets } from './skill-installer.js'
10
11
 
11
12
  const __filename = fileURLToPath(import.meta.url)
12
13
  const __dirname = dirname(__filename)
@@ -58,6 +59,13 @@ export function installSharedHookRuntime({ scope, home, cwd } = {}) {
58
59
  )
59
60
  chmodSync(join(pretoolBinDir, 'posttool-recall-nudge.sh'), 0o755)
60
61
 
62
+ // spec 2026-07-22 §D3 — 회상 없이 파일 수정 착수 시 보정 넛지(PostToolUse Bash 마커 + PreToolUse Edit 판정).
63
+ cpSync(
64
+ join(TEMPLATES, 'hook', 'bin', 'work-recall-gate.sh'),
65
+ join(pretoolBinDir, 'work-recall-gate.sh')
66
+ )
67
+ chmodSync(join(pretoolBinDir, 'work-recall-gate.sh'), 0o755)
68
+
61
69
  // spec post-v5-decisions §17 — hook offline queue 라이브러리 배포
62
70
  const libDir = join(binDir, 'lib')
63
71
  mkdirSync(libDir, { recursive: true })
@@ -102,6 +110,7 @@ export async function applyClaudeSetup({ scope, cfg, home, cwd } = {}) {
102
110
  .replaceAll('__INSTALLED_AT__', new Date().toISOString())
103
111
  .replaceAll('__INSTALLED_BY__', `rookie-cli@${CLI_VERSION}`)
104
112
  writeFileSync(join(skillDir, 'SKILL.md'), skillRendered)
113
+ syncSkillAssets({ targetDir: skillDir, templateDir: join(TEMPLATES, 'skill') })
105
114
 
106
115
  // Windows 는 claudeHookCommand 가 `bash "<posix>"` 로 감싼다(아래 hook-command.js). macOS/Linux 무변경.
107
116
  const settingsResult = await mergeSettings(settingsPath, join(TEMPLATES, 'hook', 'settings.partial.json'), {
@@ -111,6 +120,7 @@ export async function applyClaudeSetup({ scope, cfg, home, cwd } = {}) {
111
120
  posttoolRecallNudgeCmd: claudeHookCommand(join(pretoolBinDir, 'posttool-recall-nudge.sh')),
112
121
  guardCmd: claudeHookCommand(join(binDir, 'guard-destructive-db.sh')),
113
122
  skillGateCmd: claudeHookCommand(join(binDir, 'user-prompt-skill-gate.sh')),
123
+ workRecallGateCmd: claudeHookCommand(join(pretoolBinDir, 'work-recall-gate.sh')),
114
124
  })
115
125
 
116
126
  // §18 U-3 — 클라이언트 manifest 저장. 다음 hook turn 부터 미스매치 경고 비교 가능.
@@ -167,6 +177,7 @@ async function mergeSettings(settingsPath, partialPath, subs) {
167
177
  .replaceAll('__POSTTOOL_RECALL_NUDGE_CMD__', escapeJsonStringValue(subs.posttoolRecallNudgeCmd))
168
178
  .replaceAll('__GUARD_DESTRUCTIVE_DB_CMD__', escapeJsonStringValue(subs.guardCmd))
169
179
  .replaceAll('__USER_PROMPT_SKILL_GATE_CMD__', escapeJsonStringValue(subs.skillGateCmd))
180
+ .replaceAll('__WORK_RECALL_GATE_CMD__', escapeJsonStringValue(subs.workRecallGateCmd))
170
181
  const partial = JSON.parse(partialRaw)
171
182
 
172
183
  let current = {}
@@ -8,7 +8,7 @@
8
8
  // refresh-context.sh 포함 — 과거 UserPromptSubmit 로 실배포됐던 legacy hook.
9
9
  // 마커 소실 구버전 설정에서 이를 보존하면 매 turn 대용량 주입 회귀가 남는다(codex R3-P1).
10
10
  export const ROOKIE_HOOK_SCRIPTS =
11
- /[\\/]\.rookie[\\/]bin[\\/](?:bin[\\/])?(?:flush-session|session-start|user-prompt-skill-gate|guard-destructive-db|pretool-md-read|posttool-recall-nudge|sweep-recovery|refresh-context)\.sh\b/
11
+ /[\\/]\.rookie[\\/]bin[\\/](?:bin[\\/])?(?:flush-session|session-start|user-prompt-skill-gate|guard-destructive-db|pretool-md-read|posttool-recall-nudge|work-recall-gate|sweep-recovery|refresh-context)\.sh\b/
12
12
 
13
13
  export function isRookieEntry(entry) {
14
14
  if (!entry || typeof entry !== 'object') return false
@@ -1,8 +1,9 @@
1
1
  // v6 P0b — Skill 본문 install/upgrade. 기존 init/update 의 인라인 skill 쓰기 로직을
2
2
  // backup 옵션과 함께 모듈화. 같은 내용이면 no-op, 다른 내용이면 .bak.<ts> 백업 후 덮어쓰기.
3
+ // 2026-07-18 컨텍스트 다이어트 — SKILL.md 를 코어 + references/ 로 분할하며 부속 파일 동기화 추가.
3
4
 
4
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs'
5
- import { join, dirname } from 'node:path'
5
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync } from 'node:fs'
6
+ import { join, dirname, relative, sep } from 'node:path'
6
7
  import { fileURLToPath } from 'node:url'
7
8
  import { homedir } from 'node:os'
8
9
 
@@ -16,6 +17,39 @@ const DEFAULT_TEMPLATE = join(
16
17
  'SKILL.md'
17
18
  )
18
19
 
20
+ // templates/skill/ 의 SKILL.md 외 부속 파일(references/*.md 등)을 skill 폴더에 동기화.
21
+ // placeholder 렌더 없음(부속 문서는 정적) — 내용이 다를 때만 덮어쓴다. backup 없음(제품 관리 파일).
22
+ // 템플릿에서 사라진 부속 파일은 지우지 않는다(보수적 — 사용자 추가 파일 보호).
23
+ export function syncSkillAssets({ targetDir, templateDir }) {
24
+ let synced = 0
25
+ const walk = (dir) => {
26
+ let entries
27
+ try {
28
+ entries = readdirSync(dir)
29
+ } catch {
30
+ return
31
+ }
32
+ for (const entry of entries) {
33
+ const full = join(dir, entry)
34
+ const st = statSync(full)
35
+ if (st.isDirectory()) {
36
+ walk(full)
37
+ continue
38
+ }
39
+ const rel = relative(templateDir, full)
40
+ if (rel === 'SKILL.md') continue
41
+ const target = join(targetDir, ...rel.split(sep))
42
+ const content = readFileSync(full)
43
+ if (existsSync(target) && readFileSync(target).equals(content)) continue
44
+ mkdirSync(dirname(target), { recursive: true })
45
+ writeFileSync(target, content)
46
+ synced++
47
+ }
48
+ }
49
+ walk(templateDir)
50
+ return synced
51
+ }
52
+
19
53
  export function installSkill({
20
54
  home = homedir(),
21
55
  scope = 'global',
@@ -39,17 +73,18 @@ export function installSkill({
39
73
  .replaceAll('__INSTALLED_BY__', installedBy)
40
74
 
41
75
  mkdirSync(targetDir, { recursive: true })
76
+ const assetsSynced = syncSkillAssets({ targetDir, templateDir: dirname(templatePath) })
42
77
 
43
78
  let backupPath = null
44
79
  if (existsSync(targetPath)) {
45
80
  const existing = readFileSync(targetPath, 'utf8')
46
81
  if (existing === rendered) {
47
- return { installed: false, reason: 'no_change', path: targetPath }
82
+ return { installed: false, reason: 'no_change', path: targetPath, assets_synced: assetsSynced }
48
83
  }
49
84
  backupPath = `${targetPath}.bak.${Date.now()}`
50
85
  writeFileSync(backupPath, existing)
51
86
  }
52
87
 
53
88
  writeFileSync(targetPath, rendered)
54
- return { installed: true, path: targetPath, backup: backupPath }
89
+ return { installed: true, path: targetPath, backup: backupPath, assets_synced: assetsSynced }
55
90
  }
@@ -81,15 +81,32 @@ export function parseCodexMessages(filePath) {
81
81
  return capTurns(out)
82
82
  }
83
83
 
84
- export function codexCwd(filePath) {
84
+ // rollout 첫 non-empty 줄의 session_meta.payload 에서 sweep 판정에 쓰는 필드를 한 번에 뽑는다.
85
+ // spec 2026-07-20-codex-exec-prompt-pollution §4.2 — cwd 만 읽던 것을 originator/source 까지 확장.
86
+ // 파싱 실패·session_meta 부재는 빈 값(fail-open) — 캡처 누락보다 과캡처가 낫다는 기존 sweep 기조.
87
+ export function codexSessionMeta(filePath) {
85
88
  try {
86
89
  const fd = fs.openSync(filePath, 'r'); const buf = Buffer.alloc(65536)
87
90
  const n = fs.readSync(fd, buf, 0, 65536, 0); fs.closeSync(fd)
88
91
  const first = buf.toString('utf8', 0, n).split('\n').find(l => l.trim())
89
92
  const j = JSON.parse(first)
90
- if (j.type === 'session_meta' && j.payload && j.payload.cwd) return j.payload.cwd
93
+ if (j.type === 'session_meta' && j.payload) {
94
+ const p = j.payload
95
+ return { cwd: p.cwd || '', originator: p.originator || '', source: p.source || '' }
96
+ }
91
97
  } catch {}
92
- return ''
98
+ return { cwd: '', originator: '', source: '' }
99
+ }
100
+
101
+ export function codexCwd(filePath) {
102
+ return codexSessionMeta(filePath).cwd
103
+ }
104
+
105
+ // `codex exec` = 비대화형 one-shot. 차차(에이전트)가 도구로 부른 호출이지 사람의 대화가 아니다.
106
+ // 이 rollout 의 role='user' 블록은 에이전트가 쓴 프롬프트라, 저장하면 두목 발화로 오귀속된다.
107
+ // 사람의 Codex 사용(Codex Desktop·codex-tui)은 여기 걸리지 않으므로 그대로 캡처된다.
108
+ export function isCodexExecRollout(meta) {
109
+ return meta.originator === 'codex_exec' || meta.source === 'exec'
93
110
  }
94
111
 
95
112
  // Claude transcript jsonl 의 첫 cwd 필드 (없으면 ''). codexCwd 와 대칭 — 이슈② repo 역산용.
@@ -393,13 +410,21 @@ export async function recoverCodex({ endpoint, token, userId, sessionsDir, hostn
393
410
  for (const fp of cands) {
394
411
  if (Date.now() >= deadline) break
395
412
  const sid = sessionIdFromPath(fp, 'codex'); if (!sid || deferBlocked(sid, codexOpts)) continue
396
- const messages = parseCodexMessages(fp); if (!messages.length) continue
397
- const repoPath = codexCwd(fp) || undefined
413
+ // spec 2026-07-20-codex-exec-prompt-pollution §4.2.1 — session_meta 를 먼저 읽는다.
414
+ // 메시지 파싱은 skip 대상이 아닐 때로 미룬다(계약 순서 = 코드 순서, 불필요한 파싱 제거).
415
+ const meta = codexSessionMeta(fp)
416
+ const repoPath = meta.cwd || undefined
398
417
  // B6-4: Claude 경로(recoverProject)와 대칭 — cwd 불명·디렉토리 소실 보류 + 타 config 소속 skip.
399
418
  if (!repoPath || !fs.existsSync(repoPath)) continue
400
419
  if (opts.activeConfigPath && configPathForCwd(repoPath) !== opts.activeConfigPath) continue
401
420
  // B6-3: identity pin 불일치 보류 (recoverProject 와 대칭).
402
421
  if (identityPinMismatch(sid, token, opts)) continue
422
+ // §4.2.1 — 격리 게이트(위 3줄)를 **통과한 뒤에만** 소비 완료 처리한다. 앞에서 marker 를 박으면
423
+ // 타 계정 소유 세션을 현재 config 가 소비해버려 B6-4 를 깬다.
424
+ // marker 없이 그냥 continue 하면 매 sweep 재후보가 되어 최신순+cap 아래 사람 세션을 굶긴다.
425
+ // 재시도 대상이 아니므로 pending·deferred·dead-letter 는 남기지 않는다.
426
+ if (isCodexExecRollout(meta)) { markBaseline(sid, 'codex_exec', codexOpts); continue }
427
+ const messages = parseCodexMessages(fp); if (!messages.length) continue
403
428
  if (await recoverOne({ endpoint, token, userId, fp, sid, messages, hostname, repoPath, opts: codexOpts })) count++
404
429
  }
405
430
  return count
@@ -0,0 +1,123 @@
1
+ #!/bin/bash
2
+ # 작업 착수 회상 보정 넛지 — spec docs/superpowers/specs/2026-07-22-pre-work-recall-gate.md §D3
3
+ # + 되묻기 전 회상 게이트 — docs/superpowers/plans/2026-07-24-ask-before-recall-gate.md
4
+ #
5
+ # 한 스크립트가 세 갈래를 맡는다:
6
+ # PostToolUse(Bash) → 명령이 실제 recall 호출이면 `.recalled` 마커 기록
7
+ # PreToolUse(Edit|Write) → 마커가 없으면(이 세션 recall 0회) 세션당 1회 넛지 주입
8
+ # PreToolUse(AskUserQuestion) → soft-deny(pass-토글): 1차 deny + 회상 재확인 지시, 재시도 통과
9
+ #
10
+ # ⚠️ Edit/Write 는 **보정 넛지**다(additionalContext = 다음 모델 입력 리마인더, 차단 아님).
11
+ # AskUserQuestion 만 permissionDecision deny — 질문은 사용자에게 나가는 순간이 사고라
12
+ # 사후 리마인더로는 못 막는다(2026-07-24: recall 해놓고 안 믿고 되물은 사고, codex R1 P1).
13
+ # "착수 *전*" 판정은 SKILL.md 의 작업 착수 전 회상 게이트(규범)가 맡는다 — 이 훅은 하한선.
14
+ #
15
+ # 한계(설계 명시): 세션 단위로만 본다. 세션 초반에 무관한 recall 1회를 했으면 이후 다른 기능을
16
+ # 수정해도 침묵한다. 대상 단위 판정은 규범 축 담당.
17
+ #
18
+ # IMPORTANT: set -e 금지. 차단 절대 X — 항상 exit 0.
19
+ set +e
20
+
21
+ # self-reference 방지: 차차 internal LLM 호출에서 발사된 hook 은 즉시 종료.
22
+ [ "${ROOKIE_INTERNAL_INVOCATION:-}" = "1" ] && exit 0
23
+
24
+ INPUT=$(cat 2>/dev/null) || exit 0
25
+ [ -z "$INPUT" ] && exit 0
26
+ command -v node >/dev/null 2>&1 || exit 0
27
+
28
+ GATE_DIR="${ROOKIE_RECALL_GATE_DIR:-$HOME/.rookie/recall-gate}"
29
+
30
+ # payload 는 tool_response 까지 포함해 커질 수 있다 — argv 금지, stdin 으로 넘긴다(ARG_MAX).
31
+ printf '%s' "$INPUT" | ROOKIE_GATE_DIR="$GATE_DIR" node -e '
32
+ let raw="";
33
+ process.stdin.on("data",c=>raw+=c).on("end",()=>{
34
+ const MSG="✋ 이 세션에서 recall 을 한 번도 안 한 채 파일 수정에 들어갑니다. 이 파일·기능이 전에 하던 작업과 이어지는 것이라면 GET /api/rookie/recall 로 먼저 확인하세요 — 이미 고친 걸 다시 고치는 사고의 방지선입니다.";
35
+ try{
36
+ const fs=require("fs"), path=require("path"), crypto=require("crypto");
37
+ const gateDir=process.env.ROOKIE_GATE_DIR;
38
+ let j; try{ j=JSON.parse(raw); }catch{ return; }
39
+ if(!j || typeof j!=="object") return;
40
+
41
+ const sid=String(j.session_id||j.sessionId||process.env.CLAUDE_SESSION_ID||"");
42
+ if(!sid) return; // sid 없음 → 미주입(공유 마커로 영구 침묵하는 것보다 안전)
43
+ const key=crypto.createHash("sha256").update(sid).digest("hex");
44
+ const tool=j.tool_name;
45
+ const input=j.tool_input||{};
46
+
47
+ // 마커 원자적 생성 — 같은 batch 에 Edit 가 병렬로 여러 개 와도 한 번만 통과한다.
48
+ const claim=(name)=>{
49
+ try{
50
+ fs.mkdirSync(gateDir,{recursive:true,mode:0o700});
51
+ fs.writeFileSync(path.join(gateDir,key+name),JSON.stringify({session_id:sid}),{flag:"wx",mode:0o600});
52
+ return true;
53
+ }catch{ return false; } // 이미 있음(EEXIST) 또는 쓰기 실패 → 통과 못 함
54
+ };
55
+ const has=(name)=>{ try{ return fs.existsSync(path.join(gateDir,key+name)); }catch{ return false; } };
56
+
57
+ // ── recall 마커 기록 ────────────────────────────────────────────────
58
+ // MCP 경로(rookie_recall 툴)도 실제 회상이다 — 툴 이름으로 인정(codex 코드 R1 P2).
59
+ if(/recall/i.test(String(tool||""))){ claim(".recalled"); return; }
60
+
61
+ // PostToolUse(Bash): 실제 recall 호출만 인정.
62
+ // "api/rookie/recall" 문자열만 보면 rg 검색·문서 열람도 회상으로 세어지고(설계 R2 P2),
63
+ // curl 동반만 요구하면 `rg "curl.*api/rookie/recall"`·`echo curl …` 도 통과한다(코드 R1 P2).
64
+ // → curl 이 **명령어 자리**에 있어야 하고, 명령이 검색·출력 도구로 시작하면 배제한다.
65
+ // 남는 한계(허용): 셸 파서가 아니라 문자열 스캔이라, heredoc·따옴표 안의 "&& curl …/recall" 은
66
+ // 실제 호출 없이 마커를 만든다. 넛지 1건을 놓치는 비용뿐이고 셸 파싱 없이는 못 가른다.
67
+ if(tool==="Bash"){
68
+ const cmd=String(input.command||"");
69
+ // 구분자 뒤 env 대입(TOKEN=x)·sudo 옵션·개행까지 허용 — 전부 실제 command-position curl 이다(코드 R2 P2).
70
+ const curlIsCommand=/(^|[;&|(\n]|&&|\|\|)[ \t]*(?:\w+=\S*[ \t]+)*(?:sudo(?:[ \t]+-\w+)*[ \t]+)*curl\b/.test(cmd);
71
+ const startsWithProbe=/^\s*(rg|grep|egrep|fgrep|ag|ack|echo|printf|cat|less|more|head|tail|sed|awk|find)\b/.test(cmd);
72
+ if(curlIsCommand && !startsWithProbe && /api\/rookie\/recall/.test(cmd)) claim(".recalled");
73
+ return; // Bash 는 넛지 대상 아님
74
+ }
75
+
76
+ // ── PreToolUse(AskUserQuestion): soft-deny pass-토글 ────────────────────
77
+ // 되묻기는 나가는 순간이 사고 — 1차는 deny + 회상 재확인 지시, 재시도(.ask-pass 소비)는 통과.
78
+ // plan 2026-07-24 §4.1. deny 는 이 정상 판정 경로에서만 — 예외·IO 실패는 전부 통과(fail-safe).
79
+ if(tool==="AskUserQuestion"){
80
+ const DENY_A="✋ 되묻기 전에 회상부터: 이 세션에서 recall 을 한 번도 안 했습니다. 물으려는 내용으로 GET /api/rookie/recall 을 먼저 호출해 관련 있는 최신 결과가 있는지 확인하세요. 없거나 0건이면 그대로 다시 AskUserQuestion 을 호출하세요 — 이번 한 번만 멈춥니다.";
81
+ const DENY_B="✋ 이 세션에서 이미 recall 을 했습니다. 지금 물으려는 내용의 답이 그 회상 결과에 이미 있진 않은지 다시 보세요 — 단, 그 결과가 이 주제와 무관하거나 0건이었다면 이 주제로 재-recall 하세요. 회상해 놓고 안 믿고 되묻는 것이 \"처음인 것처럼\" 사고의 핵심입니다. 확인 후에도 답이 없으면 그대로 다시 AskUserQuestion 을 호출하세요 — 이번 한 번만 멈춥니다.";
82
+ const passPath=path.join(gateDir,key+".ask-pass");
83
+ const NOT_BEFORE=2000; // 같은 batch 병렬 형제가 방금 만든 pass 즉시 소비 방지(codex R2 P2)
84
+ const TTL=10*60*1000; // deny 후 재시도 없이 방치된 stale pass 무검사 통과 방지(codex R2 P2)
85
+ let st=null; try{ st=fs.statSync(passPath); }catch{}
86
+ if(st){
87
+ const age=Date.now()-st.mtimeMs;
88
+ if(age>=NOT_BEFORE && age<=TTL){
89
+ // unlink 성공 = 1회 통과권의 원자적 소비(코드검토 R1 P2). ENOENT 만 "형제가 먼저
90
+ // 소비" — pass 없는 상태로 재판정(아래 발급+deny). 그 외(EACCES·EROFS 등 실제 IO
91
+ // 실패)는 소비 불가능한 pass 가 남아 deny 가 무한 반복된다 → fail-safe 통과(R2 P1).
92
+ try{ fs.unlinkSync(passPath); return; }
93
+ catch(e){ if(e&&e.code==="ENOENT"){ st=null; } else return; }
94
+ }
95
+ else if(age>TTL){
96
+ // stale 폐기도 동일 — 못 지우는 stale pass 를 남긴 채 deny 하면 영구 봉쇄(R2 P1).
97
+ try{ fs.unlinkSync(passPath); st=null; }
98
+ catch(e){ if(e&&e.code==="ENOENT"){ st=null; } else return; }
99
+ }
100
+ // age<NOT_BEFORE: 병렬 형제 — pass 는 남기고 이 호출도 deny (아래로)
101
+ }
102
+ if(!st){
103
+ // pass 발급(wx). 발급 실패 + 실존도 안 하면(gate dir 권한·FS 오류) deny 금지 —
104
+ // 재시도 통과권 없는 deny 는 AskUserQuestion 을 영구 봉쇄한다(코드검토 R1 P1). fail-safe 통과.
105
+ let issued=false;
106
+ try{ fs.mkdirSync(gateDir,{recursive:true,mode:0o700}); fs.writeFileSync(passPath,JSON.stringify({session_id:sid}),{flag:"wx",mode:0o600}); issued=true; }catch{}
107
+ if(!issued && !has(".ask-pass")) return;
108
+ }
109
+ const reason=has(".recalled")?DENY_B:DENY_A;
110
+ process.stdout.write(JSON.stringify({hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:reason}}));
111
+ return;
112
+ }
113
+
114
+ // ── PreToolUse(Edit|Write): recall 0회면 세션당 1회 넛지 ────────────────
115
+ if(tool!=="Edit" && tool!=="Write" && tool!=="NotebookEdit") return;
116
+ if(has(".recalled")) return; // 이 세션에서 이미 회상함 → 조용
117
+ if(!claim(".nudged")) return; // 이미 넛지함(또는 마커 못 씀) → 조용
118
+ process.stdout.write(JSON.stringify({hookSpecificOutput:{hookEventName:"PreToolUse",additionalContext:MSG}}));
119
+ }catch{ /* silent — fail-safe. tool 흐름 차단 절대 X */ }
120
+ });
121
+ ' 2>/dev/null
122
+
123
+ exit 0
@@ -103,6 +103,15 @@ codex_recover() {
103
103
  if command -v identity_pin_check >/dev/null 2>&1 && ! identity_pin_check "$sid" "$token"; then
104
104
  _orphan_log_telemetry "$sid" "codex_skip_identity_mismatch"; continue
105
105
  fi
106
+ # spec 2026-07-20-codex-exec-prompt-pollution §4.2.1 — 격리 게이트(위 cwd·config·identity)를
107
+ # **통과한 뒤에만** 소비 완료 처리한다. 앞에서 marker 를 박으면 타 계정 소유 세션을 현재
108
+ # config 가 소비해버려 B6-4 를 깬다. 반대로 marker 없이 continue 하면 매 sweep 재후보가 되어
109
+ # 최신순+cap 아래 사람 세션(Desktop·tui) 회수를 굶긴다.
110
+ # 재시도 대상이 아니므로 pending·deferred 는 남기지 않는다.
111
+ if transcript_codex_is_exec "$fp"; then
112
+ orphan_mark_baseline "$sid" "codex_exec"
113
+ _orphan_log_telemetry "$sid" "codex_skip_agent_invocation"; continue
114
+ fi
106
115
  messages="$(transcript_messages_json_codex "$fp")"
107
116
  mc="$(_orphan_messages_count "$messages")"
108
117
  if [ "${mc:-0}" -le 0 ] 2>/dev/null; then
@@ -70,11 +70,18 @@ orphan_mark_flushed() {
70
70
  "$(printf '{"status":"flushed","source":"%s","message_count":%s}' "$source" "$mc")"
71
71
  }
72
72
 
73
+ # reason 은 감사 기록용 — 생략 시 기존 동작(bootstrap_old) 유지.
74
+ # spec 2026-07-20-codex-exec-prompt-pollution §4.2.1: codex_exec skip 은 reason 을 따로 남긴다
75
+ # (bootstrap_old 를 그대로 쓰면 거짓 감사 기록이 된다).
73
76
  orphan_mark_baseline() {
74
- local sid="$1"
77
+ local sid="$1" reason="${2:-bootstrap_old}"
75
78
  [ -n "$sid" ] || return 1
79
+ # reason 은 marker JSON 에 그대로 박히므로 안전 문자만 남긴다 — 따옴표·백슬래시가 섞이면
80
+ # marker 가 깨져 파싱하는 쪽(scanner·telemetry)이 오작동한다 (codex 코드검토 R1 P2).
81
+ reason="$(printf '%s' "$reason" | tr -cd 'A-Za-z0-9_.:-')"
82
+ [ -n "$reason" ] || reason="unknown"
76
83
  _orphan_write_marker "$(_orphan_baseline_dir)" "$sid" \
77
- '{"status":"baseline_skipped","reason":"bootstrap_old"}'
84
+ "$(printf '{"status":"baseline_skipped","reason":"%s"}' "$reason")"
78
85
  }
79
86
 
80
87
  # P0.4 — 짧고 내용 없는(low_value) 세션 marker. record_pending 대신 호출 → 재적재 차단.
@@ -142,6 +142,31 @@ transcript_messages_json_codex() {
142
142
  " "$path" 2>/dev/null || echo '[]'
143
143
  }
144
144
 
145
+ # Codex rollout 이 `codex exec`(비대화형 one-shot)로 생성됐는지. exit 0 = 그렇다.
146
+ # spec 2026-07-20-codex-exec-prompt-pollution §4.2 — codex exec 는 차차가 도구로 부른 호출이라
147
+ # 그 안의 role='user' 블록은 에이전트가 쓴 프롬프트다(두목 발화 아님). 사람의 Codex 사용
148
+ # (Codex Desktop·codex-tui)은 여기 걸리지 않는다.
149
+ # ⚠️ fail-open 범위: session_meta 부재·파싱 실패뿐 아니라 **node 실행 실패도** 1(= exec 아님)로
150
+ # 떨어진다(codex 코드검토 R1 P2). 실무상 노출은 거의 없다 — node 가 없거나 깨졌으면 앞선
151
+ # transcript_codex_cwd 가 먼저 빈 값을 반환해 codex_skip_unknown_cwd 로 걸러지고,
152
+ # 그마저 통과해도 transcript_messages_json_codex 가 빈 배열을 내 skip 된다.
153
+ # 과캡처(사람 대화 유실 없음) 쪽으로 여는 것이 기존 sweep 기조와도 일치한다.
154
+ transcript_codex_is_exec() {
155
+ local path="$1"
156
+ [ -f "$path" ] || return 1
157
+ node -e "
158
+ const fs=require('node:fs');
159
+ try{
160
+ const fd=fs.openSync(process.argv[1],'r'); const buf=Buffer.alloc(65536);
161
+ const n=fs.readSync(fd,buf,0,65536,0); fs.closeSync(fd);
162
+ const first=buf.toString('utf8',0,n).split('\n').find(l=>l.trim());
163
+ const j=JSON.parse(first);
164
+ const p=(j.type==='session_meta'&&j.payload)?j.payload:{};
165
+ process.exit((p.originator==='codex_exec'||p.source==='exec')?0:1);
166
+ }catch{ process.exit(1); }
167
+ " "$path" 2>/dev/null
168
+ }
169
+
145
170
  # Codex rollout 첫 줄 session_meta 의 cwd (프로젝트 매칭용). 첫 64KB 만 읽음.
146
171
  transcript_codex_cwd() {
147
172
  local path="$1"
@@ -32,6 +32,20 @@
32
32
  "hooks": [
33
33
  { "type": "command", "command": "__GUARD_DESTRUCTIVE_DB_CMD__" }
34
34
  ]
35
+ },
36
+ {
37
+ "matcher": "Edit|Write|NotebookEdit",
38
+ "__rookie_managed": true,
39
+ "hooks": [
40
+ { "type": "command", "command": "__WORK_RECALL_GATE_CMD__" }
41
+ ]
42
+ },
43
+ {
44
+ "matcher": "AskUserQuestion",
45
+ "__rookie_managed": true,
46
+ "hooks": [
47
+ { "type": "command", "command": "__WORK_RECALL_GATE_CMD__" }
48
+ ]
35
49
  }
36
50
  ],
37
51
  "PostToolUse": [
@@ -41,6 +55,13 @@
41
55
  "hooks": [
42
56
  { "type": "command", "command": "__POSTTOOL_RECALL_NUDGE_CMD__" }
43
57
  ]
58
+ },
59
+ {
60
+ "matcher": "Bash|.*rookie_recall",
61
+ "__rookie_managed": true,
62
+ "hooks": [
63
+ { "type": "command", "command": "__WORK_RECALL_GATE_CMD__" }
64
+ ]
44
65
  }
45
66
  ],
46
67
  "UserPromptSubmit": [
@@ -42,6 +42,9 @@ cat | ROOKIE_GATE_DIR="$GATE_DIR" node -e '
42
42
  const SKILL_MSG="⚠️ 이 세션에서 아직 rookie Skill 을 로드하지 않았습니다. 다른 작업·응답 전에 먼저 rookie Skill 을 호출하세요 — 회상(recall)·미link 등록 제안·기억 저장 절차가 그 안에 있습니다. (이미 호출했다면 이 안내는 사라집니다.)";
43
43
  const RECALL_MSG="🧠 이 프롬프트에 과거 맥락·방법 회상 신호가 있습니다. grep·파일 열람·자기판단·두목 되묻기 이전에 GET /api/rookie/recall 을 먼저 호출하세요 (brand.ts 회상-순서 룰).";
44
44
  const CKPT_MSG="📌 이 세션이 길어지고 있습니다. 직전 checkpoint 이후 마일스톤(커밋·배포·설계 수렴·단계 전환)이 지났다면 checkpoint 진입카드를 저장하세요 — 지나지 않았다면 무시해도 됩니다.";
45
+ // 미로드 + 회상신호 전용(spec 2026-07-22 D1). 두 메시지를 잇기만 하면 "먼저 Skill" vs "먼저 recall" 이
46
+ // 충돌하고, recall 절차 자체가 SKILL.md 안에 있어 미로드 세션은 그 절차를 아직 못 읽었다 — 순서를 못박는다.
47
+ const COMBO_MSG="🧠 이 프롬프트에 과거 맥락·방법 회상 신호가 있는데, 이 세션은 아직 rookie Skill 을 로드하지 않았습니다. 순서대로 하세요 — ① rookie Skill 을 먼저 호출하고 ② 그 안의 회상 절차로 이번 프롬프트에 대해 GET /api/rookie/recall 을 최소 1회 호출한 뒤 ③ 답하세요. grep·파일 열람·git log 추론·두목 되묻기는 그 다음입니다.";
45
48
  const injectMsg=(msg)=>{ try{ process.stdout.write(JSON.stringify({hookSpecificOutput:{hookEventName:"UserPromptSubmit",additionalContext:msg}})); }catch{} };
46
49
  try{
47
50
  const fs=require("fs"), path=require("path"), crypto=require("crypto");
@@ -51,15 +54,20 @@ cat | ROOKIE_GATE_DIR="$GATE_DIR" node -e '
51
54
  const tp=(p.transcript_path||"").toString();
52
55
  const prompt=(p.prompt||"").toString();
53
56
  // recall 신호(과거 방법 재현) — 과거형 집중. 현재형(구현해)·미래순서(배포 전에/이전에)·조건형(했으면) 제외.
54
- const PAST_TIME=/어제|그때|저번|지난번|예전/;
57
+ // spec 2026-07-22 D2 — 수량+단위+"전" 추가. 뒤에 조사·경계만 허용해 합성어(전용·전송) 배제,
58
+ // 수량 없는 "배포 전에"(미래 순서)는 여전히 미탐.
59
+ const PAST_TIME=/어제|그때|저번|지난번|예전|아까|(방금|(\d+|한|두|두어|세|네|몇)\s*(분|시간|일|주|주일|달|개월|년))\s*전(?=[\s,.!?)\]]|$|에|부터|까지|의|엔|은|는|이|가|도|만|과|와|랑)|(전|이전)에\s*(만든|만들었|했던|하던|쓰던|썼던|정한|결정한|작업한|구현한|짠)/;
55
60
  const PAST_HOW=/어떻게[\s\S]{0,15}?(했|였|만들었|제작했|구현했|처리했|뽑았|짰|썼|캡[처쳐]했)(?!으면|으려|으실|으세|었으면)|어떻게\s*(하더라|했더라|됐)/;
56
- const REPRO=/재현|기억\s*나|했었(?!으면)/;
61
+ const REPRO=/재현|기억\s*나(?!면|다면|시|실|셔|세)|했었(?!으면)|했잖|하지\s*않았(?:나|어|었)/;
62
+ // spec 2026-07-22 D2 — 두목이 회상 실패를 지적하는 발화 자체가 최우선 트리거(최후 안전망).
63
+ // 후행 배제(면|다면|시|실|셔|세)로 조건형·경어체("기억 안 나면 물어봐", "기억 못 하시면") 제외.
64
+ const AMNESIA=/기억(을|은|이)?\s*(못|안)\s*(하|해|되|나|난)(?!면|다면|시|실|셔|세)|까먹(?!지)|처음\s*(인\s*것|인|것)\s*처럼|또\s*(잊(?!지)|까먹(?!지))/;
57
65
  // spec 2026-07-10 §3.B1 — SKILL.md 회상 표와 정합 확장(작업 연속·결정 회상·환경 질의).
58
66
  const CONTINUE=/이어서|이어가|계속\s*(하|해|진행)|마저|어디까지|재개|하던\s*(거|것|작업)/;
59
67
  const DECIDED=/뭐였|뭐더라|왜\s*였|왜\s*그랬|어떻게\s*됐|결정했|정했었/;
60
68
  // ENVQ 는 명사 단독 매칭 금지(코드 얘기 중 "포트" 빈발) — 의문 신호 동반 필수.
61
69
  const ENVQ=/(포트|유저명|계정|경로|도메인|호스트|엔드포인트|서버\s*주소)[^\n]{0,12}(뭐|몇|어디|알아|기억|였)/;
62
- const recallSignal = PAST_TIME.test(prompt)||PAST_HOW.test(prompt)||REPRO.test(prompt)||CONTINUE.test(prompt)||DECIDED.test(prompt)||ENVQ.test(prompt);
70
+ const recallSignal = PAST_TIME.test(prompt)||PAST_HOW.test(prompt)||REPRO.test(prompt)||CONTINUE.test(prompt)||DECIDED.test(prompt)||ENVQ.test(prompt)||AMNESIA.test(prompt);
63
71
  const key= sid ? crypto.createHash("sha256").update(sid).digest("hex") : "";
64
72
  let skillLoaded=false;
65
73
  if(key && fs.existsSync(path.join(gateDir,key+".loaded"))){ skillLoaded=true; } // marker = 로드됨 캐시(재scan 회피), early-return 아님
@@ -96,7 +104,10 @@ cat | ROOKIE_GATE_DIR="$GATE_DIR" node -e '
96
104
  fs.writeFileSync(pf,String(n),{mode:0o600});
97
105
  }catch{ ckptDue=false; } // 카운터 실패 → 조용히 skip (fail-soft)
98
106
  }
99
- if(!skillLoaded) return injectMsg(SKILL_MSG); // 미로드 → skill 리마인더 (recall 리마인더 안 함, 한 turn 한 메시지)
107
+ // spec 2026-07-22 D1 — 미로드 세션은 SKILL.md 회상 규범이 컨텍스트에 없다. 여기서 recall 지시까지
108
+ // 꺼버리면 회상이 가장 필요한 상태가 무방비가 된다(2026-07-22 catsper 사고). 한 turn 한 메시지는 유지.
109
+ if(!skillLoaded && recallSignal) return injectMsg(COMBO_MSG);
110
+ if(!skillLoaded) return injectMsg(SKILL_MSG); // 미로드 + 무신호 → skill 리마인더
100
111
  if(recallSignal) return injectMsg(RECALL_MSG); // 로드됨 + 과거-방법 신호 → recall 리마인더
101
112
  if(ckptDue) return injectMsg(CKPT_MSG); // 로드됨 + 무신호 + 임계 도달 → checkpoint 리마인더
102
113
  return; // 로드됨 + 신호 없음 → 조용
@@ -9,6 +9,12 @@ installed_by: __INSTALLED_BY__
9
9
 
10
10
  # Rookie 연결 스킬
11
11
 
12
+ > **references/ 분리 규칙**: 저빈도 절차는 이 스킬 폴더의 `references/` 에 있다. 아래 신호가 보이면 **반드시 해당 파일을 Read 하고 절차 전부를 그대로** 수행한다 — 요지 기억만으로 축약 실행 금지.
13
+ > - ``🛟 복구 세션`` 블록 / ``🍞 컨텍스트 다이어트 권고`` 블록 → `references/recovery.md`
14
+ > - 스펙 동기화 directive(`[루키 지시]` — link 후속·소개글·scan-specs) → `references/link.md`
15
+ > - `rookie save` 본문 작성 전(세션 첫 저장 전 1회 — 스키마·all_committed 게이트·supersede 처리) → `references/save.md`
16
+ > - task status 갱신·세션 마무리 reconcile·`## 🎯 완료 후보 감지` 블록·계층 도출 → `references/tasks.md`
17
+
12
18
  ## ⚡ 로드 직후 필수 (다른 응답 전에)
13
19
 
14
20
  진입카드·미link 상태는 이 skill 이 로드한다 — **다른 말 전에** 아래 "세션 시작" 절차를 실행한다:
@@ -16,7 +22,7 @@ installed_by: __INSTALLED_BY__
16
22
  1. `GET /api/rookie/context` 를 curl 로 호출해 `system_prompt`·`current_project`·`entry_cards` 전문을 받는다.
17
23
  2. **진입 카드 확인:**
18
24
  - **1순위**: `entry_cards[]` 를 직접 읽는다. 비어있지 않으면 **그 작업을 이어가려는 사용자 의도를 우선 가정**해 먼저 언급한다. 안전 신호 — `grade` `warn`/`old` → 이미 완료됐을 수 있으니 현재 상태부터 점검 / `is_meta_card` → 메타 카드 의심·단일 fact 분리 권고·`body` 절단본(전량 단정 금지) / `is_vague` → 목표 수준 카드 — recall 로 방법 보강 후 착수 / `has_contradiction` → 단정 전 점검 / `reasoning` → 근거로 고려.
19
- - **activity-stale**: `activity_stale=true`(또는 `newer_activity_count>0`) → 카드 이후 같은 프로젝트에서 세션이 더 진행됐다. `effective_grade`(age·activity 중 높은 등급)를 권위 기준으로, recall + 현재 상태(git log·해당 파일) 점검 후 착수. "이게 마지막 작업" 단정 금지.
25
+ - **activity-stale**: `activity_stale=true`(또는 `newer_activity_count>0`) → 카드 이후 같은 프로젝트에서 세션이 더 진행됐다. `effective_grade`(age·activity 중 높은 등급)를 권위 기준으로, recall + 현재 상태(git log·해당 파일) 점검 후 착수. "이게 마지막 작업" 단정 금지. **단 이 신호는 "카드 이후 활동이 있었다"는 뜻이지 "카드 내용이 틀렸다"는 뜻이 아니다** — 카드를 대체할 내용은 `ephemeral_recovery` 또는 카드 `recorded_at` 보다 최신인 회상 결과로만 확인한다(아래 "진입 카드 = 최신성 기준선").
20
26
  - **`ephemeral_recovery`(임시 복구, 저장 아님)**: activity-stale 카드에 붙는 read-time 합성물. `id` 없음 — `supersedes`·`task_id` 연결 금지. stale 카드와 **함께** 표시("직전 카드(오래됨) + 🛟 임시 복구: <headline>"). `synthesis_tier=bare`=세부 기록 없음으로만, `evidence`="바뀐 것으로 보고됨"(확인 아님). 진짜 다음 카드를 저장하면 다음 조회부터 사라진다.
21
27
  - **폴백**(구버전 서버): `system_prompt` 의 `## 🔖 진행 중 작업 카드` 마커를 grep(유동 위치 — head/tail 훑기 금지). 텍스트 경로엔 `🔀 세션 N건`·`🛟 임시 복구` 라인으로 동기.
22
28
  - **무-주제 "이어서/계속하자"** 는 `entry_cards`(최신순) 우선. relevance recall 은 특정 주제를 명시했을 때만.
@@ -44,43 +50,11 @@ installed_by: __INSTALLED_BY__
44
50
  - 없으면 **매 세션 1회** "지금 등록할까요? ('그만 물어봐' 가능)" 제안 — git remote 있는 repo 면 특히.
45
51
  - **승인 시** 루키가 `rookie link` 직접 실행 + 1줄 보고. **거부 시** link-dismissed.json 에 cwd 추가. dismiss 후에도 "등록해줘" 하면 즉시 실행.
46
52
  - 자동 등록 금지(승인 전제). recall 0건(신규면 정상)과 미link 는 다른 신호 — **후자만** 알린다.
47
-
48
- 6.5. **link 후속 — 스펙 연결·소개글**: `system_prompt` 에 스펙 동기화 directive(`[루키 지시]`)가 있을 때만 능동 제안한다(판정은 서버 책임). **미연결** directive → "스펙을 연결할까요?" → 승인 시 (A)+(C) / **반쪽** directive → "동기화할까요?" → 승인 시 (A) 의 task 동기화만(PUT 생략).
49
- - **(A) 스펙 연결·동기화**: 로컬 스펙 문서를 찾아 `spec_doc_paths` 설정 + 문서별 task 동기화(문서 1개=task 1개). **task 는 전부 todo 시작(스펙 ≠ 구현)** — sync 에 `status` 금지(서버가 todo 고정), 재동기화해도 기존 status 보존. 진척은 `PATCH /api/rookie/tasks`.
50
- - 미연결일 때만: `PUT /api/rookie/projects/spec-source {"project_id":"<id>","spec_doc_paths":["docs/specs/**"]}`
51
- - 공통: `POST /api/rookie/tasks/sync {"project_id":"<id>","items":[{"title":"<스펙 제목>","spec_ref":"<파일경로>","spec_html":"<렌더 HTML>"}]}`
52
- - 스펙 문서가 없으면 "같이 만들까요?" → brainstorming 으로 작성 후 연결.
53
- - **(C) 소개글**: 비어 있으면 README·CLAUDE.md 파악 후 1~2문장 초안을 `PATCH /api/rookie/projects {"id","description"}` — 기존 소개글 덮어쓰기 금지. 완료 시 1줄 보고.
54
- - **거부** 시 `POST /api/rookie/projects/spec-dismiss {"project_id":"<id>","dismissed":true}` (되돌리기는 `false`). 명시 요청은 dismiss 와 무관하게 수행. 미link(§6) → 미스펙(directive) → 정상 3단계 중 해당 단계만 1회 알린다(승인 전제).
55
-
56
- 6.6. **spec 후속 자동 스캔**: link + `spec_doc_paths` 설정 시 세션 시작 1회 `rookie scan-specs`(하루 1회 throttle). 결과는 조용히 둔다(proposal 은 `/projects`). 명시 요청 시 `--force`. 미link·미설정·실패 조용히 skip.
57
-
58
- 7. **임베딩 헬스 경고**: `embedding_health.degraded:true` 면 세션당 1회 — ``⚠️ 회상 임베딩 가동률 {ratio×100}% (최근 {window_days}일 {call_count}건) — 키/quota 점검 필요.`` `false` 면 침묵.
59
- 8. **완료 후보 surface**: `## 🎯 완료 후보 감지` 블록의 `🎯 <task_id(전체 UUID)> "<title>" ← <sha> <subject>` 줄이 보이면 **다른 응답 전에** 후보별 done 승인 게이트로 여쭙는다(UUID 는 PATCH 용, 두목껜 title 만. `<sha>` 는 그대로): `"'<title>' 을 완료로 올릴까요? — 근거: 커밋 <sha> <subject>"` → OK 시에만 `PATCH {id, status:'done', detail:'<기존>\n\n✅ 완료 <date> — 커밋 <sha>'}` (**자동 done 금지**) / "아직/보류" 면 `PATCH {id, detail:'<기존>\n__done_defer:<sha>'}` 로 재알림 차단 — 이후 **다른 새 커밋** 매칭 시만 재surface. 블록 없으면 침묵. 근거는 주입된 git 출력만(self-loop 금지).
60
- 9. 실패(HTTP·네트워크)는 조용히 폴백 — 현재 프로젝트 `CLAUDE.md` 만 따릅니다.
61
-
62
- ## 복구 세션 tail 추출
63
-
64
- 비정상 종료 세션은 Stop hook 이 못 돌아 fact·진입카드가 없다 — chunk 복구는 `orphan_recover`, **fact 추출은 차차 책임**(서버 추출 off). 컨텍스트에 ``🛟 …`` 블록과 ``- <sid> :: <transcript_path>`` 목록이 보이면 **다른 응답 전에** 처리한다:
65
-
66
- 1. `rookie orphan-extract --batch=5` — 대기 세션을 오래된 순으로 읽어 rule-base 초안(요약·파일 경로·git sha, redact 통과)을 뽑는다. transcript 원문 통째 Read 금지(부족 시 보조 — Claude `~/.claude/projects/`, Codex `~/.codex/sessions/`).
67
- 2. 초안에서 **저장 가치가 있는 것만** — 운영 사실/결정→working_card, 파일·결과물→evidence, 명시적 자기규칙→self_rule, 취향→personal_fact. 잡담·민감정보 금지(redactor 원칙). 0건 세션도 정상.
68
- 3. **recall 로 dedup 후** 누락분만 `POST /api/rookie/batch-mutate`(`project_id` 포함).
69
- 4. 처리한 세션마다(0건이어도) 초안 하단의 완료 marker 명령을 **그대로** 실행해 큐에서 비운다 — 안 하면 매 세션 재노출:
70
- ```bash
71
- source "$HOME/.rookie/bin/lib/orphan-recovery.sh" 2>/dev/null && orphan_mark_client_extracted '<sid>'
72
- ```
73
- (로컬 설치면 `<project>/.rookie/bin/…`) Codex 세션은 `ROOKIE_ORPHAN_DIR="…/codex"` prefix. transcript 소실 세션도 marker 로 비운다.
74
- 5. 한 줄 보고(`[복구] 세션 N건 tail 추출·저장`). raw transcript·UUID 나열 금지. 남은 대기는 다음 세션에서.
75
-
76
- ## 컨텍스트 다이어트 (주1회, 승인 게이트)
77
-
78
- SessionStart hook 이 주 1회 임계 초과 시 ``## 🍞 컨텍스트 다이어트 권고`` 블록을 주입한다. 보이면 **다른 응답 전에 1회** 제안, 없으면 침묵. **전 항목 승인 게이트 — 자동 실행 없음.** 승인 시:
79
- - **CLAUDE.md**: 아카이브성 내용 `docs/` 이관 + 참조 1줄 치환. 의미 단위 압축(단순 truncate 금지). diff 제시 → 승인 후 커밋.
80
- - **스킬 정리**: 0회 후보 제시 → 고른 것만. settings 비활성화(가역) 우선 / 폴백 `~/.claude/skills/<name>` → `skills.disabled/` 이동(삭제 아님, symlink·플러그인은 안내만). keep 은 `~/.rookie/context-diet-keep.json`.
81
- - **루키 자기 다이어트**: rookie Skill 문서 중복 정리 — diff 제시 → 승인 후 반영(템플릿 SSOT 커밋 + 재배포).
82
-
83
- **거부/보류 snooze**: `~/.rookie/context-diet-snooze.json` 에 `{"<key>":"<ISO until>"}` 30일(전체 거부 90일. key = claude_md_project·claude_md_total·skills_agents_desc·rookie_self·unused_skills). 실패 항목만 1줄 보고. 현황 `rookie doctor`.
53
+ 7. **link 후속·spec 스캔**: `system_prompt` 에 스펙 동기화 directive(`[루키 지시]`)가 보이면 `references/link.md` 를 Read 후 절차대로(승인 게이트).
54
+ 8. **임베딩 헬스 경고**: `embedding_health.degraded:true` 면 세션당 1회 — ``⚠️ 회상 임베딩 가동률 {ratio×100}% (최근 {window_days}일 {call_count}건) — 키/quota 점검 필요.`` `false` 면 침묵.
55
+ 9. **완료 후보 surface**: `## 🎯 완료 후보 감지` 블록이 보이면 `references/tasks.md` 를 Read 후 **다른 응답 전에** 후보별 done 승인 게이트로 여쭙는다(**자동 done 금지**). 블록 없으면 침묵.
56
+ 10. **복구 세션·다이어트**: ``🛟 …`` 블록(+`- <sid> :: <transcript_path>` 목록) 또는 ``## 🍞 컨텍스트 다이어트 권고`` 블록이 보이면 `references/recovery.md` 를 Read 후 **다른 응답 전에** 그 절차대로 처리한다(다이어트는 전 항목 승인 게이트).
57
+ 11. **세션 시작 컨텍스트 조회** 실패(HTTP·네트워크)는 조용히 폴백 — 현재 프로젝트 `CLAUDE.md` 만 따릅니다. ⚠️ 이 폴백은 **§세션 시작 컨텍스트 조회 한정**이다 — 회상(recall) 실패에는 적용되지 않는다(아래 "회상 중단·실패 시" 참조).
84
58
 
85
59
  ## 회상 (recall)
86
60
 
@@ -99,54 +73,62 @@ curl -s -H "Authorization: Bearer $TOKEN" \
99
73
 
100
74
  - **출처 프로젝트 구분 (필수)**: `project_name` 이 현재 세션 프로젝트와 **다르면** 출처를 밝히고, **현 세션의 "다음 할 일" 후보로 제안하지 않는다**(두목이 명시 언급 시 예외). `null` 은 전역/개인 — "(전역)" 표기.
101
75
  - **같은 주제 "다음:" 카드가 여러 장이면** `memory_at`(ISO 시각) 최신 카드가 현재 상태 기준. 구 카드는 이력으로만.
76
+ - **`newer_related` 신호 (필수)**: 결과 항목에 `newer_related`(더 최신인 의미 유사 LIVE 카드)가 붙어 있으면 그 카드를 **그대로 인용 금지** — `newer_related.summary_head` 를 확인하고, 필요하면 그 주제로 재-recall 해 **최신 카드 기준으로** 답한다. 신·구가 상충하면 최신이 이긴다.
77
+ - **결정·확정류 카드 최신성 교차 확인**: "확정·결정·정책·방향" 문구가 박힌 카드를 근거로 쓰기 전, 같은 주제의 더 최신 카드가 없는지 확인한다(`newer_related` + 서로 다른 recall 응답에서 얻은 카드끼리도 `memory_at` 비교). 옛 "확정"이 이후 결정으로 뒤집혔을 수 있다.
78
+ - **진입 카드 = 최신성 기준선 (필수)**: 회상 결과를 근거로 **진입 카드**를 "낡았다·이미 끝났다·뒤집혔다"고 판정하려면 **두 축 모두**에서 그 결과가 카드보다 최신이어야 하고, 두 시각을 함께 제시해야 한다 — ① 의미 시각: 결과 `observed_at` > 카드 `memory_at` ② 저장 시각: 결과 `memory_at` > 카드 `recorded_at`.
79
+ - **한 축이라도 뒤지거나 `null` 이면 단독 근거로 불충분** — 지연 저장된 옛 기억이 최신으로 보일 수 있다. 실제 상태(git log·해당 파일·API)로 확인한 뒤에만 뒤집는다.
80
+ - **`age_days` 는 축간 판정에 쓰지 않는다** — 카드는 floor 정수, 회상은 0.1 소수, 기준 시각도 별개 요청이다. self_rule 은 시각이 없으면 `age_days=0`(신선해 보임)이라 특히 위험하다.
81
+ - **`kind` 가 `fact` 가 아닌 결과(chunk·evidence·personal·self_rule)는 카드를 뒤집는 근거가 아니다** — 진입 카드와 같은 층위(working card)가 아니다. 배경으로만 쓴다.
82
+ - 카드보다 **오래된** 결과는 카드를 무효화하지 못한다 — 이력·배경으로만 쓴다. 애매하면 **카드를 다음 작업 의도의 기준선으로 신뢰**한다(판정 부담은 카드를 뒤집으려는 쪽에 있다). 단 이는 "카드가 아직 안 끝났다"는 보증이 아니다 — **완료 여부는 `activity_stale`·`ephemeral_recovery`·현재 repo 상태로 별도 점검**한다.
83
+ - **동일 카드 이중 계산 금지**: 결과 `id` 가 진입 카드 `id` 와 같으면 같은 카드다. 별개 근거로 세지 않는다. id 는 대조용 — **UUID 를 화면에 출력하지 않는다**.
102
84
  - **0건 + `meta.fail_closed:true` 자가교정**(조용히 1회 재시도): `missing_project_identifier` → 식별자 추가 또는 `scope=all` / `project_not_linked` → `scope=all`.
103
- - **자기 출력 분석 금지** — 트리거는 사용자 입력에서만(self-loop 위험). **매 turn 호출 금지** — 트리거 등장 시만.
85
+ - **회상 중단·실패 시 — 코드 탐색으로 대체하지 않는다 (필수)**: recall 호출이 인터럽트·에러로 끝났으면 **1회 재시도**한다. 그래도 못 했으면 git log·grep·파일 열람 결과만으로 "언제 누가 무엇을 했다"를 **단정하지 않는다** — "회상을 못 했다"를 밝히고 확인된 범위만 말한다. 세션 시작 §11 의 "조용히 폴백"은 컨텍스트 조회 한정이며 여기에 적용되지 않는다.
86
+ - **두목이 회상 실패를 지적하면 그 자체가 최우선 트리거 (필수)**: "기억 못하네"·"또 처음인 것처럼"·"전에 했잖아"·"또 까먹었어" 류는 **두목이 과거에 그 일이 있었음을 직접 확인해 준 신호**다. 해명·되묻기·코드 탐색보다 recall 이 먼저다. 한 쿼리 0건으로 끝내지 말고 키워드를 바꿔 여러 각도로 회상한다.
87
+ - **자기 출력 분석 금지** — 키워드 트리거는 사용자 입력에서만 탐지(self-loop 위험). **매 turn 호출 금지** — 트리거 등장 시만. 예외: 아래 "결정 제안 전 회상 게이트"는 이미 낸 출력의 재분석이 아니라 **앞으로 낼 제안의 사전 점검**이라 self-loop 이 아니다.
104
88
 
105
- ## 저장 (hot write, batch-mutate 직접 호출)
89
+ ### 결정 제안 전 회상 게이트 (필수)
106
90
 
107
- 카테고리: 운영 사실(서버/포트/유저명/경로/명시 결정, 자동) → working_cards / 능력 증거(파일·결과물 산출 시, 자동) → evidence / 자기 규칙(사용자 명시 시그널 "원칙·습관·항상·절대" 만) → self_rules / 개인 취향(명시 시그널 "좋아해·선호·취향" 만) → personal_fact 별도 경로 / 잡담·일회성·민감 정보 → 저장 안 함.
91
+ 방향·규약·정책·설계·미감 판단을 **새로 정하는 출력을 만들기 직전** — 두목께 여쭙는 형태("A안/B안 중 정할까요", "방향을 먼저 정하고 갈까요")든, 단독 선언 형태("A 방식으로 진행하겠습니다", "이 규약으로 추천드립니다")든 형태 무관:
108
92
 
109
- 저장 전 **recall dedup** — 같은 취지 durable fact 존재 시 새 저장 금지. **재확인 근거가 있으면** `verifications` confirmed 1건(fact 당 하루 1회, 서버 동일 게이트 — working card·근거 없는 회상 제외)으로 반복 신호를 남긴다. 성공 시 `[기억] <요약> 저장` 1줄, 실패 시 retry 1회 + 알림.
93
+ 1. 그 주제로 recall 1회(`scope=all`) — 같은 주제·같은 판단 대상을 이 세션에서 이미 recall 했으면 생략 가능.
94
+ 2. **기존 확정 결정이 나오면 재론 제안 금지** — 그 결정을 먼저 언급하고, 결정의 틀 안에서 실행안만 제안한다. 결정을 뒤집을 근거가 새로 생겼다면 "기존 결정 X가 있는데, Y 때문에 재검토가 필요해 보입니다"로 **기존 결정의 존재를 밝힌 채** 여쭙는다.
95
+ 3. 결정이 없으면(0건) 그대로 제안 진행 — 0건은 정상.
110
96
 
111
- **`rookie:mutate` fence 를 화면에 출력하지 않는다** — 저장은 Bash 직접 호출, 화면엔 `[기억] …` 한 줄만(config 는 `~/.rookie/config.json`, 토큰 출력 금지).
97
+ **제외 (게이트 비발동)**: 되돌리기 쉬운 구현 세부 — 변수·함수명, 커밋 메시지 문구, 로컬 리팩터 방식, 일회성 명령 옵션 등. 기준: 다음 세션에도 구속력이 남는 판단인가 — 남으면 게이트, 안 남으면 제외.
112
98
 
113
- ```bash
114
- rookie save --file <body.json> # 또는: echo '<body>' | rookie save
115
- ```
116
- ⚠️ **`curl` 로 직접 batch-mutate 금지** — `rookie save` 가 cwd·git remote·hostname 을 자동 주입해 전역 누수를 막는다. 본문엔 **기억할 내용만**.
117
-
118
- ⚠️ **전역 fact 는 `project_id: null` 을 명시한다.** 식별자도 명시 의사도 없으면 서버가 `missing_workspace_context` 로 **거부**. 미등록 폴더 저장은 **그 폴더에 격리**, `rookie link` 시 자동 승격.
119
-
120
- `<body>` 스키마 (필요한 키만):
121
- ```json
122
- {
123
- "working_cards": { "items": [ { "summary": "진입 카드 본문", "verification": { "method": "ssh|curl|git|grep|lsof|read|reasoning|assertion", "evidence": "직접 명령 결과 한 줄" }, "supersedes": ["부모 fact UUID"], "tags": ["custom"], "ttl_at": "ISO8601?", "artifact_paths": ["task 매칭용? ≤20 (저장 안 함)"], "project_id": "?", "task_id": "?" } ], "repo_url": "(권장) git remote — 없으면 project 귀속 안 됨", "repo_path": "(권장) repo_url 없을 때 hostname 매칭", "project_id": "(레거시) item 이 우선, 보통 생략", "conversation_id": "uuid?" },
124
- "self_rules": [ { "category": "identity|voice|work_style|strength|preference|rule", "rule": "규칙 본문", "confidence": "high|medium|low", "supersedes": "기존 rule UUID?", "deprecation_reason": "user_override|conflict|noise|manual?" } ],
125
- "evidence": [ { "capability": "능력 진술문", "approach": "접근법?", "status": "completed|in-progress?", "context": "맥락?", "artifact_paths": ["파일 절대경로 (project 폴백 매칭에도 쓰임)"], "project_id": "? (uuid=명시 귀속 / null=전역 / 생략=세션 귀속)" } ],
126
- "verifications": [ { "fact_id": "검증 대상 fact UUID", "method": "(위와 동일 enum)", "outcome": "confirmed|refuted|ambiguous", "evidence": "검증 명령 결과 한 줄" } ]
127
- }
128
- ```
99
+ 디버깅·탐사 중 발견한 문제를 "새 문제"로 프레이밍했더라도, 해결 방향을 정하는 출력을 내는 순간 이 게이트가 걸린다 — 이미 정해진 것을 다시 정하(자고 하)는 사고의 1차 방지선.
100
+
101
+ ### 작업 착수 전 회상 게이트 (필수)
102
+
103
+ 위 게이트의 **형제**다 — 저쪽은 *정하기 전*, 이쪽은 *손대기 전*. 두목의 신고·요청을 받아 **조사·수정·구현에 착수하기 전**, 프롬프트에 과거 단서가 **없어도** 그 대상(기능·화면·파일·증상)으로 recall 1회.
104
+
105
+ 1. **버그·이상 신고** — "X가 안 된다/비어 있다/이상하다" → 착수 전 "이 증상·이 파일을 전에 만졌는가".
106
+ 2. **기능 요청** — "X 만들어줘" → "전에 만들었거나, 시도했다가 접었는가".
107
+ 3. **두목께 되묻기 직전** — 되물을 내용의 답이 이미 기억에 있는지 먼저 본다. **되묻기는 회상 다음이다.**
108
+
109
+ 같은 대상으로 이 세션에서 이미 recall 했으면 생략. **0건은 정상** — 그대로 착수한다.
110
+
111
+ **회상 결과는 소비까지가 회상이다 (필수)**: recall 로 이 주제와 관련 있는 최신 절차·맥락을 확보했다면 **그대로 실행한다** — "혹시 몰라서" 두목께 되묻거나 재조사하는 것은 백지 상태 기본자세의 재발이다. 되묻기는 회상 결과가 이 주제와 무관하거나, 0건이거나, 실행에 명백히 빠진 정보가 있을 때만 한다. (2026-07-24: 실기기 배포 절차를 recall 로 확보하고도 AskUserQuestion 으로 되물어 "처음인 것처럼" 지적받은 사고 — AskUserQuestion 은 훅이 1차 deny 로 멈춰 세운다. deny reason 을 받으면 회상 결과부터 재확인한다.)
112
+
113
+ **제외**: 두목이 방금 준 정보만으로 끝나는 일(오타 수정, 방금 붙여넣은 에러 읽기), 순수 신규 파일 생성, 이 세션에서 이미 다룬 대상.
129
114
 
130
- ### 응답 확인 — `all_committed` 게이트 + 귀속 확인
131
- **귀속 확인**(all_committed 와 무관): `repo_url`/`repo_path` 를 보냈는데 `working_cards.project_attributed=false` 또는 `evidence.unattributed>0` 이면(전역 의도 `project_id:null` 명시 제외) `⚠️ [기억] 프로젝트 미귀속 저장 — rookie link 상태 확인 필요` 1줄 경고.
115
+ > 왜 필요한가: 회상 트리거는 **두목 말투**(어제·전에·이어서)를 본다. 그런데 "세운 슬롯이 비어있어" 같은 **평범한 신고**에는 과거 단서가 없다 — 실제로 그 발화 때문에 몇 시간 전 자기가 고친 버그를 처음 보는 문제로 취급하고 조사에 착수한 사고가 있었다(2026-07-22). 어휘가 아니라 **내가 무엇을 하려는가**가 방아쇠다.
132
116
 
133
- top-level `all_committed` 가 **`true` 일 때만** `[기억] … 저장` 보고. `false` 면:
134
- - **`rejected[]`**(정책 거부 — 재시도 무의미): `meta_card_summary`·`residual_marker`(민감정보)·`self_rule_rejected` — 단일 fact 분리 또는 원인 제거 후 재저장, 민감정보 마커면 포기.
135
- - **`failed[]`** (일시 실패): `insert_failed`·`unexpected_error` 등 — retry 1회, 그래도 실패면 보고.
136
- - 미저장이 남으면 `⚠️ [기억] N건 미저장 — <사유>` 한 줄(raw JSON·UUID 금지). `all_committed:true` 전엔 "저장 완료" 라 하지 않는다.
117
+ ### 현재 상태 주장 검증 게이트 (필수)
137
118
 
138
- > **레거시 폴백**: 직접 curl 불가 환경만 응답 끝 ` ```rookie:mutate ``` ` fence(Stop hook dispatch). 일반 세션 fence 금지.
119
+ 블로커 유무·제원(길이·포맷·버전)·토큰/연결 상태·배포 상태 등 **"지금 그러하다" 류 주장을 하기 전**:
120
+ 1. recall 로 더루키 기억을 먼저 확인하고,
121
+ 2. 30초 내 실측 가능하면(ffprobe·health-check·curl·git log) **실측이 정본**이다.
122
+ - **로컬 문서(md·plan·worklog)만으로 현재 상태를 단정하지 않는다** — 문서는 기록 시점 스냅샷이고 유지가 안 돼 실제와 다른 경우가 많다. 문서 내용을 답에 쓸 때는 "문서상 X (실측 미확인)" 처럼 출처·검증 여부를 구분해 말하고, 확인된 사실처럼 서술하지 않는다. 낡은 문서를 발견하면 정정을 제안한다(자동 수정 금지).
123
+ - **파일 mtime·미커밋 여부로 "미결/진행중" 단정 금지** — 저장소 파일 상태로 작업 상태를 추론하기 전에 recall 먼저.
139
124
 
140
- ### 트리거 조건
141
- - **working_cards**: 다음 세션 진입 카드 — 세션 마무리 + 아래 checkpoint, 매 턴·매 단계 금지. **body 에 `repo_url`/`repo_path` 필수** — 서버가 세션 project 로 자동 귀속, 미link 면 NULL(정상).
142
- - **checkpoint(세션 중간 저장)**: 마일스톤(커밋/push·배포·설계 수렴·큰 단계 전환) 완결 시 `rookie save --json` 으로 카드 1건 — `tags:["checkpoint"]`, 직전 checkpoint 를 `supersedes` 로 닫기(응답 `summary.working_cards.ids[0]` 기억, `superseded_count` 확인). 마무리 저장도 마지막 checkpoint supersede. id 분실 시 recall dedup, 애매하면 supersedes 생략.
143
- - **직전 카드 supersede (필수 습관)**: 같은 작업 흐름(프로젝트·이어지는 제목/파일/브랜치)의 새 "다음:" 카드는 recall dedup 으로 확인한 직전 카드 `id` 를 `supersedes` 에 넣어 닫는다. 애매하면 생략. 프로젝트 불일치 parent 는 서버가 자동 skip.
144
- - **cross-project·전역 예외**: 명백히 다른 프로젝트/전역 fact 는 **생략이 아니라 명시적으로 `project_id: null`**(전역) 또는 그 프로젝트 uuid — 생략은 세션 project 자동 귀속(오귀속). null 도 scope=all 회상에 잡힌다.
145
- - **task 연결**: 확실히 매칭된 task 면 `task_id` 지정 — done 시 카드 자동 은퇴. project_id 는 생략해 task 에서 derive(불일치 시 400). **애매하면** task_id 없이 `__task_link_candidate:<task_id>` 태그만.
146
- - **self_rules**: 사용자 명시 자기 규칙 지시 시만. / **evidence**: 파일·결과물 산출 시. / **verifications**: 루키가 직접 명령으로 fact 검증 시.
125
+ ## 저장 (hot write) — 요지
147
126
 
148
- ### 파일 변경 알림
149
- 파일을 생성·수정했다면: ```json {"files_created":["절대경로"],"files_modified":["절대경로"]} ```
127
+ 카테고리: 운영 사실(서버/포트/유저명/경로/명시 결정, 자동) → working_cards / 능력 증거(파일·결과물 산출 시, 자동) → evidence / 자기 규칙(사용자 명시 시그널 "원칙·습관·항상·절대" 만) → self_rules / 개인 취향(명시 시그널 "좋아해·선호·취향" 만) → personal_fact 별도 경로 / 잡담·일회성·민감 정보 → 저장 안 함.
128
+
129
+ 저장 전 **recall dedup** — 같은 취지 durable fact 존재 시 새 저장 금지. **재확인 근거가 있으면** `verifications` confirmed 1건(fact 당 하루 1회, 서버 동일 게이트 — working card·근거 없는 회상 제외)으로 반복 신호를 남긴다. 성공 시 `[기억] <요약> 저장` 1줄, 실패 시 retry 1회 + 알림.
130
+
131
+ **본문 작성 전 `references/save.md` 를 Read 한다(세션 첫 저장 전 1회 필수)** — 스키마·`all_committed` 게이트·귀속 확인·supersede 후보 처리·checkpoint/durable 분리 규칙이 거기 있다. 호출은 `rookie save --file <body.json>` (curl 직접 batch-mutate 금지, `rookie:mutate` fence 화면 출력 금지, 토큰 출력 금지).
150
132
 
151
133
  ## 세션 마무리 자동 저장 (추출 책임이 루키에게)
152
134
 
@@ -154,31 +136,10 @@ top-level `all_committed` 가 **`true` 일 때만** `[기억] … 저장` 보고
154
136
 
155
137
  1. **다음 진입점 1건** — 미완결 작업·다음 액션·대기 결정을 추론해 working_cards 1건. 본문은 "다음: <바로 실행할 첫 행동>" 시작(목표형 "~이어가기" 금지, 방법 미정이면 "다음: 방법 후보 A/B 중 두목과 결정"). `[고정]`·`[실패]`·`[두목 제약]` 마커 권장 + 파일·명령·근거 1줄(속기·내부기호 금지, 평이한 한국어). 명확히 완결됐으면 생략(0건 정상).
156
138
  2. **미저장 운영 사실·능력 증거·자기 규칙** — recall dedup 후 누락분만.
157
- 3. **프로젝트 task status reconcile** (link 세션만) — `GET /api/rookie/tasks?project_id=<id>` 1회 후, **이번 세션 실제 산물과 사용자 지시**를 task 와 매칭(루키 자기 서술 근거 금지):
158
- - **(가) git 산물 수집 먼저** (git 이 진실):
159
- ```bash
160
- git log --oneline ${SESSION_START_SHA:-HEAD~20}..HEAD
161
- git diff --name-only ${SESSION_START_SHA:-HEAD~20}..HEAD
162
- git status --short # START_DIRTY 제외분만 산물
163
- ```
164
- 커밋·파일·PR 또는 DB 검증 fact 가 매칭 근거 — **양쪽 다 0 이면 어떤 status 도 바꾸지 않는다(특히 done)**. DB 검증만으로 완성되는 작업은 검증 fact 로 done 후보 인정(자동 done 금지 동일). `SESSION_START_SHA` 미캡처 시 폴백 `HEAD~20`(과탐해도 doing 까지만).
165
- - (나) 산물을 task 의 `spec_ref`/`title`/`detail` 과 의미 매칭 — 변경 파일이 `spec_ref` 영역과 겹치면 강한 매칭.
166
- - 작업했는데 todo → `PATCH {id, status:'doing'}` (자동). / 매칭 없음 → `POST /tasks {project_id, title, status:'doing', source:'agent'}`.
167
- - doing 중 근거 실재하는 완성건 → **일괄 1회 보고** `"다음 N개를 완료로 올릴까요? — A(근거…), B(근거…)"` → OK 시에만 done PATCH. "느낌상 완료" 제외.
139
+ 3. **프로젝트 task status reconcile** (link 세션만) — `references/tasks.md` 를 Read 후 그 절차대로(git 산물 수집 → 의미 매칭 → doing 자동 / done 승인 게이트).
168
140
 
169
141
  **오탐 방지**: 작업 중단의 "그만"과 세션 종료의 "그만"을 구분. 애매하면 저장(까먹는 비용 > 중복 비용). 매 turn 저장 금지 — 마무리 1회. **저장 전 recall dedup 필수.**
170
142
 
171
- ## 프로젝트 task 추적 (status 는 루키 전용)
172
-
173
- `/projects` 의 task status 는 **루키가 실제 업무·증거 기반으로만 갱신**한다(수동 토글 없음). 매 turn 금지 — **착수/완료 시점만**. 자기 출력 분석 금지(마무리 reconcile 은 위 §).
174
-
175
- - **진행중 (doing)** — 착수 시 `GET /api/rookie/tasks?project_id=$PID` 후 의미 매칭: 확실 → `PATCH {id, status:'doing'}` + 1줄 알림 / 애매 → "이 작업이 'X' task 맞나요?" 1회 확인 / 매칭 없음 → `POST /tasks {…, status:'doing', source:'agent'}` 신규.
176
- - **완료 (done)** — **자동 done 금지.** `"'X' 를 완료로 올리겠습니다 — 근거: <파일/PR/검증 한 줄>. 올릴까요?"` → OK 시 `PATCH {id, status:'done', detail:'<기존>\n\n✅ 완료 YYYY-MM-DD — <근거>'}`.
177
- - **보류 (blocked)** — `PATCH {id, status:'blocked', blocked_reason:'<사유>'}`. / **재개 (done→doing)** — 추가 작업 발생 시 루키 자율 `PATCH {id, status:'doing', detail:'<기존>\n\n🔄 재개 YYYY-MM-DD — <사유>'}` (확인 불요).
178
-
179
- ### 자동 계층 도출 (승인 게이트) — 평면 task 다수 시 상위 목표 제안(spec 2026-07-05)
143
+ ## 프로젝트 task 추적 — 요지
180
144
 
181
- - **⭐ 3단 구조 원칙 (두목 2026-07-05, 항상 적용)**: 잎사귀 task 를 목표(1단) 직계에 붙이지 않는다 — 반드시 **목표 → 중간그룹(2단) → 잎(3단)**. 중간그룹이 없으면 먼저 만들거나 제안. done 잎도 예외 없다.
182
- - **트리거**: 두목 명시 요청 또는 미분류 최상위 task 다수(예 ≥8) 시 **1회 제안**. **절차**: tasks 조회 → `title`/`detail` 의미로 배정 → 요약 제시(**raw uuid 금지, title 로**) → **승인 게이트**: OK 한 그룹만 기록.
183
- - **기록**: 신규 목표/중간그룹 `POST /api/rookie/tasks {project_id, title, detail?, status:'todo', source:'agent', parent_task_id?}` — 목표는 parent 없이, 중간그룹은 `parent_task_id`=목표. 잎은 `PATCH {id, parent_task_id:<중간그룹>}`. DB trigger 가 최종 검증(owner/project·무사이클·≤32단), 실패 자식만 400 라벨 1줄 보고 후 skip.
184
- - **안전**: 기존 `parent_task_id` 있는 task 재배정 금지(명시 지시 시만). 목표 중복이면 기존에 붙인다. 신규 목표에 자식 0건이면 명시 보고(자동 삭제 금지). 부분 실패 무손실. **보고**: "목표 N개 · 자식 M건 연결(실패 K건: 사유)" 1줄.
145
+ task status 는 루키가 실제 업무·증거 기반으로만 갱신한다(수동 토글 없음) — 매 turn 금지, **착수/완료 시점만**. **자동 done 금지**(항상 근거 제시 + 승인 게이트). status 변경·계층 도출 전 `references/tasks.md` 를 Read 하고 그 절차(착수 doing 매칭·done 승인·blocked/재개·계층 3단 구조 원칙)를 따른다.
@@ -0,0 +1,18 @@
1
+ # link 후속 — 스펙 연결·소개글·자동 스캔
2
+
3
+ SKILL.md 코어 §세션 시작에서 스펙 동기화 directive(`[루키 지시]`)가 보일 때만 읽는 절차 문서다.
4
+
5
+ ## 스펙 연결·소개글 (승인 게이트)
6
+
7
+ `system_prompt` 에 스펙 동기화 directive 가 있을 때만 능동 제안한다(판정은 서버 책임). **미연결** directive → "스펙을 연결할까요?" → 승인 시 (A)+(C) / **반쪽** directive → "동기화할까요?" → 승인 시 (A) 의 task 동기화만(PUT 생략).
8
+
9
+ - **(A) 스펙 연결·동기화**: 로컬 스펙 문서를 찾아 `spec_doc_paths` 설정 + 문서별 task 동기화(문서 1개=task 1개). **task 는 전부 todo 시작(스펙 ≠ 구현)** — sync 에 `status` 금지(서버가 todo 고정), 재동기화해도 기존 status 보존. 진척은 `PATCH /api/rookie/tasks`.
10
+ - 미연결일 때만: `PUT /api/rookie/projects/spec-source {"project_id":"<id>","spec_doc_paths":["docs/specs/**"]}`
11
+ - 공통: `POST /api/rookie/tasks/sync {"project_id":"<id>","items":[{"title":"<스펙 제목>","spec_ref":"<파일경로>","spec_html":"<렌더 HTML>"}]}`
12
+ - 스펙 문서가 없으면 "같이 만들까요?" → brainstorming 으로 작성 후 연결.
13
+ - **(C) 소개글**: 비어 있으면 README·CLAUDE.md 파악 후 1~2문장 초안을 `PATCH /api/rookie/projects {"id","description"}` — 기존 소개글 덮어쓰기 금지. 완료 시 1줄 보고.
14
+ - **거부** 시 `POST /api/rookie/projects/spec-dismiss {"project_id":"<id>","dismissed":true}` (되돌리기는 `false`). 명시 요청은 dismiss 와 무관하게 수행. 미link(§6) → 미스펙(directive) → 정상 3단계 중 해당 단계만 1회 알린다(승인 전제).
15
+
16
+ ## spec 후속 자동 스캔
17
+
18
+ link + `spec_doc_paths` 설정 시 세션 시작 1회 `rookie scan-specs`(하루 1회 throttle). 결과는 조용히 둔다(proposal 은 `/projects`). 명시 요청 시 `--force`. 미link·미설정·실패 조용히 skip.
@@ -0,0 +1,26 @@
1
+ # 저빈도 신호 대응 — 복구 세션 tail 추출 · 컨텍스트 다이어트
2
+
3
+ SKILL.md 코어에서 해당 신호가 보일 때만 읽는 절차 문서다. 절차는 여기 적힌 대로 전부 수행한다.
4
+
5
+ ## 복구 세션 tail 추출
6
+
7
+ 비정상 종료 세션은 Stop hook 이 못 돌아 fact·진입카드가 없다 — chunk 복구는 `orphan_recover`, **fact 추출은 차차 책임**(서버 추출 off). 컨텍스트에 ``🛟 …`` 블록과 ``- <sid> :: <transcript_path>`` 목록이 보이면 **다른 응답 전에** 처리한다:
8
+
9
+ 1. `rookie orphan-extract --batch=5` — 대기 세션을 오래된 순으로 읽어 rule-base 초안(요약·파일 경로·git sha, redact 통과)을 뽑는다. transcript 원문 통째 Read 금지(부족 시 보조 — Claude `~/.claude/projects/`, Codex `~/.codex/sessions/`).
10
+ 2. 초안에서 **저장 가치가 있는 것만** — 운영 사실/결정→working_card, 파일·결과물→evidence, 명시적 자기규칙→self_rule, 취향→personal_fact. 잡담·민감정보 금지(redactor 원칙). 0건 세션도 정상.
11
+ 3. **recall 로 dedup 후** 누락분만 `POST /api/rookie/batch-mutate`(`project_id` 포함).
12
+ 4. 처리한 세션마다(0건이어도) 초안 하단의 완료 marker 명령을 **그대로** 실행해 큐에서 비운다 — 안 하면 매 세션 재노출:
13
+ ```bash
14
+ source "$HOME/.rookie/bin/lib/orphan-recovery.sh" 2>/dev/null && orphan_mark_client_extracted '<sid>'
15
+ ```
16
+ (로컬 설치면 `<project>/.rookie/bin/…`) Codex 세션은 `ROOKIE_ORPHAN_DIR="…/codex"` prefix. transcript 소실 세션도 marker 로 비운다.
17
+ 5. 한 줄 보고(`[복구] 세션 N건 tail 추출·저장`). raw transcript·UUID 나열 금지. 남은 대기는 다음 세션에서.
18
+
19
+ ## 컨텍스트 다이어트 (주1회, 승인 게이트)
20
+
21
+ SessionStart hook 이 주 1회 임계 초과 시 ``## 🍞 컨텍스트 다이어트 권고`` 블록을 주입한다. 보이면 **다른 응답 전에 1회** 제안, 없으면 침묵. **전 항목 승인 게이트 — 자동 실행 없음.** 승인 시:
22
+ - **CLAUDE.md**: 아카이브성 내용 `docs/` 이관 + 참조 1줄 치환. 의미 단위 압축(단순 truncate 금지). diff 제시 → 승인 후 커밋.
23
+ - **스킬 정리**: 0회 후보 제시 → 고른 것만. settings 비활성화(가역) 우선 / 폴백 `~/.claude/skills/<name>` → `skills.disabled/` 이동(삭제 아님, symlink·플러그인은 안내만). keep 은 `~/.rookie/context-diet-keep.json`.
24
+ - **루키 자기 다이어트**: rookie Skill 문서 중복 정리 — diff 제시 → 승인 후 반영(템플릿 SSOT 커밋 + 재배포).
25
+
26
+ **거부/보류 snooze**: `~/.rookie/context-diet-snooze.json` 에 `{"<key>":"<ISO until>"}` 30일(전체 거부 90일. key = claude_md_project·claude_md_total·skills_agents_desc·rookie_self·unused_skills). 실패 항목만 1줄 보고. 현황 `rookie doctor`.
@@ -0,0 +1,53 @@
1
+ # 저장 상세 — batch-mutate 스키마·응답 게이트·supersede·트리거
2
+
3
+ `rookie save` 본문을 작성하기 전(세션 첫 저장 전 1회) 읽는 절차 문서다. SKILL.md 코어의 저장 요지를 이 문서가 구체화한다 — 여기 게이트는 전부 필수.
4
+
5
+ ## 호출 방법
6
+
7
+ ```bash
8
+ rookie save --file <body.json> # 또는: echo '<body>' | rookie save
9
+ ```
10
+ ⚠️ **`curl` 로 직접 batch-mutate 금지** — `rookie save` 가 cwd·git remote·hostname 을 자동 주입해 전역 누수를 막는다. 본문엔 **기억할 내용만**.
11
+
12
+ ⚠️ **전역 fact 는 `project_id: null` 을 명시한다.** 식별자도 명시 의사도 없으면 서버가 `missing_workspace_context` 로 **거부**. 미등록 폴더 저장은 **그 폴더에 격리**, `rookie link` 시 자동 승격.
13
+
14
+ **`rookie:mutate` fence 를 화면에 출력하지 않는다** — 저장은 Bash 직접 호출, 화면엔 `[기억] …` 한 줄만(config 는 `~/.rookie/config.json`, 토큰 출력 금지).
15
+
16
+ > **레거시 폴백**: 직접 curl 불가 환경만 응답 끝 ` ```rookie:mutate ``` ` fence(Stop hook dispatch). 일반 세션 fence 금지.
17
+
18
+ ## `<body>` 스키마 (필요한 키만)
19
+
20
+ ```json
21
+ {
22
+ "working_cards": { "items": [ { "summary": "진입 카드 본문", "verification": { "method": "ssh|curl|git|grep|lsof|read|reasoning|assertion", "evidence": "직접 명령 결과 한 줄" }, "supersedes": ["부모 fact UUID"], "tags": ["custom"], "ttl_at": "ISO8601?", "artifact_paths": ["task 매칭용? ≤20 (저장 안 함)"], "project_id": "?", "task_id": "?" } ], "repo_url": "(권장) git remote — 없으면 project 귀속 안 됨", "repo_path": "(권장) repo_url 없을 때 hostname 매칭", "project_id": "(레거시) item 이 우선, 보통 생략", "conversation_id": "uuid?" },
23
+ "self_rules": [ { "category": "identity|voice|work_style|strength|preference|rule", "rule": "규칙 본문", "confidence": "high|medium|low", "supersedes": "기존 rule UUID?", "deprecation_reason": "user_override|conflict|noise|manual?" } ],
24
+ "evidence": [ { "capability": "능력 진술문", "approach": "접근법?", "status": "completed|in-progress?", "context": "맥락?", "artifact_paths": ["파일 절대경로 (project 폴백 매칭에도 쓰임)"], "project_id": "? (uuid=명시 귀속 / null=전역 / 생략=세션 귀속)" } ],
25
+ "supersede_links": [ { "parent_id": "폐기할 구 카드 UUID", "child_id": "대체하는 새 카드 UUID" } ],
26
+ "verifications": [ { "fact_id": "검증 대상 fact UUID", "method": "(위와 동일 enum)", "outcome": "confirmed|refuted|ambiguous", "evidence": "검증 명령 결과 한 줄" } ]
27
+ }
28
+ ```
29
+
30
+ ## 응답 확인 — `all_committed` 게이트 + 귀속 확인
31
+
32
+ **귀속 확인**(all_committed 와 무관): `repo_url`/`repo_path` 를 보냈는데 `working_cards.project_attributed=false` 또는 `evidence.unattributed>0` 이면(전역 의도 `project_id:null` 명시 제외) `⚠️ [기억] 프로젝트 미귀속 저장 — rookie link 상태 확인 필요` 1줄 경고.
33
+
34
+ top-level `all_committed` 가 **`true` 일 때만** `[기억] … 저장` 보고. `false` 면:
35
+ - **`rejected[]`**(정책 거부 — 재시도 무의미): `meta_card_summary`·`residual_marker`(민감정보)·`self_rule_rejected` — 단일 fact 분리 또는 원인 제거 후 재저장, 민감정보 마커면 포기. `supersede_link_skipped` 는 id·scope 를 고쳐야 하는 것 — 같은 payload 재전송 금지.
36
+ - **`failed[]`** (일시 실패): `insert_failed`·`unexpected_error` 등 — retry 1회, 그래도 실패면 보고.
37
+ - 미저장이 남으면 `⚠️ [기억] N건 미저장 — <사유>` 한 줄(raw JSON·UUID 금지). `all_committed:true` 전엔 "저장 완료" 라 하지 않는다.
38
+
39
+ **supersede 후보 처리 (같은 턴, 필수)**: 저장 응답 `summary.working_cards.supersede_candidates[]` 에 후보가 있으면 **그 자리에서** 판단한다 — 후보(`summary_head`·`memory_at`)가 새 카드가 대체하는 같은 작업 흐름의 구 카드면 즉시 후속 `rookie save` 로 닫는다: `{"supersede_links":[{"parent_id":"<후보 id>","child_id":"<새 카드 id>"}]}`. 병행 중인 다른 트랙 카드면 무시. **후보를 보고도 무근거로 방치 금지** — 애매하면 애매한 이유가 있어야 한다. (구 결정·상태 카드가 안 닫혀 다음 세션이 폐기된 결정을 회상하는 사고의 1차 방지선.)
40
+
41
+ ## 트리거 조건 상세
42
+
43
+ - **working_cards**: 다음 세션 진입 카드 — 세션 마무리 + 아래 checkpoint, 매 턴·매 단계 금지. **body 에 `repo_url`/`repo_path` 필수** — 서버가 세션 project 로 자동 귀속, 미link 면 NULL(정상).
44
+ - **checkpoint(세션 중간 저장)**: 마일스톤(커밋/push·배포·설계 수렴·큰 단계 전환) 완결 시 `rookie save --json` 으로 카드 1건 — `tags:["checkpoint"]`, 직전 checkpoint 를 `supersedes` 로 닫기(응답 `summary.working_cards.ids[0]` 기억, `superseded_count` 확인). 마무리 저장도 마지막 checkpoint supersede. id 분실 시 recall dedup, 애매하면 supersedes 생략.
45
+ - **직전 카드 supersede (필수 습관)**: 같은 작업 흐름(프로젝트·이어지는 제목/파일/브랜치)의 새 "다음:" 카드는 recall dedup 으로 확인한 직전 카드 `id` 를 `supersedes` 에 넣어 닫는다. 애매하면 생략. 프로젝트 불일치 parent 는 서버가 자동 skip.
46
+ - **cross-project·전역 예외**: 명백히 다른 프로젝트/전역 fact 는 **생략이 아니라 명시적으로 `project_id: null`**(전역) 또는 그 프로젝트 uuid — 생략은 세션 project 자동 귀속(오귀속). null 도 scope=all 회상에 잡힌다.
47
+ - **task 연결**: 확실히 매칭된 task 면 `task_id` 지정 — done 시 카드 자동 은퇴. project_id 는 생략해 task 에서 derive(불일치 시 400). **애매하면** task_id 없이 `__task_link_candidate:<task_id>` 태그만.
48
+ - **durable 사실 분리 저장 (필수)**: 진입 카드·checkpoint 본문에 durable 운영 사실(블로커 발생/해소·환경 변화·외부 상태 전환·측정 결과)을 끼워 넣지 않는다 — **별도 working_cards item 으로 분리 저장**하고 진입 카드엔 요지만. 판별: "다음 checkpoint 가 이 카드를 닫아도 여전히 참이어야 하는 내용인가" → 그렇다면 분리. (진입 카드는 ephemeral pointer 라 체인 supersede 로 닫힌다 — 본문에만 있던 durable 사실이 회상 불가로 소실된 실사고 있음.)
49
+ - **self_rules**: 사용자 명시 자기 규칙 지시 시만. / **evidence**: 파일·결과물 산출 시. / **verifications**: 루키가 직접 명령으로 fact 검증 시.
50
+
51
+ ## 파일 변경 알림
52
+
53
+ 파일을 생성·수정했다면: ```json {"files_created":["절대경로"],"files_modified":["절대경로"]} ```
@@ -0,0 +1,35 @@
1
+ # task 추적 상세 — 마무리 reconcile·status 갱신·자동 계층 도출
2
+
3
+ 세션 마무리(마무리 징후 감지) 또는 task status 를 바꾸려는 시점에 읽는 절차 문서다. status 는 루키 전용 — `/projects` 의 task status 는 **루키가 실제 업무·증거 기반으로만 갱신**한다(수동 토글 없음). 매 turn 금지 — **착수/완료 시점만**. 자기 출력 분석 금지.
4
+
5
+ ## 세션 마무리 task status reconcile (link 세션만)
6
+
7
+ `GET /api/rookie/tasks?project_id=<id>` 1회 후, **이번 세션 실제 산물과 사용자 지시**를 task 와 매칭(루키 자기 서술 근거 금지):
8
+
9
+ - **(가) git 산물 수집 먼저** (git 이 진실):
10
+ ```bash
11
+ git log --oneline ${SESSION_START_SHA:-HEAD~20}..HEAD
12
+ git diff --name-only ${SESSION_START_SHA:-HEAD~20}..HEAD
13
+ git status --short # START_DIRTY 제외분만 산물
14
+ ```
15
+ 커밋·파일·PR 또는 DB 검증 fact 가 매칭 근거 — **양쪽 다 0 이면 어떤 status 도 바꾸지 않는다(특히 done)**. DB 검증만으로 완성되는 작업은 검증 fact 로 done 후보 인정(자동 done 금지 동일). `SESSION_START_SHA` 미캡처 시 폴백 `HEAD~20`(과탐해도 doing 까지만).
16
+ - (나) 산물을 task 의 `spec_ref`/`title`/`detail` 과 의미 매칭 — 변경 파일이 `spec_ref` 영역과 겹치면 강한 매칭.
17
+ - 작업했는데 todo → `PATCH {id, status:'doing'}` (자동). / 매칭 없음 → `POST /tasks {project_id, title, status:'doing', source:'agent'}`.
18
+ - doing 중 근거 실재하는 완성건 → **일괄 1회 보고** `"다음 N개를 완료로 올릴까요? — A(근거…), B(근거…)"` → OK 시에만 done PATCH. "느낌상 완료" 제외.
19
+
20
+ ## status 갱신 (착수/완료/보류/재개)
21
+
22
+ - **진행중 (doing)** — 착수 시 `GET /api/rookie/tasks?project_id=$PID` 후 의미 매칭: 확실 → `PATCH {id, status:'doing'}` + 1줄 알림 / 애매 → "이 작업이 'X' task 맞나요?" 1회 확인 / 매칭 없음 → `POST /tasks {…, status:'doing', source:'agent'}` 신규.
23
+ - **완료 (done)** — **자동 done 금지.** `"'X' 를 완료로 올리겠습니다 — 근거: <파일/PR/검증 한 줄>. 올릴까요?"` → OK 시 `PATCH {id, status:'done', detail:'<기존>\n\n✅ 완료 YYYY-MM-DD — <근거>'}`.
24
+ - **보류 (blocked)** — `PATCH {id, status:'blocked', blocked_reason:'<사유>'}`. / **재개 (done→doing)** — 추가 작업 발생 시 루키 자율 `PATCH {id, status:'doing', detail:'<기존>\n\n🔄 재개 YYYY-MM-DD — <사유>'}` (확인 불요).
25
+
26
+ ## 완료 후보 surface (세션 시작 블록)
27
+
28
+ `## 🎯 완료 후보 감지` 블록의 `🎯 <task_id(전체 UUID)> "<title>" ← <sha> <subject>` 줄이 보이면 **다른 응답 전에** 후보별 done 승인 게이트로 여쭙는다(UUID 는 PATCH 용, 두목껜 title 만. `<sha>` 는 그대로): `"'<title>' 을 완료로 올릴까요? — 근거: 커밋 <sha> <subject>"` → OK 시에만 `PATCH {id, status:'done', detail:'<기존>\n\n✅ 완료 <date> — 커밋 <sha>'}` (**자동 done 금지**) / "아직/보류" 면 `PATCH {id, detail:'<기존>\n__done_defer:<sha>'}` 로 재알림 차단 — 이후 **다른 새 커밋** 매칭 시만 재surface. 블록 없으면 침묵. 근거는 주입된 git 출력만(self-loop 금지).
29
+
30
+ ## 자동 계층 도출 (승인 게이트) — 평면 task 다수 시 상위 목표 제안 (spec 2026-07-05)
31
+
32
+ - **⭐ 3단 구조 원칙 (두목 2026-07-05, 항상 적용)**: 잎사귀 task 를 목표(1단) 직계에 붙이지 않는다 — 반드시 **목표 → 중간그룹(2단) → 잎(3단)**. 중간그룹이 없으면 먼저 만들거나 제안. done 잎도 예외 없다.
33
+ - **트리거**: 두목 명시 요청 또는 미분류 최상위 task 다수(예 ≥8) 시 **1회 제안**. **절차**: tasks 조회 → `title`/`detail` 의미로 배정 → 요약 제시(**raw uuid 금지, title 로**) → **승인 게이트**: OK 한 그룹만 기록.
34
+ - **기록**: 신규 목표/중간그룹 `POST /api/rookie/tasks {project_id, title, detail?, status:'todo', source:'agent', parent_task_id?}` — 목표는 parent 없이, 중간그룹은 `parent_task_id`=목표. 잎은 `PATCH {id, parent_task_id:<중간그룹>}`. DB trigger 가 최종 검증(owner/project·무사이클·≤32단), 실패 자식만 400 라벨 1줄 보고 후 skip.
35
+ - **안전**: 기존 `parent_task_id` 있는 task 재배정 금지(명시 지시 시만). 목표 중복이면 기존에 붙인다. 신규 목표에 자식 0건이면 명시 보고(자동 삭제 금지). 부분 실패 무손실. **보고**: "목표 N개 · 자식 M건 연결(실패 K건: 사유)" 1줄.
package/version.json CHANGED
@@ -1,4 +1,4 @@
1
1
  {
2
- "cli_version": "0.4.5",
3
- "skill_version": "1.26.0"
2
+ "cli_version": "0.4.12",
3
+ "skill_version": "1.34.0"
4
4
  }