leerness 1.9.210 โ†’ 1.9.212

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,98 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.212 โ€” 2026-05-22
4
+
5
+ **๐Ÿ” ๋ฉฑ๋“ฑ์„ฑ ๊ฐ์‚ฌ + ruleAdd/taskAdd dedup ๋ณด๊ฐ• (์‚ฌ์šฉ์ž ๋ช…์‹œ).**
6
+
7
+ ### ์‚ฌ์šฉ์ž ๋ช…์‹œ
8
+ > *"๋‹ค์–‘ํ•œ ๋ถ€๋ถ„์—์„œ ๋ฉฑ๋“ฑ์„ฑ์ด ํ•„์š”ํ•œ ๋ถ€๋ถ„์ด ๊ณ ๋ ค๋˜๊ณ ์žˆ๋Š”์ง€ ํ™•์ธํ•ด์ฃผ๊ณ  ์˜ฌ๋ฐ”๋ฅธ ๋ฐฉํ–ฅ์ธ์ง€ ํŒ๋‹จํ•ด์ค˜"*
9
+
10
+ ### 1. ruleAdd dedup (line 10229+)
11
+ - ๊ฐ™์€ description + trigger + active ๋ฃฐ ์ด๋ฏธ ์กด์žฌ ์‹œ skip
12
+ - `--force` ํ”Œ๋ž˜๊ทธ๋กœ ์šฐํšŒ ๊ฐ€๋Šฅ
13
+ - ์ถœ๋ ฅ: `rule exists (skip): R-XXXX [trigger] description (--force ๋กœ ๋ฎ์–ด์“ฐ๊ธฐ)`
14
+
15
+ ### 2. taskAdd dedup (line 3709+)
16
+ - progress-tracker.md ์˜ markdown table ํŒŒ์‹ฑ (`| T-XXXX | status | request | โ€ฆ |`)
17
+ - ๊ฐ™์€ request ํ…์ŠคํŠธ + ํ™œ์„ฑ ์ƒํƒœ(`requested`/`in-progress`) ์ด๋ฏธ ์กด์žฌ ์‹œ skip
18
+ - `--force` ํ”Œ๋ž˜๊ทธ๋กœ ์šฐํšŒ ๊ฐ€๋Šฅ
19
+
20
+ ### 3. `_runIdempotencyAudit(root)` โ€” 4์˜์—ญ ์ ๊ฒ€
21
+ - **rules**: ๊ฐ™์€ description + trigger + active ์ค‘๋ณต (severity: medium)
22
+ - **tasks**: ๊ฐ™์€ request ํ…์ŠคํŠธ + ํ™œ์„ฑ ์ƒํƒœ ์ค‘๋ณต (severity: medium)
23
+ - **user-requests**: 1.9.207 ์ž์ฒด dedup ๊ฒ€์ฆ (severity: low)
24
+ - **wakeups**: 1.9.205 ๋™์ผ expectedFireAt ๊ฒ€์ฆ (severity: high)
25
+
26
+ ### 4. `leerness idempotency audit` CLI
27
+ - ์œ„๋ฐ˜ ๋ถ„๋ฅ˜ (high/medium/low)
28
+ - ์˜์—ญ๋ณ„ verified / violations ๋ณด๊ณ 
29
+ - `--json` ์˜ต์…˜
30
+
31
+ ### 5. ๋ˆ„์  ํšŒ๊ท€ (1.9.200~211) โ€” ๋ชจ๋‘ ์œ ์ง€
32
+
33
+ ### 6. stress-v157 โ€” 17/17 PASS
34
+ - 1.9.212 (8) + ์„ฑ๋Šฅ (2) + ๋ˆ„์  ํšŒ๊ท€ (7)
35
+ - ๊ฒฉ๋ฆฌ tmp dir์—์„œ ruleAdd 2ํšŒ โ†’ 1๊ฑด๋งŒ ๋“ฑ๋ก ๊ฒ€์ฆ
36
+ - `--force` ์šฐํšŒ ๊ฒ€์ฆ
37
+ - ์ˆ˜๋™ ์ค‘๋ณต rule ์‚ฝ์ž… โ†’ audit์ด ์ •ํ™•ํžˆ 1๊ฑด violation ํƒ์ง€
38
+
39
+ ### 7. ์ž๋™ release (74 ๋ผ์šด๋“œ main-push streak ยท 35 ๋ผ์šด๋“œ npm publish streak)
40
+
41
+ ---
42
+
43
+ ## 1.9.211 โ€” 2026-05-22
44
+
45
+ **๐Ÿ“‚ `.harness` โ†’ `.leerness` opt-in ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ + AI ์ฐธ์กฐ ๊ฐ€์ด๋“œ (์‚ฌ์šฉ์ž ๋ช…์‹œ).**
46
+
47
+ ### ์‚ฌ์šฉ์ž ๋ช…์‹œ
48
+ > *"leerness์„ค์น˜์‹œ ์ƒ์„ฑ๋˜๋Š” .harness๋ฅผ .leerness์œผ๋กœ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ์„๊นŒ / ๊ธฐ์กด ๋ฒ„์ „์—์„œ๋„ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์‹œ AI๊ฐ€ ์ฐธ์กฐํ•  ์ˆ˜ ์žˆ๊ธฐ๋„ํ•˜๋ฉด ์–ด๋–จ๊นŒ"*
49
+
50
+ ### 1. workspace-dir ํ—ฌํผ (default-safe opt-in)
51
+ - `_workspaceDirName(root)` โ€” env `LEERNESS_WORKSPACE_DIR` > `.leerness/MIGRATED_FROM_HARNESS` ๋งˆ์ปค > default `.harness`
52
+ - `_workspaceDirAbs(root)` โ€” ์ ˆ๋Œ€ ๊ฒฝ๋กœ
53
+ - ๊ธฐ๋ณธ ๋™์ž‘: **`.harness` ์œ ์ง€** (226+ path reference ์•ˆ์ •) โ€” breaking change 0
54
+
55
+ ### 2. ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹œ์Šคํ…œ
56
+ - `_migrateWorkspaceDir(root, opts)` โ€” `.harness` ์ „์ฒด ์žฌ๊ท€ copy โ†’ `.leerness`
57
+ ยท `--dry-run` ์ง€์›
58
+ ยท `--force` (๊ธฐ์กด dst ๋ฎ์–ด์“ฐ๊ธฐ)
59
+ ยท skip / errors / copiedFiles ๋ถ„๋ฅ˜ ๋ณด๊ณ 
60
+ - ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๊ฒฐ๊ณผ๋ฌผ:
61
+ ยท `.leerness/MIGRATED_FROM_HARNESS` ๋งˆ์ปค
62
+ ยท `.leerness/WHERE_TO_FIND.md` AI ์ฐธ์กฐ ๊ฐ€์ด๋“œ (์ž๋™ ์ƒ์„ฑ)
63
+ ยท `.harness/MIGRATED_TO_LEERNESS.md` redirect ์•ˆ๋‚ด (backward compat ์œ ์ง€)
64
+
65
+ ### 3. AI ์ฐธ์กฐ ๊ฐ€์ด๋“œ (`_buildWorkspaceReferenceGuide`)
66
+ - ๋””๋ ‰ํ† ๋ฆฌ ๊ตฌ์กฐ ์‹œ๊ฐํ™” (ํ•ต์‹ฌ ํŒŒ์ผ 25๊ฐœ)
67
+ - "์ž์ฃผ ๋ฌป๋Š” ์œ„์น˜" ๋งคํ•‘ ํ‘œ (ํ˜„์žฌ task / ์˜๊ตฌ ๋ฃฐ / pre-wake / ๋ฏธ๋‹ต ์š”์ฒญ / ๋‹ค์Œ ๋ผ์šด๋“œ plan / API ์ œ์•ฝ / wakeup ๊ถŒ์žฅ)
68
+ - ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์•ˆ๋‚ด (3๊ฐ€์ง€ ์ƒํƒœ ๋ถ„๊ธฐ)
69
+ - 1.9.207~210 ์‹ ๊ทœ ๋ฉ”ํƒ€ํŒŒ์ผ ์œ„์น˜ ๋ช…์‹œ
70
+
71
+ ### 4. `leerness workspace-dir <get|guide>` CLI
72
+ - `get` โ€” ํ˜„์žฌ ๋””๋ ‰ํ† ๋ฆฌ + .harness/.leerness ์กด์žฌ ์—ฌ๋ถ€ + ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ƒํƒœ
73
+ - `guide` โ€” AI ์ฐธ์กฐ ๊ฐ€์ด๋“œ ์ถœ๋ ฅ
74
+
75
+ ### 5. `leerness migrate-workspace-dir` CLI
76
+ - `--dry-run` / `--force` ์ง€์›
77
+ - copied / skipped / errors ๋ถ„๋ฅ˜
78
+ - ์ž๋™ ๋งˆ์ปค + ๊ฐ€์ด๋“œ + redirect ์ƒ์„ฑ
79
+
80
+ ### 6. ๋ˆ„์  ํšŒ๊ท€ (1.9.200~210) โ€” ๋ชจ๋‘ ์œ ์ง€
81
+
82
+ ### 7. stress-v156 โ€” 19/19 PASS
83
+ - 1.9.211 (10) + ์„ฑ๋Šฅ (2) + ๋ˆ„์  ํšŒ๊ท€ (7)
84
+ - ๊ฒฉ๋ฆฌ tmp dir ์ „์ฒด ์‚ฌ์ดํด (default โ†’ dry-run โ†’ migrate โ†’ auto-detect โ†’ env override)
85
+ - AI ๊ฐ€์ด๋“œ ํ‚ค ์„น์…˜ ๊ฒ€์ฆ
86
+ - ์„ฑ๋Šฅ: --version cold start avg 452ms ยท MCP 54 ๋„๊ตฌ 445ms
87
+
88
+ ### 8. ์ž๋™ release (73 ๋ผ์šด๋“œ main-push streak ยท 34 ๋ผ์šด๋“œ npm publish streak)
89
+
90
+ ### 9. ํ›„์† ๋ผ์šด๋“œ ๋กœ๋“œ๋งต
91
+ - ์ด๋ฒˆ ๋ผ์šด๋“œ๋Š” **opt-in ํ—ฌํผ + CLI**๋งŒ ์ถ”๊ฐ€ (breaking change 0)
92
+ - ํ›„์†์—์„œ 226+ path reference ๋ฅผ `_workspaceDirAbs(root)` ๋กœ ์ ์ง„ ์ „ํ™˜
93
+
94
+ ---
95
+
3
96
  ## 1.9.210 โ€” 2026-05-22
4
97
 
5
98
  **โšก adaptive wakeup interval โ€” ์‚ฌ์šฉ์ž ํ™œ๋™ ๊ธฐ๋ฐ˜ ์ž๋™ ์กฐ์ ˆ (์‚ฌ์šฉ์ž ๋ช…์‹œ).**
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.210-green)]() [![tests](https://img.shields.io/badge/e2e-217%2F217-success)]() [![stress](https://img.shields.io/badge/stress--v155-18%2F18-success)]() [![mcp](https://img.shields.io/badge/MCP--tools-54-brightgreen)]() [![rounds](https://img.shields.io/badge/autonomous--rounds-140-blueviolet)]() [![main-push](https://img.shields.io/badge/release--main--push-72_rounds-success)]() [![goal-matrix](https://img.shields.io/badge/๋ชฉ์ _๋งคํŠธ๋ฆญ์Šค-98%2F100_(5์ถ•)-brightgreen)]() [![adaptive](https://img.shields.io/badge/wakeup-adaptive_10~45min-success)]() [![perf](https://img.shields.io/badge/cold_start-440ms-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.212-green)]() [![tests](https://img.shields.io/badge/e2e-217%2F217-success)]() [![stress](https://img.shields.io/badge/stress--v157-17%2F17-success)]() [![mcp](https://img.shields.io/badge/MCP--tools-54-brightgreen)]() [![rounds](https://img.shields.io/badge/autonomous--rounds-142-blueviolet)]() [![main-push](https://img.shields.io/badge/release--main--push-74_rounds-success)]() [![goal-matrix](https://img.shields.io/badge/๋ชฉ์ _๋งคํŠธ๋ฆญ์Šค-98%2F100_(5์ถ•)-brightgreen)]() [![idempotency](https://img.shields.io/badge/idempotency-audit_4์˜์—ญ-success)]() [![perf](https://img.shields.io/badge/cold_start-395ms-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.210 AI Agent Reliability Harness + Sandbox โ•‘
15
+ โ•‘ v1.9.212 AI Agent Reliability Harness + Sandbox โ•‘
16
16
  โ•‘ verify ยท remember ยท orchestrate ยท audit ยท sandbox ยท drift โ•‘
17
- โ•‘ โšก adaptive wakeup 10~45min (ํ™œ๋™ ๊ธฐ๋ฐ˜ ์ž๋™) (์‚ฌ์šฉ์ž ๋ช…์‹œ) โ•‘
17
+ โ•‘ ๐Ÿ” ๋ฉฑ๋“ฑ์„ฑ ๊ฐ์‚ฌ + ruleAdd/taskAdd dedup (์‚ฌ์šฉ์ž ๋ช…์‹œ) โ•‘
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.210';
10
+ const VERSION = '1.9.212';
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 ์ฒ˜๋ฆฌ).
@@ -2364,6 +2364,168 @@ function _computeAdaptiveInterval(root) {
2364
2364
  };
2365
2365
  }
2366
2366
 
2367
+ // 1.9.211: .harness โ†’ .leerness rename + migration + AI ์ฐธ์กฐ ๊ฐ€์ด๋“œ (์‚ฌ์šฉ์ž ๋ช…์‹œ)
2368
+ // "leerness์„ค์น˜์‹œ ์ƒ์„ฑ๋˜๋Š” .harness๋ฅผ .leerness์œผ๋กœ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ์„๊นŒ / ๊ธฐ์กด ๋ฒ„์ „์—์„œ๋„ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์‹œ AI๊ฐ€ ์ฐธ์กฐํ•  ์ˆ˜ ์žˆ๊ธฐ๋„ํ•˜๋ฉด ์–ด๋–จ๊นŒ"
2369
+ // ์ ‘๊ทผ ๋ฐฉ์‹: opt-in ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ (breaking change ์ตœ์†Œํ™”)
2370
+ // - default: .harness ์œ ์ง€ (226+ path reference ์•ˆ์ •)
2371
+ // - LEERNESS_WORKSPACE_DIR env ๋˜๋Š” .leerness/MARKER ์กด์žฌ ์‹œ .leerness ์‚ฌ์šฉ
2372
+ // - leerness migrate-workspace-dir CLI: .harness โ†’ .leerness copy + AI reference guide ์ƒ์„ฑ
2373
+ // - AI ์ฐธ์กฐ ๊ฐ€์ด๋“œ: WHERE_TO_FIND.md (๋””๋ ‰ํ† ๋ฆฌ ๊ตฌ์กฐ + ํŒŒ์ผ๋ณ„ ์—ญํ•  + ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ history)
2374
+ function _workspaceDirName(root) {
2375
+ // 1) env override
2376
+ if (process.env.LEERNESS_WORKSPACE_DIR) {
2377
+ const v = process.env.LEERNESS_WORKSPACE_DIR.trim();
2378
+ if (v) return v;
2379
+ }
2380
+ // 2) .leerness ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋งˆ์ปค ์กด์žฌ ์‹œ .leerness ์šฐ์„ 
2381
+ try {
2382
+ if (root && exists(path.join(root, '.leerness', 'MIGRATED_FROM_HARNESS'))) {
2383
+ return '.leerness';
2384
+ }
2385
+ } catch {}
2386
+ // 3) default
2387
+ return '.harness';
2388
+ }
2389
+ function _workspaceDirAbs(root) {
2390
+ return path.join(root, _workspaceDirName(root));
2391
+ }
2392
+ // .harness โ†’ .leerness ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ (copy + reference guide ์ƒ์„ฑ)
2393
+ function _migrateWorkspaceDir(root, opts = {}) {
2394
+ const dryRun = opts.dryRun === true;
2395
+ const srcDir = path.join(root, '.harness');
2396
+ const dstDir = path.join(root, '.leerness');
2397
+ const report = {
2398
+ at: new Date().toISOString(),
2399
+ version: VERSION,
2400
+ src: srcDir,
2401
+ dst: dstDir,
2402
+ srcExists: exists(srcDir),
2403
+ dstExists: exists(dstDir),
2404
+ copiedFiles: [],
2405
+ skippedFiles: [],
2406
+ errors: [],
2407
+ dryRun
2408
+ };
2409
+ if (!report.srcExists) {
2410
+ report.errors.push('source .harness not found');
2411
+ return report;
2412
+ }
2413
+ if (!dryRun) {
2414
+ mkdirp(dstDir);
2415
+ }
2416
+ // ์žฌ๊ท€ copy ํ—ฌํผ
2417
+ function copyRec(rel) {
2418
+ const srcAbs = path.join(srcDir, rel);
2419
+ const dstAbs = path.join(dstDir, rel);
2420
+ let stat;
2421
+ try { stat = fs.statSync(srcAbs); } catch (e) { report.errors.push(`stat ${rel}: ${e.message}`); return; }
2422
+ if (stat.isDirectory()) {
2423
+ if (!dryRun) mkdirp(dstAbs);
2424
+ let entries;
2425
+ try { entries = fs.readdirSync(srcAbs); } catch (e) { report.errors.push(`readdir ${rel}: ${e.message}`); return; }
2426
+ for (const e of entries) copyRec(path.join(rel, e));
2427
+ } else if (stat.isFile()) {
2428
+ // ๊ธฐ์กด dst ํŒŒ์ผ์ด ์žˆ๊ณ  ์šฐ๋ฆฌ๊ฐ€ force๊ฐ€ ์•„๋‹ˆ๋ฉด skip
2429
+ if (exists(dstAbs) && !opts.force) {
2430
+ report.skippedFiles.push(rel);
2431
+ return;
2432
+ }
2433
+ if (!dryRun) {
2434
+ try {
2435
+ fs.copyFileSync(srcAbs, dstAbs);
2436
+ report.copiedFiles.push(rel);
2437
+ } catch (e) {
2438
+ report.errors.push(`copy ${rel}: ${e.message}`);
2439
+ }
2440
+ } else {
2441
+ report.copiedFiles.push(rel);
2442
+ }
2443
+ }
2444
+ }
2445
+ copyRec('');
2446
+ // ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋งˆ์ปค + AI ์ฐธ์กฐ ๊ฐ€์ด๋“œ ์ž‘์„ฑ
2447
+ if (!dryRun) {
2448
+ const marker = `Migrated from .harness at ${report.at} by leerness ${VERSION}\n`;
2449
+ try { writeUtf8(path.join(dstDir, 'MIGRATED_FROM_HARNESS'), marker); } catch (e) { report.errors.push(`marker: ${e.message}`); }
2450
+ const guide = _buildWorkspaceReferenceGuide(root, '.leerness');
2451
+ try { writeUtf8(path.join(dstDir, 'WHERE_TO_FIND.md'), guide); } catch (e) { report.errors.push(`guide: ${e.message}`); }
2452
+ // .harness ์—๋„ redirect ์•ˆ๋‚ด ์ถ”๊ฐ€ (AI๊ฐ€ .harness ๋จผ์ € ๋ด๋„ .leerness ๋กœ ๊ฐ€๊ฒŒ)
2453
+ try {
2454
+ const redirect = `# .harness โ†’ .leerness migration (${report.at})\n\n` +
2455
+ `์ด ์›Œํฌ์ŠคํŽ˜์ด์Šค๋Š” \`.harness\` โ†’ \`.leerness\` ๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜๋˜์—ˆ์Šต๋‹ˆ๋‹ค (leerness ${VERSION}).\n\n` +
2456
+ `**AI ์—์ด์ „ํŠธ๋Š” ${'.leerness/'} ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ์šฐ์„  ์ฐธ์กฐํ•˜์‹ญ์‹œ์˜ค.**\n\n` +
2457
+ `- ์‹ ๊ทœ ๋ฉ”ํƒ€ํŒŒ์ผ ์œ„์น˜: \`.leerness/\`\n` +
2458
+ `- ์ฐธ์กฐ ๊ฐ€์ด๋“œ: \`.leerness/WHERE_TO_FIND.md\`\n` +
2459
+ `- ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋งˆ์ปค: \`.leerness/MIGRATED_FROM_HARNESS\`\n\n` +
2460
+ `\`.harness/\` ๋Š” backward compatibility ๋ฅผ ์œ„ํ•ด ๋ณด์กด๋ฉ๋‹ˆ๋‹ค (read-only ๊ถŒ์žฅ).\n`;
2461
+ writeUtf8(path.join(srcDir, 'MIGRATED_TO_LEERNESS.md'), redirect);
2462
+ } catch (e) { report.errors.push(`redirect: ${e.message}`); }
2463
+ }
2464
+ return report;
2465
+ }
2466
+ // AI ์ฐธ์กฐ ๊ฐ€์ด๋“œ ๋นŒ๋” โ€” ์›Œํฌ์ŠคํŽ˜์ด์Šค ๋””๋ ‰ํ† ๋ฆฌ ๊ตฌ์กฐ + ํŒŒ์ผ๋ณ„ ์—ญํ•  ๋งคํ•‘
2467
+ function _buildWorkspaceReferenceGuide(root, dirName) {
2468
+ const lines = [];
2469
+ lines.push(`# Leerness Workspace Reference Guide`);
2470
+ lines.push('');
2471
+ lines.push(`> AI ์—์ด์ „ํŠธ๊ฐ€ leerness ์›Œํฌ์ŠคํŽ˜์ด์Šค์—์„œ ์–ด๋–ค ํŒŒ์ผ์„ ์–ด๋””์„œ ์ฐพ๋Š”์ง€ ์•ˆ๋‚ดํ•ฉ๋‹ˆ๋‹ค (1.9.211).`);
2472
+ lines.push('');
2473
+ lines.push(`Generated: ${new Date().toISOString()} by leerness ${VERSION}`);
2474
+ lines.push(`Workspace dir: \`${dirName}/\``);
2475
+ lines.push('');
2476
+ lines.push(`## ๐Ÿ“ ๋””๋ ‰ํ† ๋ฆฌ ๊ตฌ์กฐ (ํ•ต์‹ฌ)`);
2477
+ lines.push('');
2478
+ lines.push('```');
2479
+ lines.push(`${dirName}/`);
2480
+ lines.push(`โ”œโ”€โ”€ plan.md โ† ๋ฌด์—‡์„ ํ•  ๊ฒƒ์ธ๊ฐ€ (์‚ฌ์šฉ์ž ๋ฉ”๋ชจ๋ฆฌ)`);
2481
+ lines.push(`โ”œโ”€โ”€ progress-tracker.md โ† ๋ฌด์—‡์„ ํ–ˆ๋Š”๊ฐ€ (์ฆ๊ฑฐ ํฌํ•จ, ์‚ฌ์šฉ์ž ๋ฉ”๋ชจ๋ฆฌ)`);
2482
+ lines.push(`โ”œโ”€โ”€ decisions.md โ† ์™œ ๊ทธ๋ ‡๊ฒŒ ํ–ˆ๋Š”๊ฐ€ (์‚ฌ์šฉ์ž ๋ฉ”๋ชจ๋ฆฌ)`);
2483
+ lines.push(`โ”œโ”€โ”€ session-handoff.md โ† ๋‹ค์Œ ์„ธ์…˜ ์ธ๊ณ„ (์‚ฌ์šฉ์ž ๋ฉ”๋ชจ๋ฆฌ)`);
2484
+ lines.push(`โ”œโ”€โ”€ lessons.md โ† ๊ณผ๊ฑฐ ๊ตํ›ˆ (์ž๋™ fuzzy ํšŒ์ˆ˜)`);
2485
+ lines.push(`โ”œโ”€โ”€ rules.md โ† ์ž์—ฐ์–ด ๋ฃฐ (๋งค ์„ธ์…˜ ์ž๋™ ๋…ธ์ถœ, R-XXXX)`);
2486
+ lines.push(`โ”œโ”€โ”€ task-log.md โ† in-progress / dropped task ์ด๋ ฅ`);
2487
+ lines.push(`โ”œโ”€โ”€ reuse-map.md โ† ์›Œํฌ์ŠคํŽ˜์ด์Šค capability ๋งคํ•‘`);
2488
+ lines.push(`โ”œโ”€โ”€ skill-suggestions.md โ† skill rolling history`);
2489
+ lines.push(`โ”œโ”€โ”€ feature-graph.md โ† ๊ธฐ๋Šฅ ์˜์กด ๊ทธ๋ž˜ํ”„ (F-XXXX)`);
2490
+ lines.push(`โ”œโ”€โ”€ manifest.json โ† ์›Œํฌ์ŠคํŽ˜์ด์Šค ๋ฉ”ํƒ€`);
2491
+ lines.push(`โ”œโ”€โ”€ leerness-config.json โ† ๋น„์‹œํฌ๋ฆฟ LEERNESS_* ์„ค์ • (1.9.187, AI ๊ฐ€์‹œ)`);
2492
+ lines.push(`โ”œโ”€โ”€ user-requests.json โ† ์‚ฌ์šฉ์ž ๋ช…์‹œ ์š”์ฒญ ๋ˆ„์  (1.9.207)`);
2493
+ lines.push(`โ”œโ”€โ”€ active-wakeups.json โ† ScheduleWakeup ์ƒํƒœ (1.9.205)`);
2494
+ lines.push(`โ”œโ”€โ”€ pre-wake-report.json โ† sleep ์ „ sub-agent audit (1.9.209)`);
2495
+ lines.push(`โ”œโ”€โ”€ wakeup-history.json โ† adaptive wakeup ์ด๋ ฅ (1.9.210)`);
2496
+ lines.push(`โ”œโ”€โ”€ platform-constraints.json โ† API ์ œ์•ฝ catalog (1.9.208)`);
2497
+ lines.push(`โ”œโ”€โ”€ auto-resume-plan.json โ† ๋‹ค์Œ ๋ผ์šด๋“œ plan (1.9.203)`);
2498
+ lines.push(`โ”œโ”€โ”€ next-action-queue.json โ† ๋‹ค์Œ next-action ํ (1.9.201)`);
2499
+ lines.push(`โ”œโ”€โ”€ last-handoff.json โ† ๋งˆ์ง€๋ง‰ handoff timestamp`);
2500
+ lines.push(`โ”œโ”€โ”€ environment.json โ† ํ™˜๊ฒฝ ๋ณ€๋™ ์ถ”์  (1.9.145)`);
2501
+ lines.push(`โ”œโ”€โ”€ skills/ โ† ์„ค์น˜๋œ skill ๋””๋ ‰ํ† ๋ฆฌ`);
2502
+ lines.push(`โ””โ”€โ”€ templates/ โ† ์›Œํฌ์ŠคํŽ˜์ด์Šค ํ…œํ”Œ๋ฆฟ`);
2503
+ lines.push('```');
2504
+ lines.push('');
2505
+ lines.push(`## ๐Ÿงญ ์ž์ฃผ ๋ฌป๋Š” ์œ„์น˜`);
2506
+ lines.push('');
2507
+ lines.push(`| ์ฐพ๋Š” ๊ฒƒ | ์œ„์น˜ |`);
2508
+ lines.push(`|---|---|`);
2509
+ lines.push(`| ํ˜„์žฌ ์ง„ํ–‰ ์ค‘์ธ task | \`${dirName}/progress-tracker.md\` (status: in-progress) |`);
2510
+ lines.push(`| ์‚ฌ์šฉ์ž๊ฐ€ ๋ช…์‹œํ•œ ์˜๊ตฌ ๋ฃฐ | \`${dirName}/rules.md\` (active R-XXXX) |`);
2511
+ lines.push(`| ์ง์ „ sleep ์ „ audit ๊ฒฐ๊ณผ | \`${dirName}/pre-wake-report.json\` (1.9.209) |`);
2512
+ lines.push(`| ๋ฏธ๋‹ต ์‚ฌ์šฉ์ž ์š”์ฒญ | \`${dirName}/user-requests.json\` (status: open) |`);
2513
+ lines.push(`| ๋‹ค์Œ ๋ผ์šด๋“œ ๊ถŒ์žฅ ๋‹จ๊ณ„ | \`${dirName}/auto-resume-plan.json\` (1.9.203) |`);
2514
+ lines.push(`| API ์ œ์•ฝ catalog | \`${dirName}/platform-constraints.json\` (1.9.208) |`);
2515
+ lines.push(`| ์ž๋™ wakeup ๊ถŒ์žฅ ๊ฐ„๊ฒฉ | \`${dirName}/wakeup-history.json\` (1.9.210) |`);
2516
+ lines.push('');
2517
+ lines.push(`## ๐Ÿ”„ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์•ˆ๋‚ด`);
2518
+ lines.push('');
2519
+ lines.push(`์ด ์›Œํฌ์ŠคํŽ˜์ด์Šค๋Š” \`.harness\` โ†’ \`.leerness\` ๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜๋˜์—ˆ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.`);
2520
+ lines.push(`- \`.leerness/MIGRATED_FROM_HARNESS\` ์กด์žฌ โ†’ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์™„๋ฃŒ, \`.leerness\` ์šฐ์„  ์‚ฌ์šฉ`);
2521
+ lines.push(`- \`.harness/MIGRATED_TO_LEERNESS.md\` ์กด์žฌ โ†’ \`.leerness/\` ๋กœ ๊ฐ€์•ผ ํ•จ`);
2522
+ lines.push(`- ์–‘์ชฝ ๋ชจ๋‘ ์—†์Œ โ†’ ๊ธฐ๋ณธ \`.harness\` ์‚ฌ์šฉ ์ค‘`);
2523
+ lines.push('');
2524
+ lines.push(`AI ์—์ด์ „ํŠธ๋Š” \`leerness handoff .\` ๊ฒฐ๊ณผ๋ฅผ ์‹ ๋ขฐํ•˜์‹ญ์‹œ์˜ค โ€” ์ž๋™์œผ๋กœ ์˜ฌ๋ฐ”๋ฅธ ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.`);
2525
+ lines.push('');
2526
+ return lines.join('\n');
2527
+ }
2528
+
2367
2529
  // 1.9.203: ์ž๋™ ๋ผ์šด๋“œ plan ์ •๋ฆฌ โ€” ์‚ฌ์šฉ์ž ๋ช…์‹œ
2368
2530
  // "๋ฐฑ๊ทธ๋ผ์šด๋“œ์— ๋‹ค์Œ ์ž‘์—…์ด ์‹œ์ž‘๊ฐ€๋Šฅํ•œ ์˜ˆ์ƒ ์‹œ๊ฐ„ + ์ผ์–ด๋‚ฌ์„๋•Œ ํ•ด์•ผํ•˜๋Š” ์ผ์„ ์ •๋ฆฌ"
2369
2531
  // ๋ผ์šด๋“œ ๋งˆ๋ฌด๋ฆฌ ์‹œ .harness/auto-resume-plan.json ์ž๋™ ์ €์žฅ โ†’ ๋‹ค์Œ wakeup ์‹œ ์ฆ‰์‹œ ์‹คํ–‰ ๊ฐ€๋Šฅ
@@ -2875,6 +3037,286 @@ function wakeupIntervalCmd(root, sub, val) {
2875
3037
  process.exit(1);
2876
3038
  }
2877
3039
 
3040
+ // 1.9.211: leerness workspace-dir <get|guide> โ€” ์›Œํฌ์ŠคํŽ˜์ด์Šค ๋””๋ ‰ํ† ๋ฆฌ ์ •๋ณด + AI ์ฐธ์กฐ ๊ฐ€์ด๋“œ (์‚ฌ์šฉ์ž ๋ช…์‹œ)
3041
+ function workspaceDirCmd(root, sub) {
3042
+ root = absRoot(root);
3043
+ const isTty = process.stdout && process.stdout.isTTY;
3044
+ const cyan = s => isTty ? `\x1b[36m${s}\x1b[0m` : s;
3045
+ const grn = s => isTty ? `\x1b[32m${s}\x1b[0m` : s;
3046
+ const dim = s => isTty ? `\x1b[2m${s}\x1b[0m` : s;
3047
+
3048
+ if (!sub || sub === 'get') {
3049
+ const dirName = _workspaceDirName(root);
3050
+ const dirAbs = _workspaceDirAbs(root);
3051
+ const hasHarness = exists(path.join(root, '.harness'));
3052
+ const hasLeerness = exists(path.join(root, '.leerness'));
3053
+ const migrated = exists(path.join(root, '.leerness', 'MIGRATED_FROM_HARNESS'));
3054
+ const result = { current: dirName, abs: dirAbs, hasHarness, hasLeerness, migrated };
3055
+ if (has('--json')) { log(JSON.stringify(result, null, 2)); return; }
3056
+ log(cyan(`# leerness workspace-dir (1.9.211)`));
3057
+ log(` ํ˜„์žฌ ๋””๋ ‰ํ† ๋ฆฌ: ${grn(dirName + '/')}`);
3058
+ log(` abs: ${dirAbs}`);
3059
+ log(` .harness ์กด์žฌ: ${hasHarness ? 'โœ“' : 'โœ—'}`);
3060
+ log(` .leerness ์กด์žฌ: ${hasLeerness ? 'โœ“' : 'โœ—'}`);
3061
+ log(` ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์™„๋ฃŒ: ${migrated ? 'โœ“' : 'โœ—'}`);
3062
+ log('');
3063
+ if (hasHarness && !hasLeerness) {
3064
+ log(dim(` โ†’ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜: leerness migrate-workspace-dir`));
3065
+ } else if (migrated) {
3066
+ log(dim(` โ†’ AI ์ฐธ์กฐ ๊ฐ€์ด๋“œ: leerness workspace-dir guide`));
3067
+ }
3068
+ return;
3069
+ }
3070
+
3071
+ if (sub === 'guide') {
3072
+ const dirName = _workspaceDirName(root);
3073
+ const guide = _buildWorkspaceReferenceGuide(root, dirName);
3074
+ if (has('--json')) { log(JSON.stringify({ workspaceDir: dirName, guide }, null, 2)); return; }
3075
+ log(guide);
3076
+ return;
3077
+ }
3078
+
3079
+ console.error(`Usage: leerness workspace-dir [get|guide]`);
3080
+ process.exit(1);
3081
+ }
3082
+
3083
+ // 1.9.211: leerness migrate-workspace-dir โ€” .harness โ†’ .leerness ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ (์‚ฌ์šฉ์ž ๋ช…์‹œ)
3084
+ function migrateWorkspaceDirCmd(root) {
3085
+ root = absRoot(root);
3086
+ const isTty = process.stdout && process.stdout.isTTY;
3087
+ const cyan = s => isTty ? `\x1b[36m${s}\x1b[0m` : s;
3088
+ const grn = s => isTty ? `\x1b[32m${s}\x1b[0m` : s;
3089
+ const yel = s => isTty ? `\x1b[33m${s}\x1b[0m` : s;
3090
+ const red = s => isTty ? `\x1b[31m${s}\x1b[0m` : s;
3091
+ const dim = s => isTty ? `\x1b[2m${s}\x1b[0m` : s;
3092
+
3093
+ const dryRun = has('--dry-run');
3094
+ const force = has('--force');
3095
+ const report = _migrateWorkspaceDir(root, { dryRun, force });
3096
+
3097
+ if (has('--json')) { log(JSON.stringify(report, null, 2)); return; }
3098
+ log(cyan(`# leerness migrate-workspace-dir (1.9.211)${dryRun ? ' [DRY-RUN]' : ''}`));
3099
+ log(` src: ${report.src} ${report.srcExists ? 'โœ“' : red('โœ— ์—†์Œ')}`);
3100
+ log(` dst: ${report.dst} ${report.dstExists ? yel('โš  ์ด๋ฏธ ์กด์žฌ') : '(์ƒ์„ฑ)'}`);
3101
+ log('');
3102
+ log(` ๐Ÿ“Š copied: ${grn(report.copiedFiles.length)}, skipped: ${report.skippedFiles.length}, errors: ${report.errors.length}`);
3103
+ if (report.copiedFiles.length > 0 && report.copiedFiles.length <= 30) {
3104
+ log('');
3105
+ log(` ๋ณต์‚ฌ๋œ ํŒŒ์ผ:`);
3106
+ report.copiedFiles.slice(0, 20).forEach(f => log(dim(` + ${f}`)));
3107
+ if (report.copiedFiles.length > 20) log(dim(` ... +${report.copiedFiles.length - 20}๊ฑด`));
3108
+ } else if (report.copiedFiles.length > 30) {
3109
+ log(dim(` (${report.copiedFiles.length}๊ฐœ โ€” ์ƒ์„ธ๋Š” --json)`));
3110
+ }
3111
+ if (report.skippedFiles.length > 0) {
3112
+ log('');
3113
+ log(yel(` ์Šคํ‚ต๋œ ํŒŒ์ผ (์ด๋ฏธ ์กด์žฌ, --force ๋กœ ๋ฎ์–ด์“ฐ๊ธฐ ๊ฐ€๋Šฅ):`));
3114
+ report.skippedFiles.slice(0, 5).forEach(f => log(dim(` - ${f}`)));
3115
+ if (report.skippedFiles.length > 5) log(dim(` ... +${report.skippedFiles.length - 5}๊ฑด`));
3116
+ }
3117
+ if (report.errors.length > 0) {
3118
+ log('');
3119
+ log(red(` โš  ์—๋Ÿฌ:`));
3120
+ report.errors.forEach(e => log(red(` - ${e}`)));
3121
+ }
3122
+ log('');
3123
+ if (!dryRun && report.copiedFiles.length > 0) {
3124
+ log(grn(`โœ“ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์™„๋ฃŒ โ€” .leerness/WHERE_TO_FIND.md + MIGRATED_FROM_HARNESS ๋งˆ์ปค ์ƒ์„ฑ`));
3125
+ log(dim(` โ†’ ๋‹ค์Œ handoff๋ถ€ํ„ฐ .leerness ์šฐ์„  ์‚ฌ์šฉ`));
3126
+ log(dim(` โ†’ .harness/MIGRATED_TO_LEERNESS.md ์•ˆ๋‚ด ์ถ”๊ฐ€๋จ (backward compat ๋ณด์กด)`));
3127
+ } else if (dryRun) {
3128
+ log(yel(` (dry-run โ€” ์‹ค์ œ ๋ณ€๊ฒฝ ์—†์Œ. ์‹คํ–‰: leerness migrate-workspace-dir)`));
3129
+ }
3130
+ }
3131
+
3132
+ // 1.9.212: leerness idempotency <audit> โ€” ๋ฉฑ๋“ฑ์„ฑ ์œ„๋ฐ˜ ํƒ์ง€ + ๊ถŒ์žฅ fix (์‚ฌ์šฉ์ž ๋ช…์‹œ)
3133
+ // "๋‹ค์–‘ํ•œ ๋ถ€๋ถ„์—์„œ ๋ฉฑ๋“ฑ์„ฑ์ด ํ•„์š”ํ•œ ๋ถ€๋ถ„์ด ๊ณ ๋ ค๋˜๊ณ ์žˆ๋Š”์ง€ ํ™•์ธํ•ด์ฃผ๊ณ  ์˜ฌ๋ฐ”๋ฅธ ๋ฐฉํ–ฅ์ธ์ง€ ํŒ๋‹จ"
3134
+ // ํƒ์ง€ ์˜์—ญ:
3135
+ // 1. rules.md โ€” ๊ฐ™์€ description + trigger + active ์ค‘๋ณต
3136
+ // 2. progress-tracker.md โ€” ๊ฐ™์€ request ํ…์ŠคํŠธ ์ค‘๋ณต (ํ™œ์„ฑ ์ƒํƒœ)
3137
+ // 3. user-requests.json โ€” ๋™์ผ ํ…์ŠคํŠธ + open (1.9.207 ์ž์ฒด dedup ๊ฒ€์ฆ)
3138
+ // 4. active-wakeups.json โ€” ๋™์ผ expectedFireAt ์ค‘๋ณต (1.9.205 dedup ๊ฒ€์ฆ)
3139
+ // 5. next-action-queue.json โ€” ๋™์ผ title ์ค‘๋ณต (1.9.201 dedup ๊ฒ€์ฆ)
3140
+ function _runIdempotencyAudit(root) {
3141
+ const audit = {
3142
+ auditedAt: new Date().toISOString(),
3143
+ auditVersion: VERSION,
3144
+ violations: [],
3145
+ verified: []
3146
+ };
3147
+ // 1) rules.md dup ๊ฒ€์‚ฌ
3148
+ try {
3149
+ const rules = readRules(root);
3150
+ const active = rules.filter(r => r.status === 'active');
3151
+ const seen = new Map();
3152
+ for (const r of active) {
3153
+ const key = `${r.trigger}::${r.rule}`;
3154
+ if (seen.has(key)) {
3155
+ audit.violations.push({
3156
+ kind: 'rule-duplicate',
3157
+ location: '.harness/rules.md',
3158
+ detail: `์ค‘๋ณต ๋ฃฐ: ${r.id} == ${seen.get(key)} (${r.trigger}: ${r.rule.slice(0, 60)})`,
3159
+ severity: 'medium',
3160
+ fix: `leerness rule remove ${r.id}`
3161
+ });
3162
+ } else {
3163
+ seen.set(key, r.id);
3164
+ }
3165
+ }
3166
+ if (audit.violations.filter(v => v.kind === 'rule-duplicate').length === 0) {
3167
+ audit.verified.push({ kind: 'rules', detail: `${active.length} active rules, ์ค‘๋ณต 0๊ฑด` });
3168
+ }
3169
+ } catch (e) { audit.violations.push({ kind: 'audit-error', area: 'rules', detail: String(e.message || e), severity: 'low' }); }
3170
+
3171
+ // 2) progress-tracker.md ์ค‘๋ณต request ๊ฒ€์‚ฌ
3172
+ // ํฌ๋งท: | T-XXXX | status | request | evidence | nextAction | date |
3173
+ try {
3174
+ const pt = path.join(root, '.harness', 'progress-tracker.md');
3175
+ if (exists(pt)) {
3176
+ const content = read(pt);
3177
+ const lines = content.split(/\r?\n/);
3178
+ const requests = new Map(); // text โ†’ { id, status, line }
3179
+ for (let i = 0; i < lines.length; i++) {
3180
+ if (!/^\|\s*T-\d{4,}\s*\|/.test(lines[i])) continue;
3181
+ const cells = lines[i].split('|').slice(1, -1).map(s => s.trim());
3182
+ if (cells.length < 3) continue;
3183
+ const id = cells[0];
3184
+ const status = (cells[1] || '').toLowerCase();
3185
+ const text = cells[2];
3186
+ if (text.length < 5) continue;
3187
+ const isActive = !/^(done|dropped|blocked|completed)$/i.test(status);
3188
+ if (!isActive) continue;
3189
+ if (requests.has(text)) {
3190
+ const prev = requests.get(text);
3191
+ audit.violations.push({
3192
+ kind: 'task-duplicate-request',
3193
+ location: '.harness/progress-tracker.md',
3194
+ detail: `์ค‘๋ณต request: "${text.slice(0, 50)}โ€ฆ" (${prev.id} & ${id})`,
3195
+ severity: 'medium',
3196
+ fix: `์ค‘๋ณต task ์ค‘ ํ•˜๋‚˜๋ฅผ leerness task drop <id> ์ฒ˜๋ฆฌ`
3197
+ });
3198
+ } else {
3199
+ requests.set(text, { id, status, line: i + 1 });
3200
+ }
3201
+ }
3202
+ if (audit.violations.filter(v => v.kind === 'task-duplicate-request').length === 0) {
3203
+ audit.verified.push({ kind: 'tasks', detail: `${requests.size} active tasks, request ์ค‘๋ณต 0๊ฑด` });
3204
+ }
3205
+ }
3206
+ } catch (e) { audit.violations.push({ kind: 'audit-error', area: 'tasks', detail: String(e.message || e), severity: 'low' }); }
3207
+
3208
+ // 3) user-requests.json ์ž์ฒด dedup ๊ฒ€์ฆ (1.9.207)
3209
+ try {
3210
+ const ur = _loadUserRequests(root);
3211
+ const seen = new Map();
3212
+ for (const r of ur.requests) {
3213
+ if (r.status !== 'open' && r.status !== 'in-progress') continue;
3214
+ const k = r.text.trim();
3215
+ if (seen.has(k)) {
3216
+ audit.violations.push({
3217
+ kind: 'user-request-duplicate',
3218
+ location: '.harness/user-requests.json',
3219
+ detail: `์ค‘๋ณต open ์š”์ฒญ: ${r.id} == ${seen.get(k)} ("${k.slice(0, 50)}โ€ฆ")`,
3220
+ severity: 'low',
3221
+ fix: `leerness requests drop ${r.id}`
3222
+ });
3223
+ } else {
3224
+ seen.set(k, r.id);
3225
+ }
3226
+ }
3227
+ if (audit.violations.filter(v => v.kind === 'user-request-duplicate').length === 0) {
3228
+ audit.verified.push({ kind: 'user-requests', detail: `${seen.size} open requests, ์ค‘๋ณต 0๊ฑด (1.9.207 dedup OK)` });
3229
+ }
3230
+ } catch (e) { audit.violations.push({ kind: 'audit-error', area: 'user-requests', detail: String(e.message || e), severity: 'low' }); }
3231
+
3232
+ // 4) active-wakeups.json ๊ฒ€์ฆ (1.9.205)
3233
+ try {
3234
+ const wk = _loadActiveWakeups(root);
3235
+ const pending = wk.wakeups.filter(w => w.status === 'pending');
3236
+ const seenT = new Map();
3237
+ for (const w of pending) {
3238
+ if (seenT.has(w.expectedFireAt)) {
3239
+ audit.violations.push({
3240
+ kind: 'wakeup-duplicate',
3241
+ location: '.harness/active-wakeups.json',
3242
+ detail: `๋™์ผ expectedFireAt ์ค‘๋ณต: ${w.expectedFireAt}`,
3243
+ severity: 'high',
3244
+ fix: `leerness ์ž๋™ _recordWakeup filter dedup ๊ฒ€ํ†  ํ•„์š”`
3245
+ });
3246
+ } else {
3247
+ seenT.set(w.expectedFireAt, true);
3248
+ }
3249
+ }
3250
+ if (audit.violations.filter(v => v.kind === 'wakeup-duplicate').length === 0) {
3251
+ audit.verified.push({ kind: 'wakeups', detail: `${pending.length} pending wakeups, ์ค‘๋ณต 0๊ฑด (1.9.205 dedup OK)` });
3252
+ }
3253
+ } catch {}
3254
+
3255
+ audit.summary = {
3256
+ totalViolations: audit.violations.length,
3257
+ highSeverity: audit.violations.filter(v => v.severity === 'high').length,
3258
+ mediumSeverity: audit.violations.filter(v => v.severity === 'medium').length,
3259
+ lowSeverity: audit.violations.filter(v => v.severity === 'low').length,
3260
+ verifiedAreas: audit.verified.length,
3261
+ overall: audit.violations.length === 0 ? 'clean' : 'violations-found'
3262
+ };
3263
+ return audit;
3264
+ }
3265
+ function idempotencyCmd(root, sub) {
3266
+ root = absRoot(root);
3267
+ const isTty = process.stdout && process.stdout.isTTY;
3268
+ const cyan = s => isTty ? `\x1b[36m${s}\x1b[0m` : s;
3269
+ const grn = s => isTty ? `\x1b[32m${s}\x1b[0m` : s;
3270
+ const yel = s => isTty ? `\x1b[33m${s}\x1b[0m` : s;
3271
+ const red = s => isTty ? `\x1b[31m${s}\x1b[0m` : s;
3272
+ const dim = s => isTty ? `\x1b[2m${s}\x1b[0m` : s;
3273
+
3274
+ if (!sub || sub === 'help' || sub === '--help') {
3275
+ log(`# leerness idempotency (1.9.212) โ€” ๋ฉฑ๋“ฑ์„ฑ ์œ„๋ฐ˜ ํƒ์ง€`);
3276
+ log('');
3277
+ log(` audit โ†’ ์›Œํฌ์ŠคํŽ˜์ด์Šค ๋ฉฑ๋“ฑ์„ฑ ์ ๊ฒ€ (rules / tasks / user-requests / wakeups) (--json ๊ฐ€๋Šฅ)`);
3278
+ log('');
3279
+ log(dim(` dedup ์ ์šฉ ์˜์—ญ: ruleAdd / taskAdd (1.9.212) + _recordUserRequest (1.9.207) + _recordWakeup (1.9.205)`));
3280
+ log(dim(` opt-out: --force ํ”Œ๋ž˜๊ทธ๋กœ dedup ์šฐํšŒ ๊ฐ€๋Šฅ`));
3281
+ return;
3282
+ }
3283
+
3284
+ if (sub === 'audit') {
3285
+ const audit = _runIdempotencyAudit(root);
3286
+ if (has('--json')) { log(JSON.stringify(audit, null, 2)); return; }
3287
+ log(cyan(`# leerness idempotency audit (1.9.212)`));
3288
+ log(` audited at: ${audit.auditedAt}`);
3289
+ log('');
3290
+ const s = audit.summary;
3291
+ if (s.overall === 'clean') {
3292
+ log(grn(` โœ“ ๋ฉฑ๋“ฑ์„ฑ ์œ„๋ฐ˜ ์—†์Œ โ€” verified ${s.verifiedAreas} ์˜์—ญ`));
3293
+ } else {
3294
+ log(red(` โš  ${s.totalViolations} ์œ„๋ฐ˜ ๋ฐœ๊ฒฌ (high: ${s.highSeverity}, medium: ${s.mediumSeverity}, low: ${s.lowSeverity})`));
3295
+ }
3296
+ log('');
3297
+ if (audit.verified.length) {
3298
+ log(grn(`## โœ… verified (${audit.verified.length})`));
3299
+ audit.verified.forEach(v => log(dim(` - [${v.kind}] ${v.detail}`)));
3300
+ log('');
3301
+ }
3302
+ if (audit.violations.length) {
3303
+ log(red(`## โš  violations (${audit.violations.length})`));
3304
+ audit.violations.forEach(v => {
3305
+ const icon = v.severity === 'high' ? '๐Ÿšจ' : (v.severity === 'medium' ? 'โš ' : 'โ„น');
3306
+ log(` ${icon} [${v.kind}] (${v.severity})`);
3307
+ log(dim(` ${v.location}`));
3308
+ log(` ${v.detail}`);
3309
+ if (v.fix) log(dim(` fix: ${v.fix}`));
3310
+ });
3311
+ }
3312
+ return;
3313
+ }
3314
+
3315
+ console.error(`Unknown subcommand: ${sub}`);
3316
+ console.error(`Run: leerness idempotency help`);
3317
+ process.exit(1);
3318
+ }
3319
+
2878
3320
  // 1.9.199: wakeup miss ์ž๋™ ํšŒ๋ณต ๊ฐ•ํ™” โ€” last-handoff timestamp ๊ธฐ๋ก + 25min ๊ธฐ๋Œ€ interval ์ •๋ฐ€ ์ธก์ •
2879
3321
  // ์‚ฌ์šฉ์ž ๋ช…์‹œ (1.9.198 ํ›„์†): "22:01์— ๋ผ์šด๋“œ์— ์ž๋™์ง„์ž…๋˜์ง€์•Š์•˜์–ด"
2880
3322
  // 1.9.196 task-log mtime ๊ธฐ๋ฐ˜ detector ๋Š” task-log ๊ฐ€ ์ž์ฃผ ์•ˆ ์—…๋ฐ์ดํŠธ๋˜๋ฉด false positive.
@@ -3453,6 +3895,30 @@ function taskList(root) {
3453
3895
  for (const r of filtered) log(`| ${r.id} | ${r.status} | ${r.request} | ${r.evidence} | ${r.nextAction} | ${r.updated} |`);
3454
3896
  }
3455
3897
  function taskAdd(root, text) {
3898
+ // 1.9.212: ๋ฉฑ๋“ฑ์„ฑ โ€” ๊ฐ™์€ request ํ…์ŠคํŠธ + ํ™œ์„ฑ ์ƒํƒœ (requested/in-progress) ์ด๋ฏธ ์กด์žฌ ์‹œ skip (์‚ฌ์šฉ์ž ๋ช…์‹œ)
3899
+ // progress-tracker.md ํฌ๋งท: | T-XXXX | status | request | evidence | nextAction | date |
3900
+ if (!has('--force') && text && text.trim()) {
3901
+ try {
3902
+ const pt = path.join(absRoot(root), '.harness', 'progress-tracker.md');
3903
+ if (exists(pt)) {
3904
+ const content = read(pt);
3905
+ const norm = text.trim();
3906
+ const lines = content.split(/\r?\n/);
3907
+ for (const line of lines) {
3908
+ if (!/^\|\s*T-\d{4,}\s*\|/.test(line)) continue;
3909
+ const cells = line.split('|').slice(1, -1).map(s => s.trim());
3910
+ if (cells.length < 3) continue;
3911
+ const id = cells[0];
3912
+ const status = (cells[1] || '').toLowerCase();
3913
+ const request = cells[2];
3914
+ if (request === norm && !/^(done|dropped|blocked|completed)$/i.test(status)) {
3915
+ ok(`task exists (skip): ${id} request="${norm.slice(0, 60)}โ€ฆ" (--force ๋กœ ๋ฎ์–ด์“ฐ๊ธฐ)`);
3916
+ return;
3917
+ }
3918
+ }
3919
+ }
3920
+ } catch {}
3921
+ }
3456
3922
  const id = nextId(root, 'T');
3457
3923
  upsertProgress(root, { id, status: arg('--status','requested'), request: text, evidence: arg('--evidence','user-request'), nextAction: arg('--next','๋‹ค์Œ ์•ก์…˜ ์ž‘์„ฑ') });
3458
3924
  ok(`task added: ${id}`);
@@ -9981,8 +10447,16 @@ function ruleAdd(root, description) {
9981
10447
  if (!validTriggers.has(trigger)) {
9982
10448
  warn(`unknown trigger "${trigger}" โ€” ์‚ฌ์šฉ ๊ฐ€๋Šฅ: ${[...validTriggers].join(', ')}. ๊ทธ๋Œ€๋กœ ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค.`);
9983
10449
  }
9984
- const id = nextRuleId(root);
9985
10450
  const rules = readRules(root);
10451
+ // 1.9.212: ๋ฉฑ๋“ฑ์„ฑ ๋ณด์žฅ โ€” ๊ฐ™์€ description + trigger + active ์ƒํƒœ ์ด๋ฏธ ์กด์žฌ ์‹œ skip (์‚ฌ์šฉ์ž ๋ช…์‹œ)
10452
+ if (!has('--force')) {
10453
+ const dup = rules.find(r => r.rule === description && r.trigger === trigger && r.status === 'active');
10454
+ if (dup) {
10455
+ ok(`rule exists (skip): ${dup.id} [${dup.trigger}] ${description} (--force ๋กœ ๋ฎ์–ด์“ฐ๊ธฐ)`);
10456
+ return;
10457
+ }
10458
+ }
10459
+ const id = nextRuleId(root);
9986
10460
  rules.push({ id, trigger, rule: description, added: today(), status: 'active', lastVerified: '-' });
9987
10461
  writeRules(root, rules);
9988
10462
  ok(`rule added: ${id} [${trigger}] ${description}`);
@@ -16280,6 +16754,12 @@ async function main() {
16280
16754
  if (cmd === 'pre-wake-audit') return preWakeAuditCmd(arg('--path', process.cwd()), args[1]);
16281
16755
  // 1.9.210: leerness wakeup-interval <get|set|auto|history|record> โ€” adaptive interval (์‚ฌ์šฉ์ž ๋ช…์‹œ)
16282
16756
  if (cmd === 'wakeup-interval') return wakeupIntervalCmd(arg('--path', process.cwd()), args[1], args[2]);
16757
+ // 1.9.211: leerness workspace-dir <get|guide> โ€” ํ˜„์žฌ ์›Œํฌ์ŠคํŽ˜์ด์Šค ๋””๋ ‰ํ† ๋ฆฌ / AI ์ฐธ์กฐ ๊ฐ€์ด๋“œ (์‚ฌ์šฉ์ž ๋ช…์‹œ)
16758
+ if (cmd === 'workspace-dir') return workspaceDirCmd(arg('--path', process.cwd()), args[1]);
16759
+ // 1.9.211: leerness migrate-workspace-dir โ€” .harness โ†’ .leerness ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ (์‚ฌ์šฉ์ž ๋ช…์‹œ)
16760
+ if (cmd === 'migrate-workspace-dir') return migrateWorkspaceDirCmd(arg('--path', process.cwd()));
16761
+ // 1.9.212: leerness idempotency audit โ€” ๋ฉฑ๋“ฑ์„ฑ ์œ„๋ฐ˜ ํƒ์ง€ (์‚ฌ์šฉ์ž ๋ช…์‹œ)
16762
+ if (cmd === 'idempotency') return idempotencyCmd(arg('--path', process.cwd()), args[1]);
16283
16763
  if (cmd === 'rule' && args[1] === 'add') return ruleAdd(arg('--path', process.cwd()), args.slice(2).filter(x => !x.startsWith('-')).join(' '));
16284
16764
  if (cmd === 'rule' && args[1] === 'list') return ruleList(arg('--path', process.cwd()));
16285
16765
  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.210",
3
+ "version": "1.9.212",
4
4
  "description": "Leerness: ๋น„ํŒŒ๊ดด ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜, ์ž๋™ ๋ฒ„์ „ ๊ฐ์ง€ยท์—…๋ฐ์ดํŠธ, ๊ณ„ํš/์ง„ํ–‰/ํ•ธ๋“œ์˜คํ”„ ์ž๋™ํ™”, ๊ฒŒ์œผ๋ฆ„ยท์‹œํฌ๋ฆฟยท์ธ์ฝ”๋”ฉ ์ž๋™ ๊ฐ€๋“œ, Claude Code ์Šฌ๋ž˜์‹œ ํ†ตํ•ฉ์„ ๊ฐ–์ถ˜ ํ•œ๊ตญ์–ด ์šฐ์„  AI ๊ฐœ๋ฐœ ํ•˜๋„ค์Šค.",
5
5
  "keywords": [
6
6
  "leerness",