leerness 1.36.113 → 1.36.114
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/README.md +4 -4
- package/bin/leerness.js +67 -28
- package/package.json +1 -1
- package/scripts/e2e.js +179 -0
package/README.md
CHANGED
|
@@ -122,7 +122,7 @@ MIT
|
|
|
122
122
|
<!-- leerness:project-readme:start -->
|
|
123
123
|
## Leerness Project Harness
|
|
124
124
|
|
|
125
|
-
이 프로젝트는 Leerness v1.36.
|
|
125
|
+
이 프로젝트는 Leerness v1.36.114 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
|
|
126
126
|
|
|
127
127
|
### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
|
|
128
128
|
|
|
@@ -176,7 +176,7 @@ leerness memory restore decision <date|title>
|
|
|
176
176
|
|
|
177
177
|
### MCP server (외부 AI 통합)
|
|
178
178
|
|
|
179
|
-
Leerness v1.36.
|
|
179
|
+
Leerness v1.36.114는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **89개 도구**를 노출:
|
|
180
180
|
|
|
181
181
|
```jsonc
|
|
182
182
|
// 카테고리별
|
|
@@ -197,7 +197,7 @@ Leerness v1.36.113는 stdio JSON-RPC MCP server를 내장합니다 — Claude Co
|
|
|
197
197
|
`<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
|
|
198
198
|
1) 다음 라운드 후보 선정 → 2) 코드 변경 → 3) 회귀 테스트 갱신 → 4) 전체 e2e 스위트 통과 → 5) npm publish + git tag → 6) main push → 7) session close → 8) 다음 라운드 예약.
|
|
199
199
|
|
|
200
|
-
현재 누적: **v1.9.x → 1.36.
|
|
200
|
+
현재 누적: **v1.9.x → 1.36.114 릴리스 태그 이력** (수백 라운드) · _reports/는 비공개 보존.
|
|
201
201
|
|
|
202
202
|
### 성능 가이드
|
|
203
203
|
|
|
@@ -235,5 +235,5 @@ leerness release pack --close --auto-main-push
|
|
|
235
235
|
- `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
|
|
236
236
|
- `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
|
|
237
237
|
|
|
238
|
-
Last synced by Leerness v1.36.
|
|
238
|
+
Last synced by Leerness v1.36.114: 2026-08-11
|
|
239
239
|
<!-- leerness:project-readme:end -->
|
package/bin/leerness.js
CHANGED
|
@@ -34,7 +34,7 @@ const { CAPABILITY_SURFACE, POWERFUL_COMMANDS, ADAPTERS, REUSE_CATEGORIES, REUSE
|
|
|
34
34
|
const { tokenizeForRank: _tokenizeForRank, expandQuery: _expandQuery, scoreHits: _scoreHits, suggestTerms: _suggestTerms } = require('../lib/search-core'); // 1.36.23: memory search 랭킹 코어(순수·0-deps)
|
|
35
35
|
const { findCorruptedStateJson: _findCorruptedStateJson } = require('../lib/state-integrity'); // 1.36.1 (클린룸 리뷰 FN): .harness/*.json 상태 무결성 (audit/health/check 공유)
|
|
36
36
|
|
|
37
|
-
const VERSION = '1.36.
|
|
37
|
+
const VERSION = '1.36.114';
|
|
38
38
|
|
|
39
39
|
// 1.9.290 (UR-0037, Codex gpt-5.5 #4 수렴): CLI 전용 부작용은 require 시 실행하지 않는다.
|
|
40
40
|
// 이전: warning listener 제거 / NODE_OPTIONS 변경 / chcp IIFE 가 top-level 즉시 실행 → require('harness') 시 호스트 프로세스 오염.
|
|
@@ -7085,9 +7085,17 @@ function _assertStoreParsable(file, label) {
|
|
|
7085
7085
|
catch { throw Object.assign(new Error(`${label} 저장 파일이 손상돼(JSON 파싱 실패) 덮어쓰기를 거부합니다: ${file} — 파일을 복구하거나 삭제 후 재시도하세요`), { code: 'E_STORE_CORRUPT', file }); }
|
|
7086
7086
|
}
|
|
7087
7087
|
}
|
|
7088
|
+
// 1.36.114: 동기 try/catch 만 있어 **async 명령에서는 통째로 무력**이었다 — 던진 예외가 거부된 프로미스가 되어
|
|
7089
|
+
// 래퍼를 그냥 지나갔다. 실측: `next-action add --json` 이 사람용 `✗` 줄을 JSON **앞에** 흘리고
|
|
7090
|
+
// code 가 `store_corrupt` 대신 `error` 였다(기계 계약 파손: JSON.parse(stdout) 실패).
|
|
7091
|
+
// 1.36.107 이 `_withLock` 에서 고친 것과 같은 형태다 — thenable 이면 프로미스 경로로도 같은 처리를 한다.
|
|
7088
7092
|
function _guardStore(jsonMode, fn) {
|
|
7089
|
-
|
|
7090
|
-
|
|
7093
|
+
const handle = (e) => { if (e && e.code === 'E_STORE_CORRUPT') { failJson(jsonMode, 'store_corrupt', e.message); return undefined; } throw e; };
|
|
7094
|
+
let out;
|
|
7095
|
+
try { out = fn(); }
|
|
7096
|
+
catch (e) { return handle(e); }
|
|
7097
|
+
if (out && typeof out.then === 'function') return out.then(v => v, handle);
|
|
7098
|
+
return out;
|
|
7091
7099
|
}
|
|
7092
7100
|
// 1.36.28 (#4): 0-deps 짧은 결정적 해시 (URL 충돌 구분용, 보안 아님).
|
|
7093
7101
|
function _shortHash(s) {
|
|
@@ -9433,9 +9441,13 @@ function _loadWakeupHistory(root) {
|
|
|
9433
9441
|
function _writeWakeupHistory(root, state) {
|
|
9434
9442
|
try {
|
|
9435
9443
|
mkdirp(path.join(root, '.harness'));
|
|
9444
|
+
_assertStoreParsable(_wakeupHistoryPath(root), 'wakeup-history'); // 1.36.114: 손상 스토어 덮어쓰기 거부
|
|
9436
9445
|
writeUtf8(_wakeupHistoryPath(root), JSON.stringify({ ...state, updatedAt: new Date().toISOString() }, null, 2));
|
|
9437
9446
|
return true;
|
|
9438
|
-
} catch {
|
|
9447
|
+
} catch (e) {
|
|
9448
|
+
if (e && e.code === 'E_STORE_CORRUPT') throw e; // 1.36.114: 조용한 무동작 + 성공 표시 방지(위 _writeNextActionQueue 와 같은 이유)
|
|
9449
|
+
return false;
|
|
9450
|
+
}
|
|
9439
9451
|
}
|
|
9440
9452
|
// 1.36.108 (T-0097): append 형 RMW — 락 밖이면 동시 기록이 서로를 지운다.
|
|
9441
9453
|
// 이 스토어는 자동(스케줄)과 사용자 트리거가 동시에 들어올 수 있어 겹칠 여지가 실제로 있다.
|
|
@@ -10147,11 +10159,11 @@ function constraintsCmd(root, sub, ...rest) {
|
|
|
10147
10159
|
for (const [pid, plat] of Object.entries(catalog.platforms)) {
|
|
10148
10160
|
// 1.31.2: aliases are matchers (kept for matching); hide Hangul-only aliases from EN display
|
|
10149
10161
|
const aliasList = _L === 'en' ? (plat.aliases || []).filter(a => !/[가-힣]/.test(a)) : (plat.aliases || []);
|
|
10150
|
-
log(grn(` 📦 ${pid}`) + dim(` aliases: ${aliasList.join(', ')}`));
|
|
10151
|
-
log(dim(` docs: ${plat.docs || '-'}`));
|
|
10162
|
+
log(grn(` 📦 ${_lineSafe(pid)}`) + dim(` aliases: ${_lineSafe(aliasList.join(', '))}`));
|
|
10163
|
+
log(dim(` docs: ${_lineSafe(plat.docs || '-')}`));
|
|
10152
10164
|
for (const c of plat.constraints || []) {
|
|
10153
10165
|
const icon = c.kind === 'rate-limit' ? '🚦' : (c.kind === 'cost' ? '💰' : (c.kind === 'auth' ? '🔐' : '📋'));
|
|
10154
|
-
log(` ${icon} [${c.kind}] ${_L === 'en' && c.detailEn ? c.detailEn : c.detail}`);
|
|
10166
|
+
log(` ${icon} [${_lineSafe(c.kind)}] ${_lineSafe(_L === 'en' && c.detailEn ? c.detailEn : c.detail)}`); // 1.36.114 (검수): ID·alias 만 막고 kind/detail 이 raw 였다
|
|
10155
10167
|
}
|
|
10156
10168
|
log('');
|
|
10157
10169
|
}
|
|
@@ -10180,11 +10192,11 @@ function constraintsCmd(root, sub, ...rest) {
|
|
|
10180
10192
|
log(red(_t(` ⚠ ${result.matched.length}개 플랫폼 매칭 — 제약 사전 확인 필요:`, ` ⚠ ${result.matched.length} platform(s) matched — review constraints before building:`)));
|
|
10181
10193
|
log('');
|
|
10182
10194
|
for (const m of result.matched) {
|
|
10183
|
-
log(grn(` 📦 ${m.platform}`) + dim(` (matched: "${m.matchedAlias}")`));
|
|
10184
|
-
log(dim(` docs: ${m.docs || '-'}`));
|
|
10195
|
+
log(grn(` 📦 ${_lineSafe(m.platform)}`) + dim(` (matched: "${_lineSafe(m.matchedAlias)}")`));
|
|
10196
|
+
log(dim(` docs: ${_lineSafe(m.docs || '-')}`));
|
|
10185
10197
|
for (const c of m.constraints || []) {
|
|
10186
10198
|
const icon = c.kind === 'rate-limit' ? '🚦' : (c.kind === 'cost' ? '💰' : (c.kind === 'auth' ? '🔐' : '📋'));
|
|
10187
|
-
log(` ${icon} [${c.kind}] ${_L === 'en' && c.detailEn ? c.detailEn : c.detail}`);
|
|
10199
|
+
log(` ${icon} [${_lineSafe(c.kind)}] ${_lineSafe(_L === 'en' && c.detailEn ? c.detailEn : c.detail)}`); // 1.36.114 (검수): ID·alias 만 막고 kind/detail 이 raw 였다
|
|
10188
10200
|
}
|
|
10189
10201
|
log('');
|
|
10190
10202
|
}
|
|
@@ -10215,7 +10227,7 @@ function constraintsCmd(root, sub, ...rest) {
|
|
|
10215
10227
|
if (!_cons.some(c => c.kind === kindTrim && c.detail === detail)) _cons.push({ kind: kindTrim, detail });
|
|
10216
10228
|
_writePlatformConstraints(root, catalog);
|
|
10217
10229
|
if (has('--json')) { log(JSON.stringify(catalog.platforms[id], null, 2)); return; }
|
|
10218
|
-
log(grn(_t(`✓ platform "${id}" 갱신 — constraints: ${catalog.platforms[id].constraints.length}`, `✓ platform "${id}" updated — constraints: ${catalog.platforms[id].constraints.length}`)));
|
|
10230
|
+
log(grn(_t(`✓ platform "${_lineSafe(id)}" 갱신 — constraints: ${catalog.platforms[id].constraints.length}`, `✓ platform "${_lineSafe(id)}" updated — constraints: ${catalog.platforms[id].constraints.length}`)));
|
|
10219
10231
|
return;
|
|
10220
10232
|
}
|
|
10221
10233
|
|
|
@@ -10965,7 +10977,7 @@ function intentCmd(root, sub, ...rest) {
|
|
|
10965
10977
|
log(` total domains: ${Object.keys(catalog.domains).length}`);
|
|
10966
10978
|
log('');
|
|
10967
10979
|
for (const [name, info] of Object.entries(catalog.domains)) {
|
|
10968
|
-
log(grn(` 📦 ${name}`) + dim(` aliases: ${(info.aliases || []).join(', ')}`));
|
|
10980
|
+
log(grn(` 📦 ${_lineSafe(name)}`) + dim(` aliases: ${_lineSafe((info.aliases || []).join(', '))}`));
|
|
10969
10981
|
for (const c of info.components || []) {
|
|
10970
10982
|
log(` • ${c.key.padEnd(12)} ${dim(c.desc)}`);
|
|
10971
10983
|
}
|
|
@@ -11090,9 +11102,16 @@ function _loadNextActionQueue(root) {
|
|
|
11090
11102
|
function _writeNextActionQueue(root, queue) {
|
|
11091
11103
|
try {
|
|
11092
11104
|
mkdirp(path.join(root, '.harness'));
|
|
11105
|
+
_assertStoreParsable(_nextActionQueuePath(root), 'next-action-queue'); // 1.36.114: 손상 스토어 덮어쓰기 거부
|
|
11093
11106
|
writeUtf8(_nextActionQueuePath(root), JSON.stringify({ queue, at: new Date().toISOString() }, null, 2));
|
|
11094
11107
|
return true;
|
|
11095
|
-
} catch {
|
|
11108
|
+
} catch (e) {
|
|
11109
|
+
// 1.36.114: 이 catch 가 손상 신호까지 삼켜, 아무것도 저장되지 않았는데 호출부가 `✓ 추가` 를 찍고 exit 0 이었다
|
|
11110
|
+
// (실측). 조용한 무동작에 성공 표시를 붙이는 것은 조용한 유실보다 나쁘다 — 사용자는 저장됐다고 믿는다.
|
|
11111
|
+
// 손상만 위로 올린다(그 외 I/O 실패는 종전대로 false).
|
|
11112
|
+
if (e && e.code === 'E_STORE_CORRUPT') throw e;
|
|
11113
|
+
return false;
|
|
11114
|
+
}
|
|
11096
11115
|
}
|
|
11097
11116
|
// handoff 에서 제안된 next-action 들을 큐에 자동 저장 (중복 방지: 이미 있는 title 은 skip)
|
|
11098
11117
|
// 1.36.108 (T-0097): 읽기~쓰기를 락으로 직렬화. 이 큐는 handoff 가 자동으로도 채우기 때문에
|
|
@@ -11141,7 +11160,10 @@ async function nextActionCmd(root, sub, ...rest) {
|
|
|
11141
11160
|
}
|
|
11142
11161
|
if (sub === 'take') {
|
|
11143
11162
|
const n = rest[0] !== undefined ? Number(rest[0]) : state.queue.length - 1;
|
|
11144
|
-
|
|
11163
|
+
// 1.36.114 (검수 #3, 재현됨): 로더가 손상 파일을 **빈 큐**로 폴백해 "큐 비어있음 — handoff 먼저 실행" 이라고
|
|
11164
|
+
// 오진했다. 사용자는 큐가 비었다고 믿고 handoff 를 다시 돌린다 — 원인(손상)은 끝까지 안 보인다.
|
|
11165
|
+
// 읽기 자체는 종전대로 resilient 하게 두고, **판정 지점에서만** 빈 것과 손상된 것을 가른다.
|
|
11166
|
+
if (state.queue.length === 0) { _assertStoreParsable(_nextActionQueuePath(root), 'next-action-queue'); fail('큐 비어있음 — handoff 먼저 실행'); return process.exit(1); }
|
|
11145
11167
|
if (isNaN(n) || n < 0 || n >= state.queue.length) { fail(`잘못된 index: ${n} (0~${state.queue.length - 1})`); return process.exit(1); }
|
|
11146
11168
|
const action = state.queue[n];
|
|
11147
11169
|
log(`# leerness next-action take [${n}] (1.9.201)`);
|
|
@@ -11169,7 +11191,11 @@ async function nextActionCmd(root, sub, ...rest) {
|
|
|
11169
11191
|
log(` ⚠ task add 실패 (exit ${taskResult.status}) — 수동: leerness task add "${taskTitle}"`);
|
|
11170
11192
|
}
|
|
11171
11193
|
} catch (e) {
|
|
11172
|
-
|
|
11194
|
+
// 1.36.114 (검수 지적, 재현됨): 이 catch 가 손상 신호까지 삼켜 `take` 는 경고만 찍고 **exit 0** 이었다.
|
|
11195
|
+
// `_guardStore` 의 thenable 처리가 정상이어도 예외가 여기서 죽으면 래퍼까지 도달하지 못한다 —
|
|
11196
|
+
// 가드는 예외가 실제로 올라올 때만 가드다. 손상만 위로 올린다(그 외는 종전대로 경고).
|
|
11197
|
+
if (e && e.code === 'E_STORE_CORRUPT') throw e;
|
|
11198
|
+
log(` ⚠ 처리 실패: ${_lineSafe(e.message)}`);
|
|
11173
11199
|
}
|
|
11174
11200
|
return;
|
|
11175
11201
|
}
|
|
@@ -11402,6 +11428,9 @@ function _loadDecisions(root) {
|
|
|
11402
11428
|
// canonical 저장 — decisions.json(canonical) + decisions.md(projection) 동시 기록 (단일 진실소스 write path).
|
|
11403
11429
|
function _saveDecisions(root, decisions) {
|
|
11404
11430
|
const arr = Array.isArray(decisions) ? decisions : [];
|
|
11431
|
+
// 1.36.114 (스토어 전수 대조에서 발견): 손상된 decisions.json 을 빈 배열로 오인해 덮어써 **영구 기억이 사라졌다**.
|
|
11432
|
+
// 표적 사냥(creds 등)으로는 못 봤고, .harness 아래 JSON 스토어를 **열거해 하나씩 손상시키는** 전수 스윕이 잡았다.
|
|
11433
|
+
_assertStoreParsable(decisionsJsonPath(root), 'decisions');
|
|
11405
11434
|
mkdirp(path.dirname(decisionsJsonPath(root)));
|
|
11406
11435
|
writeUtf8(decisionsJsonPath(root), JSON.stringify(arr, null, 2) + '\n');
|
|
11407
11436
|
writeUtf8(decisionsPath(root), _renderDecisionsMd(arr));
|
|
@@ -11454,6 +11483,7 @@ function _loadLessons(root) {
|
|
|
11454
11483
|
}
|
|
11455
11484
|
function _saveLessons(root, lessons) {
|
|
11456
11485
|
const arr = Array.isArray(lessons) ? lessons : [];
|
|
11486
|
+
_assertStoreParsable(lessonsJsonPath(root), 'lessons'); // 1.36.114: decisions 와 같은 클래스 — 손상 위에 덮어써 교훈이 사라졌다
|
|
11457
11487
|
mkdirp(path.dirname(lessonsJsonPath(root)));
|
|
11458
11488
|
writeUtf8(lessonsJsonPath(root), JSON.stringify(arr, null, 2) + '\n');
|
|
11459
11489
|
writeUtf8(lessonsPath(root), _renderLessonsMd(arr));
|
|
@@ -22410,6 +22440,7 @@ function _writePermissionsPreset(root, mode) {
|
|
|
22410
22440
|
preset.generatedAt = new Date().toISOString();
|
|
22411
22441
|
preset.leernessVersion = VERSION;
|
|
22412
22442
|
mkdirp(path.dirname(_permissionsPath(root)));
|
|
22443
|
+
_assertStoreParsable(_permissionsPath(root), 'agent-permissions'); // 1.36.114: 손상 스토어 덮어쓰기 거부(1.36.28 클래스 누락 지점)
|
|
22413
22444
|
writeUtf8(_permissionsPath(root), JSON.stringify(preset, null, 2) + '\n');
|
|
22414
22445
|
return preset;
|
|
22415
22446
|
}
|
|
@@ -24827,6 +24858,11 @@ function _readCredentials(root) {
|
|
|
24827
24858
|
}
|
|
24828
24859
|
function _writeCredentials(root, data) {
|
|
24829
24860
|
const p = _credentialsPath(root);
|
|
24861
|
+
// 1.36.114 (방치 명령 사냥): 손상된 스토어를 "빈 값" 으로 오인해 덮어쓰던 fail-open — 1.36.28 이 teams/
|
|
24862
|
+
// platform-constraints 에서 고친 클래스인데 여기는 빠져 있었다. 실측: 파일을 잘라 두고 `creds register` 하면
|
|
24863
|
+
// exit 0 으로 성공하며 **기존 서비스 등록이 사라졌다**(_readCredentials 의 catch 가 {} 를 돌려주기 때문).
|
|
24864
|
+
// 술어는 이미 있으니 재구현하지 않고 공유한다.
|
|
24865
|
+
_assertStoreParsable(p, 'credentials');
|
|
24830
24866
|
mkdirp(path.dirname(p));
|
|
24831
24867
|
writeUtf8(p, JSON.stringify(data, null, 2) + '\n');
|
|
24832
24868
|
// 1.9.147: gitignore + npmignore 자동 보강 (보안)
|
|
@@ -24846,14 +24882,16 @@ function credsListCmd(root) {
|
|
|
24846
24882
|
if (has('--json')) { log(JSON.stringify(j, null, 2)); return; }
|
|
24847
24883
|
log(`# leerness creds list (1.9.147)`);
|
|
24848
24884
|
const services = Object.entries(j.services || {});
|
|
24849
|
-
|
|
24885
|
+
// 1.36.114: "없음" 과 "손상" 을 가른다 — 손상인데 '없음' 이라 말하면 사용자는 등록이 지워졌다고 오해한다.
|
|
24886
|
+
// 읽기 자체는 resilient 하게 두고 **판정 지점에서만** 구분한다(check/refresh 와 같은 방식).
|
|
24887
|
+
if (!services.length) { _assertStoreParsable(_credentialsPath(root), 'credentials'); log('(등록된 자격증명 없음 — leerness creds register <service> --env-var <NAME>)'); return; }
|
|
24850
24888
|
log(`총 ${services.length}개 서비스 (값 미저장 — env-ref 만)`);
|
|
24851
24889
|
for (const [name, meta] of services) {
|
|
24852
24890
|
const present = meta.envVars.every(v => process.env[v] !== undefined && process.env[v] !== '');
|
|
24853
24891
|
const last = meta.lastRefreshed ? new Date(meta.lastRefreshed) : null;
|
|
24854
24892
|
const ageDays = last ? Math.floor((Date.now() - last.getTime()) / 86400000) : null;
|
|
24855
24893
|
const ageWarn = (meta.tokenLifetimeHours && last && (Date.now() - last.getTime()) > meta.tokenLifetimeHours * 3600 * 1000);
|
|
24856
|
-
log(` ${name}: env=${meta.envVars.join(',')} · ${present ? '✓ 환경변수 있음' : '⚠ 미설정'}${ageDays !== null ? ` · ${ageDays}일 전 refresh${ageWarn ? ' (만료 가능)' : ''}` : ''}`);
|
|
24894
|
+
log(` ${_lineSafe(name)}: env=${_lineSafe(meta.envVars.join(','))} · ${present ? '✓ 환경변수 있음' : '⚠ 미설정'}${ageDays !== null ? ` · ${ageDays}일 전 refresh${ageWarn ? ' (만료 가능)' : ''}` : ''}`);
|
|
24857
24895
|
if (meta.deployCommand) log(` deploy: ${meta.deployCommand}`);
|
|
24858
24896
|
}
|
|
24859
24897
|
}
|
|
@@ -24885,7 +24923,8 @@ function credsCheckCmd(root, service) {
|
|
|
24885
24923
|
const j = _readCredentials(root);
|
|
24886
24924
|
const result = { service: service || null, services: {}, ok: true };
|
|
24887
24925
|
const targets = service ? (j.services[service] ? { [service]: j.services[service] } : {}) : (j.services || {});
|
|
24888
|
-
|
|
24926
|
+
// 1.36.114 (검수 #2, 재현됨): 손상 파일을 빈 registry 로 폴백해 "등록된 서비스 없음" 이라 오진했다 — 판정 지점에서 가른다
|
|
24927
|
+
if (!Object.keys(targets).length) { _assertStoreParsable(_credentialsPath(root), 'credentials'); failJson(has('--json'), 'no_service', `등록된 서비스 없음${service ? ` (${service})` : ''}`); return; } // 1.9.404 (UR-0105 잔여): --json 에러 구조화
|
|
24889
24928
|
for (const [name, meta] of Object.entries(targets)) {
|
|
24890
24929
|
// 1.36.34 (codex 3차 #9): 손상 레코드(null/비객체) → raw TypeError 크래시, 파싱 불가 날짜(NaN) → false-ready. 구조화 판정.
|
|
24891
24930
|
if (!meta || typeof meta !== 'object' || !Array.isArray(meta.envVars)) {
|
|
@@ -24915,7 +24954,7 @@ function credsRefreshTimestampCmd(root, service) {
|
|
|
24915
24954
|
root = absRoot(root || process.cwd());
|
|
24916
24955
|
if (!service) return fail('service 이름 필요');
|
|
24917
24956
|
const j = _readCredentials(root);
|
|
24918
|
-
if (!j.services[service]) return fail(`등록된 서비스 없음: ${service} — leerness creds register 먼저`);
|
|
24957
|
+
if (!j.services[service]) { _assertStoreParsable(_credentialsPath(root), 'credentials'); return fail(`등록된 서비스 없음: ${service} — leerness creds register 먼저`); } // 1.36.114: 손상과 미등록을 가른다
|
|
24919
24958
|
j.services[service].lastRefreshed = new Date().toISOString();
|
|
24920
24959
|
_writeCredentials(root, j);
|
|
24921
24960
|
ok(`creds refreshed: ${service} · lastRefreshed=${j.services[service].lastRefreshed}`);
|
|
@@ -26577,17 +26616,17 @@ async function main() {
|
|
|
26577
26616
|
if (cmd === 'env' && args[1] === 'detect') return envDetectCmd(args[2] || arg('--path', process.cwd()));
|
|
26578
26617
|
// 1.9.146: agent 권한 시스템 + CLI 에이전트 모드 (사용자 명시 요청 #4, #5)
|
|
26579
26618
|
if (cmd === 'permissions' && args[1] === 'list') return permissionsListCmd(arg('--path', process.cwd()));
|
|
26580
|
-
if (cmd === 'permissions' && args[1] === 'set') return permissionsSetCmd(arg('--path', process.cwd()), args[2]);
|
|
26619
|
+
if (cmd === 'permissions' && args[1] === 'set') return _guardStore(has('--json'), () => permissionsSetCmd(arg('--path', process.cwd()), args[2]));
|
|
26581
26620
|
if (cmd === 'agent') return agentCmd(arg('--path', process.cwd()), args.slice(1).filter(x => !x.startsWith('--')).join(' '));
|
|
26582
26621
|
// 1.9.147: 자동 유지보수 시스템 (사용자 명시 요청)
|
|
26583
26622
|
if (cmd === 'webhook' && args[1] === 'serve') return webhookServeCmd(arg('--path', process.cwd()));
|
|
26584
26623
|
if (cmd === 'incident' && args[1] === 'list') return incidentListCmd(arg('--path', process.cwd()));
|
|
26585
26624
|
if (cmd === 'incident' && args[1] === 'show') return incidentShowCmd(arg('--path', process.cwd()), args[2]);
|
|
26586
26625
|
if (cmd === 'incident' && args[1] === 'handle') return incidentHandleCmd(arg('--path', process.cwd()), args[2]);
|
|
26587
|
-
if (cmd === 'creds' && args[1] === 'list') return credsListCmd(arg('--path', process.cwd()));
|
|
26588
|
-
if (cmd === 'creds' && args[1] === 'register') return credsRegisterCmd(arg('--path', process.cwd()), args[2]);
|
|
26589
|
-
if (cmd === 'creds' && args[1] === 'check') return credsCheckCmd(arg('--path', process.cwd()), args[2]);
|
|
26590
|
-
if (cmd === 'creds' && args[1] === 'refresh') return credsRefreshTimestampCmd(arg('--path', process.cwd()), args[2]);
|
|
26626
|
+
if (cmd === 'creds' && args[1] === 'list') return _guardStore(has('--json'), () => credsListCmd(arg('--path', process.cwd())));
|
|
26627
|
+
if (cmd === 'creds' && args[1] === 'register') return _guardStore(has('--json'), () => credsRegisterCmd(arg('--path', process.cwd()), args[2]));
|
|
26628
|
+
if (cmd === 'creds' && args[1] === 'check') return _guardStore(has('--json'), () => credsCheckCmd(arg('--path', process.cwd()), args[2]));
|
|
26629
|
+
if (cmd === 'creds' && args[1] === 'refresh') return _guardStore(has('--json'), () => credsRefreshTimestampCmd(arg('--path', process.cwd()), args[2]));
|
|
26591
26630
|
if (cmd === 'deploy' && args[1] === 'auto') return deployAutoCmd(arg('--path', process.cwd()), args[2]);
|
|
26592
26631
|
// 1.9.149: observability lite + runs list/show
|
|
26593
26632
|
if (cmd === 'runs' && args[1] === 'list') return runsListCmd(absRoot(_resolveRoot(args[2]))); // 1.9.412 (UR-0100): positional path 지원
|
|
@@ -26643,7 +26682,7 @@ async function main() {
|
|
|
26643
26682
|
if (cmd === 'roadmap' && args[1] === 'auto') return roadmapAutoCmd(arg('--path', process.cwd()), args[2]);
|
|
26644
26683
|
if (cmd === 'roadmap') return roadmapCmd(arg('--path', args[1] || process.cwd()));
|
|
26645
26684
|
// 1.9.201: next-action queue CLI (E축 9.5→10)
|
|
26646
|
-
if (cmd === 'next-action') return nextActionCmd(arg('--path', process.cwd()), args[1], ...args.slice(2));
|
|
26685
|
+
if (cmd === 'next-action') return _guardStore(has('--json'), () => nextActionCmd(arg('--path', process.cwd()), args[1], ...args.slice(2)));
|
|
26647
26686
|
// 1.9.203: leerness resume — auto-resume-plan 읽고 다음 라운드 즉시 안내 (사용자 명시)
|
|
26648
26687
|
if (cmd === 'resume') return resumeCmd(arg('--path', null) || _taskPositionalPath(args, 1) || process.cwd());
|
|
26649
26688
|
// 1.9.207: leerness requests <audit|add|list|complete|drop> — 사용자 요청 누락 확인 절차 (사용자 명시)
|
|
@@ -26809,7 +26848,7 @@ async function main() {
|
|
|
26809
26848
|
// 1.9.209: leerness pre-wake-audit — sleep 전 sub-agent audit (사용자 명시)
|
|
26810
26849
|
if (cmd === 'pre-wake-audit') return preWakeAuditCmd(arg('--path', process.cwd()), args[1]);
|
|
26811
26850
|
// 1.9.210: leerness wakeup-interval <get|set|auto|history|record> — adaptive interval (사용자 명시)
|
|
26812
|
-
if (cmd === 'wakeup-interval') return wakeupIntervalCmd(arg('--path', process.cwd()), args[1], args[2]);
|
|
26851
|
+
if (cmd === 'wakeup-interval') return _guardStore(has('--json'), () => wakeupIntervalCmd(arg('--path', process.cwd()), args[1], args[2]));
|
|
26813
26852
|
// 1.9.211: leerness workspace-dir <get|guide> — 현재 워크스페이스 디렉토리 / AI 참조 가이드 (사용자 명시)
|
|
26814
26853
|
if (cmd === 'workspace-dir') return workspaceDirCmd(arg('--path', process.cwd()), args[1]);
|
|
26815
26854
|
// 1.36.79 (도그푸딩 P1-C): positional 경로(`parent detect <path>`)를 무시하고 cwd 를 분석해 자신만만한 오답(exit 0)을 내던 것
|
|
@@ -26958,7 +26997,7 @@ async function main() {
|
|
|
26958
26997
|
const _text = textParts.join(' ');
|
|
26959
26998
|
// 1.30.4 (14th리뷰 F6): 빈 입력 시 --json 에서도 구조화 JSON(task/decision add 와 일관). 종전엔 lessonSave 내부 fail() 가 평문 출력.
|
|
26960
26999
|
if (!_text) { failJson(has('--json'), 'empty_text', 'lesson save "<text>" 필요 (빈/경로-only 거부)'); return process.exit(process.exitCode || 1); }
|
|
26961
|
-
return lessonSave(root, _text);
|
|
27000
|
+
return _guardStore(has('--json'), () => lessonSave(root, _text)); // 1.36.114: 손상 스토어 사유를 구조화(store_corrupt)
|
|
26962
27001
|
}
|
|
26963
27002
|
if (sub === 'list') {
|
|
26964
27003
|
return lessonListCmd(root, { json: has('--json') });
|
|
@@ -26983,7 +27022,7 @@ async function main() {
|
|
|
26983
27022
|
// 1.9.351 (UR-0064) → 1.9.416 (UR-0122): 공유 헬퍼 _parseAddTitle 로 단일화(flag/경로 break) + 빈 입력 거부
|
|
26984
27023
|
const title = _parseAddTitle(args, 2);
|
|
26985
27024
|
if (!title) { failJson(has('--json'), 'empty_title', 'decision add "<제목>" 필요'); return process.exit(process.exitCode || 1); }
|
|
26986
|
-
return decisionAdd(root, title);
|
|
27025
|
+
return _guardStore(has('--json'), () => decisionAdd(root, title)); // 1.36.114: 손상 스토어 사유를 구조화(store_corrupt)
|
|
26987
27026
|
}
|
|
26988
27027
|
if (sub === 'list') {
|
|
26989
27028
|
return decisionListCmd(absRoot(_resolveRoot(args[2])), { json: has('--json') }); // 1.9.412 (UR-0100): positional path 지원(add 의 args[2]=title 와 분리)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leerness",
|
|
3
|
-
"version": "1.36.
|
|
3
|
+
"version": "1.36.114",
|
|
4
4
|
"description": "The AI-coding operations layer that makes \"done\" require evidence — persistent memory, evidence-gated completion checks, and clean handoffs for any AI agent (Claude Code, Codex, Cursor). State lives as plain files in your repo. CLI + MCP, 0 runtime dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"leerness",
|
package/scripts/e2e.js
CHANGED
|
@@ -10966,6 +10966,185 @@ total++;
|
|
|
10966
10966
|
if (!ok) failed++;
|
|
10967
10967
|
}
|
|
10968
10968
|
|
|
10969
|
+
// ── 1.36.114 블록 R: **방치된 명령**에 남아 있던 두 기지(旣知) 클래스를 고정한다.
|
|
10970
|
+
// 왜 여기까지 왔나: e2e 실행 커버리지에서 17개 명령이 한 번도 안 돌고 있었다. 그 방치 구역에는
|
|
10971
|
+
// leerness 가 **다른 곳에서는 이미 고친** 결함이 그대로 남아 있었다.
|
|
10972
|
+
// 클래스 A(1.36.28): 손상된 JSON 스토어를 "빈 값" 으로 오인해 덮어써 기존 데이터를 잃는다.
|
|
10973
|
+
// 실측 유실: credentials.local.json · agent-permissions.json · wakeup-history.json · next-action-queue.json
|
|
10974
|
+
// (보호돼 있던 대조군: user-requests.json · previews.json · toggles.json — 술어 `_assertStoreParsable` 는 이미 있었다.)
|
|
10975
|
+
// 클래스 B(1.36.113): 사용자 텍스트의 개행이 목록에서 가짜 항목을 만든다. creds·constraints 가 지난 라운드 목록 밖이었다.
|
|
10976
|
+
// ⚠ 이 라운드가 만든 회귀도 여기서 잡았다: 손상 시 저장은 막혔는데 명령이 `✓ 추가` 를 찍고 exit 0 이었다.
|
|
10977
|
+
// 조용한 무동작 + 성공 표시는 조용한 유실보다 나쁘다 — 사용자는 저장됐다고 믿는다. 그래서 **세 가지를 함께** 단언한다:
|
|
10978
|
+
// 안 쓴다 · 성공이라 말하지 않는다 · 정상 스토어에서는 오차단하지 않는다.
|
|
10979
|
+
{
|
|
10980
|
+
total++;
|
|
10981
|
+
let ok = false; const dbg = {};
|
|
10982
|
+
const sb = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-neg114-'));
|
|
10983
|
+
const ENV = Object.assign({}, process.env, { TMPDIR: sb, TEMP: sb, TMP: sb, LEERNESS_NO_PROMPT: '1' });
|
|
10984
|
+
try {
|
|
10985
|
+
let seq = 0;
|
|
10986
|
+
const mk = () => {
|
|
10987
|
+
const d = path.join(sb, 'p' + (++seq)); fs.mkdirSync(d, { recursive: true });
|
|
10988
|
+
fs.writeFileSync(path.join(d, 'package.json'), '{"name":"p","version":"0.1.0"}');
|
|
10989
|
+
cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes'], { cwd: d, encoding: 'utf8', timeout: 300000, env: ENV });
|
|
10990
|
+
return d;
|
|
10991
|
+
};
|
|
10992
|
+
const R = (d, a) => cp.spawnSync(process.execPath, [CLI, ...a, '--path', d], { cwd: d, encoding: 'utf8', timeout: 300000, env: ENV });
|
|
10993
|
+
const bad = [];
|
|
10994
|
+
|
|
10995
|
+
// ── 클래스 A: 손상 스토어. 대조군(이미 보호되던 표면)을 **같은 표에** 넣어야 판정이 의미를 갖는다.
|
|
10996
|
+
const STORES = [
|
|
10997
|
+
['creds', 'credentials.local.json', ['creds', 'register', 'aaa', '--env-var', 'A_KEY'], ['creds', 'register', 'bbb', '--env-var', 'B_KEY']],
|
|
10998
|
+
['permissions', 'agent-permissions.json', ['permissions', 'set', 'extended'], ['permissions', 'set', 'basic']],
|
|
10999
|
+
['wakeup-interval', 'wakeup-history.json', ['wakeup-interval', 'set', '900'], ['wakeup-interval', 'set', '1200']],
|
|
11000
|
+
['next-action', 'next-action-queue.json', ['next-action', 'add', 'aaa'], ['next-action', 'add', 'bbb']],
|
|
11001
|
+
['requests(대조)', 'user-requests.json', ['requests', 'add', 'aaa'], ['requests', 'add', 'bbb']],
|
|
11002
|
+
['preview(대조)', 'previews.json', ['preview', 'add', 'aaa'], ['preview', 'add', 'bbb']],
|
|
11003
|
+
// 대조군 3종을 다 넣는다 — toggles 가 빠져 있어 "보호 대조군 유지" 주장이 2/3 만 증명됐다(검수 지적).
|
|
11004
|
+
['toggle(대조)', 'toggles.json', ['toggle', 'set', 'gate', 'off'], ['toggle', 'set', 'lens', 'off']],
|
|
11005
|
+
];
|
|
11006
|
+
let setupOk = 0;
|
|
11007
|
+
for (const [name, file, first, second] of STORES) {
|
|
11008
|
+
const d = mk();
|
|
11009
|
+
const r1 = R(d, first);
|
|
11010
|
+
const fp = path.join(d, '.harness', file);
|
|
11011
|
+
if (r1.status !== 0 || !fs.existsSync(fp)) { bad.push(`${name}:셋업실패`); continue; } // 조용한 셋업 실패가 '통과' 로 둔갑하지 못하게
|
|
11012
|
+
setupOk++;
|
|
11013
|
+
fs.writeFileSync(fp, '{ "broken": ');
|
|
11014
|
+
const before = fs.readFileSync(fp, 'utf8');
|
|
11015
|
+
const r2 = R(d, second);
|
|
11016
|
+
const out = String(r2.stdout || '') + String(r2.stderr || '');
|
|
11017
|
+
if (fs.readFileSync(fp, 'utf8') !== before) bad.push(`${name}:손상위덮어씀`);
|
|
11018
|
+
if (r2.status === 0 || /^\s*✓/m.test(out)) bad.push(`${name}:성공이라말함`); // 이 라운드가 낸 회귀를 고정
|
|
11019
|
+
if (/\n\s+at .*:\d+:\d+/.test(out)) bad.push(`${name}:스택노출`);
|
|
11020
|
+
// 기계 계약 — `--json` 은 **순수 JSON**(stdout 에 사람용 줄이 섞이면 JSON.parse 가 깨진다) + ok:false + exit≠0.
|
|
11021
|
+
// 이 단언이 없어서 `_guardStore` 를 떼는 변이가 살아남았다 — 그 래퍼는 async 명령에서 아예 무력이었고,
|
|
11022
|
+
// 실측에서 `next-action add --json` 이 `✗` 줄을 JSON 앞에 흘리고 있었다.
|
|
11023
|
+
// ⚠ 사유 코드는 표면마다 다르다(store_corrupt / store_invalid / error). 통일은 기존 두 표면의 계약을
|
|
11024
|
+
// 바꾸는 일이라 이 라운드에서 하지 않는다 — **이번에 고친 4종만** store_corrupt 를 요구하고, 나머지는 형태만 본다.
|
|
11025
|
+
fs.writeFileSync(fp, '{ "broken": ');
|
|
11026
|
+
const brokenJ = fs.readFileSync(fp, 'utf8');
|
|
11027
|
+
const rj = R(d, second.concat(['--json']));
|
|
11028
|
+
let parsed = null; try { parsed = JSON.parse(String(rj.stdout || '').trim()); } catch {}
|
|
11029
|
+
if (!parsed) bad.push(`${name}:json_stdout오염`);
|
|
11030
|
+
else {
|
|
11031
|
+
if (parsed.ok !== false) bad.push(`${name}:json_ok아님`);
|
|
11032
|
+
if (!/대조/.test(name) && parsed.code !== 'store_corrupt') bad.push(`${name}:json_코드(${parsed.code})`);
|
|
11033
|
+
}
|
|
11034
|
+
if (rj.status === 0) bad.push(`${name}:json_exit0`);
|
|
11035
|
+
// ⚠ JSON 경로에서도 **파일이 그대로인지** 봐야 한다. 검수 지적: 종전엔 파싱·ok·exit 만 봐서
|
|
11036
|
+
// "덮어쓰고 나서 {ok:false} 를 돌려주는" 변이가 그대로 통과했다. stderr 스택도 함께 본다.
|
|
11037
|
+
if (fs.readFileSync(fp, 'utf8') !== brokenJ) bad.push(`${name}:json경로_손상위덮어씀`);
|
|
11038
|
+
if (/\n\s+at .*:\d+:\d+/.test(String(rj.stderr || ''))) bad.push(`${name}:json_stderr스택`);
|
|
11039
|
+
// 오차단 방지 — 정상 스토어에서는 성공해야 하고, **실제로 저장돼야** 한다.
|
|
11040
|
+
// 종전엔 exit code 만 봐서 조용한 no-op(성공 코드 + 성공 메시지, 저장 없음)이 통과했다(검수 지적).
|
|
11041
|
+
const d2 = mk(); R(d2, first);
|
|
11042
|
+
const fp2 = path.join(d2, '.harness', file);
|
|
11043
|
+
const pre = fs.existsSync(fp2) ? fs.readFileSync(fp2, 'utf8') : null;
|
|
11044
|
+
if (R(d2, second).status !== 0) bad.push(`${name}:정상스토어_오차단`);
|
|
11045
|
+
const post = fs.existsSync(fp2) ? fs.readFileSync(fp2, 'utf8') : null;
|
|
11046
|
+
if (pre === null || post === null || post === pre) bad.push(`${name}:정상스토어_저장안됨`);
|
|
11047
|
+
}
|
|
11048
|
+
dbg.setupOk = setupOk;
|
|
11049
|
+
if (setupOk !== STORES.length) bad.push(`셋업 ${setupOk}/${STORES.length}`);
|
|
11050
|
+
|
|
11051
|
+
// ── 클래스 B: 개행 위조. 지난 라운드 9표면 목록 **밖**이었던 두 곳.
|
|
11052
|
+
for (const [name, add, list] of [
|
|
11053
|
+
['creds', (d, t) => R(d, ['creds', 'register', t, '--env-var', 'X_KEY']), (d) => R(d, ['creds', 'list'])],
|
|
11054
|
+
['constraints', (d, t) => R(d, ['constraints', 'add', t, '--constraint', 'k:v']), (d) => R(d, ['constraints', 'list'])],
|
|
11055
|
+
// ⚠ ID 에만 개행을 넣으면 **값 쪽 필드**를 놓친다 — 검수가 `--constraint` 의 kind/detail 이 raw 임을 지적했다.
|
|
11056
|
+
// 같은 명령이라도 인자마다 따로 확인해야 한다(1.36.113 에서 plan 의 --progress 를 놓친 것과 같은 형태).
|
|
11057
|
+
['constraints(detail)', (d, t) => R(d, ['constraints', 'add', 'PLAT', '--constraint', `auth:${t}`]), (d) => R(d, ['constraints', 'list'])],
|
|
11058
|
+
]) {
|
|
11059
|
+
const d = mk();
|
|
11060
|
+
if (add(d, 'real ZR4A\n fake ZR4F · 등록됨').status !== 0) { bad.push(`${name}:주입add실패`); continue; }
|
|
11061
|
+
const o = String(list(d).stdout || '');
|
|
11062
|
+
if (!o.includes('ZR4A')) bad.push(`${name}:항목미노출`); // 계측 판별력
|
|
11063
|
+
if (o.split('\n').some(l => l.includes('ZR4F') && !l.includes('ZR4A'))) bad.push(`${name}:개행위조`);
|
|
11064
|
+
}
|
|
11065
|
+
|
|
11066
|
+
// ── 전수 대조: `.harness` 아래 **사용자 데이터 JSON 스토어 전부**를 하나씩 손상시키고 변경 명령을 돌린다.
|
|
11067
|
+
// 표적 사냥으로는 4종만 봤는데, 이 열거식 스윕이 decisions.json · lessons.json 을 더 찾았다
|
|
11068
|
+
// (leerness 가 내세우는 '영구 메모리' 표면이 손상 위에 덮어써져 사라지고 있었다).
|
|
11069
|
+
// 목록을 박아 두면 **가드 없는 새 스토어가 추가될 때** 실패한다 — 개별 발견이 아니라 목록을 지키는 가드다.
|
|
11070
|
+
{
|
|
11071
|
+
const SEED = [['requests', 'add', 'seed'], ['preview', 'add', 'seed'], ['toggle', 'set', 'gate', 'off'],
|
|
11072
|
+
['creds', 'register', 'seed', '--env-var', 'K'], ['permissions', 'set', 'extended'], ['wakeup-interval', 'set', '900'],
|
|
11073
|
+
['next-action', 'add', 'seed'], ['constraints', 'add', 'seed', '--constraint', 'k:v'],
|
|
11074
|
+
['decision', 'add', 'seed', '--why', 'r'], ['lesson', 'save', 'seed']];
|
|
11075
|
+
const MUT = [['requests', 'add', 'x2'], ['preview', 'add', 'x2'], ['toggle', 'set', 'lens', 'off'],
|
|
11076
|
+
['creds', 'register', 'x2', '--env-var', 'K2'], ['permissions', 'set', 'basic'], ['wakeup-interval', 'set', '1200'],
|
|
11077
|
+
['next-action', 'add', 'x2'], ['constraints', 'add', 'x2', '--constraint', 'k:v'],
|
|
11078
|
+
['decision', 'add', 'x2', '--why', 'r'], ['lesson', 'save', 'x2'], ['task', 'add', 'x2'], ['plan', 'add', 'x2']];
|
|
11079
|
+
const DERIVED = new Set(['manifest.json', 'skills-lock.json', 'leerness-config.json', 'environment.json']);
|
|
11080
|
+
const seeded = () => { const d = mk(); for (const a of SEED) R(d, a); return d; };
|
|
11081
|
+
const base = seeded();
|
|
11082
|
+
const stores = [];
|
|
11083
|
+
(function w(x, rel) { for (const e of fs.readdirSync(x, { withFileTypes: true })) {
|
|
11084
|
+
const p = path.join(x, e.name), r = rel ? rel + '/' + e.name : e.name;
|
|
11085
|
+
if (e.isDirectory()) { if (!/cache|archive|skills$/.test(e.name)) w(p, r); }
|
|
11086
|
+
else if (/\.json$/.test(e.name)) stores.push(r); } })(path.join(base, '.harness'), '');
|
|
11087
|
+
const userStores = stores.filter(f => !DERIVED.has(path.basename(f)));
|
|
11088
|
+
dbg.stores = userStores.length;
|
|
11089
|
+
// 계측 판별력 — 스토어가 갑자기 줄면(시드 실패 등) "유실 0" 이 공허해진다. 실측 9종 이상.
|
|
11090
|
+
if (userStores.length < 9) bad.push(`스토어열거부족(${userStores.length})`);
|
|
11091
|
+
for (const rel of userStores) {
|
|
11092
|
+
const d = seeded();
|
|
11093
|
+
const fp = path.join(d, '.harness', rel.replace(/\//g, path.sep));
|
|
11094
|
+
if (!fs.existsSync(fp)) continue;
|
|
11095
|
+
fs.writeFileSync(fp, '{ "broken": ');
|
|
11096
|
+
const broken = fs.readFileSync(fp, 'utf8');
|
|
11097
|
+
for (const a of MUT) {
|
|
11098
|
+
R(d, a);
|
|
11099
|
+
if (fs.existsSync(fp) && fs.readFileSync(fp, 'utf8') !== broken) { bad.push(`전수:${rel}←${a.join(' ')}`); break; }
|
|
11100
|
+
}
|
|
11101
|
+
}
|
|
11102
|
+
}
|
|
11103
|
+
|
|
11104
|
+
// ── "손상 ≠ 없음": 읽기 계열 명령이 손상 스토어를 **빈 값**으로 오인해 엉뚱한 진단을 내지 않는가.
|
|
11105
|
+
// 검수가 지적한 형태다 — `creds refresh` 가 "등록된 서비스 없음", `next-action take` 가 "큐 비어있음" 이라 했다.
|
|
11106
|
+
// 사용자는 등록이 지워졌다고 믿고 다시 등록하거나 handoff 를 또 돌린다. 원인(손상)은 끝까지 안 보인다.
|
|
11107
|
+
// ⚠ 대조군이 반드시 필요하다 — **정상 스토어에서 진짜로 없는** 경우까지 store_corrupt 라 하면 오차단이다.
|
|
11108
|
+
{
|
|
11109
|
+
const codeOf = (r) => { try { return (JSON.parse(String(r.stdout || '').trim()) || {}).code; } catch { return null; } };
|
|
11110
|
+
const seedCreds = (d) => R(d, ['creds', 'register', 'svc', '--env-var', 'K']);
|
|
11111
|
+
const corrupt = (d, f) => fs.writeFileSync(path.join(d, '.harness', f), '{ "broken": ');
|
|
11112
|
+
// (a) 손상 → store_corrupt 로 진단
|
|
11113
|
+
for (const [label, seed, file, act] of [
|
|
11114
|
+
['creds check', seedCreds, 'credentials.local.json', ['creds', 'check']],
|
|
11115
|
+
['creds refresh', seedCreds, 'credentials.local.json', ['creds', 'refresh', 'svc']],
|
|
11116
|
+
['next-action take', (d) => R(d, ['next-action', 'add', 'aaa']), 'next-action-queue.json', ['next-action', 'take', '0']],
|
|
11117
|
+
]) {
|
|
11118
|
+
const d = mk(); if (seed(d).status !== 0) { bad.push(`손상진단:${label}:셋업실패`); continue; }
|
|
11119
|
+
corrupt(d, file);
|
|
11120
|
+
const r = R(d, act.concat(['--json']));
|
|
11121
|
+
if (r.status === 0) bad.push(`손상진단:${label}:exit0`);
|
|
11122
|
+
if (codeOf(r) !== 'store_corrupt') bad.push(`손상진단:${label}:코드(${codeOf(r)})`);
|
|
11123
|
+
}
|
|
11124
|
+
// (b) 손상 → 사람용 읽기(list)도 조용히 "없음" 이라 하지 않는다
|
|
11125
|
+
{ const d = mk(); seedCreds(d); corrupt(d, 'credentials.local.json');
|
|
11126
|
+
const r = R(d, ['creds', 'list']);
|
|
11127
|
+
if (r.status === 0) bad.push('손상진단:creds list:조용히통과'); }
|
|
11128
|
+
// (c) 대조군 — 정상 스토어에서 **진짜 없는** 경우는 store_corrupt 가 아니어야 한다(오차단 방지)
|
|
11129
|
+
{ const d = mk(); seedCreds(d);
|
|
11130
|
+
const r = R(d, ['creds', 'refresh', 'nosuch', '--json']);
|
|
11131
|
+
if (codeOf(r) === 'store_corrupt') bad.push('손상진단:대조군_미등록을_손상이라함'); }
|
|
11132
|
+
{ const d = mk(); // 빈 registry(정상)는 그냥 "없음" 이어야 한다
|
|
11133
|
+
const r = R(d, ['creds', 'list']);
|
|
11134
|
+
if (r.status !== 0) bad.push('손상진단:대조군_빈registry_오차단'); }
|
|
11135
|
+
{ const d = mk(); R(d, ['next-action', 'add', 'aaa']); R(d, ['next-action', 'take', '0']);
|
|
11136
|
+
const r = R(d, ['next-action', 'take', '0', '--json']); // 정상인데 비어 있음 → 손상 아님
|
|
11137
|
+
if (codeOf(r) === 'store_corrupt') bad.push('손상진단:대조군_빈큐를_손상이라함'); }
|
|
11138
|
+
}
|
|
11139
|
+
|
|
11140
|
+
dbg.bad = bad.slice(0, 12);
|
|
11141
|
+
ok = bad.length === 0;
|
|
11142
|
+
} catch (e) { dbg.err = String(e && e.message).slice(0, 200); } finally { try { fs.rmSync(sb, { recursive: true, force: true }); } catch {} }
|
|
11143
|
+
console.log(ok ? `✓ R(1.36.114) 방치 명령의 기지 클래스: 손상 스토어 6종(안 씀·성공이라 안 함·스택 없음·정상 오차단 없음·--json 계약) · 사용자 스토어 ${dbg.stores}종 전수 무유실 · 개행 위조 0(creds·constraints)`
|
|
11144
|
+
: '✗ 1.36.114 방치 명령 클래스 실패 ' + JSON.stringify(dbg));
|
|
11145
|
+
if (!ok) failed++;
|
|
11146
|
+
}
|
|
11147
|
+
|
|
10969
11148
|
console.log(`\nE2E result: ${total - failed}/${total} passed · ${((Date.now() - _e2eStart) / 1000).toFixed(0)}s`);
|
|
10970
11149
|
if (failed > 0) process.exit(1);
|
|
10971
11150
|
|