leerness 1.9.211 โ†’ 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,45 @@
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
+
3
43
  ## 1.9.211 โ€” 2026-05-22
4
44
 
5
45
  **๐Ÿ“‚ `.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.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.211 AI Agent Reliability Harness + Sandbox โ•‘
15
+ โ•‘ v1.9.212 AI Agent Reliability Harness + Sandbox โ•‘
16
16
  โ•‘ verify ยท remember ยท orchestrate ยท audit ยท sandbox ยท drift โ•‘
17
- โ•‘ ๐Ÿ“‚ .harness โ†’ .leerness opt-in ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ (์‚ฌ์šฉ์ž ๋ช…์‹œ) โ•‘
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.211';
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 ์ฒ˜๋ฆฌ).
@@ -3129,6 +3129,194 @@ function migrateWorkspaceDirCmd(root) {
3129
3129
  }
3130
3130
  }
3131
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
+
3132
3320
  // 1.9.199: wakeup miss ์ž๋™ ํšŒ๋ณต ๊ฐ•ํ™” โ€” last-handoff timestamp ๊ธฐ๋ก + 25min ๊ธฐ๋Œ€ interval ์ •๋ฐ€ ์ธก์ •
3133
3321
  // ์‚ฌ์šฉ์ž ๋ช…์‹œ (1.9.198 ํ›„์†): "22:01์— ๋ผ์šด๋“œ์— ์ž๋™์ง„์ž…๋˜์ง€์•Š์•˜์–ด"
3134
3322
  // 1.9.196 task-log mtime ๊ธฐ๋ฐ˜ detector ๋Š” task-log ๊ฐ€ ์ž์ฃผ ์•ˆ ์—…๋ฐ์ดํŠธ๋˜๋ฉด false positive.
@@ -3707,6 +3895,30 @@ function taskList(root) {
3707
3895
  for (const r of filtered) log(`| ${r.id} | ${r.status} | ${r.request} | ${r.evidence} | ${r.nextAction} | ${r.updated} |`);
3708
3896
  }
3709
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
+ }
3710
3922
  const id = nextId(root, 'T');
3711
3923
  upsertProgress(root, { id, status: arg('--status','requested'), request: text, evidence: arg('--evidence','user-request'), nextAction: arg('--next','๋‹ค์Œ ์•ก์…˜ ์ž‘์„ฑ') });
3712
3924
  ok(`task added: ${id}`);
@@ -10235,8 +10447,16 @@ function ruleAdd(root, description) {
10235
10447
  if (!validTriggers.has(trigger)) {
10236
10448
  warn(`unknown trigger "${trigger}" โ€” ์‚ฌ์šฉ ๊ฐ€๋Šฅ: ${[...validTriggers].join(', ')}. ๊ทธ๋Œ€๋กœ ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค.`);
10237
10449
  }
10238
- const id = nextRuleId(root);
10239
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);
10240
10460
  rules.push({ id, trigger, rule: description, added: today(), status: 'active', lastVerified: '-' });
10241
10461
  writeRules(root, rules);
10242
10462
  ok(`rule added: ${id} [${trigger}] ${description}`);
@@ -16538,6 +16758,8 @@ async function main() {
16538
16758
  if (cmd === 'workspace-dir') return workspaceDirCmd(arg('--path', process.cwd()), args[1]);
16539
16759
  // 1.9.211: leerness migrate-workspace-dir โ€” .harness โ†’ .leerness ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ (์‚ฌ์šฉ์ž ๋ช…์‹œ)
16540
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]);
16541
16763
  if (cmd === 'rule' && args[1] === 'add') return ruleAdd(arg('--path', process.cwd()), args.slice(2).filter(x => !x.startsWith('-')).join(' '));
16542
16764
  if (cmd === 'rule' && args[1] === 'list') return ruleList(arg('--path', process.cwd()));
16543
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.211",
3
+ "version": "1.9.212",
4
4
  "description": "Leerness: ๋น„ํŒŒ๊ดด ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜, ์ž๋™ ๋ฒ„์ „ ๊ฐ์ง€ยท์—…๋ฐ์ดํŠธ, ๊ณ„ํš/์ง„ํ–‰/ํ•ธ๋“œ์˜คํ”„ ์ž๋™ํ™”, ๊ฒŒ์œผ๋ฆ„ยท์‹œํฌ๋ฆฟยท์ธ์ฝ”๋”ฉ ์ž๋™ ๊ฐ€๋“œ, Claude Code ์Šฌ๋ž˜์‹œ ํ†ตํ•ฉ์„ ๊ฐ–์ถ˜ ํ•œ๊ตญ์–ด ์šฐ์„  AI ๊ฐœ๋ฐœ ํ•˜๋„ค์Šค.",
5
5
  "keywords": [
6
6
  "leerness",