therookie 0.4.19 → 0.4.21

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.19",
3
+ "version": "0.4.21",
4
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": [
@@ -124,6 +124,7 @@ export async function runDoctor(flags) {
124
124
  await printDirectiveMetric(cfg)
125
125
  await printHotWriteMetric(cfg)
126
126
  await printAttributionMetric(cfg)
127
+ await printLayerInflowMetric(cfg)
127
128
  await printPromotionMetric(cfg)
128
129
  await printViolationTop(cfg)
129
130
  await printEmbeddingGuard(cfg)
@@ -612,6 +613,53 @@ async function printEmbeddingGuard(cfg) {
612
613
  }
613
614
  }
614
615
 
616
+ /**
617
+ * spec 2026-09-01-durable-fact-inflow-restoration §4-1 — 판정식 복구.
618
+ *
619
+ * 예전 식은 `examined > 0 ? skipped / examined : 0` 이라, 볼 것이 아예 없으면
620
+ * 비율이 0 이 되어 **완전 고갈이 초록(✓)으로 표시**됐다. 나빠질수록 경고하다가
621
+ * 죽으면 정상으로 돌아오는 계기판이었고, durable fact 유입이 끊긴 두 달 반 동안
622
+ * 아무 신호도 없던 이유가 이것이다.
623
+ *
624
+ * 그래서 "볼 것이 없다"(starved)를 "다 걸러냈다"(degraded)와 별도 상태로 뗀다 —
625
+ * 둘은 원인이 다르다. 전자는 입력 쪽, 후자는 임계 쪽 문제다.
626
+ */
627
+ export function classifyPromotionHealth({ calls, examined, skipped }) {
628
+ if (calls === 0) return { level: 'idle', prefix: '⚙️ ', note: '미가동' }
629
+ if (examined === 0)
630
+ return { level: 'starved', prefix: '⚠️ ', note: '검토할 cluster 0 — 입력 고갈' }
631
+ if (skipped / examined >= 0.8)
632
+ return { level: 'degraded', prefix: '⚠️ ', note: null }
633
+ return { level: 'ok', prefix: '✓ ', note: null }
634
+ }
635
+
636
+ // spec 2026-09-01-durable-fact-inflow-restoration §6-4 — 기억층별 유입.
637
+ // 이 라인이 없어서 durable fact 유입이 끊긴 것을 두 달 반 뒤에야 알았다.
638
+ // 임계 판정은 서버가 한다(starved) — 여기서 다시 계산하면 두 기준이 갈린다.
639
+ async function printLayerInflowMetric(cfg) {
640
+ if (!cfg?.endpoint || !cfg?.token) return
641
+ try {
642
+ const s = await apiGet(cfg.endpoint, cfg.token, '/api/rookie/metric/layer-inflow/summary')
643
+ if (!s || !Array.isArray(s.layers)) return
644
+ if (s.fetchFailed) {
645
+ console.log('⚠️ 기억층 유입: 조회 실패 — 지표를 신뢰하지 말 것')
646
+ return
647
+ }
648
+ const parts = s.layers.map((l) => {
649
+ const age = l.daysSinceLast === null ? '유입 없음' : `${l.daysSinceLast}일 전`
650
+ return `${l.layer} ${l.count}${l.starved ? `(⚠️ ${age})` : ''}`
651
+ })
652
+ console.log(
653
+ `${s.anyStarved ? '⚠️ ' : '✓ '}기억층 유입 /${s.windowDays ?? 14}일: ${parts.join(' · ')}`
654
+ )
655
+ if (s.anyStarved) {
656
+ console.log(' → 유입이 끊긴 층이 있습니다. 저장 경로가 막혔는지 확인하세요.')
657
+ }
658
+ } catch {
659
+ // silent — endpoint 미응답 또는 미land 시 본 라인만 누락.
660
+ }
661
+ }
662
+
615
663
  async function printPromotionMetric(cfg) {
616
664
  if (!cfg?.endpoint || !cfg?.token) return
617
665
  try {
@@ -625,17 +673,16 @@ async function printPromotionMetric(cfg) {
625
673
  const promoted = summary?.totalPromoted ?? 0
626
674
  const skipped = summary?.totalSkipped ?? 0
627
675
  const window = summary?.windowDays ?? 7
628
- if (calls === 0) {
629
- console.log(`⚙️ promotion: 0건/${window}일 (미가동)`)
676
+ const health = classifyPromotionHealth({ calls, examined, skipped })
677
+ if (health.level === 'idle') {
678
+ console.log(`${health.prefix}promotion: 0건/${window}일 (${health.note})`)
630
679
  return
631
680
  }
632
- const skippedRatio = examined > 0 ? skipped / examined : 0
633
681
  const promotionPct =
634
682
  examined > 0 ? Math.round((promoted / examined) * 100) : 0
635
- let prefix = '✓ '
636
- if (skippedRatio >= 0.8) prefix = '⚠️ '
683
+ const tail = health.note ? ` — ${health.note}` : ''
637
684
  console.log(
638
- `${prefix}promotion: ${calls}건/${window}일, examined ${examined} / promoted ${promoted} / skipped ${skipped} (승격률 ${promotionPct}%)`
685
+ `${health.prefix}promotion: ${calls}건/${window}일, examined ${examined} / promoted ${promoted} / skipped ${skipped} (승격률 ${promotionPct}%)${tail}`
639
686
  )
640
687
  } catch {
641
688
  // silent — endpoint 미응답 또는 미land 시 본 라인만 누락.
@@ -91,17 +91,58 @@ export async function runSave(flags) {
91
91
  return
92
92
  }
93
93
 
94
- if (flags.quiet) return
95
94
  const s = res.summary ?? {}
95
+
96
+ // --quiet 는 **출력**만 줄인다. 종료 코드까지 삼키면 자동화가 검토 게이트에 걸린 저장을
97
+ // 성공으로 읽는다 — 두목이 가르친 규칙이 조용히 사라지는 경로다.
98
+ if (flags.quiet) {
99
+ if (res.all_committed === false) process.exit(1)
100
+ return
101
+ }
102
+
103
+ // audit 2026-08-24 §4-2 Phase 3 — 검토 게이트에 걸린 자기규칙은 목록을 그대로 펼친다.
104
+ // 요약해 버리면 판단 재료가 사라져 게이트가 그냥 "저장 실패"로 읽힌다.
105
+ for (const r of s.self_rules?.rejected ?? []) {
106
+ if (r.reason_code !== 'peer_review_required') continue
107
+ console.error(
108
+ `\n⚠ 자기규칙 미저장 (self_rules[${r.index}]) — 같은 category 활성 규칙 ${r.peers_total ?? 0}건을 먼저 보세요` +
109
+ (r.peers_truncated ? ` (앞 ${r.peers?.length ?? 0}건만 표시)` : '')
110
+ )
111
+ if (typeof r.peers_same_as_index === 'number') {
112
+ // 같은 category 목록은 첫 건에만 실린다 — 어디를 보라고 가리켜 준다.
113
+ console.error(` (목록은 self_rules[${r.peers_same_as_index}] 것과 같습니다)`)
114
+ }
115
+ for (const p of r.peers ?? []) {
116
+ console.error(` ${p.id} [강화 ${p.reinforcement_count}] ${p.head}`)
117
+ }
118
+ console.error(
119
+ '→ 같은 취지가 있으면 reinforces:"<id>"(강화) 또는 supersedes:"<id>"(정정), 없으면 peers_reviewed:true 로 재전송하세요.'
120
+ )
121
+ }
96
122
  const parts = []
97
123
  if (s.working_cards?.ids?.length) parts.push(`카드 ${s.working_cards.ids.length}`)
98
124
  if (s.evidence?.applied) parts.push(`증거 ${s.evidence.applied}`)
99
- if (s.self_rules?.applied) parts.push(`자기규칙 ${s.self_rules.applied}`)
125
+ if (s.self_rules?.applied) {
126
+ const reinforced = s.self_rules.reinforced ?? 0
127
+ parts.push(
128
+ reinforced > 0
129
+ ? `자기규칙 ${s.self_rules.applied}(강화 ${reinforced})`
130
+ : `자기규칙 ${s.self_rules.applied}`
131
+ )
132
+ }
100
133
  if (s.verifications?.applied) parts.push(`검증 ${s.verifications.applied}`)
101
134
  const label = parts.length > 0 ? parts.join(' · ') : '0건'
102
135
 
103
136
  if (res.all_committed === false) {
104
- console.error(`⚠ 일부 미저장 — ${label}`)
137
+ // 검토 게이트만 걸린 저장은 "미저장 0건" 이 아니라 다음 행동이 남은 상태다.
138
+ const pendingReview = (s.self_rules?.rejected ?? []).some(
139
+ (r) => r.reason_code === 'peer_review_required'
140
+ )
141
+ if (pendingReview && parts.length === 0) {
142
+ console.error('⚠ 자기규칙 검토 대기 — 위 목록을 보고 재전송하세요.')
143
+ } else {
144
+ console.error(`⚠ 일부 미저장 — ${label}`)
145
+ }
105
146
  process.exit(1)
106
147
  }
107
148
  console.log(`✓ 저장: ${label}`)
@@ -193,6 +193,17 @@ _handle_200() {
193
193
  return 0
194
194
  fi
195
195
 
196
+ # audit 2026-08-24 §10 — 자기 규칙 검토 게이트(peer_review_required)는 rejected[] 로만 온다.
197
+ # hook 경로에는 목록을 읽고 재전송할 에이전트가 없어 그 규칙은 저장되지 않는다. 재큐해도
198
+ # 같은 payload 라 또 거부되므로 큐에 넣지 않고 **로그로 드러낸다** — silent drop 만은 막는다.
199
+ local peer_gate_count
200
+ peer_gate_count=$(jq -r '[.rejected[]? | select(.reason_code == "peer_review_required")] | length // 0' < "$body_path" 2>>"$ROOKIE_DROP_LOG")
201
+ if [ -n "$peer_gate_count" ] && [ "$peer_gate_count" != "null" ] && [ "$peer_gate_count" -gt 0 ] 2>/dev/null; then
202
+ printf '%s self_rule_peer_review_required_unsaved count=%s session=%s\n' \
203
+ "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$peer_gate_count" "$session_id" \
204
+ >> "$ROOKIE_DROP_LOG" 2>/dev/null || true
205
+ fi
206
+
196
207
  if [ "$failed_count" -le 0 ] 2>/dev/null; then
197
208
  return 0
198
209
  fi
@@ -127,11 +127,15 @@ curl -s -H "Authorization: Bearer $TOKEN" \
127
127
 
128
128
  ## 저장 (hot write) — 요지
129
129
 
130
- 카테고리: 운영 사실(서버/포트/유저명/경로/명시 결정, 자동) → working_cards / 능력 증거(파일·결과물 산출 시, 자동) → evidence / 자기 규칙(사용자 명시 시그널 "원칙·습관·항상·절대" 만) → self_rules / 개인 취향(명시 시그널 "좋아해·선호·취향" 만) → personal_fact 별도 경로 / 잡담·일회성·민감 정보 → 저장 안 함.
130
+ 카테고리: **운영 사실(서버/포트/유저명/경로/명시 결정·측정 결과, 자동) → `facts`(durable 영구층)** / 다음 세션 진입점 → `working_cards` / 능력 증거(파일·결과물 산출 시, 자동) → `evidence` / 자기 규칙(사용자 명시 시그널 "원칙·습관·항상·절대" 만) → `self_rules` / 개인 취향(명시 시그널 "좋아해·선호·취향" 만) → `personal_facts` / 잡담·일회성·민감 정보 → 저장 안 함.
131
+
132
+ ⚠️ **운영 사실을 `working_cards` 로 보내지 않는다.** 진입카드는 은퇴·supersede 로 사라지도록 설계된 층이라, 거기 넣은 영구 지식은 다음 카드가 닫으면 같이 사라진다(2026-06~08 실사고 — 영구층 유입 0). 판별: "다음 checkpoint 가 이 카드를 닫아도 여전히 참인가" → 그렇다면 `facts`.
131
133
 
132
134
  저장 전 **recall dedup** — 같은 취지 durable fact 존재 시 새 저장 금지. **재확인 근거가 있으면** `verifications` confirmed 1건(fact 당 하루 1회, 서버 동일 게이트 — working card·근거 없는 회상 제외)으로 반복 신호를 남긴다. 성공 시 `[기억] <요약> 저장` 1줄, 실패 시 retry 1회 + 알림.
133
135
 
134
- **본문 작성 전 `references/save.md` 를 Read 한다(세션 첫 저장 전 1회 필수)** — 스키마·`all_committed` 게이트·귀속 확인·supersede 후보 처리·checkpoint/durable 분리 규칙이 거기 있다. 호출은 `rookie save --file <body.json>` (curl 직접 batch-mutate 금지, `rookie:mutate` fence 화면 출력 금지, 토큰 출력 금지).
136
+ **자기 규칙은 처음 보낼 때 저장되지 않는다** — 서버가 같은 category 기존 규칙 전량을 `peer_review_required` 로 돌려주고, 그 목록을 읽어 강화(`reinforces`)·정정(`supersedes`)·신규(`peers_reviewed:true`) 중 하나로 재전송해야 한다. 절차는 `references/save.md` §자기 규칙 검토 게이트.
137
+
138
+ **본문 작성 전 `references/save.md` 를 Read 한다(세션 첫 저장 전 1회 필수)** — 스키마·`all_committed` 게이트·귀속 확인·supersede 후보 처리·자기 규칙 검토 게이트·checkpoint/durable 분리 규칙이 거기 있다. 호출은 `rookie save --file <body.json>` (curl 직접 batch-mutate 금지, `rookie:mutate` fence 화면 출력 금지, 토큰 출력 금지).
135
139
 
136
140
  ## 세션 마무리 자동 저장 (추출 책임이 루키에게)
137
141
 
@@ -7,7 +7,7 @@ SKILL.md 코어에서 해당 신호가 보일 때만 읽는 절차 문서다.
7
7
  비정상 종료 세션은 Stop hook 이 못 돌아 fact·진입카드가 없다 — chunk 복구는 `orphan_recover`, **fact 추출은 차차 책임**(서버 추출 off). 컨텍스트에 ``🛟 …`` 블록과 ``- <sid> :: <transcript_path>`` 목록이 보이면 **다른 응답 전에** 처리한다:
8
8
 
9
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건 세션도 정상.
10
+ 2. 초안에서 **저장 가치가 있는 것만** — durable 운영 사실→`facts`, 다음 진입점→working_card, 파일·결과물→evidence, 명시적 자기규칙→self_rule, 취향→personal_fact. 잡담·민감정보 금지(redactor 원칙). 0건 세션도 정상.
11
11
  3. **recall 로 dedup 후** 누락분만 `POST /api/rookie/batch-mutate`(`project_id` 포함).
12
12
  4. 처리한 세션마다(0건이어도) 초안 하단의 완료 marker 명령을 **그대로** 실행해 큐에서 비운다 — 안 하면 매 세션 재노출:
13
13
  ```bash
@@ -20,10 +20,12 @@ rookie save --file <body.json> # 또는: echo '<body>' | rookie save
20
20
  ```json
21
21
  {
22
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?", "ownership_scope": "personal|project?", "project_id": "scope=project 일 때 필수 uuid" } ],
23
+ "self_rules": [ { "category": "identity|voice|work_style|strength|preference|rule", "rule": "규칙 본문", "confidence": "high|medium|low", "peers_reviewed": "기존 규칙 목록을 보고 새 규칙이라 판단했을 때만 true (아래 검토 게이트)", "reinforces": "같은 취지 기존 규칙 UUID — 새로 저장하지 않고 강화만", "supersedes": "기존 rule UUID?", "deprecation_reason": "user_override|conflict|noise|manual?", "ownership_scope": "personal|project?", "project_id": "scope=project 일 때 필수 uuid" } ],
24
24
  "evidence": [ { "capability": "능력 진술문", "approach": "접근법?", "status": "completed|in-progress?", "context": "맥락?", "artifact_paths": ["파일 절대경로 (project 폴백 매칭에도 쓰임)"], "project_id": "? (uuid=명시 귀속 / null=전역 / 생략=세션 귀속)" } ],
25
25
  "supersede_links": [ { "parent_id": "폐기할 구 카드 UUID", "child_id": "대체하는 새 카드 UUID" } ],
26
- "verifications": [ { "fact_id": "검증 대상 fact UUID", "method": "(위와 동일 enum)", "outcome": "confirmed|refuted|ambiguous", "evidence": "검증 명령 결과 한 줄" } ]
26
+ "verifications": [ { "fact_id": "검증 대상 fact UUID", "method": "(위와 동일 enum)", "outcome": "confirmed|refuted|ambiguous", "evidence": "검증 명령 결과 한 줄" } ],
27
+ "facts": { "items": [ { "body": "durable 운영 사실 본문", "tags": ["custom"], "reasoning": "왜 그런지?", "observed_at": "ISO8601?", "project_id": "? (uuid=명시 귀속 / null=명시 전역 / 생략=세션 귀속)", "linked_ids": ["이 사실이 대체하는 기존 durable fact UUID (같은 범위여야 함)"] } ], "repo_url": "(권장)", "repo_path": "(권장)", "conversation_id": "uuid?" },
28
+ "personal_facts": [ { "body": "두목 개인 취향·배경", "tags": ["preference"], "observed_at": "ISO8601?" } ]
27
29
  }
28
30
  ```
29
31
 
@@ -32,12 +34,22 @@ rookie save --file <body.json> # 또는: echo '<body>' | rookie save
32
34
  **귀속 확인**(all_committed 와 무관): `repo_url`/`repo_path` 를 보냈는데 `working_cards.project_attributed=false` 또는 `evidence.unattributed>0` 이면(전역 의도 `project_id:null` 명시 제외) `⚠️ [기억] 프로젝트 미귀속 저장 — rookie link 상태 확인 필요` 1줄 경고.
33
35
 
34
36
  top-level `all_committed` 가 **`true` 일 때만** `[기억] … 저장` 보고. `false` 면:
35
- - **`rejected[]`**(정책 거부 — 재시도 무의미): `meta_card_summary`·`residual_marker`(민감정보)·`self_rule_rejected` — 단일 fact 분리 또는 원인 제거 후 재저장, 민감정보 마커면 포기. `supersede_link_skipped` 는 id·scope 를 고쳐야 하는 것 — 같은 payload 재전송 금지.
37
+ - **`rejected[]`**(정책 거부 — 같은 payload 재전송 무의미): `meta_card_summary`·`residual_marker`(민감정보)·`self_rule_rejected` — 단일 fact 분리 또는 원인 제거 후 재저장, 민감정보 마커면 포기. **`reason_code:"peer_review_required"` 는 예외적으로 반드시 이어서 처리한다** — 아래 "자기 규칙 검토 게이트". `supersede_link_skipped` 는 id·scope 를 고쳐야 하는 것 — 같은 payload 재전송 금지.
36
38
  - **`failed[]`** (일시 실패): `insert_failed`·`unexpected_error` 등 — retry 1회, 그래도 실패면 보고.
37
39
  - 미저장이 남으면 `⚠️ [기억] N건 미저장 — <사유>` 한 줄(raw JSON·UUID 금지). `all_committed:true` 전엔 "저장 완료" 라 하지 않는다.
38
40
 
39
41
  **supersede 후보 처리 (같은 턴, 필수)**: 저장 응답 `summary.working_cards.supersede_candidates[]` 에 후보가 있으면 **그 자리에서** 판단한다 — 후보(`summary_head`·`memory_at`)가 새 카드가 대체하는 같은 작업 흐름의 구 카드면 즉시 후속 `rookie save` 로 닫는다: `{"supersede_links":[{"parent_id":"<후보 id>","child_id":"<새 카드 id>"}]}`. 병행 중인 다른 트랙 카드면 무시. **후보를 보고도 무근거로 방치 금지** — 애매하면 애매한 이유가 있어야 한다. (구 결정·상태 카드가 안 닫혀 다음 세션이 폐기된 결정을 회상하는 사고의 1차 방지선.)
40
42
 
43
+ **자기 규칙 검토 게이트 (같은 턴, 필수)**: self_rule 은 **처음 보낼 때 저장되지 않는다.** 서버가 같은 category 활성 규칙 전량(`peers[]` — `id`·본문 앞부분·강화 횟수)을 실어 `reason_code:"peer_review_required"` 로 돌려준다. 그 목록을 **읽고** 셋 중 하나로 재전송한다:
44
+
45
+ - **같은 취지가 이미 있다** → `{"reinforces":"<그 규칙 id>"}` — 새 행을 만들지 않고 강화 횟수만 올린다. 문면이 달라도 지시가 같으면 강화다. **id 는 방금 받은 목록에서 고른다** — 목록 밖 id(비활성·다른 category)는 `reinforce_target_mismatch` 로 거부된다(엉뚱한 행만 올리고 본문이 사라지는 것을 막는다).
46
+ - **그 규칙을 정정·확장하는 것이다** → `{"peers_reviewed":true,"supersedes":"<구 규칙 id>","deprecation_reason":"user_override"}` — 구 규칙은 비활성으로 닫히고 행은 남는다. 대상도 목록에서 고른다(비활성·다른 category 면 `supersede_target_mismatch` 로 **저장 자체가 거부**된다 — 새 규칙만 들어가고 구 규칙이 살아남는 반쪽 상태를 막는다). 저장 뒤 `rejected[]` 에 `supersede_failed` 가 보이면 새 규칙은 들어갔고 구 규칙만 못 닫힌 것이다 — **재전송하지 말고** 구 규칙 상태를 확인한다.
47
+ - **정말 새 규칙이다** → `{"peers_reviewed":true}` 만 더해 재전송.
48
+
49
+ 한 배치에 같은 category 가 여러 건이면 목록은 **첫 건에만** 실린다 — 나머지는 `peers_same_as_index` 가 가리키는 항목의 목록을 보면 된다.
50
+
51
+ 목록이 길어도 **훑지 말고 같은 취지를 찾는다.** 이 게이트가 없던 동안 "원문자 금지" 한 지시가 다섯 번 저장됐고, 정정본과 피정정본이 나란히 살아남아 서로 다른 말을 했다. 애매하면 강화가 안전하다 — 새 행은 되돌리려면 두목 손이 필요하지만 강화는 손실이 없다.
52
+
41
53
  ## 트리거 조건 상세
42
54
 
43
55
  - **working_cards**: 다음 세션 진입 카드 — 세션 마무리 + 아래 checkpoint, 매 턴·매 단계 금지. **body 에 `repo_url`/`repo_path` 필수** — 서버가 세션 project 로 자동 귀속, 미link 면 NULL(정상).
@@ -45,7 +57,12 @@ top-level `all_committed` 가 **`true` 일 때만** `[기억] … 저장` 보고
45
57
  - **직전 카드 supersede (필수 습관)**: 같은 작업 흐름(프로젝트·이어지는 제목/파일/브랜치)의 새 "다음:" 카드는 recall dedup 으로 확인한 직전 카드 `id` 를 `supersedes` 에 넣어 닫는다. 애매하면 생략. 프로젝트 불일치 parent 는 서버가 자동 skip.
46
58
  - **cross-project·전역 예외**: 명백히 다른 프로젝트/전역 fact 는 **생략이 아니라 명시적으로 `project_id: null`**(전역) 또는 그 프로젝트 uuid — 생략은 세션 project 자동 귀속(오귀속). null 도 scope=all 회상에 잡힌다.
47
59
  - **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 사실이 회상 불가로 소실된 실사고 있음.)
60
+ - **durable 사실 분리 저장 (필수)**: 진입 카드·checkpoint 본문에 durable 운영 사실(블로커 발생/해소·환경 변화·외부 상태 전환·측정 결과)을 끼워 넣지 않는다 — **`facts` 키로 분리 저장**하고 진입 카드엔 요지만. (2026-09-01 이전엔 '별도 working_cards item' 이라고 안내했는데, 그것도 결국 진입카드라 은퇴 대상이었다 — 그래서 문을 따로 냈다.) 판별: "다음 checkpoint 가 이 카드를 닫아도 여전히 참이어야 하는 내용인가" → 그렇다면 분리. (진입 카드는 ephemeral pointer 라 체인 supersede 로 닫힌다 — 본문에만 있던 durable 사실이 회상 불가로 소실된 실사고 있음.)
61
+ - **facts (durable 영구층)**: 다음 checkpoint 가 닫아도 여전히 참인 운영 사실 — 블로커 발생·해소, 환경·경로·포트, 외부 상태 전환, 측정 결과, 함정. **`is_working=false` 로 저장돼 은퇴·TTL 대상이 아니다.**
62
+ - **working_cards 와 고르는 법**: "다음에 뭘 할지"면 working_cards, "무엇이 참인지"면 facts. 진입 카드 문장(`다음:` 으로 시작 등)을 facts 로 보내면 `looks_like_working_card` 로 거부된다 — 문을 잘못 고른 것이다.
63
+ - 거부 사유: `residual_marker`(본문에 redactor 출력 마커) / `project_not_accessible` / `supersede_scope_mismatch`(남의·다른 프로젝트·진입카드를 닫으려 함) / `too_many_items`(8 초과). 응답 `summary.facts` 의 `skipped_duplicate` 는 같은 범위에 같은 사실이 이미 있었다는 뜻이라 정상이다.
64
+ - ⚠️ 2026-06-16~09-01 이 문이 없어서 영구층 유입이 0이었다. 운영 사실을 진입 카드에 끼워 넣지 말고 여기로 보낸다.
65
+ - **personal_facts**: 두목 개인 취향·배경 (명시 시그널 "좋아해·선호·취향" 만).
49
66
  - **self_rules**: 사용자 명시 자기 규칙 지시 시만.
50
67
  - **범위를 반드시 판단한다 (필수)**: 규칙 본문이 **특정 프로젝트·앱·환경에서만 뜻이 통하면** `ownership_scope:"project"` + `project_id`(세션 시작 context 응답의 `current_project.id`)를 단다. 판별 한 문장 — **"다른 프로젝트 세션에서도 이 규칙이 필요한가."** 필요하면 전역(생략=personal), 아니면 project.
51
68
  - project 예: 특정 앱의 빌드·배포 절차, 그 제품의 콘텐츠 선정 기준, 그 저장소의 파일 구조 규약, 특정 기기·계정에서만 쓰는 명령
package/version.json CHANGED
@@ -1,4 +1,4 @@
1
1
  {
2
- "cli_version": "0.4.19",
3
- "skill_version": "1.37.0"
2
+ "cli_version": "0.4.21",
3
+ "skill_version": "1.39.0"
4
4
  }