leerness 1.9.211 โ†’ 1.9.213

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,120 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.213 โ€” 2026-05-22
4
+
5
+ **๐ŸŽฏ intent inference + scope expansion ๊ฒŒ์ดํŠธ (์‚ฌ์šฉ์ž ๋ช…์‹œ) โ€” ๋งˆ์ง€๋ง‰ pending task ์™„์„ฑ.**
6
+
7
+ ### ์‚ฌ์šฉ์ž ๋ช…์‹œ
8
+ > *"์‚ฌ์šฉ์ž๊ฐ€ ๋งํ•œ ๋‚ด์šฉ๋งŒ ์ˆ˜์ •ํ•ด์•ผํ•˜๋Š” ๊ฒฝ์šฐ๋„ ์žˆ์ง€๋งŒ, ๊ด€๋ จ์žˆ๋Š”๊ฒƒ์ด๋‚˜ ๋ณด๊ฐ•ํ•˜๋ฉด ์ข‹์„ ๋ถ€๋ถ„์„ ํŒŒ์•…ํ•˜์—ฌ ์ง„ํ–‰ / ๊ฒŒ์ž„ ๊ฐœ๋ฐœ์—์„œ ๋งต+์บ๋ฆญํ„ฐ+๊ธฐ๋ณธ๊ธฐ๋Šฅ ์š”์ฒญ ์‹œ ์˜๋„ ํŒŒ์•…"*
9
+
10
+ ### 1. 3์›์น™ ์•ˆ์ „ ์„ค๊ณ„
11
+ 1. **Always-Off, Opt-In** โ€” ๊ธฐ๋ณธ ๋น„ํ™œ์„ฑ, ๋ช…์‹œ์  ํ˜ธ์ถœ (`leerness intent expand`) ๋˜๋Š” review-request ํ†ตํ•ฉ ์‹œ๋งŒ ์ž‘๋™
12
+ 2. **Dry-run ๊ธฐ๋ณธ** โ€” ์‹ค์ œ task add **์ ˆ๋Œ€ X**, ๋ชจ๋“  ์ถœ๋ ฅ์€ ํ›„๋ณด ๋ณด๊ณ ๋งŒ
13
+ 3. **๋ช…์‹œ vs ์ถ”๋ก  ๋ถ„๋ฆฌ ๋ผ๋ฒจ๋ง** โ€” `๐Ÿ‘ค ์‚ฌ์šฉ์ž ๋ช…์‹œ` vs `๐Ÿค– AI ์ถ”๋ก  ํ™•์žฅ` ํ•ญ์ƒ ๊ตฌ๋ถ„
14
+
15
+ ### 2. `_classifyIntent(text)` โ€” 3๋‹จ๊ณ„ ๋ถ„๋ฅ˜
16
+ - **precise** ("์ •ํ™•ํžˆ", "๊ทธ๊ฒƒ๋งŒ", "๊ทธ๋Œ€๋กœ", "๋งํ•œ๋Œ€๋กœ", "only") โ†’ AI ์ถ”๋ก  ํ™•์žฅ **๋น„ํ™œ์„ฑ**
17
+ - **broad** ("๊ธฐ๋ณธ", "ํฌ๊ด„์ ", "๋“ฑ๋“ฑ", "๋‹ค์–‘ํ•œ", "์ „์ฒด", "ํ•„์š”ํ•œ") โ†’ ํ™•์žฅ ํ›„๋ณด N๊ฑด dry-run ์ œ์‹œ
18
+ - **default** (๋ช…์‹œ ์—†์Œ) โ†’ ๋ช…์‹œ ์šฐ์„ , ํ™•์žฅ ํ›„๋ณด ๊ฒ€ํ† ์šฉ์œผ๋กœ ํ‘œ์‹œ
19
+
20
+ ### 3. `.harness/domain-catalog.json` โ€” 5 default + user ํŽธ์ง‘ ๊ฐ€๋Šฅ
21
+ - **game** (10 ์ปดํฌ๋„ŒํŠธ): map, character, gameLoop, collision, camera, hud, audio, save, menu, input
22
+ - **web** (8 ์ปดํฌ๋„ŒํŠธ): routing, state, auth, api, db, ui, test, deploy
23
+ - **api** (7 ์ปดํฌ๋„ŒํŠธ): endpoint, auth, rate-limit, validation, error, logging, docs
24
+ - **cli** (6 ์ปดํฌ๋„ŒํŠธ): argParser, help, config, output, error, completion
25
+ - **data** (6 ์ปดํฌ๋„ŒํŠธ): ingest, transform, storage, query, validation, lineage
26
+
27
+ ### 4. `_inferScopeExpansion(text, root)` โ€” ํ™•์žฅ ํ›„๋ณด dry-run
28
+ - ๋„๋ฉ”์ธ ์ž๋™ ํƒ์ง€ (alias ๋งค์นญ)
29
+ - ์‚ฌ์šฉ์ž ๋ช…์‹œ mention ์ถ”์ถœ
30
+ - ๋‚˜๋จธ์ง€ โ†’ AI ์ถ”๋ก  ํ™•์žฅ ํ›„๋ณด
31
+ - **mode: 'dry-run' ๊ฐ•์ œ** (์‹คํ–‰ X)
32
+
33
+ ### 5. `leerness intent <classify|expand|domains>` CLI
34
+ - `classify "<req>"` โ€” ์˜๋„ ๋ถ„๋ฅ˜ + ์‹ ํ˜ธ
35
+ - `expand "<req>"` โ€” ๋„๋ฉ”์ธ ํƒ์ง€ + ํ™•์žฅ ํ›„๋ณด dry-run
36
+ - `domains` โ€” catalog ์ถœ๋ ฅ
37
+ - ๋ชจ๋‘ `--json` ์ง€์›
38
+
39
+ ### 6. ์‚ฌ์šฉ์ž ๊ฒŒ์ž„ ์˜ˆ์‹œ ๊ฒ€์ฆ
40
+ ```
41
+ $ leerness intent expand "๋งต๊ณผ ์บ๋ฆญํ„ฐ + ๊ธฐ๋ณธ ๊ฒŒ์ž„ ๊ธฐ๋Šฅ ํฌ๊ด„์ ์œผ๋กœ ๋งŒ๋“ค์–ด์ค˜"
42
+ ๐ŸŒ intent: broad ๐Ÿ“ฆ domain: game (matched: "๊ฒŒ์ž„")
43
+
44
+ ## ๐Ÿ‘ค ์‚ฌ์šฉ์ž ๋ช…์‹œ (2)
45
+ โ€ข character โ€” ์บ๋ฆญํ„ฐ/์Šคํ”„๋ผ์ดํŠธ + ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ƒํƒœ๋จธ์‹ 
46
+ โ€ข gameLoop โ€” ๊ฒŒ์ž„ ๋ฃจํ”„ (tick/render/update)
47
+
48
+ ## ๐Ÿค– AI ์ถ”๋ก  ํ™•์žฅ ํ›„๋ณด (8, dry-run)
49
+ [1] map / [2] collision / [3] camera / [4] hud / [5] audio / [6] save / [7] menu / [8] input
50
+
51
+ โ†’ ์ง„ํ–‰ ์‹œ ๋ช…์‹œ ์Šน์ธ ํ•„์š”: leerness task add "<์„ ํƒํ•œ ํ›„๋ณด ์š”์ฒญ>"
52
+ โ†’ ์ „๋ถ€ ๋ฌด์‹œํ•˜๋ ค๋ฉด ์ถ”๊ฐ€ task add ์—†์ด ๋ช…์‹œ ์š”์ฒญ๋งŒ ์ง„ํ–‰
53
+ ```
54
+
55
+ ### 7. precise ์˜๋„ ๋ณดํ˜ธ ๊ฒ€์ฆ
56
+ ```
57
+ $ leerness intent expand "์ •ํ™•ํžˆ ๊ฒŒ์ž„์˜ ๋งต๋งŒ ์ˆ˜์ •ํ•ด์ค˜ ๊ทธ๊ฒƒ๋งŒ"
58
+ ๐Ÿ›ก intent: precise โ€” AI ์ถ”๋ก  ํ™•์žฅ ๋น„ํ™œ์„ฑ (์‚ฌ์šฉ์ž ์˜๋„ ๋ณดํ˜ธ)
59
+ ```
60
+
61
+ ### 8. ๋ˆ„์  ํšŒ๊ท€ (1.9.200~212) โ€” ๋ชจ๋‘ ์œ ์ง€
62
+
63
+ ### 9. stress-v158 โ€” 19/19 PASS
64
+ - 1.9.213 (10) + ์„ฑ๋Šฅ (2) + ๋ˆ„์  ํšŒ๊ท€ (7)
65
+ - ๊ฒŒ์ž„ ์˜ˆ์‹œ broad โ†’ 8 ํ™•์žฅ ํ›„๋ณด / precise โ†’ 0 ํ™•์žฅ (์˜๋„ ๋ณดํ˜ธ) / web ๋„๋ฉ”์ธ ํƒ์ง€
66
+ - ์„ฑ๋Šฅ: --version cold start avg 388ms ยท MCP 54 ๋„๊ตฌ 434ms
67
+
68
+ ### 10. ์ž๋™ release (75 ๋ผ์šด๋“œ main-push streak ยท 36 ๋ผ์šด๋“œ npm publish streak)
69
+
70
+ ### 11. ๐ŸŽ‰ ์‚ฌ์šฉ์ž ๋ช…์‹œ task ๋ฐฑ๋กœ๊ทธ **์™„์ „ ์†Œ์ง„** (#307๊นŒ์ง€ ๋ชจ๋‘ completed)
71
+ - 1.9.207~213 7 ๋ผ์šด๋“œ ์—ฐ์† ์‚ฌ์šฉ์ž ๋ช…์‹œ ์š”์ฒญ ์ฒ˜๋ฆฌ:
72
+ - 207 user-requests audit / 208 platform constraints / 209 pre-wake audit
73
+ - 210 adaptive wakeup / 211 .harnessโ†’.leerness migration / 212 idempotency dedup
74
+ - 213 intent inference + scope expansion ๊ฒŒ์ดํŠธ
75
+
76
+ ---
77
+
78
+ ## 1.9.212 โ€” 2026-05-22
79
+
80
+ **๐Ÿ” ๋ฉฑ๋“ฑ์„ฑ ๊ฐ์‚ฌ + ruleAdd/taskAdd dedup ๋ณด๊ฐ• (์‚ฌ์šฉ์ž ๋ช…์‹œ).**
81
+
82
+ ### ์‚ฌ์šฉ์ž ๋ช…์‹œ
83
+ > *"๋‹ค์–‘ํ•œ ๋ถ€๋ถ„์—์„œ ๋ฉฑ๋“ฑ์„ฑ์ด ํ•„์š”ํ•œ ๋ถ€๋ถ„์ด ๊ณ ๋ ค๋˜๊ณ ์žˆ๋Š”์ง€ ํ™•์ธํ•ด์ฃผ๊ณ  ์˜ฌ๋ฐ”๋ฅธ ๋ฐฉํ–ฅ์ธ์ง€ ํŒ๋‹จํ•ด์ค˜"*
84
+
85
+ ### 1. ruleAdd dedup (line 10229+)
86
+ - ๊ฐ™์€ description + trigger + active ๋ฃฐ ์ด๋ฏธ ์กด์žฌ ์‹œ skip
87
+ - `--force` ํ”Œ๋ž˜๊ทธ๋กœ ์šฐํšŒ ๊ฐ€๋Šฅ
88
+ - ์ถœ๋ ฅ: `rule exists (skip): R-XXXX [trigger] description (--force ๋กœ ๋ฎ์–ด์“ฐ๊ธฐ)`
89
+
90
+ ### 2. taskAdd dedup (line 3709+)
91
+ - progress-tracker.md ์˜ markdown table ํŒŒ์‹ฑ (`| T-XXXX | status | request | โ€ฆ |`)
92
+ - ๊ฐ™์€ request ํ…์ŠคํŠธ + ํ™œ์„ฑ ์ƒํƒœ(`requested`/`in-progress`) ์ด๋ฏธ ์กด์žฌ ์‹œ skip
93
+ - `--force` ํ”Œ๋ž˜๊ทธ๋กœ ์šฐํšŒ ๊ฐ€๋Šฅ
94
+
95
+ ### 3. `_runIdempotencyAudit(root)` โ€” 4์˜์—ญ ์ ๊ฒ€
96
+ - **rules**: ๊ฐ™์€ description + trigger + active ์ค‘๋ณต (severity: medium)
97
+ - **tasks**: ๊ฐ™์€ request ํ…์ŠคํŠธ + ํ™œ์„ฑ ์ƒํƒœ ์ค‘๋ณต (severity: medium)
98
+ - **user-requests**: 1.9.207 ์ž์ฒด dedup ๊ฒ€์ฆ (severity: low)
99
+ - **wakeups**: 1.9.205 ๋™์ผ expectedFireAt ๊ฒ€์ฆ (severity: high)
100
+
101
+ ### 4. `leerness idempotency audit` CLI
102
+ - ์œ„๋ฐ˜ ๋ถ„๋ฅ˜ (high/medium/low)
103
+ - ์˜์—ญ๋ณ„ verified / violations ๋ณด๊ณ 
104
+ - `--json` ์˜ต์…˜
105
+
106
+ ### 5. ๋ˆ„์  ํšŒ๊ท€ (1.9.200~211) โ€” ๋ชจ๋‘ ์œ ์ง€
107
+
108
+ ### 6. stress-v157 โ€” 17/17 PASS
109
+ - 1.9.212 (8) + ์„ฑ๋Šฅ (2) + ๋ˆ„์  ํšŒ๊ท€ (7)
110
+ - ๊ฒฉ๋ฆฌ tmp dir์—์„œ ruleAdd 2ํšŒ โ†’ 1๊ฑด๋งŒ ๋“ฑ๋ก ๊ฒ€์ฆ
111
+ - `--force` ์šฐํšŒ ๊ฒ€์ฆ
112
+ - ์ˆ˜๋™ ์ค‘๋ณต rule ์‚ฝ์ž… โ†’ audit์ด ์ •ํ™•ํžˆ 1๊ฑด violation ํƒ์ง€
113
+
114
+ ### 7. ์ž๋™ release (74 ๋ผ์šด๋“œ main-push streak ยท 35 ๋ผ์šด๋“œ npm publish streak)
115
+
116
+ ---
117
+
3
118
  ## 1.9.211 โ€” 2026-05-22
4
119
 
5
120
  **๐Ÿ“‚ `.harness` โ†’ `.leerness` opt-in ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ + AI ์ฐธ์กฐ ๊ฐ€์ด๋“œ (์‚ฌ์šฉ์ž ๋ช…์‹œ).**
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.211-green)]() [![tests](https://img.shields.io/badge/e2e-217%2F217-success)]() [![stress](https://img.shields.io/badge/stress--v156-19%2F19-success)]() [![mcp](https://img.shields.io/badge/MCP--tools-54-brightgreen)]() [![rounds](https://img.shields.io/badge/autonomous--rounds-141-blueviolet)]() [![main-push](https://img.shields.io/badge/release--main--push-73_rounds-success)]() [![goal-matrix](https://img.shields.io/badge/๋ชฉ์ _๋งคํŠธ๋ฆญ์Šค-98%2F100_(5์ถ•)-brightgreen)]() [![workspace](https://img.shields.io/badge/.harnessโ†’.leerness-opt--in_migration-success)]() [![perf](https://img.shields.io/badge/cold_start-452ms-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.213-green)]() [![tests](https://img.shields.io/badge/e2e-217%2F217-success)]() [![stress](https://img.shields.io/badge/stress--v158-19%2F19-success)]() [![mcp](https://img.shields.io/badge/MCP--tools-54-brightgreen)]() [![rounds](https://img.shields.io/badge/autonomous--rounds-143-blueviolet)]() [![main-push](https://img.shields.io/badge/release--main--push-75_rounds-success)]() [![goal-matrix](https://img.shields.io/badge/๋ชฉ์ _๋งคํŠธ๋ฆญ์Šค-98%2F100_(5์ถ•)-brightgreen)]() [![intent](https://img.shields.io/badge/intent-inference_5๋„๋ฉ”์ธ-success)]() [![perf](https://img.shields.io/badge/cold_start-388ms-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.211 AI Agent Reliability Harness + Sandbox โ•‘
15
+ โ•‘ v1.9.213 AI Agent Reliability Harness + Sandbox โ•‘
16
16
  โ•‘ verify ยท remember ยท orchestrate ยท audit ยท sandbox ยท drift โ•‘
17
- โ•‘ ๐Ÿ“‚ .harness โ†’ .leerness opt-in ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ (์‚ฌ์šฉ์ž ๋ช…์‹œ) โ•‘
17
+ โ•‘ ๐ŸŽฏ intent inference + scope expansion 5๋„๋ฉ”์ธ (์‚ฌ์šฉ์ž ๋ช…์‹œ) โ•‘
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.211';
10
+ const VERSION = '1.9.213';
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 ์ฒ˜๋ฆฌ).
@@ -2526,6 +2526,175 @@ function _buildWorkspaceReferenceGuide(root, dirName) {
2526
2526
  return lines.join('\n');
2527
2527
  }
2528
2528
 
2529
+ // 1.9.213: intent inference + scope expansion ๊ฒŒ์ดํŠธ (์‚ฌ์šฉ์ž ๋ช…์‹œ)
2530
+ // "์‚ฌ์šฉ์ž๊ฐ€ ๋งํ•œ ๋‚ด์šฉ๋งŒ ์ˆ˜์ •ํ•ด์•ผํ•˜๋Š” ๊ฒฝ์šฐ๋„ ์žˆ์ง€๋งŒ, ๊ด€๋ จ์žˆ๋Š”๊ฒƒ์ด๋‚˜ ๋ณด๊ฐ•ํ•˜๋ฉด ์ข‹์„ ๋ถ€๋ถ„์„ ํŒŒ์•…ํ•˜์—ฌ ์ง„ํ–‰"
2531
+ // "๊ฒŒ์ž„ ๊ฐœ๋ฐœ์—์„œ ๋งต+์บ๋ฆญํ„ฐ+๊ธฐ๋ณธ๊ธฐ๋Šฅ ์š”์ฒญ ์‹œ ์˜๋„ ํŒŒ์•…"
2532
+ // 3์›์น™: (1) Always-Off Opt-In, (2) Dry-run ๊ธฐ๋ณธ (์‹คํ–‰ X), (3) ๋ช…์‹œ vs ์ถ”๋ก  ๋ถ„๋ฆฌ ๋ผ๋ฒจ๋ง
2533
+ // .harness/domain-catalog.json ์‚ฌ์šฉ์ž ํŽธ์ง‘ ๊ฐ€๋Šฅ + default catalog 5์ข… (game/web/api/cli/data)
2534
+ function _domainCatalogPath(root) { return path.join(root, '.harness', 'domain-catalog.json'); }
2535
+ const _DEFAULT_DOMAIN_CATALOG = {
2536
+ version: '1.9.213',
2537
+ domains: {
2538
+ game: {
2539
+ aliases: ['๊ฒŒ์ž„', 'game', 'unity', 'unreal', 'godot', 'phaser', 'gamedev'],
2540
+ components: [
2541
+ { key: 'map', desc: '๋งต/ํƒ€์ผ ์‹œ์Šคํ…œ + ์˜์—ญ/์Šคํฐ' },
2542
+ { key: 'character', desc: '์บ๋ฆญํ„ฐ/์Šคํ”„๋ผ์ดํŠธ + ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ƒํƒœ๋จธ์‹ ' },
2543
+ { key: 'gameLoop', desc: '๊ฒŒ์ž„ ๋ฃจํ”„ (tick/render/update)' },
2544
+ { key: 'collision', desc: '์ถฉ๋Œ ๊ฐ์ง€ (AABB/SAT/grid)' },
2545
+ { key: 'camera', desc: '์นด๋ฉ”๋ผ follow + ์˜์—ญ ์ œํ•œ' },
2546
+ { key: 'hud', desc: 'HUD/UI (HP/score/inventory)' },
2547
+ { key: 'audio', desc: '์‚ฌ์šด๋“œ ๋งค๋‹ˆ์ € (BGM/SFX)' },
2548
+ { key: 'save', desc: '์ €์žฅ/๋กœ๋“œ (slot ์‹œ์Šคํ…œ)' },
2549
+ { key: 'menu', desc: '๋ฉ”๋‰ด (๋ฉ”์ธ/์ผ์‹œ์ •์ง€/์„ค์ •)' },
2550
+ { key: 'input', desc: '์ž…๋ ฅ ํ•ธ๋“ค๋Ÿฌ (ํ‚ค๋ณด๋“œ/ํŒจ๋“œ/ํ„ฐ์น˜)' }
2551
+ ]
2552
+ },
2553
+ web: {
2554
+ aliases: ['์›น', 'web', 'website', 'webapp', 'nextjs', 'react', 'vue', 'svelte', 'frontend'],
2555
+ components: [
2556
+ { key: 'routing', desc: '๋ผ์šฐํŒ… (path/dynamic/nested)' },
2557
+ { key: 'state', desc: '์ƒํƒœ ๊ด€๋ฆฌ (context/redux/zustand)' },
2558
+ { key: 'auth', desc: '์ธ์ฆ (OAuth/JWT/session)' },
2559
+ { key: 'api', desc: 'API ํด๋ผ์ด์–ธํŠธ (fetch/axios/tanstack-query)' },
2560
+ { key: 'db', desc: 'DB ์—ฐ๋™ (ORM/migration/seed)' },
2561
+ { key: 'ui', desc: 'UI ์ปดํฌ๋„ŒํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ' },
2562
+ { key: 'test', desc: 'ํ…Œ์ŠคํŠธ (unit/e2e/visual)' },
2563
+ { key: 'deploy', desc: '๋ฐฐํฌ (Vercel/Netlify/Cloudflare)' }
2564
+ ]
2565
+ },
2566
+ api: {
2567
+ aliases: ['api', 'rest', 'graphql', 'endpoint', 'backend', 'server'],
2568
+ components: [
2569
+ { key: 'endpoint', desc: '์—”๋“œํฌ์ธํŠธ ๋ผ์šฐํŒ… + HTTP method' },
2570
+ { key: 'auth', desc: '์ธ์ฆ/์ธ๊ฐ€ (API key/OAuth/JWT)' },
2571
+ { key: 'rate-limit', desc: 'rate limit (RPS/RPM/token bucket)' },
2572
+ { key: 'validation', desc: '์ž…๋ ฅ ๊ฒ€์ฆ (zod/joi/yup)' },
2573
+ { key: 'error', desc: '์—๋Ÿฌ ํ•ธ๋“ค๋ง + ์‘๋‹ต ํ˜•์‹' },
2574
+ { key: 'logging', desc: '๋กœ๊น… + ๋ชจ๋‹ˆํ„ฐ๋ง (structured logs)' },
2575
+ { key: 'docs', desc: 'API ๋ฌธ์„œ (OpenAPI/Swagger)' }
2576
+ ]
2577
+ },
2578
+ cli: {
2579
+ aliases: ['cli', 'command-line', 'tool', 'utility', 'shell'],
2580
+ components: [
2581
+ { key: 'argParser', desc: '์ธ์ž ํŒŒ์‹ฑ (yargs/commander/clipanion)' },
2582
+ { key: 'help', desc: 'help / man / examples ํ…์ŠคํŠธ' },
2583
+ { key: 'config', desc: '์„ค์ • ํŒŒ์ผ + env ๋ณ€์ˆ˜' },
2584
+ { key: 'output', desc: '์ถœ๋ ฅ (TTY ์ƒ‰์ƒ/JSON/quiet)' },
2585
+ { key: 'error', desc: '์—๋Ÿฌ ์ฒ˜๋ฆฌ + exit code ๊ทœ์•ฝ' },
2586
+ { key: 'completion', desc: 'shell completion (bash/zsh/fish)' }
2587
+ ]
2588
+ },
2589
+ data: {
2590
+ aliases: ['data', 'pipeline', 'etl', 'analytics', 'ingest'],
2591
+ components: [
2592
+ { key: 'ingest', desc: '๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ (file/API/stream)' },
2593
+ { key: 'transform', desc: '๋ณ€ํ™˜ (cleaning/normalization/joining)' },
2594
+ { key: 'storage', desc: '์ €์žฅ์†Œ (parquet/db/blob)' },
2595
+ { key: 'query', desc: '์ฟผ๋ฆฌ/๋ถ„์„ (SQL/aggregations)' },
2596
+ { key: 'validation', desc: '๋ฐ์ดํ„ฐ ๊ฒ€์ฆ (schema/contracts)' },
2597
+ { key: 'lineage', desc: '๋ฐ์ดํ„ฐ lineage ์ถ”์ ' }
2598
+ ]
2599
+ }
2600
+ }
2601
+ };
2602
+ function _loadDomainCatalog(root) {
2603
+ try {
2604
+ const fp = _domainCatalogPath(root);
2605
+ if (!exists(fp)) return _DEFAULT_DOMAIN_CATALOG;
2606
+ const j = JSON.parse(read(fp));
2607
+ // user catalog merge โ€” user override ์šฐ์„ 
2608
+ const merged = JSON.parse(JSON.stringify(_DEFAULT_DOMAIN_CATALOG));
2609
+ if (j.domains) {
2610
+ for (const k of Object.keys(j.domains)) merged.domains[k] = j.domains[k];
2611
+ }
2612
+ return merged;
2613
+ } catch { return _DEFAULT_DOMAIN_CATALOG; }
2614
+ }
2615
+ function _writeDomainCatalog(root, catalog) {
2616
+ try {
2617
+ mkdirp(path.join(root, '.harness'));
2618
+ writeUtf8(_domainCatalogPath(root), JSON.stringify({ ...catalog, updatedAt: new Date().toISOString() }, null, 2));
2619
+ return true;
2620
+ } catch { return false; }
2621
+ }
2622
+ // intent ๋ถ„๋ฅ˜: precise / broad / default
2623
+ function _classifyIntent(text) {
2624
+ if (!text || typeof text !== 'string') return { intent: 'default', signals: [] };
2625
+ const signals = [];
2626
+ // precise ์‹ ํ˜ธ: "์ •ํ™•ํžˆ / ๊ทธ๊ฒƒ๋งŒ / ๊ทธ๋Œ€๋กœ / only / just / ๋งŒ"
2627
+ const preciseKws = ['์ •ํ™•ํžˆ', '๊ทธ๊ฒƒ๋งŒ', '๊ทธ๋Œ€๋กœ', 'only', 'just only', '๋งํ•œ๋Œ€๋กœ', '๋งํ•œ ๊ทธ๋Œ€๋กœ'];
2628
+ for (const kw of preciseKws) {
2629
+ if (text.toLowerCase().includes(kw.toLowerCase())) signals.push({ kind: 'precise', match: kw });
2630
+ }
2631
+ // broad ์‹ ํ˜ธ: "๊ธฐ๋ณธ / ํฌ๊ด„์  / ๋“ฑ๋“ฑ / ๋‹ค์–‘ํ•œ / ์ „์ฒด / ๊ธฐ๋ณธ์ ์ธ / etc / overall"
2632
+ const broadKws = ['๊ธฐ๋ณธ', 'ํฌ๊ด„์ ', '๋“ฑ๋“ฑ', '๋‹ค์–‘ํ•œ', '์ „์ฒด', '๊ธฐ๋ณธ์ ์ธ', 'etc', 'overall', 'ํ•„์š”ํ•œ', '๊ด€๋ จ', 'comprehensive', 'including'];
2633
+ for (const kw of broadKws) {
2634
+ if (text.toLowerCase().includes(kw.toLowerCase())) signals.push({ kind: 'broad', match: kw });
2635
+ }
2636
+ // ๊ฒฐ์ •
2637
+ const preciseCount = signals.filter(s => s.kind === 'precise').length;
2638
+ const broadCount = signals.filter(s => s.kind === 'broad').length;
2639
+ let intent;
2640
+ if (preciseCount > broadCount && preciseCount >= 1) intent = 'precise';
2641
+ else if (broadCount >= 1) intent = 'broad';
2642
+ else intent = 'default';
2643
+ return { intent, signals, preciseCount, broadCount };
2644
+ }
2645
+ function _detectDomain(text, root) {
2646
+ if (!text) return { domain: null, alias: null };
2647
+ const lower = text.toLowerCase();
2648
+ const catalog = _loadDomainCatalog(root);
2649
+ for (const [domain, info] of Object.entries(catalog.domains)) {
2650
+ for (const a of info.aliases || []) {
2651
+ if (lower.includes(a.toLowerCase())) {
2652
+ return { domain, alias: a, components: info.components };
2653
+ }
2654
+ }
2655
+ }
2656
+ return { domain: null, alias: null };
2657
+ }
2658
+ function _inferScopeExpansion(text, root) {
2659
+ const classify = _classifyIntent(text);
2660
+ const det = _detectDomain(text, root);
2661
+ const result = {
2662
+ text,
2663
+ intent: classify.intent,
2664
+ intentSignals: classify.signals,
2665
+ domain: det.domain,
2666
+ matchedAlias: det.alias,
2667
+ explicitMentions: [],
2668
+ expansionCandidates: [],
2669
+ mode: 'dry-run' // ํ•ญ์ƒ dry-run โ€” ์‹คํ–‰ X
2670
+ };
2671
+ if (!det.domain || !det.components) {
2672
+ result.note = '๋„๋ฉ”์ธ ๋ฏธํƒ์ง€ โ€” ๋ช…์‹œ ์š”์ฒญ๋งŒ ์ง„ํ–‰ (ํ™•์žฅ ํ›„๋ณด ์—†์Œ)';
2673
+ return result;
2674
+ }
2675
+ // ์‚ฌ์šฉ์ž ํ…์ŠคํŠธ์—์„œ component key ์ง์ ‘ ์–ธ๊ธ‰ ์ถ”์ถœ
2676
+ const lowerText = text.toLowerCase();
2677
+ for (const c of det.components) {
2678
+ const mentionPatterns = [c.key.toLowerCase(), ...(c.desc.split(/[\s/()]/).filter(w => w.length >= 2).map(w => w.toLowerCase()))];
2679
+ const mentioned = mentionPatterns.some(p => lowerText.includes(p));
2680
+ if (mentioned) {
2681
+ result.explicitMentions.push({ key: c.key, desc: c.desc, source: 'user-explicit' });
2682
+ } else {
2683
+ result.expansionCandidates.push({ key: c.key, desc: c.desc, source: 'ai-inference' });
2684
+ }
2685
+ }
2686
+ // intent === precise ๋ฉด expansion ๋น„์–ด์žˆ๊ฒŒ (์˜๋„ ๋ณดํ˜ธ)
2687
+ if (classify.intent === 'precise') {
2688
+ result.expansionCandidates = [];
2689
+ result.note = `intent: precise โ€” ๋ช…์‹œ ์š”์ฒญ๋งŒ ์ง„ํ–‰, AI ์ถ”๋ก  ํ™•์žฅ ๋น„ํ™œ์„ฑ (์‚ฌ์šฉ์ž ์˜๋„ ๋ณดํ˜ธ)`;
2690
+ } else if (classify.intent === 'broad') {
2691
+ result.note = `intent: broad โ€” ${result.expansionCandidates.length} ํ™•์žฅ ํ›„๋ณด ์ œ์‹œ (--expand-all ๋˜๋Š” --select N ์œผ๋กœ ๋ช…์‹œ ์Šน์ธ ํ•„์š”)`;
2692
+ } else {
2693
+ result.note = `intent: default โ€” ๋ช…์‹œ ${result.explicitMentions.length}๊ฑด๋งŒ ์šฐ์„ , ํ™•์žฅ ํ›„๋ณด ${result.expansionCandidates.length}๊ฑด ๊ฒ€ํ† ์šฉ`;
2694
+ }
2695
+ return result;
2696
+ }
2697
+
2529
2698
  // 1.9.203: ์ž๋™ ๋ผ์šด๋“œ plan ์ •๋ฆฌ โ€” ์‚ฌ์šฉ์ž ๋ช…์‹œ
2530
2699
  // "๋ฐฑ๊ทธ๋ผ์šด๋“œ์— ๋‹ค์Œ ์ž‘์—…์ด ์‹œ์ž‘๊ฐ€๋Šฅํ•œ ์˜ˆ์ƒ ์‹œ๊ฐ„ + ์ผ์–ด๋‚ฌ์„๋•Œ ํ•ด์•ผํ•˜๋Š” ์ผ์„ ์ •๋ฆฌ"
2531
2700
  // ๋ผ์šด๋“œ ๋งˆ๋ฌด๋ฆฌ ์‹œ .harness/auto-resume-plan.json ์ž๋™ ์ €์žฅ โ†’ ๋‹ค์Œ wakeup ์‹œ ์ฆ‰์‹œ ์‹คํ–‰ ๊ฐ€๋Šฅ
@@ -3129,6 +3298,287 @@ function migrateWorkspaceDirCmd(root) {
3129
3298
  }
3130
3299
  }
3131
3300
 
3301
+ // 1.9.212: leerness idempotency <audit> โ€” ๋ฉฑ๋“ฑ์„ฑ ์œ„๋ฐ˜ ํƒ์ง€ + ๊ถŒ์žฅ fix (์‚ฌ์šฉ์ž ๋ช…์‹œ)
3302
+ // "๋‹ค์–‘ํ•œ ๋ถ€๋ถ„์—์„œ ๋ฉฑ๋“ฑ์„ฑ์ด ํ•„์š”ํ•œ ๋ถ€๋ถ„์ด ๊ณ ๋ ค๋˜๊ณ ์žˆ๋Š”์ง€ ํ™•์ธํ•ด์ฃผ๊ณ  ์˜ฌ๋ฐ”๋ฅธ ๋ฐฉํ–ฅ์ธ์ง€ ํŒ๋‹จ"
3303
+ // ํƒ์ง€ ์˜์—ญ:
3304
+ // 1. rules.md โ€” ๊ฐ™์€ description + trigger + active ์ค‘๋ณต
3305
+ // 2. progress-tracker.md โ€” ๊ฐ™์€ request ํ…์ŠคํŠธ ์ค‘๋ณต (ํ™œ์„ฑ ์ƒํƒœ)
3306
+ // 3. user-requests.json โ€” ๋™์ผ ํ…์ŠคํŠธ + open (1.9.207 ์ž์ฒด dedup ๊ฒ€์ฆ)
3307
+ // 4. active-wakeups.json โ€” ๋™์ผ expectedFireAt ์ค‘๋ณต (1.9.205 dedup ๊ฒ€์ฆ)
3308
+ // 5. next-action-queue.json โ€” ๋™์ผ title ์ค‘๋ณต (1.9.201 dedup ๊ฒ€์ฆ)
3309
+ function _runIdempotencyAudit(root) {
3310
+ const audit = {
3311
+ auditedAt: new Date().toISOString(),
3312
+ auditVersion: VERSION,
3313
+ violations: [],
3314
+ verified: []
3315
+ };
3316
+ // 1) rules.md dup ๊ฒ€์‚ฌ
3317
+ try {
3318
+ const rules = readRules(root);
3319
+ const active = rules.filter(r => r.status === 'active');
3320
+ const seen = new Map();
3321
+ for (const r of active) {
3322
+ const key = `${r.trigger}::${r.rule}`;
3323
+ if (seen.has(key)) {
3324
+ audit.violations.push({
3325
+ kind: 'rule-duplicate',
3326
+ location: '.harness/rules.md',
3327
+ detail: `์ค‘๋ณต ๋ฃฐ: ${r.id} == ${seen.get(key)} (${r.trigger}: ${r.rule.slice(0, 60)})`,
3328
+ severity: 'medium',
3329
+ fix: `leerness rule remove ${r.id}`
3330
+ });
3331
+ } else {
3332
+ seen.set(key, r.id);
3333
+ }
3334
+ }
3335
+ if (audit.violations.filter(v => v.kind === 'rule-duplicate').length === 0) {
3336
+ audit.verified.push({ kind: 'rules', detail: `${active.length} active rules, ์ค‘๋ณต 0๊ฑด` });
3337
+ }
3338
+ } catch (e) { audit.violations.push({ kind: 'audit-error', area: 'rules', detail: String(e.message || e), severity: 'low' }); }
3339
+
3340
+ // 2) progress-tracker.md ์ค‘๋ณต request ๊ฒ€์‚ฌ
3341
+ // ํฌ๋งท: | T-XXXX | status | request | evidence | nextAction | date |
3342
+ try {
3343
+ const pt = path.join(root, '.harness', 'progress-tracker.md');
3344
+ if (exists(pt)) {
3345
+ const content = read(pt);
3346
+ const lines = content.split(/\r?\n/);
3347
+ const requests = new Map(); // text โ†’ { id, status, line }
3348
+ for (let i = 0; i < lines.length; i++) {
3349
+ if (!/^\|\s*T-\d{4,}\s*\|/.test(lines[i])) continue;
3350
+ const cells = lines[i].split('|').slice(1, -1).map(s => s.trim());
3351
+ if (cells.length < 3) continue;
3352
+ const id = cells[0];
3353
+ const status = (cells[1] || '').toLowerCase();
3354
+ const text = cells[2];
3355
+ if (text.length < 5) continue;
3356
+ const isActive = !/^(done|dropped|blocked|completed)$/i.test(status);
3357
+ if (!isActive) continue;
3358
+ if (requests.has(text)) {
3359
+ const prev = requests.get(text);
3360
+ audit.violations.push({
3361
+ kind: 'task-duplicate-request',
3362
+ location: '.harness/progress-tracker.md',
3363
+ detail: `์ค‘๋ณต request: "${text.slice(0, 50)}โ€ฆ" (${prev.id} & ${id})`,
3364
+ severity: 'medium',
3365
+ fix: `์ค‘๋ณต task ์ค‘ ํ•˜๋‚˜๋ฅผ leerness task drop <id> ์ฒ˜๋ฆฌ`
3366
+ });
3367
+ } else {
3368
+ requests.set(text, { id, status, line: i + 1 });
3369
+ }
3370
+ }
3371
+ if (audit.violations.filter(v => v.kind === 'task-duplicate-request').length === 0) {
3372
+ audit.verified.push({ kind: 'tasks', detail: `${requests.size} active tasks, request ์ค‘๋ณต 0๊ฑด` });
3373
+ }
3374
+ }
3375
+ } catch (e) { audit.violations.push({ kind: 'audit-error', area: 'tasks', detail: String(e.message || e), severity: 'low' }); }
3376
+
3377
+ // 3) user-requests.json ์ž์ฒด dedup ๊ฒ€์ฆ (1.9.207)
3378
+ try {
3379
+ const ur = _loadUserRequests(root);
3380
+ const seen = new Map();
3381
+ for (const r of ur.requests) {
3382
+ if (r.status !== 'open' && r.status !== 'in-progress') continue;
3383
+ const k = r.text.trim();
3384
+ if (seen.has(k)) {
3385
+ audit.violations.push({
3386
+ kind: 'user-request-duplicate',
3387
+ location: '.harness/user-requests.json',
3388
+ detail: `์ค‘๋ณต open ์š”์ฒญ: ${r.id} == ${seen.get(k)} ("${k.slice(0, 50)}โ€ฆ")`,
3389
+ severity: 'low',
3390
+ fix: `leerness requests drop ${r.id}`
3391
+ });
3392
+ } else {
3393
+ seen.set(k, r.id);
3394
+ }
3395
+ }
3396
+ if (audit.violations.filter(v => v.kind === 'user-request-duplicate').length === 0) {
3397
+ audit.verified.push({ kind: 'user-requests', detail: `${seen.size} open requests, ์ค‘๋ณต 0๊ฑด (1.9.207 dedup OK)` });
3398
+ }
3399
+ } catch (e) { audit.violations.push({ kind: 'audit-error', area: 'user-requests', detail: String(e.message || e), severity: 'low' }); }
3400
+
3401
+ // 4) active-wakeups.json ๊ฒ€์ฆ (1.9.205)
3402
+ try {
3403
+ const wk = _loadActiveWakeups(root);
3404
+ const pending = wk.wakeups.filter(w => w.status === 'pending');
3405
+ const seenT = new Map();
3406
+ for (const w of pending) {
3407
+ if (seenT.has(w.expectedFireAt)) {
3408
+ audit.violations.push({
3409
+ kind: 'wakeup-duplicate',
3410
+ location: '.harness/active-wakeups.json',
3411
+ detail: `๋™์ผ expectedFireAt ์ค‘๋ณต: ${w.expectedFireAt}`,
3412
+ severity: 'high',
3413
+ fix: `leerness ์ž๋™ _recordWakeup filter dedup ๊ฒ€ํ†  ํ•„์š”`
3414
+ });
3415
+ } else {
3416
+ seenT.set(w.expectedFireAt, true);
3417
+ }
3418
+ }
3419
+ if (audit.violations.filter(v => v.kind === 'wakeup-duplicate').length === 0) {
3420
+ audit.verified.push({ kind: 'wakeups', detail: `${pending.length} pending wakeups, ์ค‘๋ณต 0๊ฑด (1.9.205 dedup OK)` });
3421
+ }
3422
+ } catch {}
3423
+
3424
+ audit.summary = {
3425
+ totalViolations: audit.violations.length,
3426
+ highSeverity: audit.violations.filter(v => v.severity === 'high').length,
3427
+ mediumSeverity: audit.violations.filter(v => v.severity === 'medium').length,
3428
+ lowSeverity: audit.violations.filter(v => v.severity === 'low').length,
3429
+ verifiedAreas: audit.verified.length,
3430
+ overall: audit.violations.length === 0 ? 'clean' : 'violations-found'
3431
+ };
3432
+ return audit;
3433
+ }
3434
+ function idempotencyCmd(root, sub) {
3435
+ root = absRoot(root);
3436
+ const isTty = process.stdout && process.stdout.isTTY;
3437
+ const cyan = s => isTty ? `\x1b[36m${s}\x1b[0m` : s;
3438
+ const grn = s => isTty ? `\x1b[32m${s}\x1b[0m` : s;
3439
+ const yel = s => isTty ? `\x1b[33m${s}\x1b[0m` : s;
3440
+ const red = s => isTty ? `\x1b[31m${s}\x1b[0m` : s;
3441
+ const dim = s => isTty ? `\x1b[2m${s}\x1b[0m` : s;
3442
+
3443
+ if (!sub || sub === 'help' || sub === '--help') {
3444
+ log(`# leerness idempotency (1.9.212) โ€” ๋ฉฑ๋“ฑ์„ฑ ์œ„๋ฐ˜ ํƒ์ง€`);
3445
+ log('');
3446
+ log(` audit โ†’ ์›Œํฌ์ŠคํŽ˜์ด์Šค ๋ฉฑ๋“ฑ์„ฑ ์ ๊ฒ€ (rules / tasks / user-requests / wakeups) (--json ๊ฐ€๋Šฅ)`);
3447
+ log('');
3448
+ log(dim(` dedup ์ ์šฉ ์˜์—ญ: ruleAdd / taskAdd (1.9.212) + _recordUserRequest (1.9.207) + _recordWakeup (1.9.205)`));
3449
+ log(dim(` opt-out: --force ํ”Œ๋ž˜๊ทธ๋กœ dedup ์šฐํšŒ ๊ฐ€๋Šฅ`));
3450
+ return;
3451
+ }
3452
+
3453
+ if (sub === 'audit') {
3454
+ const audit = _runIdempotencyAudit(root);
3455
+ if (has('--json')) { log(JSON.stringify(audit, null, 2)); return; }
3456
+ log(cyan(`# leerness idempotency audit (1.9.212)`));
3457
+ log(` audited at: ${audit.auditedAt}`);
3458
+ log('');
3459
+ const s = audit.summary;
3460
+ if (s.overall === 'clean') {
3461
+ log(grn(` โœ“ ๋ฉฑ๋“ฑ์„ฑ ์œ„๋ฐ˜ ์—†์Œ โ€” verified ${s.verifiedAreas} ์˜์—ญ`));
3462
+ } else {
3463
+ log(red(` โš  ${s.totalViolations} ์œ„๋ฐ˜ ๋ฐœ๊ฒฌ (high: ${s.highSeverity}, medium: ${s.mediumSeverity}, low: ${s.lowSeverity})`));
3464
+ }
3465
+ log('');
3466
+ if (audit.verified.length) {
3467
+ log(grn(`## โœ… verified (${audit.verified.length})`));
3468
+ audit.verified.forEach(v => log(dim(` - [${v.kind}] ${v.detail}`)));
3469
+ log('');
3470
+ }
3471
+ if (audit.violations.length) {
3472
+ log(red(`## โš  violations (${audit.violations.length})`));
3473
+ audit.violations.forEach(v => {
3474
+ const icon = v.severity === 'high' ? '๐Ÿšจ' : (v.severity === 'medium' ? 'โš ' : 'โ„น');
3475
+ log(` ${icon} [${v.kind}] (${v.severity})`);
3476
+ log(dim(` ${v.location}`));
3477
+ log(` ${v.detail}`);
3478
+ if (v.fix) log(dim(` fix: ${v.fix}`));
3479
+ });
3480
+ }
3481
+ return;
3482
+ }
3483
+
3484
+ console.error(`Unknown subcommand: ${sub}`);
3485
+ console.error(`Run: leerness idempotency help`);
3486
+ process.exit(1);
3487
+ }
3488
+
3489
+ // 1.9.213: leerness intent <classify|expand|domains> CLI โ€” intent inference + scope expansion (์‚ฌ์šฉ์ž ๋ช…์‹œ)
3490
+ function intentCmd(root, sub, ...rest) {
3491
+ root = absRoot(root);
3492
+ const isTty = process.stdout && process.stdout.isTTY;
3493
+ const cyan = s => isTty ? `\x1b[36m${s}\x1b[0m` : s;
3494
+ const grn = s => isTty ? `\x1b[32m${s}\x1b[0m` : s;
3495
+ const yel = s => isTty ? `\x1b[33m${s}\x1b[0m` : s;
3496
+ const red = s => isTty ? `\x1b[31m${s}\x1b[0m` : s;
3497
+ const dim = s => isTty ? `\x1b[2m${s}\x1b[0m` : s;
3498
+
3499
+ if (!sub || sub === 'help' || sub === '--help') {
3500
+ log(`# leerness intent (1.9.213) โ€” ์‚ฌ์šฉ์ž ์˜๋„ ํŒŒ์•… + scope expansion ๊ฒŒ์ดํŠธ`);
3501
+ log('');
3502
+ log(` classify "<request>" โ†’ intent ๋ถ„๋ฅ˜ (precise/broad/default) + ์‹ ํ˜ธ (--json ๊ฐ€๋Šฅ)`);
3503
+ log(` expand "<request>" โ†’ ๋„๋ฉ”์ธ ํƒ์ง€ + ํ™•์žฅ ํ›„๋ณด dry-run (--json ๊ฐ€๋Šฅ)`);
3504
+ log(` domains โ†’ ๋“ฑ๋ก๋œ ๋„๋ฉ”์ธ catalog ์ถœ๋ ฅ (game/web/api/cli/data)`);
3505
+ log('');
3506
+ log(dim(` 3์›์น™: (1) Always-Off Opt-In (2) Dry-run ๊ธฐ๋ณธ (์‹คํ–‰ X) (3) ๋ช…์‹œ vs ์ถ”๋ก  ๋ถ„๋ฆฌ ๋ผ๋ฒจ๋ง`));
3507
+ log(dim(` ์˜ˆ: leerness intent expand "๋งต๊ณผ ์บ๋ฆญํ„ฐ + ๊ธฐ๋ณธ ๊ฒŒ์ž„ ๊ธฐ๋Šฅ ๋งŒ๋“ค์–ด์ค˜"`));
3508
+ return;
3509
+ }
3510
+
3511
+ if (sub === 'classify') {
3512
+ const text = rest.filter(x => !x.startsWith('-')).join(' ');
3513
+ if (!text) { console.error('Usage: leerness intent classify "<request text>"'); process.exit(1); }
3514
+ const result = _classifyIntent(text);
3515
+ if (has('--json')) { log(JSON.stringify({ text, ...result }, null, 2)); return; }
3516
+ log(cyan(`# leerness intent classify (1.9.213)`));
3517
+ log(` text: ${text.slice(0, 100)}${text.length > 100 ? 'โ€ฆ' : ''}`);
3518
+ log('');
3519
+ const icon = result.intent === 'precise' ? '๐ŸŽฏ' : (result.intent === 'broad' ? '๐ŸŒ' : '๐Ÿ”');
3520
+ log(` ${icon} intent: ${grn(result.intent)} (precise=${result.preciseCount} / broad=${result.broadCount})`);
3521
+ if (result.signals.length) {
3522
+ log('');
3523
+ log(` ๐Ÿ“ก ์‹ ํ˜ธ:`);
3524
+ result.signals.forEach(s => log(dim(` โ€ข [${s.kind}] "${s.match}"`)));
3525
+ }
3526
+ return;
3527
+ }
3528
+
3529
+ if (sub === 'expand') {
3530
+ const text = rest.filter(x => !x.startsWith('-')).join(' ');
3531
+ if (!text) { console.error('Usage: leerness intent expand "<request text>"'); process.exit(1); }
3532
+ const result = _inferScopeExpansion(text, root);
3533
+ if (has('--json')) { log(JSON.stringify(result, null, 2)); return; }
3534
+ log(cyan(`# leerness intent expand (1.9.213) [DRY-RUN]`));
3535
+ log(` text: ${text.slice(0, 100)}${text.length > 100 ? 'โ€ฆ' : ''}`);
3536
+ log('');
3537
+ const iIcon = result.intent === 'precise' ? '๐ŸŽฏ' : (result.intent === 'broad' ? '๐ŸŒ' : '๐Ÿ”');
3538
+ log(` ${iIcon} intent: ${grn(result.intent)}` + (result.domain ? ` ๐Ÿ“ฆ domain: ${grn(result.domain)}` + (result.matchedAlias ? dim(` (matched: "${result.matchedAlias}")`) : '') : ` ${dim('domain ๋ฏธํƒ์ง€')}`));
3539
+ log('');
3540
+ if (result.explicitMentions.length) {
3541
+ log(grn(`## ๐Ÿ‘ค ์‚ฌ์šฉ์ž ๋ช…์‹œ (${result.explicitMentions.length})`));
3542
+ result.explicitMentions.forEach(m => log(` โ€ข ${m.key} โ€” ${m.desc}`));
3543
+ log('');
3544
+ }
3545
+ if (result.expansionCandidates.length) {
3546
+ log(yel(`## ๐Ÿค– AI ์ถ”๋ก  ํ™•์žฅ ํ›„๋ณด (${result.expansionCandidates.length}, dry-run)`));
3547
+ result.expansionCandidates.forEach((c, i) => log(` [${i + 1}] ${c.key} โ€” ${c.desc}`));
3548
+ log('');
3549
+ log(dim(` โ†’ ์ง„ํ–‰ ์‹œ ๋ช…์‹œ ์Šน์ธ ํ•„์š”: leerness task add "<์„ ํƒํ•œ ํ›„๋ณด ์š”์ฒญ>"`));
3550
+ log(dim(` โ†’ ์ „๋ถ€ ๋ฌด์‹œํ•˜๋ ค๋ฉด ์ถ”๊ฐ€ task add ์—†์ด ๋ช…์‹œ ์š”์ฒญ๋งŒ ์ง„ํ–‰`));
3551
+ } else if (result.domain && result.intent === 'precise') {
3552
+ log(red(` ๐Ÿ›ก intent: precise โ€” AI ์ถ”๋ก  ํ™•์žฅ ๋น„ํ™œ์„ฑ (์‚ฌ์šฉ์ž ์˜๋„ ๋ณดํ˜ธ)`));
3553
+ }
3554
+ if (result.note) {
3555
+ log('');
3556
+ log(dim(` โ„น ${result.note}`));
3557
+ }
3558
+ return;
3559
+ }
3560
+
3561
+ if (sub === 'domains') {
3562
+ const catalog = _loadDomainCatalog(root);
3563
+ if (has('--json')) { log(JSON.stringify(catalog, null, 2)); return; }
3564
+ log(cyan(`# leerness intent domains (1.9.213)`));
3565
+ log(` total domains: ${Object.keys(catalog.domains).length}`);
3566
+ log('');
3567
+ for (const [name, info] of Object.entries(catalog.domains)) {
3568
+ log(grn(` ๐Ÿ“ฆ ${name}`) + dim(` aliases: ${(info.aliases || []).join(', ')}`));
3569
+ for (const c of info.components || []) {
3570
+ log(` โ€ข ${c.key.padEnd(12)} ${dim(c.desc)}`);
3571
+ }
3572
+ log('');
3573
+ }
3574
+ return;
3575
+ }
3576
+
3577
+ console.error(`Unknown subcommand: ${sub}`);
3578
+ console.error(`Run: leerness intent help`);
3579
+ process.exit(1);
3580
+ }
3581
+
3132
3582
  // 1.9.199: wakeup miss ์ž๋™ ํšŒ๋ณต ๊ฐ•ํ™” โ€” last-handoff timestamp ๊ธฐ๋ก + 25min ๊ธฐ๋Œ€ interval ์ •๋ฐ€ ์ธก์ •
3133
3583
  // ์‚ฌ์šฉ์ž ๋ช…์‹œ (1.9.198 ํ›„์†): "22:01์— ๋ผ์šด๋“œ์— ์ž๋™์ง„์ž…๋˜์ง€์•Š์•˜์–ด"
3134
3584
  // 1.9.196 task-log mtime ๊ธฐ๋ฐ˜ detector ๋Š” task-log ๊ฐ€ ์ž์ฃผ ์•ˆ ์—…๋ฐ์ดํŠธ๋˜๋ฉด false positive.
@@ -3707,6 +4157,30 @@ function taskList(root) {
3707
4157
  for (const r of filtered) log(`| ${r.id} | ${r.status} | ${r.request} | ${r.evidence} | ${r.nextAction} | ${r.updated} |`);
3708
4158
  }
3709
4159
  function taskAdd(root, text) {
4160
+ // 1.9.212: ๋ฉฑ๋“ฑ์„ฑ โ€” ๊ฐ™์€ request ํ…์ŠคํŠธ + ํ™œ์„ฑ ์ƒํƒœ (requested/in-progress) ์ด๋ฏธ ์กด์žฌ ์‹œ skip (์‚ฌ์šฉ์ž ๋ช…์‹œ)
4161
+ // progress-tracker.md ํฌ๋งท: | T-XXXX | status | request | evidence | nextAction | date |
4162
+ if (!has('--force') && text && text.trim()) {
4163
+ try {
4164
+ const pt = path.join(absRoot(root), '.harness', 'progress-tracker.md');
4165
+ if (exists(pt)) {
4166
+ const content = read(pt);
4167
+ const norm = text.trim();
4168
+ const lines = content.split(/\r?\n/);
4169
+ for (const line of lines) {
4170
+ if (!/^\|\s*T-\d{4,}\s*\|/.test(line)) continue;
4171
+ const cells = line.split('|').slice(1, -1).map(s => s.trim());
4172
+ if (cells.length < 3) continue;
4173
+ const id = cells[0];
4174
+ const status = (cells[1] || '').toLowerCase();
4175
+ const request = cells[2];
4176
+ if (request === norm && !/^(done|dropped|blocked|completed)$/i.test(status)) {
4177
+ ok(`task exists (skip): ${id} request="${norm.slice(0, 60)}โ€ฆ" (--force ๋กœ ๋ฎ์–ด์“ฐ๊ธฐ)`);
4178
+ return;
4179
+ }
4180
+ }
4181
+ }
4182
+ } catch {}
4183
+ }
3710
4184
  const id = nextId(root, 'T');
3711
4185
  upsertProgress(root, { id, status: arg('--status','requested'), request: text, evidence: arg('--evidence','user-request'), nextAction: arg('--next','๋‹ค์Œ ์•ก์…˜ ์ž‘์„ฑ') });
3712
4186
  ok(`task added: ${id}`);
@@ -10235,8 +10709,16 @@ function ruleAdd(root, description) {
10235
10709
  if (!validTriggers.has(trigger)) {
10236
10710
  warn(`unknown trigger "${trigger}" โ€” ์‚ฌ์šฉ ๊ฐ€๋Šฅ: ${[...validTriggers].join(', ')}. ๊ทธ๋Œ€๋กœ ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค.`);
10237
10711
  }
10238
- const id = nextRuleId(root);
10239
10712
  const rules = readRules(root);
10713
+ // 1.9.212: ๋ฉฑ๋“ฑ์„ฑ ๋ณด์žฅ โ€” ๊ฐ™์€ description + trigger + active ์ƒํƒœ ์ด๋ฏธ ์กด์žฌ ์‹œ skip (์‚ฌ์šฉ์ž ๋ช…์‹œ)
10714
+ if (!has('--force')) {
10715
+ const dup = rules.find(r => r.rule === description && r.trigger === trigger && r.status === 'active');
10716
+ if (dup) {
10717
+ ok(`rule exists (skip): ${dup.id} [${dup.trigger}] ${description} (--force ๋กœ ๋ฎ์–ด์“ฐ๊ธฐ)`);
10718
+ return;
10719
+ }
10720
+ }
10721
+ const id = nextRuleId(root);
10240
10722
  rules.push({ id, trigger, rule: description, added: today(), status: 'active', lastVerified: '-' });
10241
10723
  writeRules(root, rules);
10242
10724
  ok(`rule added: ${id} [${trigger}] ${description}`);
@@ -16538,6 +17020,10 @@ async function main() {
16538
17020
  if (cmd === 'workspace-dir') return workspaceDirCmd(arg('--path', process.cwd()), args[1]);
16539
17021
  // 1.9.211: leerness migrate-workspace-dir โ€” .harness โ†’ .leerness ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ (์‚ฌ์šฉ์ž ๋ช…์‹œ)
16540
17022
  if (cmd === 'migrate-workspace-dir') return migrateWorkspaceDirCmd(arg('--path', process.cwd()));
17023
+ // 1.9.212: leerness idempotency audit โ€” ๋ฉฑ๋“ฑ์„ฑ ์œ„๋ฐ˜ ํƒ์ง€ (์‚ฌ์šฉ์ž ๋ช…์‹œ)
17024
+ if (cmd === 'idempotency') return idempotencyCmd(arg('--path', process.cwd()), args[1]);
17025
+ // 1.9.213: leerness intent <classify|expand|domains> โ€” intent inference + scope expansion (์‚ฌ์šฉ์ž ๋ช…์‹œ)
17026
+ if (cmd === 'intent') return intentCmd(arg('--path', process.cwd()), args[1], ...args.slice(2));
16541
17027
  if (cmd === 'rule' && args[1] === 'add') return ruleAdd(arg('--path', process.cwd()), args.slice(2).filter(x => !x.startsWith('-')).join(' '));
16542
17028
  if (cmd === 'rule' && args[1] === 'list') return ruleList(arg('--path', process.cwd()));
16543
17029
  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.211",
3
+ "version": "1.9.213",
4
4
  "description": "Leerness: ๋น„ํŒŒ๊ดด ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜, ์ž๋™ ๋ฒ„์ „ ๊ฐ์ง€ยท์—…๋ฐ์ดํŠธ, ๊ณ„ํš/์ง„ํ–‰/ํ•ธ๋“œ์˜คํ”„ ์ž๋™ํ™”, ๊ฒŒ์œผ๋ฆ„ยท์‹œํฌ๋ฆฟยท์ธ์ฝ”๋”ฉ ์ž๋™ ๊ฐ€๋“œ, Claude Code ์Šฌ๋ž˜์‹œ ํ†ตํ•ฉ์„ ๊ฐ–์ถ˜ ํ•œ๊ตญ์–ด ์šฐ์„  AI ๊ฐœ๋ฐœ ํ•˜๋„ค์Šค.",
5
5
  "keywords": [
6
6
  "leerness",