leerness 1.9.206 β†’ 1.9.208

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/CHANGELOG.md CHANGED
@@ -1,5 +1,94 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.208 β€” 2026-05-22
4
+
5
+ **🚦 ν”Œλž«νΌ/API μ œμ•½ 사전 체크 (μ‚¬μš©μž λͺ…μ‹œ).**
6
+
7
+ ### μ‚¬μš©μž λͺ…μ‹œ
8
+ > *"μ‚¬μš©μžμ˜ λͺ…령을 λ°›μœΌλ©΄, κ³ λ €ν•΄μ•Όν•˜λŠ” 뢀뢄을 λ¨Όμ € ν™•μΈν•˜κ³  μ§„ν–‰ν•˜κ²Œ ν•˜λ©΄ μ–΄λ–¨μ§€ / νŠΉμ • ν”Œλž«νΌμ˜ API ... ν˜ΈμΆœμ†λ„κ°€ μ΄ˆλ‹Ή 5회 μ΄ν•˜λ‘œλ§Œ ν•΄μ•Όν•œλ‹€λŠ” κ·œμ •μ„ λ¨Όμ € 확인"*
9
+
10
+ ### 1. `.harness/platform-constraints.json` + κΈ°λ³Έ catalog 6μ’…
11
+ - **stripe** (rate: 100 req/s, idempotency key, webhook signing)
12
+ - **openai** (tier-based RPM, TPM, cost per 1M tokens)
13
+ - **anthropic** (claude tier, context-window, cost)
14
+ - **github** (5K req/hr auth, search 30 req/min, secondary)
15
+ - **discord** (50 req/s global, invalid ban)
16
+ - **twitter** (tier-based, OAuth 2.0 PKCE)
17
+ - 각 alias array λ§€μΉ­ + docs URL 동봉
18
+
19
+ ### 2. 헬퍼 ν•¨μˆ˜
20
+ - `_loadPlatformConstraints(root)` β€” default merge user override
21
+ - `_writePlatformConstraints(root, catalog)` β€” μ‚¬μš©μž catalog μ €μž₯
22
+ - `_checkRequestConstraints(root, text)` β€” ν…μŠ€νŠΈ β†’ λ§€μΉ­ ν”Œλž«νΌ + μ œμ•½ + suggestions
23
+
24
+ ### 3. `leerness constraints <list|check|add>` CLI
25
+ - `list` β€” 등둝 catalog 좜λ ₯ (--json)
26
+ - `check "<req>"` β€” μ‚¬μš©μž μš”μ²­ λ§€μΉ­ β†’ μ œμ•½ 보고 (--json)
27
+ - `add <id> --alias name --constraint "kind:detail"` β€” μ‚¬μš©μž μ •μ˜
28
+
29
+ ### 4. review-request μžλ™ 톡합
30
+ - `_checkRequestConstraints()` review-request λ‚΄λΆ€μ—μ„œ μžλ™ 호좜
31
+ - JSON 좜λ ₯에 `platformConstraints` + `constraintSuggestions` ν•„λ“œ μΆ”κ°€
32
+ - human 좜λ ₯에 `## 🚦 ν”Œλž«νΌ/API μ œμ•½ 사전 체크` μ„Ήμ…˜
33
+ - efficiencyHints 에 λ§€μΉ­ N건 μ•Œλ¦Ό
34
+
35
+ ### 5. λˆ„μ  νšŒκ·€ (1.9.200~207) β€” λͺ¨λ‘ μœ μ§€
36
+
37
+ ### 6. stress-v153 β€” 18/18 PASS
38
+ - 1.9.208 (9) + μ„±λŠ₯ (2) + λˆ„μ  νšŒκ·€ (7)
39
+ - κΈ°λ³Έ 6 catalog + μ‚¬μš©μž μ •μ˜ add + review-request 톡합 검증
40
+ - μ„±λŠ₯: --version cold start avg 483ms Β· MCP 54 도ꡬ 431ms
41
+
42
+ ### 7. μžλ™ release (70 λΌμš΄λ“œ main-push streak Β· 31 λΌμš΄λ“œ npm publish streak)
43
+ - `release sync-main` μžλ™ β†’ main merge + npm publish leerness@1.9.208
44
+
45
+ ---
46
+
47
+ ## 1.9.207 β€” 2026-05-22
48
+
49
+ **πŸ“₯ μ‚¬μš©μž μš”μ²­ λˆ„λ½ 확인 절차 MVP (μ‚¬μš©μž λͺ…μ‹œ).**
50
+
51
+ ### μ‚¬μš©μž λͺ…μ‹œ
52
+ > *"leernessκ°€ 적용된 ν”„λ‘œμ νŠΈμ—μ„œ, μ΅œκ·Όμ— λͺ…λ Ήλ°›μ•„ μ§„ν–‰ν•œ 것쀑에 λˆ„λ½λœκ²Œ μ—†λŠ”μ§€ ν™•μΈν•˜λŠ” μ ˆμ°¨λ„ μžˆλŠ”μ§€ ν™•μΈν•΄μ€˜ / λ‹€μŒ λΌμš΄λ“œμ‹œ μ°Έμ‘°"*
53
+
54
+ ### 1. `.harness/user-requests.json` β€” μ‚¬μš©μž μš”μ²­ λˆ„μ  기둝
55
+ - `_userRequestsPath/_loadUserRequests/_writeUserRequests` 파일 I/O 헬퍼
56
+ - `_recordUserRequest(root, text, opts)` β€” μžλ™ ID λΆ€μ—¬(UR-XXXX) + 쀑볡 λ°©μ§€(동일 ν…μŠ€νŠΈ + open μƒνƒœ)
57
+ - `_updateUserRequest(root, id, patch)` β€” status μ „ν™˜ (open β†’ in-progress β†’ completed/dropped)
58
+ - 졜근 200개 μœ μ§€ (rotate)
59
+
60
+ ### 2. `_auditUserRequests(root)` β€” λˆ„λ½ 후보 탐지
61
+ - open/in-progress μš”μ²­ ν…μŠ€νŠΈμ˜ 첫 12 단어 μΆ”μΆœ β†’ task-log/plan/decisions 와 비ꡐ
62
+ - hits β‰₯ min(3, wordsΓ—0.5) β†’ **tracked** (μ–΄λ”˜κ°€ μž‘μ—…μ΄ μ‹œμž‘λ¨)
63
+ - κ·Έ μ™Έ β†’ **missing** (λˆ„λ½ 후보, AI/μ‚¬μš©μžκ°€ μžŠμ€ κ°€λŠ₯μ„±)
64
+ - 7일+ open β†’ **stale** (μž¬κ²€ν†  ν•„μš”)
65
+
66
+ ### 3. `leerness requests <sub>` CLI (μ‚¬μš©μž λͺ…μ‹œ β€” "확인 절차")
67
+ - `audit` β€” missing/tracked/stale 보고 (--json κ°€λŠ₯)
68
+ - `add "<text>"` β€” μ‚¬μš©μž μš”μ²­ μˆ˜λ™ 기둝
69
+ - `list [--status open|completed|dropped]` β€” 전체 좜λ ₯
70
+ - `complete <UR-id>` / `drop <UR-id>` β€” μƒνƒœ μ „ν™˜
71
+
72
+ ### 4. handoff ν—€λ“œλΌμΈ 톡합 (13번째 μš”μ†Œ)
73
+ - missing β‰₯ 1 μ‹œ: `πŸ“₯ λ―Έλ‹΅ μš”μ²­ N건` (red)
74
+ - open > 0 (λͺ¨λ‘ tracked) μ‹œ: `πŸ“₯ μš”μ²­ N (tracked)` (green)
75
+ - ν—€λ“œλΌμΈ 라벨: `1.9.81/93/113/152/162/192/197/204/207`
76
+
77
+ ### 5. λˆ„μ  νšŒκ·€ (1.9.200~206) β€” λͺ¨λ‘ μœ μ§€
78
+
79
+ ### 6. stress-v152 β€” 17/17 PASS
80
+ - 1.9.207 (8) + μ„±λŠ₯ (2) + λˆ„μ  νšŒκ·€ (7)
81
+ - μ‚¬μš©μž μš”μ²­ 라이프사이클 격리 tmpdir ν…ŒμŠ€νŠΈ (add β†’ list β†’ audit β†’ complete β†’ drop)
82
+ - 쀑볡 λ°©μ§€ 확인
83
+ - μ„±λŠ₯: --version cold start avg 2206ms Β· MCP 54 도ꡬ 1597ms
84
+
85
+ ### 7. μžλ™ release (69 λΌμš΄λ“œ main-push streak Β· 30 λΌμš΄λ“œ npm publish streak)
86
+ - `release sync-main` μžλ™ β†’ main merge + npm publish leerness@1.9.207
87
+ - 1.9.140λΆ€ν„° main μžλ™ push (69 λΌμš΄λ“œ 무쀑단)
88
+ - 1.9.178λΆ€ν„° npm publish (30 λΌμš΄λ“œ 무쀑단)
89
+
90
+ ---
91
+
3
92
  ## 1.9.206 β€” 2026-05-22
4
93
 
5
94
  **🌐 i18n MVP + UI/UX 헬퍼 (μ‚¬μš©μž λͺ…μ‹œ 2μ’…).**
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > **AI μ½”λ”© μ—μ΄μ „νŠΈμ˜ κ±°μ§“ μ™„λ£ŒΒ·μ€‘λ³΅Β·λ§κ°Β·μΆ©λŒμ„ λ§‰μ•„μ£ΌλŠ” κ²€μˆ˜Β·κΈ°μ–΅Β·ν˜‘μ—… CLI ν•˜λ„€μŠ€.**
4
4
 
5
- [![npm](https://img.shields.io/badge/npm-leerness-blue)](https://www.npmjs.com/package/leerness) [![version](https://img.shields.io/badge/version-1.9.206-green)]() [![tests](https://img.shields.io/badge/e2e-217%2F217-success)]() [![stress](https://img.shields.io/badge/stress--v151-16%2F16-success)]() [![mcp](https://img.shields.io/badge/MCP--tools-54-brightgreen)]() [![rounds](https://img.shields.io/badge/autonomous--rounds-136-blueviolet)]() [![main-push](https://img.shields.io/badge/release--main--push-68_rounds-success)]() [![goal-matrix](https://img.shields.io/badge/λͺ©μ _맀트릭슀-98%2F100_(5μΆ•)-brightgreen)]() [![i18n](https://img.shields.io/badge/i18n-ko%2Fen_MVP-success)]() [![perf](https://img.shields.io/badge/cold_start-1049ms-success)]() [![install](https://img.shields.io/badge/npm_i_leerness-μ¦‰μ‹œ_μ‚¬μš©_κ°€λŠ₯-success)]() [![sandbox](https://img.shields.io/badge/runCommandSafe-cwd_jail%2Benv_scrub-success)]() [![license](https://img.shields.io/badge/license-MIT-lightgrey)]()
5
+ [![npm](https://img.shields.io/badge/npm-leerness-blue)](https://www.npmjs.com/package/leerness) [![version](https://img.shields.io/badge/version-1.9.208-green)]() [![tests](https://img.shields.io/badge/e2e-217%2F217-success)]() [![stress](https://img.shields.io/badge/stress--v153-18%2F18-success)]() [![mcp](https://img.shields.io/badge/MCP--tools-54-brightgreen)]() [![rounds](https://img.shields.io/badge/autonomous--rounds-138-blueviolet)]() [![main-push](https://img.shields.io/badge/release--main--push-70_rounds-success)]() [![goal-matrix](https://img.shields.io/badge/λͺ©μ _맀트릭슀-98%2F100_(5μΆ•)-brightgreen)]() [![constraints](https://img.shields.io/badge/constraints-API_사전체크_6plat-success)]() [![perf](https://img.shields.io/badge/cold_start-483ms-success)]() [![install](https://img.shields.io/badge/npm_i_leerness-μ¦‰μ‹œ_μ‚¬μš©_κ°€λŠ₯-success)]() [![sandbox](https://img.shields.io/badge/runCommandSafe-cwd_jail%2Benv_scrub-success)]() [![license](https://img.shields.io/badge/license-MIT-lightgrey)]()
6
6
 
7
7
  ```
8
8
  ╔══════════════════════════════════════════════════════════════╗
@@ -12,9 +12,9 @@
12
12
  β•‘ β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•”β•β•β• β–ˆβ–ˆβ•”β•β•β• β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘β•šβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β•β•β• β•šβ•β•β•β•β–ˆβ–ˆβ•‘ β•‘
13
13
  β•‘ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘ β•šβ–ˆβ–ˆβ–ˆβ–ˆβ•‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•‘ β•‘
14
14
  β•‘ β•šβ•β•β•β•β•β•β•β•šβ•β•β•β•β•β•β•β•šβ•β•β•β•β•β•β•β•šβ•β• β•šβ•β•β•šβ•β• β•šβ•β•β•β•β•šβ•β•β•β•β•β•β•β•šβ•β•β•β•β•β•β• β•‘
15
- β•‘ v1.9.206 AI Agent Reliability Harness + Sandbox β•‘
15
+ β•‘ v1.9.208 AI Agent Reliability Harness + Sandbox β•‘
16
16
  β•‘ verify Β· remember Β· orchestrate Β· audit Β· sandbox Β· drift β•‘
17
- β•‘ 🌐 i18n ko/en MVP Β· _typewrite/_ui UI/UX 헬퍼 (μ‚¬μš©μž λͺ…μ‹œ) β•‘
17
+ β•‘ 🚦 ν”Œλž«νΌ/API μ œμ•½ 사전 체크 6μ’… (μ‚¬μš©μž λͺ…μ‹œ) β•‘
18
18
  β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•
19
19
  ```
20
20
 
package/bin/harness.js CHANGED
@@ -7,7 +7,7 @@ const cp = require('child_process');
7
7
  const os = require('os'); // 1.9.178: _publishToNpm μ—μ„œ os.tmpdir() μ‚¬μš© (μ „μ—­ import)
8
8
  const readline = require('readline');
9
9
 
10
- const VERSION = '1.9.206';
10
+ const VERSION = '1.9.208';
11
11
 
12
12
  // 1.9.184: DEP0190 (child_process shell: true) deprecation warning μ–΅μ œ (μ‚¬μš©μž λͺ…μ‹œ).
13
13
  // leerness λŠ” cross-platform PATH resolution 을 μœ„ν•΄ shell: true λ₯Ό μ˜λ„μ μœΌλ‘œ μ‚¬μš© (claude.cmd / ollama.cmd λ“± Windows .cmd 처리).
@@ -1918,6 +1918,199 @@ function _analyzeWakeupStatus(root) {
1918
1918
  return { hasWakeup: true, latest, deltaMin, kind, pending: pending.length };
1919
1919
  }
1920
1920
 
1921
+ // 1.9.207: μ‚¬μš©μž μš”μ²­ λˆ„λ½ 확인 절차 β€” μ‚¬μš©μž λͺ…μ‹œ
1922
+ // "μ΅œκ·Όμ— λͺ…λ Ήλ°›μ•„ μ§„ν–‰ν•œ 것쀑에 λˆ„λ½λœκ²Œ μ—†λŠ”μ§€ ν™•μΈν•˜λŠ” μ ˆμ°¨λ„ μžˆλŠ”μ§€"
1923
+ // .harness/user-requests.json 에 μ‚¬μš©μž λͺ…μ‹œ μš”μ²­μ„ λˆ„μ  기둝 β†’ audit μ‹œ task-log/plan/decisions 와 λ§€μΉ­ β†’ λˆ„λ½ 보고
1924
+ // λ ˆμ½”λ“œ ꡬ쑰: { id, text, recordedAt, status: 'open'|'in-progress'|'completed'|'dropped',
1925
+ // linkedTaskIds?, linkedPlanIds?, linkedDecisionIds?, notes?, completedAt?, droppedAt? }
1926
+ function _userRequestsPath(root) { return path.join(root, '.harness', 'user-requests.json'); }
1927
+ function _loadUserRequests(root) {
1928
+ try {
1929
+ const fp = _userRequestsPath(root);
1930
+ if (!exists(fp)) return { requests: [] };
1931
+ const j = JSON.parse(read(fp));
1932
+ return { requests: Array.isArray(j.requests) ? j.requests : [] };
1933
+ } catch { return { requests: [] }; }
1934
+ }
1935
+ function _writeUserRequests(root, requests) {
1936
+ try {
1937
+ mkdirp(path.join(root, '.harness'));
1938
+ writeUtf8(_userRequestsPath(root), JSON.stringify({ requests, updatedAt: new Date().toISOString() }, null, 2));
1939
+ return true;
1940
+ } catch { return false; }
1941
+ }
1942
+ function _recordUserRequest(root, text, opts = {}) {
1943
+ if (!text || typeof text !== 'string') return null;
1944
+ const state = _loadUserRequests(root);
1945
+ // 쀑볡 λ°©μ§€: 동일 ν…μŠ€νŠΈ + open μƒνƒœ 이미 쑴재 μ‹œ skip
1946
+ const norm = text.trim().slice(0, 500);
1947
+ const dup = state.requests.find(r => r.text === norm && (r.status === 'open' || r.status === 'in-progress'));
1948
+ if (dup) return dup;
1949
+ const id = 'UR-' + String(state.requests.length + 1).padStart(4, '0');
1950
+ const entry = {
1951
+ id, text: norm, recordedAt: new Date().toISOString(),
1952
+ status: opts.status || 'open',
1953
+ linkedTaskIds: opts.linkedTaskIds || [],
1954
+ linkedPlanIds: opts.linkedPlanIds || [],
1955
+ linkedDecisionIds: opts.linkedDecisionIds || [],
1956
+ notes: opts.notes || ''
1957
+ };
1958
+ state.requests.push(entry);
1959
+ // 졜근 200개만 μœ μ§€
1960
+ if (state.requests.length > 200) state.requests = state.requests.slice(-200);
1961
+ _writeUserRequests(root, state.requests);
1962
+ return entry;
1963
+ }
1964
+ function _updateUserRequest(root, id, patch) {
1965
+ const state = _loadUserRequests(root);
1966
+ const idx = state.requests.findIndex(r => r.id === id);
1967
+ if (idx < 0) return null;
1968
+ state.requests[idx] = { ...state.requests[idx], ...patch, updatedAt: new Date().toISOString() };
1969
+ if (patch.status === 'completed' && !state.requests[idx].completedAt) state.requests[idx].completedAt = new Date().toISOString();
1970
+ if (patch.status === 'dropped' && !state.requests[idx].droppedAt) state.requests[idx].droppedAt = new Date().toISOString();
1971
+ _writeUserRequests(root, state.requests);
1972
+ return state.requests[idx];
1973
+ }
1974
+ // audit: open/in-progress μš”μ²­ 쀑 task-log 에 λ§€μΉ­λ˜λŠ” μž‘μ—…μ΄ μ—†λŠ” ν•­λͺ©μ„ "λˆ„λ½ 후보"둜 보고
1975
+ function _auditUserRequests(root) {
1976
+ const state = _loadUserRequests(root);
1977
+ const open = state.requests.filter(r => r.status === 'open' || r.status === 'in-progress');
1978
+ const taskLog = (() => { try { return read(path.join(root, '.harness', 'task-log.md')); } catch { return ''; } })();
1979
+ const plan = (() => { try { return read(path.join(root, '.harness', 'plan.md')); } catch { return ''; } })();
1980
+ const decisions = (() => { try { return read(path.join(root, '.harness', 'decisions.md')); } catch { return ''; } })();
1981
+ const haystack = (taskLog + '\n' + plan + '\n' + decisions).toLowerCase();
1982
+ const missing = [];
1983
+ const tracked = [];
1984
+ open.forEach(req => {
1985
+ const linked = (req.linkedTaskIds && req.linkedTaskIds.length > 0)
1986
+ || (req.linkedPlanIds && req.linkedPlanIds.length > 0)
1987
+ || (req.linkedDecisionIds && req.linkedDecisionIds.length > 0);
1988
+ // ν‚€μ›Œλ“œ λ§€μΉ­ β€” μš”μ²­ ν…μŠ€νŠΈμ˜ 처음 8단어 쀑 3단어 이상 task-log/plan/decisions 에 쑴재 β†’ tracked
1989
+ const words = req.text.toLowerCase().split(/[\s,.()/\\-]+/).filter(w => w.length >= 2).slice(0, 12);
1990
+ const hits = words.filter(w => haystack.includes(w)).length;
1991
+ const matched = hits >= Math.min(3, Math.ceil(words.length * 0.5));
1992
+ if (linked || matched) tracked.push({ ...req, hits, words: words.length });
1993
+ else missing.push({ ...req, hits, words: words.length });
1994
+ });
1995
+ // 7일 이상 묡은 open μš”μ²­ (stale)
1996
+ const now = Date.now();
1997
+ const stale = open.filter(r => (now - new Date(r.recordedAt).getTime()) > 7 * 24 * 60 * 60 * 1000);
1998
+ return {
1999
+ total: state.requests.length,
2000
+ open: open.length,
2001
+ missing,
2002
+ tracked,
2003
+ stale,
2004
+ completed: state.requests.filter(r => r.status === 'completed').length,
2005
+ dropped: state.requests.filter(r => r.status === 'dropped').length
2006
+ };
2007
+ }
2008
+
2009
+ // 1.9.208: ν”Œλž«νΌ/API μ œμ•½ 사전 체크 (μ‚¬μš©μž λͺ…μ‹œ)
2010
+ // "μ‚¬μš©μžμ˜ λͺ…령을 λ°›μœΌλ©΄, κ³ λ €ν•΄μ•Όν•˜λŠ” 뢀뢄을 λ¨Όμ € ν™•μΈν•˜κ³  μ§„ν–‰ν•˜κ²Œ ν•˜λ©΄ μ–΄λ–¨μ§€"
2011
+ // "νŠΉμ • ν”Œλž«νΌμ˜ API ... ν˜ΈμΆœμ†λ„κ°€ μ΄ˆλ‹Ή 5회 μ΄ν•˜λ‘œλ§Œ ν•΄μ•Όν•œλ‹€λŠ” κ·œμ •μ„ λ¨Όμ € 확인"
2012
+ // .harness/platform-constraints.json 에 ν”Œλž«νΌλ³„ μ œμ•½ catalog μ €μž₯ β†’ μš”μ²­ ν…μŠ€νŠΈ ν‚€μ›Œλ“œ λ§€μΉ­ β†’ 적용 κ°€λŠ₯ν•œ μ œμ•½ 보고
2013
+ // κΈ°λ³Έ catalog 6μ’…: Stripe / OpenAI / Anthropic / GitHub / Discord / Twitter
2014
+ function _platformConstraintsPath(root) { return path.join(root, '.harness', 'platform-constraints.json'); }
2015
+ const _DEFAULT_PLATFORM_CONSTRAINTS = {
2016
+ version: '1.9.208',
2017
+ platforms: {
2018
+ stripe: {
2019
+ aliases: ['stripe', 'stripe api', 'payment', '결제'],
2020
+ docs: 'https://stripe.com/docs/rate-limits',
2021
+ constraints: [
2022
+ { kind: 'rate-limit', detail: 'read: 100 req/s, write: 100 req/s (live mode), test mode: 25 req/s' },
2023
+ { kind: 'idempotency', detail: 'Idempotency-Key 헀더 24h μœ μ§€ β€” 쀑볡 결제 λ°©μ§€ ν•„μˆ˜' },
2024
+ { kind: 'webhook', detail: 'webhook μ„œλͺ… 검증 ν•„μˆ˜ (Stripe-Signature header + endpoint secret)' }
2025
+ ]
2026
+ },
2027
+ openai: {
2028
+ aliases: ['openai', 'gpt', 'chatgpt', 'gpt-4', 'gpt-3'],
2029
+ docs: 'https://platform.openai.com/docs/guides/rate-limits',
2030
+ constraints: [
2031
+ { kind: 'rate-limit', detail: 'tier-based: Free 3 RPM / Tier 1 500 RPM / Tier 5 10,000 RPM' },
2032
+ { kind: 'token-limit', detail: 'TPM (tokens/min) 별도 β€” 큰 μž…λ ₯ μ‹œ RPM 도달 μ „ 차단 κ°€λŠ₯' },
2033
+ { kind: 'cost', detail: 'gpt-4: $30/$60 per 1M input/output tokens β€” λŒ€λŸ‰ 호좜 μ „ λΉ„μš© μΆ”μ • ν•„μˆ˜' }
2034
+ ]
2035
+ },
2036
+ anthropic: {
2037
+ aliases: ['anthropic', 'claude', 'claude api', 'sonnet', 'opus', 'haiku'],
2038
+ docs: 'https://docs.anthropic.com/claude/reference/rate-limits',
2039
+ constraints: [
2040
+ { kind: 'rate-limit', detail: 'tier-based: Free 5 RPM / Tier 1 50 RPM / Tier 4 4,000 RPM' },
2041
+ { kind: 'context-window', detail: 'claude-sonnet 200K context, claude-opus 200K, 1M tier 별도' },
2042
+ { kind: 'cost', detail: 'sonnet: $3/$15 per 1M tokens (1M context tier 2x)' }
2043
+ ]
2044
+ },
2045
+ github: {
2046
+ aliases: ['github', 'github api', 'gh api', 'octokit'],
2047
+ docs: 'https://docs.github.com/en/rest/rate-limit',
2048
+ constraints: [
2049
+ { kind: 'rate-limit', detail: 'authenticated: 5,000 req/hr, unauthenticated: 60 req/hr' },
2050
+ { kind: 'rate-limit', detail: 'search API: 30 req/min (authenticated)' },
2051
+ { kind: 'secondary', detail: 'secondary rate limit β€” concurrent + content creation 별도 κ°€λ“œ' }
2052
+ ]
2053
+ },
2054
+ discord: {
2055
+ aliases: ['discord', 'discord api', 'discord bot'],
2056
+ docs: 'https://discord.com/developers/docs/topics/rate-limits',
2057
+ constraints: [
2058
+ { kind: 'rate-limit', detail: 'global: 50 req/s, per-route 별도' },
2059
+ { kind: 'invalid', detail: '10,000 invalid req/10min β†’ 1h ban μœ„ν—˜' }
2060
+ ]
2061
+ },
2062
+ twitter: {
2063
+ aliases: ['twitter', 'twitter api', 'x api', 'x.com api'],
2064
+ docs: 'https://developer.twitter.com/en/docs/twitter-api/rate-limits',
2065
+ constraints: [
2066
+ { kind: 'rate-limit', detail: 'tier-based: Free 1,500 posts/month, Basic 50,000 posts/month' },
2067
+ { kind: 'auth', detail: 'OAuth 2.0 PKCE ν•„μˆ˜ (user context), App-onlyλŠ” 별도 endpoint' }
2068
+ ]
2069
+ }
2070
+ }
2071
+ };
2072
+ function _loadPlatformConstraints(root) {
2073
+ try {
2074
+ const fp = _platformConstraintsPath(root);
2075
+ if (!exists(fp)) return _DEFAULT_PLATFORM_CONSTRAINTS;
2076
+ const j = JSON.parse(read(fp));
2077
+ // user catalog 와 default merge β€” user override μš°μ„ 
2078
+ const merged = { ...JSON.parse(JSON.stringify(_DEFAULT_PLATFORM_CONSTRAINTS)) };
2079
+ if (j.platforms) {
2080
+ for (const k of Object.keys(j.platforms)) {
2081
+ merged.platforms[k] = j.platforms[k];
2082
+ }
2083
+ }
2084
+ return merged;
2085
+ } catch { return _DEFAULT_PLATFORM_CONSTRAINTS; }
2086
+ }
2087
+ function _writePlatformConstraints(root, catalog) {
2088
+ try {
2089
+ mkdirp(path.join(root, '.harness'));
2090
+ writeUtf8(_platformConstraintsPath(root), JSON.stringify({ ...catalog, updatedAt: new Date().toISOString() }, null, 2));
2091
+ return true;
2092
+ } catch { return false; }
2093
+ }
2094
+ // μ‚¬μš©μž μš”μ²­ ν…μŠ€νŠΈμ—μ„œ ν”Œλž«νΌ alias λ§€μΉ­ β†’ 적용 μ œμ•½ λͺ©λ‘ λ°˜ν™˜
2095
+ function _checkRequestConstraints(root, text) {
2096
+ if (!text || typeof text !== 'string') return { matched: [], suggestions: [] };
2097
+ const catalog = _loadPlatformConstraints(root);
2098
+ const lower = text.toLowerCase();
2099
+ const matched = [];
2100
+ for (const [pid, plat] of Object.entries(catalog.platforms)) {
2101
+ const aliases = plat.aliases || [];
2102
+ const hit = aliases.find(a => lower.includes(a.toLowerCase()));
2103
+ if (hit) matched.push({ platform: pid, matchedAlias: hit, docs: plat.docs, constraints: plat.constraints });
2104
+ }
2105
+ // suggestions β€” 보편 ν‚€μ›Œλ“œ (api/rate/limit/integration) μ‹œ catalog κ²€ν†  ꢌμž₯
2106
+ const suggestions = [];
2107
+ const generic = /\bapi\b|연동|integration|호좜|rate|limit|quota|webhook/i.test(text);
2108
+ if (generic && matched.length === 0) {
2109
+ suggestions.push('일반적 API 연동 ν‚€μ›Œλ“œ 감지 β€” leerness constraints list 둜 사전 λ“±λ‘λœ ν”Œλž«νΌ catalog 확인 ꢌμž₯');
2110
+ }
2111
+ return { matched, suggestions, totalPlatforms: Object.keys(catalog.platforms).length };
2112
+ }
2113
+
1921
2114
  // 1.9.203: μžλ™ λΌμš΄λ“œ plan 정리 β€” μ‚¬μš©μž λͺ…μ‹œ
1922
2115
  // "λ°±κ·ΈλΌμš΄λ“œμ— λ‹€μŒ μž‘μ—…μ΄ μ‹œμž‘κ°€λŠ₯ν•œ μ˜ˆμƒ μ‹œκ°„ + μΌμ–΄λ‚¬μ„λ•Œ ν•΄μ•Όν•˜λŠ” 일을 정리"
1923
2116
  // λΌμš΄λ“œ 마무리 μ‹œ .harness/auto-resume-plan.json μžλ™ μ €μž₯ β†’ λ‹€μŒ wakeup μ‹œ μ¦‰μ‹œ μ‹€ν–‰ κ°€λŠ₯
@@ -2027,6 +2220,220 @@ function resumeCmd(root) {
2027
2220
  log(grn(`βœ“ resume μ€€λΉ„ μ™„λ£Œ β€” ꢌμž₯: leerness handoff . λ˜λŠ” leerness next-action take`));
2028
2221
  }
2029
2222
 
2223
+ // 1.9.207: leerness requests <sub> β€” μ‚¬μš©μž μš”μ²­ λˆ„λ½ 확인 절차 CLI (μ‚¬μš©μž λͺ…μ‹œ)
2224
+ // audit β†’ open μš”μ²­ 쀑 task-log/plan/decisions 에 λ§€μΉ­ μ•ˆ λ˜λŠ” ν•­λͺ© 보고 (λˆ„λ½ 후보)
2225
+ // add β†’ μ‚¬μš©μž μš”μ²­ μˆ˜λ™ 기둝 (예: leerness requests add "API rate limit 5/s 적용")
2226
+ // list β†’ λͺ¨λ“  μš”μ²­ 좜λ ₯ (--json μ˜΅μ…˜)
2227
+ // complete <id> β†’ μš”μ²­ μ™„λ£Œ ν‘œμ‹œ
2228
+ // drop <id> β†’ μš”μ²­ λ“œλ‘­ ν‘œμ‹œ (μ‚¬μš©μž λͺ…μ‹œ μ·¨μ†Œ)
2229
+ function requestsCmd(root, sub, ...rest) {
2230
+ root = absRoot(root);
2231
+ const isTty = process.stdout && process.stdout.isTTY;
2232
+ const cyan = s => isTty ? `\x1b[36m${s}\x1b[0m` : s;
2233
+ const grn = s => isTty ? `\x1b[32m${s}\x1b[0m` : s;
2234
+ const yel = s => isTty ? `\x1b[33m${s}\x1b[0m` : s;
2235
+ const red = s => isTty ? `\x1b[31m${s}\x1b[0m` : s;
2236
+ const dim = s => isTty ? `\x1b[2m${s}\x1b[0m` : s;
2237
+
2238
+ if (!sub || sub === 'help' || sub === '--help') {
2239
+ log(`# leerness requests (1.9.207) β€” μ‚¬μš©μž μš”μ²­ λˆ„λ½ 확인 절차`);
2240
+ log('');
2241
+ log(` audit β†’ λˆ„λ½ 후보 + tracked + stale 보고 (--json κ°€λŠ₯)`);
2242
+ log(` add "<text>" β†’ μ‚¬μš©μž μš”μ²­ μˆ˜λ™ 기둝`);
2243
+ log(` list β†’ 전체 μš”μ²­ 좜λ ₯ (--json κ°€λŠ₯, --status open|completed|dropped)`);
2244
+ log(` complete <id> β†’ μš”μ²­ μ™„λ£Œ ν‘œμ‹œ (예: UR-0003)`);
2245
+ log(` drop <id> β†’ μš”μ²­ λ“œλ‘­ ν‘œμ‹œ`);
2246
+ return;
2247
+ }
2248
+
2249
+ if (sub === 'add') {
2250
+ const text = rest.filter(x => !x.startsWith('-')).join(' ');
2251
+ if (!text) { console.error('Usage: leerness requests add "<request text>"'); process.exit(1); }
2252
+ const entry = _recordUserRequest(root, text);
2253
+ if (!entry) { console.error('failed to record'); process.exit(1); }
2254
+ if (has('--json')) { log(JSON.stringify(entry, null, 2)); return; }
2255
+ log(grn(`βœ“ recorded ${entry.id}: ${entry.text}`));
2256
+ return;
2257
+ }
2258
+
2259
+ if (sub === 'list') {
2260
+ const state = _loadUserRequests(root);
2261
+ const filterStatus = arg('--status', null);
2262
+ let list = state.requests;
2263
+ if (filterStatus) list = list.filter(r => r.status === filterStatus);
2264
+ if (has('--json')) { log(JSON.stringify({ total: state.requests.length, filtered: list.length, requests: list }, null, 2)); return; }
2265
+ log(cyan(`# leerness requests list (1.9.207)`));
2266
+ log(` total ${state.requests.length}${filterStatus ? ` Β· filter status=${filterStatus} β†’ ${list.length}` : ''}`);
2267
+ log('');
2268
+ if (list.length === 0) { log(dim(' (μ—†μŒ)')); return; }
2269
+ for (const r of list) {
2270
+ const statusIcon = r.status === 'completed' ? 'βœ“' : (r.status === 'dropped' ? 'βœ—' : (r.status === 'in-progress' ? 'β–Ά' : 'β—―'));
2271
+ const recordedDay = (r.recordedAt || '').slice(0, 10);
2272
+ log(` ${statusIcon} [${r.id}] ${dim(recordedDay)} ${r.text.slice(0, 100)}${r.text.length > 100 ? '…' : ''}`);
2273
+ if (r.linkedTaskIds && r.linkedTaskIds.length > 0) log(dim(` linked tasks: ${r.linkedTaskIds.join(', ')}`));
2274
+ }
2275
+ return;
2276
+ }
2277
+
2278
+ if (sub === 'complete') {
2279
+ const id = rest[0];
2280
+ if (!id) { console.error('Usage: leerness requests complete <id>'); process.exit(1); }
2281
+ const updated = _updateUserRequest(root, id, { status: 'completed' });
2282
+ if (!updated) { console.error(`request not found: ${id}`); process.exit(1); }
2283
+ if (has('--json')) { log(JSON.stringify(updated, null, 2)); return; }
2284
+ log(grn(`βœ“ completed ${updated.id}`));
2285
+ return;
2286
+ }
2287
+
2288
+ if (sub === 'drop') {
2289
+ const id = rest[0];
2290
+ if (!id) { console.error('Usage: leerness requests drop <id>'); process.exit(1); }
2291
+ const updated = _updateUserRequest(root, id, { status: 'dropped' });
2292
+ if (!updated) { console.error(`request not found: ${id}`); process.exit(1); }
2293
+ if (has('--json')) { log(JSON.stringify(updated, null, 2)); return; }
2294
+ log(yel(`βœ— dropped ${updated.id}`));
2295
+ return;
2296
+ }
2297
+
2298
+ if (sub === 'audit') {
2299
+ const audit = _auditUserRequests(root);
2300
+ if (has('--json')) { log(JSON.stringify(audit, null, 2)); return; }
2301
+ log(cyan(`# leerness requests audit (1.9.207) β€” μ‚¬μš©μž μš”μ²­ λˆ„λ½ 확인 절차`));
2302
+ log('');
2303
+ log(` πŸ“Š 톡계: total ${audit.total} Β· open ${audit.open} Β· completed ${audit.completed} Β· dropped ${audit.dropped}`);
2304
+ log('');
2305
+ if (audit.missing.length === 0) {
2306
+ log(grn(` βœ“ λˆ„λ½ μ—†μŒ β€” open μš”μ²­ λͺ¨λ‘ task-log/plan/decisions 에 흔적 있음`));
2307
+ } else {
2308
+ log(red(` ⚠ λˆ„λ½ 후보 ${audit.missing.length}건 (open μƒνƒœμ΄λ‚˜ task/plan/decisions λ§€μΉ­ μ—†μŒ):`));
2309
+ for (const m of audit.missing) {
2310
+ log(` β€’ [${m.id}] ${m.text.slice(0, 90)}${m.text.length > 90 ? '…' : ''}`);
2311
+ log(dim(` ${m.recordedAt.slice(0, 10)} Β· hits=${m.hits}/${m.words}`));
2312
+ }
2313
+ log('');
2314
+ log(dim(` β†’ ꢌμž₯: λˆ„λ½ ν•­λͺ©λ³„ leerness task add "<μš”μ²­>" λ˜λŠ” leerness requests complete <id>`));
2315
+ }
2316
+ if (audit.tracked.length > 0) {
2317
+ log('');
2318
+ log(grn(` βœ“ tracked ${audit.tracked.length}건 (open + task/plan/decisions 맀칭됨):`));
2319
+ for (const t of audit.tracked.slice(0, 5)) {
2320
+ log(` β€’ [${t.id}] ${t.text.slice(0, 80)}${t.text.length > 80 ? '…' : ''} ${dim(`(hits=${t.hits})`)}`);
2321
+ }
2322
+ }
2323
+ if (audit.stale.length > 0) {
2324
+ log('');
2325
+ log(yel(` ⏳ stale ${audit.stale.length}건 (7일+ open):`));
2326
+ for (const s of audit.stale.slice(0, 5)) {
2327
+ const days = Math.floor((Date.now() - new Date(s.recordedAt).getTime()) / 86400000);
2328
+ log(` β€’ [${s.id}] ${days}일 ${s.text.slice(0, 70)}…`);
2329
+ }
2330
+ }
2331
+ return;
2332
+ }
2333
+
2334
+ console.error(`Unknown subcommand: ${sub}`);
2335
+ console.error(`Run: leerness requests help`);
2336
+ process.exit(1);
2337
+ }
2338
+
2339
+ // 1.9.208: leerness constraints <list|check|add> β€” ν”Œλž«νΌ/API μ œμ•½ 사전 체크 CLI (μ‚¬μš©μž λͺ…μ‹œ)
2340
+ // "νŠΉμ • ν”Œλž«νΌμ˜ API ... ν˜ΈμΆœμ†λ„κ°€ μ΄ˆλ‹Ή 5회 μ΄ν•˜λ‘œλ§Œ ν•΄μ•Όν•œλ‹€λŠ” κ·œμ •μ„ λ¨Όμ € 확인"
2341
+ // list β†’ λ“±λ‘λœ λͺ¨λ“  ν”Œλž«νΌ catalog 좜λ ₯
2342
+ // check "<req>" β†’ μ‚¬μš©μž μš”μ²­ ν…μŠ€νŠΈμ—μ„œ ν”Œλž«νΌ λ§€μΉ­ β†’ μ œμ•½ 보고 (--json κ°€λŠ₯)
2343
+ // add <id> --aliases ... --constraint "kind:detail" β†’ μ‚¬μš©μž μ •μ˜ ν”Œλž«νΌ μΆ”κ°€
2344
+ function constraintsCmd(root, sub, ...rest) {
2345
+ root = absRoot(root);
2346
+ const isTty = process.stdout && process.stdout.isTTY;
2347
+ const cyan = s => isTty ? `\x1b[36m${s}\x1b[0m` : s;
2348
+ const grn = s => isTty ? `\x1b[32m${s}\x1b[0m` : s;
2349
+ const yel = s => isTty ? `\x1b[33m${s}\x1b[0m` : s;
2350
+ const red = s => isTty ? `\x1b[31m${s}\x1b[0m` : s;
2351
+ const dim = s => isTty ? `\x1b[2m${s}\x1b[0m` : s;
2352
+
2353
+ if (!sub || sub === 'help' || sub === '--help') {
2354
+ log(`# leerness constraints (1.9.208) β€” ν”Œλž«νΌ/API μ œμ•½ 사전 체크`);
2355
+ log('');
2356
+ log(` list β†’ λ“±λ‘λœ λͺ¨λ“  ν”Œλž«νΌ catalog 좜λ ₯ (--json κ°€λŠ₯)`);
2357
+ log(` check "<request>" β†’ μ‚¬μš©μž μš”μ²­μ—μ„œ ν”Œλž«νΌ λ§€μΉ­ β†’ μ œμ•½ 보고 (--json κ°€λŠ₯)`);
2358
+ log(` add <id> --constraint "kind:detail" --alias name`);
2359
+ log('');
2360
+ log(dim(` 예: leerness constraints check "Stripe API 연동 결제 λͺ¨λ“ˆ"`));
2361
+ return;
2362
+ }
2363
+
2364
+ if (sub === 'list') {
2365
+ const catalog = _loadPlatformConstraints(root);
2366
+ if (has('--json')) { log(JSON.stringify(catalog, null, 2)); return; }
2367
+ log(cyan(`# leerness constraints list (1.9.208)`));
2368
+ log(` total platforms: ${Object.keys(catalog.platforms).length}`);
2369
+ log('');
2370
+ for (const [pid, plat] of Object.entries(catalog.platforms)) {
2371
+ log(grn(` πŸ“¦ ${pid}`) + dim(` aliases: ${(plat.aliases || []).join(', ')}`));
2372
+ log(dim(` docs: ${plat.docs || '-'}`));
2373
+ for (const c of plat.constraints || []) {
2374
+ const icon = c.kind === 'rate-limit' ? '🚦' : (c.kind === 'cost' ? 'πŸ’°' : (c.kind === 'auth' ? 'πŸ”' : 'πŸ“‹'));
2375
+ log(` ${icon} [${c.kind}] ${c.detail}`);
2376
+ }
2377
+ log('');
2378
+ }
2379
+ return;
2380
+ }
2381
+
2382
+ if (sub === 'check') {
2383
+ const text = rest.filter(x => !x.startsWith('-')).join(' ');
2384
+ if (!text) { console.error('Usage: leerness constraints check "<request text>"'); process.exit(1); }
2385
+ const result = _checkRequestConstraints(root, text);
2386
+ if (has('--json')) { log(JSON.stringify({ query: text, ...result }, null, 2)); return; }
2387
+ log(cyan(`# leerness constraints check (1.9.208)`));
2388
+ log(` query: ${text.slice(0, 80)}${text.length > 80 ? '…' : ''}`);
2389
+ log('');
2390
+ if (result.matched.length === 0) {
2391
+ log(grn(` βœ“ 맀칭된 ν”Œλž«νΌ μ—†μŒ (catalog ${result.totalPlatforms}μ’… κ²€ν†  μ™„λ£Œ)`));
2392
+ if (result.suggestions.length) {
2393
+ log('');
2394
+ log(yel(` πŸ’‘ μ œμ•ˆ:`));
2395
+ result.suggestions.forEach(s => log(` β€’ ${s}`));
2396
+ }
2397
+ return;
2398
+ }
2399
+ log(red(` ⚠ ${result.matched.length}개 ν”Œλž«νΌ λ§€μΉ­ β€” μ œμ•½ 사전 확인 ν•„μš”:`));
2400
+ log('');
2401
+ for (const m of result.matched) {
2402
+ log(grn(` πŸ“¦ ${m.platform}`) + dim(` (matched: "${m.matchedAlias}")`));
2403
+ log(dim(` docs: ${m.docs || '-'}`));
2404
+ for (const c of m.constraints || []) {
2405
+ const icon = c.kind === 'rate-limit' ? '🚦' : (c.kind === 'cost' ? 'πŸ’°' : (c.kind === 'auth' ? 'πŸ”' : 'πŸ“‹'));
2406
+ log(` ${icon} [${c.kind}] ${c.detail}`);
2407
+ }
2408
+ log('');
2409
+ }
2410
+ log(dim(` β†’ κ΅¬ν˜„ μ „ μœ„ μ œμ•½μ„ λ°˜μ˜ν•œ 섀계 ꢌμž₯ (rate limiter / idempotency key / λΉ„μš© μΆ”μ •)`));
2411
+ return;
2412
+ }
2413
+
2414
+ if (sub === 'add') {
2415
+ const id = rest[0];
2416
+ if (!id) { console.error('Usage: leerness constraints add <platform-id> --constraint "kind:detail"'); process.exit(1); }
2417
+ const catalog = _loadPlatformConstraints(root);
2418
+ const aliases = (arg('--alias', '') || '').split(',').map(s => s.trim()).filter(Boolean);
2419
+ const detailRaw = arg('--constraint', '');
2420
+ if (!detailRaw) { console.error('--constraint "kind:detail" required'); process.exit(1); }
2421
+ const [kind, ...rest2] = detailRaw.split(':');
2422
+ const detail = rest2.join(':').trim();
2423
+ if (!catalog.platforms[id]) catalog.platforms[id] = { aliases: [], constraints: [] };
2424
+ if (aliases.length) catalog.platforms[id].aliases = [...new Set([...(catalog.platforms[id].aliases || []), ...aliases])];
2425
+ if (kind && detail) catalog.platforms[id].constraints.push({ kind: kind.trim(), detail });
2426
+ _writePlatformConstraints(root, catalog);
2427
+ if (has('--json')) { log(JSON.stringify(catalog.platforms[id], null, 2)); return; }
2428
+ log(grn(`βœ“ platform "${id}" κ°±μ‹  β€” constraints: ${catalog.platforms[id].constraints.length}`));
2429
+ return;
2430
+ }
2431
+
2432
+ console.error(`Unknown subcommand: ${sub}`);
2433
+ console.error(`Run: leerness constraints help`);
2434
+ process.exit(1);
2435
+ }
2436
+
2030
2437
  // 1.9.199: wakeup miss μžλ™ 회볡 κ°•ν™” β€” last-handoff timestamp 기둝 + 25min κΈ°λŒ€ interval μ •λ°€ μΈ‘μ •
2031
2438
  // μ‚¬μš©μž λͺ…μ‹œ (1.9.198 후속): "22:01에 λΌμš΄λ“œμ— μžλ™μ§„μž…λ˜μ§€μ•Šμ•˜μ–΄"
2032
2439
  // 1.9.196 task-log mtime 기반 detector λŠ” task-log κ°€ 자주 μ•ˆ μ—…λ°μ΄νŠΈλ˜λ©΄ false positive.
@@ -4061,10 +4468,19 @@ function handoff(root) {
4061
4468
  parts.push(`πŸ”Œ backend ${pb.ready}/${pb.total} (${ageLabel}${pb.expired ? '⚠' : 'βœ“'})`);
4062
4469
  }
4063
4470
  } catch {}
4471
+ // 13) 1.9.207: μ‚¬μš©μž μš”μ²­ λˆ„λ½ 확인 절차 (μ‚¬μš©μž λͺ…μ‹œ) β€” λˆ„λ½ 후보 β‰₯ 1 μ‹œ ν—€λ“œλΌμΈ λ…ΈμΆœ
4472
+ try {
4473
+ const audit = _auditUserRequests(root);
4474
+ if (audit.missing && audit.missing.length > 0) {
4475
+ parts.push(`πŸ“₯ λ―Έλ‹΅ μš”μ²­ ${audit.missing.length}건`);
4476
+ } else if (audit.open > 0) {
4477
+ parts.push(`πŸ“₯ μš”μ²­ ${audit.open} (tracked)`);
4478
+ }
4479
+ } catch {}
4064
4480
  if (parts.length) {
4065
4481
  const isTty = process.stdout && process.stdout.isTTY;
4066
4482
  const cy = s => isTty ? `\x1b[36m${s}\x1b[0m` : s;
4067
- log(cy(`πŸ“Š ν—€λ“œλΌμΈ (1.9.81/93/113/152/162/192/197/204): ${parts.join(' Β· ')}`));
4483
+ log(cy(`πŸ“Š ν—€λ“œλΌμΈ (1.9.81/93/113/152/162/192/197/204/207): ${parts.join(' Β· ')}`));
4068
4484
  }
4069
4485
  } catch {}
4070
4486
  }
@@ -15025,6 +15441,15 @@ function reviewRequestCmd(root, request) {
15025
15441
  efficiencyHints.push('✨ 좩돌 μ‹ ν˜Έ μ—†μŒ β€” μ¦‰μ‹œ μ§„ν–‰ μ•ˆμ „');
15026
15442
  }
15027
15443
 
15444
+ // 6.5) 1.9.208: ν”Œλž«νΌ/API μ œμ•½ 사전 체크 β€” μ‚¬μš©μž λͺ…μ‹œ ("ν˜ΈμΆœμ†λ„ μ΄ˆλ‹Ή 5회" 같은 κ·œμ • 사전 확인)
15445
+ let constraintsCheck = { matched: [], suggestions: [] };
15446
+ try {
15447
+ constraintsCheck = _checkRequestConstraints(root, text);
15448
+ if (constraintsCheck.matched.length > 0) {
15449
+ efficiencyHints.push(`⚠ ν”Œλž«νΌ μ œμ•½ ${constraintsCheck.matched.length}건 β€” leerness constraints check 둜 상세 확인`);
15450
+ }
15451
+ } catch {}
15452
+
15028
15453
  // 7) proceed ꢌμž₯ (좩돌 critical μ‹œ false)
15029
15454
  const proceed = conflictHints.length < 3 && planConflicts.length === 0;
15030
15455
 
@@ -15039,6 +15464,8 @@ function reviewRequestCmd(root, request) {
15039
15464
  featureConflicts,
15040
15465
  recommendedSteps,
15041
15466
  efficiencyHints,
15467
+ platformConstraints: constraintsCheck.matched,
15468
+ constraintSuggestions: constraintsCheck.suggestions,
15042
15469
  proceed,
15043
15470
  proceedReason: proceed ? 'μ•ˆμ „ β€” 좩돌 μ‹ ν˜Έ < 3 + plan 좩돌 0' : '⚠ 좩돌 critical β€” μ‚¬μš©μž 확인 ν›„ μ§„ν–‰',
15044
15471
  durationMs: dt
@@ -15082,6 +15509,18 @@ function reviewRequestCmd(root, request) {
15082
15509
  log(`## πŸ’‘ 효율 μ œμ•ˆ`);
15083
15510
  efficiencyHints.forEach(h => log(` ${h}`));
15084
15511
  log('');
15512
+ // 1.9.208: ν”Œλž«νΌ/API μ œμ•½ 사전 λ…ΈμΆœ (μ‚¬μš©μž λͺ…μ‹œ)
15513
+ if (constraintsCheck.matched.length > 0) {
15514
+ log(`## 🚦 ν”Œλž«νΌ/API μ œμ•½ 사전 체크 (${constraintsCheck.matched.length})`);
15515
+ for (const m of constraintsCheck.matched) {
15516
+ log(` - πŸ“¦ ${m.platform} (docs: ${m.docs || '-'})`);
15517
+ for (const c of (m.constraints || []).slice(0, 3)) {
15518
+ log(` β€’ [${c.kind}] ${c.detail}`);
15519
+ }
15520
+ }
15521
+ log(` β†’ leerness constraints check "${text.slice(0, 40)}…" 둜 상세 확인`);
15522
+ log('');
15523
+ }
15085
15524
  if (recommendedSteps.length) {
15086
15525
  log(`## πŸ“ ꢌμž₯ 단계 (${estimatedType})`);
15087
15526
  recommendedSteps.forEach(s => log(` ${s}`));
@@ -15353,6 +15792,10 @@ async function main() {
15353
15792
  if (cmd === 'next-action') return nextActionCmd(arg('--path', process.cwd()), args[1], ...args.slice(2));
15354
15793
  // 1.9.203: leerness resume β€” auto-resume-plan 읽고 λ‹€μŒ λΌμš΄λ“œ μ¦‰μ‹œ μ•ˆλ‚΄ (μ‚¬μš©μž λͺ…μ‹œ)
15355
15794
  if (cmd === 'resume') return resumeCmd(arg('--path', process.cwd()));
15795
+ // 1.9.207: leerness requests <audit|add|list|complete|drop> β€” μ‚¬μš©μž μš”μ²­ λˆ„λ½ 확인 절차 (μ‚¬μš©μž λͺ…μ‹œ)
15796
+ if (cmd === 'requests') return requestsCmd(arg('--path', process.cwd()), args[1], ...args.slice(2));
15797
+ // 1.9.208: leerness constraints <list|check|add> β€” ν”Œλž«νΌ/API μ œμ•½ 사전 체크 (μ‚¬μš©μž λͺ…μ‹œ)
15798
+ if (cmd === 'constraints') return constraintsCmd(arg('--path', process.cwd()), args[1], ...args.slice(2));
15356
15799
  if (cmd === 'rule' && args[1] === 'add') return ruleAdd(arg('--path', process.cwd()), args.slice(2).filter(x => !x.startsWith('-')).join(' '));
15357
15800
  if (cmd === 'rule' && args[1] === 'list') return ruleList(arg('--path', process.cwd()));
15358
15801
  if (cmd === 'rule' && args[1] === 'remove') return ruleRemove(arg('--path', process.cwd()), args[2]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.9.206",
3
+ "version": "1.9.208",
4
4
  "description": "Leerness: λΉ„νŒŒκ΄΄ λ§ˆμ΄κ·Έλ ˆμ΄μ…˜, μžλ™ 버전 κ°μ§€Β·μ—…λ°μ΄νŠΈ, κ³„νš/μ§„ν–‰/ν•Έλ“œμ˜€ν”„ μžλ™ν™”, κ²ŒμœΌλ¦„Β·μ‹œν¬λ¦ΏΒ·μΈμ½”λ”© μžλ™ κ°€λ“œ, Claude Code μŠ¬λž˜μ‹œ 톡합을 κ°–μΆ˜ ν•œκ΅­μ–΄ μš°μ„  AI 개발 ν•˜λ„€μŠ€.",
5
5
  "keywords": [
6
6
  "leerness",