leerness 1.36.178 → 1.36.180
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 +8 -0
- package/README.md +4 -4
- package/bin/leerness.js +54 -44
- package/lib/agents.js +44 -12
- package/lib/toggles.js +36 -13
- package/package.json +6 -4
- package/scripts/e2e.js +2 -1
- package/scripts/i18n-priority-surface-probe.js +145 -0
- package/scripts/parent-detect-selftest-probe.js +104 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.36.180 — 2026-08-30
|
|
4
|
+
|
|
5
|
+
- English-mode agents list, insights, and toggle output now contain zero Hangul; a dedicated red/green probe and 39-command ratchet reduce measured leakage from 104 to 58 lines while preserving Korean output.
|
|
6
|
+
|
|
7
|
+
## 1.36.179 — 2026-08-30
|
|
8
|
+
|
|
9
|
+
- T-0091: parent-detect selftest를 OS 임시 디렉터리 상위의 외부 .leerness와 격리하고, 두 번째 temp 생성 실패 시 첫 fixture를 정리하도록 셋업 수명을 보강. 동일한 rule-add selftest 형제 경로와 결정론적 회귀 probe를 fast/core/full 게이트에 연결.
|
|
10
|
+
|
|
3
11
|
## 1.36.178 — 2026-08-29
|
|
4
12
|
|
|
5
13
|
- CI: env encoding-check --apply 전체 E2E를 Windows BOM 교체와 POSIX byte-exact no-op 계약으로 분기해 v1.36.177 Linux/Node 22의 466/467 실패를 수정.
|
package/README.md
CHANGED
|
@@ -138,7 +138,7 @@ MIT
|
|
|
138
138
|
<!-- leerness:project-readme:start -->
|
|
139
139
|
## Leerness Project Harness
|
|
140
140
|
|
|
141
|
-
이 프로젝트는 Leerness v1.36.
|
|
141
|
+
이 프로젝트는 Leerness v1.36.180 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
|
|
142
142
|
|
|
143
143
|
### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
|
|
144
144
|
|
|
@@ -192,7 +192,7 @@ leerness memory restore decision <date|title>
|
|
|
192
192
|
|
|
193
193
|
### MCP server (외부 AI 통합)
|
|
194
194
|
|
|
195
|
-
Leerness v1.36.
|
|
195
|
+
Leerness v1.36.180는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **89개 도구**를 노출:
|
|
196
196
|
|
|
197
197
|
```jsonc
|
|
198
198
|
// 카테고리별
|
|
@@ -213,7 +213,7 @@ Leerness v1.36.178는 stdio JSON-RPC MCP server를 내장합니다 — Claude Co
|
|
|
213
213
|
`<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
|
|
214
214
|
1) 다음 라운드 후보 선정 → 2) 코드 변경 → 3) 회귀 테스트 갱신 → 4) 전체 e2e 스위트 통과 → 5) npm publish + git tag → 6) main push → 7) session close → 8) 다음 라운드 예약.
|
|
215
215
|
|
|
216
|
-
현재 누적: **v1.9.x → 1.36.
|
|
216
|
+
현재 누적: **v1.9.x → 1.36.180 릴리스 태그 이력** (수백 라운드) · _reports/는 비공개 보존.
|
|
217
217
|
|
|
218
218
|
### 성능 가이드
|
|
219
219
|
|
|
@@ -251,5 +251,5 @@ leerness release pack --close --auto-main-push
|
|
|
251
251
|
- `.leerness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
|
|
252
252
|
- `.leerness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
|
|
253
253
|
|
|
254
|
-
Last synced by Leerness v1.36.
|
|
254
|
+
Last synced by Leerness v1.36.180: 2026-08-30
|
|
255
255
|
<!-- leerness:project-readme:end -->
|
package/bin/leerness.js
CHANGED
|
@@ -49,7 +49,7 @@ const {
|
|
|
49
49
|
migrateLegacyWorkspace,
|
|
50
50
|
} = require('../lib/workspace-dir');
|
|
51
51
|
|
|
52
|
-
const VERSION = '1.36.
|
|
52
|
+
const VERSION = '1.36.180';
|
|
53
53
|
|
|
54
54
|
// MCP lifecycle 주소 표식은 현재 CLI 호출 한 번에만 유효하다. CLI bootstrap에서 즉시 env에서
|
|
55
55
|
// 떼어 두어 `--no-record`/hook처럼 presence 기록 함수에 도달하지 않는 경로도 후속 child에 유출하지 않는다.
|
|
@@ -5283,10 +5283,11 @@ function _selfTestCases() {
|
|
|
5283
5283
|
// (b) positional path 가 root 로 쓰이고 cwd 는 오염되지 않는지 확인한다.
|
|
5284
5284
|
const m = require('../lib/pure-utils');
|
|
5285
5285
|
const u = m._parseAddTitle(['rule', 'add', '세션', '점검', '--trigger', 'every-session', '/p'], 2) === '세션 점검';
|
|
5286
|
-
|
|
5287
|
-
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_rulecwd_'));
|
|
5286
|
+
let proj = null, cwd = null;
|
|
5288
5287
|
let wired = false;
|
|
5289
5288
|
try {
|
|
5289
|
+
proj = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_ruleproj_'));
|
|
5290
|
+
cwd = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_rulecwd_'));
|
|
5290
5291
|
for (const d of [proj, cwd]) { fs.mkdirSync(path.join(d, '.leerness'), { recursive: true }); fs.writeFileSync(path.join(d, '.leerness', 'HARNESS_VERSION'), VERSION); }
|
|
5291
5292
|
const r = cp.spawnSync(process.execPath, [__filename, 'rule', 'add', '세션', '점검', '--trigger', 'every-session', proj, '--json'], { encoding: 'utf8', cwd, timeout: 30000, env: { ...process.env, LEERNESS_INTERNAL: '1', LEERNESS_NO_BANNER: '1' } });
|
|
5292
5293
|
const line = (r.stdout || '').split(/\r?\n/).map(x => x.trim()).filter(x => x.startsWith('{')).pop();
|
|
@@ -5294,7 +5295,7 @@ function _selfTestCases() {
|
|
|
5294
5295
|
const titleOk = j.ok === true && j.rule === '세션 점검' && j.trigger === 'every-session';
|
|
5295
5296
|
const rootOk = fs.existsSync(path.join(proj, '.leerness', 'rules.md')) && !fs.existsSync(path.join(cwd, '.leerness', 'rules.md'));
|
|
5296
5297
|
wired = titleOk && rootOk;
|
|
5297
|
-
} catch (e) { wired = false; } finally { for (const d of [proj, cwd]) { try { fs.rmSync(d, { recursive: true, force: true }); } catch {} } }
|
|
5298
|
+
} catch (e) { wired = false; } finally { for (const d of [proj, cwd]) { if (d) { try { fs.rmSync(d, { recursive: true, force: true }); } catch {} } } }
|
|
5298
5299
|
return wired && u;
|
|
5299
5300
|
} },
|
|
5300
5301
|
{ name: '클린룸 (UR-0184): feature add/show/link/impact positional-path 와이어 + 미초기화 게이트 + _taskPositionalPath 값-플래그 skip (1.36.2)', run: () => {
|
|
@@ -7090,18 +7091,24 @@ function _selfTestCases() {
|
|
|
7090
7091
|
} },
|
|
7091
7092
|
{ name: 'parent detect (1.30.2 #157): 상위 leerness 부모 탐지(행위) + 독립 null + assetCount', run: () => {
|
|
7092
7093
|
const osx = require('os'); const fsx = require('fs');
|
|
7093
|
-
|
|
7094
|
-
const alone = fsx.mkdtempSync(path.join(osx.tmpdir(), 'leer-alone-st-'));
|
|
7094
|
+
let base = null, alone = null;
|
|
7095
7095
|
try {
|
|
7096
|
+
base = fsx.mkdtempSync(path.join(osx.tmpdir(), 'leer-parent-st-'));
|
|
7097
|
+
alone = fsx.mkdtempSync(path.join(osx.tmpdir(), 'leer-alone-st-'));
|
|
7096
7098
|
fsx.mkdirSync(path.join(base, '.leerness'), { recursive: true });
|
|
7097
7099
|
fsx.writeFileSync(path.join(base, '.leerness', 'design-system.md'), '# ds');
|
|
7098
7100
|
const sub = path.join(base, 'sub'); fsx.mkdirSync(sub, { recursive: true });
|
|
7099
|
-
|
|
7100
|
-
|
|
7101
|
+
// The fixture owns only the immediate parent. Do not let an unrelated
|
|
7102
|
+
// workspace above os.tmpdir() change this selftest's standalone control.
|
|
7103
|
+
const found = _findParentWorkspace(sub, { maxDepth: 1 });
|
|
7104
|
+
const standalone = _findParentWorkspace(alone, { maxDepth: 1 });
|
|
7101
7105
|
// 부모 탐지: 워크스페이스 .leerness + assetCount≥1(design-system) · 독립: null · read-only(소스에 파일쓰기 없음 — adopt 미구현)
|
|
7102
7106
|
const readOnly = /이 명령은 아무 파일도 쓰지 않는다/.test(read(__filename));
|
|
7103
7107
|
return !!found && found.workspaceDir === '.leerness' && found.assetCount >= 1 && standalone === null && readOnly;
|
|
7104
|
-
} finally {
|
|
7108
|
+
} finally {
|
|
7109
|
+
if (base) { try { fsx.rmSync(base, { recursive: true, force: true }); } catch {} }
|
|
7110
|
+
if (alone) { try { fsx.rmSync(alone, { recursive: true, force: true }); } catch {} }
|
|
7111
|
+
}
|
|
7105
7112
|
} },
|
|
7106
7113
|
{ name: 'VERSION 형식 (x.y.z)', run: () => /^\d+\.\d+\.\d+$/.test(VERSION) },
|
|
7107
7114
|
{ name: 'VERSION ↔ package.json 일치 (1.30.2: 한쪽만 bump 하던 실수 2초 내 차단)', run: () => {
|
|
@@ -21601,7 +21608,7 @@ function _dispatchCommand(agentId, task, writeMode, model) {
|
|
|
21601
21608
|
|
|
21602
21609
|
const _agents = require('../lib/agents');
|
|
21603
21610
|
// 1.9.424 (UR-0025/UR-0125 큰 핸들러 모듈화 9번째): agentsCmd → lib/agents.js (DI 위임, rest→array)
|
|
21604
|
-
function agentsCmd(root, sub, ...args) { return _agents.agentsCmd(root, sub, args, { VERSION, has, arg, _agentSlashHint, _allProviders, _checkAgent, _cliChat, _dispatchCommand, _harnessBrief: _tgl.toggleOn(root, 'delegation-brief') ? _harnessBrief : undefined, _loadEnvFile, _normalizeRole, _policyEnforce, _readUserProviders, _recommendAgent, _recordRun, _resolveRole, lessonsPath, taskLogPath }); } // 1.36.30: delegation-brief 토글 OFF 면 브리프 미접두(=--raw 경로)
|
|
21611
|
+
function agentsCmd(root, sub, ...args) { return _agents.agentsCmd(root, sub, args, { VERSION, has, arg, uiLang: _uiLang(root), _agentSlashHint, _allProviders, _checkAgent, _cliChat, _dispatchCommand, _harnessBrief: _tgl.toggleOn(root, 'delegation-brief') ? _harnessBrief : undefined, _loadEnvFile, _normalizeRole, _policyEnforce, _readUserProviders, _recommendAgent, _recordRun, _resolveRole, lessonsPath, taskLogPath }); } // 1.36.30: delegation-brief 토글 OFF 면 브리프 미접두(=--raw 경로)
|
|
21605
21612
|
|
|
21606
21613
|
function personaCmd(root, sub, idOrName, ...rest) {
|
|
21607
21614
|
root = absRoot(root || process.cwd());
|
|
@@ -22062,7 +22069,8 @@ function _retroOneLine(agg, lang) {
|
|
|
22062
22069
|
// 1.9.169 fix: --include 명시되면 cwd 자동 추가 안 함 (explicit-only).
|
|
22063
22070
|
// 기존: cwd/.leerness 자동 추가 → 잔존 .leerness 시 의도치 않은 카운트 증가 (e2e flake 원인)
|
|
22064
22071
|
// 변경: --include 시 사용자가 명시한 경로만 사용. --all-apps 단독은 기존 동작 유지.
|
|
22065
|
-
function _collectWorkspacePaths(rootBase) {
|
|
22072
|
+
function _collectWorkspacePaths(rootBase, uiLang = 'ko') {
|
|
22073
|
+
const t = (ko, en) => (uiLang === 'en' ? en : ko);
|
|
22066
22074
|
const set = new Set();
|
|
22067
22075
|
const include = arg('--include', null);
|
|
22068
22076
|
// --include 명시 시 cwd 자동 추가 스킵 (explicit-only 보장)
|
|
@@ -22087,7 +22095,7 @@ function _collectWorkspacePaths(rootBase) {
|
|
|
22087
22095
|
for (const p of String(include).split(',')) {
|
|
22088
22096
|
const abs = path.resolve(p.trim());
|
|
22089
22097
|
if (exists(path.join(abs, '.leerness'))) set.add(abs);
|
|
22090
|
-
else warn(`--include 무시: ${abs} (.leerness 없음)`);
|
|
22098
|
+
else warn(t(`--include 무시: ${abs} (.leerness 없음)`, `Ignoring --include: ${abs} (no .leerness directory)`));
|
|
22091
22099
|
}
|
|
22092
22100
|
}
|
|
22093
22101
|
return Array.from(set);
|
|
@@ -22214,9 +22222,10 @@ function _retroWorkspace(rootBase, cutoff) {
|
|
|
22214
22222
|
|
|
22215
22223
|
function insightsCmd(root) {
|
|
22216
22224
|
root = absRoot(root);
|
|
22225
|
+
const _L = _uiLang(root); const t = (ko, en) => (_L === 'en' ? en : ko);
|
|
22217
22226
|
// 1.9.15: --all-apps / --include 통합 모드
|
|
22218
22227
|
if (has('--all-apps') || arg('--include', null)) {
|
|
22219
|
-
return _insightsWorkspace(root);
|
|
22228
|
+
return _insightsWorkspace(root, _L);
|
|
22220
22229
|
}
|
|
22221
22230
|
const agg = _retroAggregate(root); // insights 는 누적 지표 명령 — 기간 필터 없음(retro 만 --days). 1.36.38 광역치환 오적용 교정.
|
|
22222
22231
|
// 1.9.16: --json
|
|
@@ -22226,49 +22235,50 @@ function insightsCmd(root) {
|
|
|
22226
22235
|
return;
|
|
22227
22236
|
}
|
|
22228
22237
|
const sc = readSessionCounter(root);
|
|
22229
|
-
log(`# Insights — 누적
|
|
22230
|
-
log(`\n## 📊 핵심
|
|
22231
|
-
log(` - 누적 task: ${agg.totalTasks} (done ${agg.doneCount}, in-progress ${agg.statusCounts['in-progress']}, planned ${agg.statusCounts.planned})`);
|
|
22232
|
-
log(` - 누적 결정 (decisions.md): ${agg.decisionBlocks}
|
|
22233
|
-
log(` - 누적 스킬: ${agg.skillUsage.length}
|
|
22234
|
-
log(` - 총 스킬 사용: ${agg.totalSkillUsage}
|
|
22235
|
-
log(` - 총 최적화 누적: ${agg.totalOptimizations}
|
|
22236
|
-
log(` - 활성 룰: ${agg.activeRules}건 (검증 ${agg.verifiedRules}건)`);
|
|
22237
|
-
log(` - session close 횟수: ${sc.count}회${sc.lastCloseAt ? ' (마지막: ' + sc.lastCloseAt.slice(0, 16) + ')' : ''}`);
|
|
22238
|
+
log(t(`# Insights — 누적 통계`, `# Insights — cumulative statistics`));
|
|
22239
|
+
log(t(`\n## 📊 핵심 지표`, `\n## 📊 Key metrics`));
|
|
22240
|
+
log(t(` - 누적 task: ${agg.totalTasks} (done ${agg.doneCount}, in-progress ${agg.statusCounts['in-progress']}, planned ${agg.statusCounts.planned})`, ` - Tasks: ${agg.totalTasks} (done ${agg.doneCount}, in-progress ${agg.statusCounts['in-progress']}, planned ${agg.statusCounts.planned})`));
|
|
22241
|
+
log(t(` - 누적 결정 (decisions.md): ${agg.decisionBlocks}건`, ` - Decisions (decisions.md): ${agg.decisionBlocks}`));
|
|
22242
|
+
log(t(` - 누적 스킬: ${agg.skillUsage.length}종`, ` - Skills: ${agg.skillUsage.length}`));
|
|
22243
|
+
log(t(` - 총 스킬 사용: ${agg.totalSkillUsage}회`, ` - Total skill uses: ${agg.totalSkillUsage}`));
|
|
22244
|
+
log(t(` - 총 최적화 누적: ${agg.totalOptimizations}건`, ` - Total optimizations: ${agg.totalOptimizations}`));
|
|
22245
|
+
log(t(` - 활성 룰: ${agg.activeRules}건 (검증 ${agg.verifiedRules}건)`, ` - Active rules: ${agg.activeRules} (${agg.verifiedRules} verified)`));
|
|
22246
|
+
log(t(` - session close 횟수: ${sc.count}회${sc.lastCloseAt ? ' (마지막: ' + sc.lastCloseAt.slice(0, 16) + ')' : ''}`, ` - Session closes: ${sc.count}${sc.lastCloseAt ? ' (last: ' + sc.lastCloseAt.slice(0, 16) + ')' : ''}`));
|
|
22238
22247
|
|
|
22239
22248
|
if (agg.skillUsage.length) {
|
|
22240
|
-
log(`\n## 🏆 가장 활용도 높은 스킬 (top 5)`);
|
|
22241
|
-
agg.skillUsage.slice(0, 5).forEach((s, i) => log(` ${i + 1}. ${s.id} (${s.displayNameKo}) — 사용 ${s.count}회, 최적화 ${s.optimizations}
|
|
22249
|
+
log(t(`\n## 🏆 가장 활용도 높은 스킬 (top 5)`, `\n## 🏆 Most-used skills (top 5)`));
|
|
22250
|
+
agg.skillUsage.slice(0, 5).forEach((s, i) => log(t(` ${i + 1}. ${s.id} (${s.displayNameKo}) — 사용 ${s.count}회, 최적화 ${s.optimizations}건`, ` ${i + 1}. ${s.id} — ${s.count} uses, ${s.optimizations} optimizations`)));
|
|
22242
22251
|
}
|
|
22243
22252
|
|
|
22244
22253
|
if (agg.durations.length) {
|
|
22245
22254
|
const total = agg.durations.reduce((a, b) => a + b, 0);
|
|
22246
|
-
log(`\n## ⏱ 검증 시간 (verify-code)`);
|
|
22247
|
-
log(` - 실행: ${agg.durations.length}회 / 총 ${total}ms / 평균 ${Math.round(total / agg.durations.length)}ms`);
|
|
22248
|
-
log(` - 최소 ${Math.min(...agg.durations)}ms / 최대 ${Math.max(...agg.durations)}ms`);
|
|
22255
|
+
log(t(`\n## ⏱ 검증 시간 (verify-code)`, `\n## ⏱ Verification time (verify-code)`));
|
|
22256
|
+
log(t(` - 실행: ${agg.durations.length}회 / 총 ${total}ms / 평균 ${Math.round(total / agg.durations.length)}ms`, ` - Runs: ${agg.durations.length} / total ${total}ms / average ${Math.round(total / agg.durations.length)}ms`));
|
|
22257
|
+
log(t(` - 최소 ${Math.min(...agg.durations)}ms / 최대 ${Math.max(...agg.durations)}ms`, ` - Minimum ${Math.min(...agg.durations)}ms / maximum ${Math.max(...agg.durations)}ms`));
|
|
22249
22258
|
}
|
|
22250
22259
|
|
|
22251
|
-
log(`\n## 🔁 안정성
|
|
22252
|
-
log(` - pass 시그널: ${agg.passSignals} · fix 시그널: ${agg.fixSignals}`);
|
|
22260
|
+
log(t(`\n## 🔁 안정성 지표`, `\n## 🔁 Stability signals`));
|
|
22261
|
+
log(t(` - pass 시그널: ${agg.passSignals} · fix 시그널: ${agg.fixSignals}`, ` - Pass signals: ${agg.passSignals} · fix signals: ${agg.fixSignals}`));
|
|
22253
22262
|
const ratio = agg.fixSignals > 0 ? (agg.passSignals / agg.fixSignals).toFixed(2) : '∞';
|
|
22254
|
-
log(` - pass/fix 비율: ${ratio}${ratio === '∞' || parseFloat(ratio) > 3 ? ' (안정)' : parseFloat(ratio) < 1 ? ' (디버그 위주)' : ' (보통)'}`);
|
|
22263
|
+
log(t(` - pass/fix 비율: ${ratio}${ratio === '∞' || parseFloat(ratio) > 3 ? ' (안정)' : parseFloat(ratio) < 1 ? ' (디버그 위주)' : ' (보통)'}`, ` - Pass/fix ratio: ${ratio}${ratio === '∞' || parseFloat(ratio) > 3 ? ' (stable)' : parseFloat(ratio) < 1 ? ' (debug-heavy)' : ' (normal)'}`));
|
|
22255
22264
|
|
|
22256
|
-
log(`\n## 📈
|
|
22257
|
-
if (agg.totalOptimizations === 0) log(` - 스킬에 최적화 누적 없음 — \`leerness skill optimize <id> --before --after\`로 더 나은 방법
|
|
22258
|
-
if (sc.count >= 5 && sc.count % 5 === 0) log(` - 5세션마다 자동 깊은 회고가 예정되어 있습니다 — session close가 자동
|
|
22259
|
-
if (agg.statusCounts.blocked > 0) log(` - blocked 작업 ${agg.statusCounts.blocked}건 — \`leerness lessons --query "blocked"\`로 과거 패턴
|
|
22265
|
+
log(t(`\n## 📈 권장`, `\n## 📈 Recommendations`));
|
|
22266
|
+
if (agg.totalOptimizations === 0) log(t(` - 스킬에 최적화 누적 없음 — \`leerness skill optimize <id> --before --after\`로 더 나은 방법 기록`, ` - No skill optimizations recorded — use \`leerness skill optimize <id> --before --after\` to record a better method`));
|
|
22267
|
+
if (sc.count >= 5 && sc.count % 5 === 0) log(t(` - 5세션마다 자동 깊은 회고가 예정되어 있습니다 — session close가 자동 호출`, ` - An automatic deep retrospective is scheduled every five sessions and runs during session close`));
|
|
22268
|
+
if (agg.statusCounts.blocked > 0) log(t(` - blocked 작업 ${agg.statusCounts.blocked}건 — \`leerness lessons --query "blocked"\`로 과거 패턴 회수`, ` - ${agg.statusCounts.blocked} blocked task(s) — use \`leerness lessons --query "blocked"\` to retrieve past patterns`));
|
|
22260
22269
|
}
|
|
22261
22270
|
|
|
22262
|
-
function _insightsWorkspace(rootBase) {
|
|
22263
|
-
const
|
|
22264
|
-
|
|
22271
|
+
function _insightsWorkspace(rootBase, uiLang = _uiLang(rootBase)) {
|
|
22272
|
+
const t = (ko, en) => (uiLang === 'en' ? en : ko);
|
|
22273
|
+
const paths = _collectWorkspacePaths(rootBase, uiLang);
|
|
22274
|
+
if (!paths.length) return fail(t('대상 프로젝트 없음. --include 또는 --all-apps 사용.', 'No target projects. Use --include or --all-apps.'));
|
|
22265
22275
|
// 1.9.16: --json
|
|
22266
22276
|
if (has('--json')) {
|
|
22267
22277
|
const projects = paths.map(p => ({ project: path.basename(p), path: p, data: _retroJsonData(_retroAggregate(p)) }));
|
|
22268
22278
|
log(JSON.stringify({ projects, projectCount: paths.length }, null, 2));
|
|
22269
22279
|
return;
|
|
22270
22280
|
}
|
|
22271
|
-
log(`# Workspace Insights — ${paths.length}개
|
|
22281
|
+
log(t(`# Workspace Insights — ${paths.length}개 프로젝트`, `# Workspace Insights — ${paths.length} project(s)`));
|
|
22272
22282
|
log(`\n| Project | Task | Done % | Decisions | Skills | Usage | Opts | Pass/Fix |`);
|
|
22273
22283
|
log(`|---|---|---|---|---|---|---|---|`);
|
|
22274
22284
|
const totals = { tasks: 0, done: 0, decisions: 0, skills: 0, usage: 0, opts: 0, pass: 0, fix: 0 };
|
|
@@ -22284,11 +22294,11 @@ function _insightsWorkspace(rootBase) {
|
|
|
22284
22294
|
const tpf = totals.fix ? (totals.pass / totals.fix).toFixed(1) : '∞';
|
|
22285
22295
|
const tDonePct = totals.tasks ? Math.round(totals.done / totals.tasks * 100) : 0;
|
|
22286
22296
|
log(`| **TOTAL** | **${totals.tasks}** | **${tDonePct}%** | **${totals.decisions}** | **${totals.skills}** | **${totals.usage}** | **${totals.opts}** | **${totals.pass}/${totals.fix} (${tpf})** |`);
|
|
22287
|
-
log(`\n## 📈
|
|
22288
|
-
if (totals.pass > totals.fix * 3) log(` - 안정성: 우수 (pass÷fix = ${tpf})`);
|
|
22289
|
-
else if (totals.pass > totals.fix) log(` - 안정성: 보통 (pass÷fix = ${tpf})`);
|
|
22290
|
-
else if (totals.fix > 0) log(` - 안정성: 주의 (fix가 pass보다 많음) — verify-code 자동화
|
|
22291
|
-
if (totals.opts === 0) log(` - 최적화 누적 없음 — \`leerness skill optimize\` 활용
|
|
22297
|
+
log(t(`\n## 📈 평가`, `\n## 📈 Assessment`));
|
|
22298
|
+
if (totals.pass > totals.fix * 3) log(t(` - 안정성: 우수 (pass÷fix = ${tpf})`, ` - Stability: strong (pass÷fix = ${tpf})`));
|
|
22299
|
+
else if (totals.pass > totals.fix) log(t(` - 안정성: 보통 (pass÷fix = ${tpf})`, ` - Stability: moderate (pass÷fix = ${tpf})`));
|
|
22300
|
+
else if (totals.fix > 0) log(t(` - 안정성: 주의 (fix가 pass보다 많음) — verify-code 자동화 검토`, ` - Stability: caution (more fix than pass signals) — consider automating verify-code`));
|
|
22301
|
+
if (totals.opts === 0) log(t(` - 최적화 누적 없음 — \`leerness skill optimize\` 활용 권장`, ` - No optimizations recorded — consider using \`leerness skill optimize\``));
|
|
22292
22302
|
}
|
|
22293
22303
|
|
|
22294
22304
|
// 1.9.16: brainstorm 핵심 로직 분리 — 단일 프로젝트 결과 반환
|
|
@@ -31596,7 +31606,7 @@ async function main() {
|
|
|
31596
31606
|
if (cmd === 'anchors') return anchorsCmd(arg('--path', null) || _taskPositionalPath(args, 1) || process.cwd(), args[1] && !args[1].startsWith('-') ? args[1] : null); // 1.36.36: 정체성앵커 초안
|
|
31597
31607
|
// 1.36.108 (T-0097): _withLock 을 넘긴다 — lib 모듈이 락을 deps 로 받는 기존 관례(clarify·referee·routing·bugfix)에
|
|
31598
31608
|
// toggles 만 빠져 있어 `toggle set` 이 락 밖 read-modify-write 였다(런타임 계측으로 실측).
|
|
31599
|
-
if (cmd === 'toggle') return _tgl.toggleCmd(arg('--path', process.cwd()), args[1], args[2], args[3], { has, VERSION, _withLock }); // 1.36.30: 기능 토글 (그래프 ⚙ 탭 연동)
|
|
31609
|
+
if (cmd === 'toggle') return _tgl.toggleCmd(arg('--path', process.cwd()), args[1], args[2], args[3], { has, VERSION, uiLang: _uiLang(arg('--path', process.cwd())), _withLock }); // 1.36.30: 기능 토글 (그래프 ⚙ 탭 연동)
|
|
31600
31610
|
// 1.36.53 (UR-0062): 기술 프로필 · 1.36.67 (F15): 변경 시 기존 leerness.html 동반 갱신(있을 때만)
|
|
31601
31611
|
if (cmd === 'tech') return _tech.techCmd(arg('--path', null) || _taskPositionalPath(args, 1) || process.cwd(), _optSub(args), { has, regenGraph: (r) => { if (exists(path.join(r, 'leerness.html'))) _graph.graphHtmlCmd(r, { _roadmapData, _loadDecisions, _loadLessons, _parseFeatureGraph, _loadToggles: _tgl.loadToggles, _toggleRegistry: _tgl.TOGGLE_REGISTRY, _loadTechProfile: _tech.loadTechProfile, quiet: true }); } });
|
|
31602
31612
|
// 1.36.98 (P-0013): 재사용 인벤토리 — 새 화면 조각을 만들기 전에 '이미 있는 것' 을 먼저 본다(reuse-map 의 UI 판).
|
package/lib/agents.js
CHANGED
|
@@ -44,6 +44,8 @@ function agentsCmd(root, sub, args = [], deps = {}) {
|
|
|
44
44
|
const { VERSION, has, arg, _agentSlashHint, _allProviders, _checkAgent, _cliChat, _dispatchCommand, _harnessBrief, _loadEnvFile, _normalizeRole, _policyEnforce, _readUserProviders, _recommendAgent, _recordRun, _resolveRole, lessonsPath, taskLogPath } = deps;
|
|
45
45
|
const _spawnPortable = deps._spawnPortable || spawnPortable;
|
|
46
46
|
root = absRoot(root || process.cwd());
|
|
47
|
+
const en = deps.uiLang === 'en';
|
|
48
|
+
const t = (ko, enText) => (en ? enText : ko);
|
|
47
49
|
// 1.9.435 (11th 외부평가 Codex P2, UR-0137): dispatch/multi task 파싱 — flag 값이 task 본문에 흡수되던 버그 수정.
|
|
48
50
|
// 상위(bin)에서 args 는 '--to' flag 만 제거되고 값(codex)은 positional 로 남아 기존 filter 가 task 에 흡수시켰음.
|
|
49
51
|
// → flag 에서 break + lib 가 소비하는 값-flag(--to/--model/--role/--only) 값을 제외. 명시 task 는 --task 폴백.
|
|
@@ -63,36 +65,66 @@ function agentsCmd(root, sub, args = [], deps = {}) {
|
|
|
63
65
|
const userIds = new Set(_readUserProviders(root).map(u => u.id));
|
|
64
66
|
const checks = providers.map(a => ({ ...(_checkAgent(a)), source: userIds.has(a.id) ? 'user' : 'builtin' }));
|
|
65
67
|
if (has('--json')) { log(JSON.stringify({ agents: checks }, null, 2)); return; }
|
|
66
|
-
log(`# 외부 AI CLI 오케스트레이션 (1.9.30)`);
|
|
68
|
+
log(t(`# 외부 AI CLI 오케스트레이션 (1.9.30)`, `# External AI CLI orchestration (1.9.30)`));
|
|
67
69
|
log('');
|
|
68
|
-
log(`| Agent | source | env (${'env=1 활성'}) | 설치 | 버전 | 상태 |`);
|
|
70
|
+
log(t(`| Agent | source | env (${'env=1 활성'}) | 설치 | 버전 | 상태 |`, `| Agent | source | env (env=1 enabled) | installed | version | status |`));
|
|
69
71
|
log(`|---|---|---|---|---|---|`);
|
|
70
72
|
for (const c of checks) {
|
|
71
73
|
const envMark = c.enabled ? '✓' : '✗';
|
|
72
74
|
const instMark = c.installed ? '✓' : '✗';
|
|
73
|
-
const statusEmoji = c.status === 'ready' ? '🟢 ready'
|
|
75
|
+
const statusEmoji = c.status === 'ready' ? '🟢 ready'
|
|
76
|
+
: c.status === 'not-installed' ? t('⚪ 미설치', '⚪ not installed')
|
|
77
|
+
: c.status === 'disabled' ? t('🟡 비활성', '🟡 disabled') : '❓';
|
|
74
78
|
log(`| ${c.id} | ${c.source} | ${envMark} ${c.envFlag} | ${instMark} | ${c.version || '-'} | ${statusEmoji} |`);
|
|
75
79
|
}
|
|
76
80
|
// 1.36.94: 무시한 설정을 **평문에서도** 말한다 — JSON 에만 넣으면 사람은 왜 미설치로 보이는지 모른다.
|
|
77
81
|
const _rejected = checks.filter(c => c.binRejected || c.versionArgsRejected);
|
|
78
82
|
if (_rejected.length) {
|
|
79
83
|
log('');
|
|
80
|
-
_rejected.forEach(c =>
|
|
84
|
+
_rejected.forEach(c => {
|
|
85
|
+
// Rejection reasons are canonical diagnostic fields and remain unchanged
|
|
86
|
+
// in --json. Human English output uses a semantic rendering so Korean
|
|
87
|
+
// sanitizer text (including provider-specific reasons) cannot leak.
|
|
88
|
+
const reason = en
|
|
89
|
+
? (c.binRejected
|
|
90
|
+
? 'providers.json bin is not a safe executable name/path, so it was not run'
|
|
91
|
+
: 'providers.json versionArgs contain unsafe shell syntax, so they were ignored and --version was used')
|
|
92
|
+
: (c.binRejected || c.versionArgsRejected);
|
|
93
|
+
log(`⚠ ${c.id}: ${reason}`);
|
|
94
|
+
});
|
|
81
95
|
}
|
|
82
96
|
const ready = checks.filter(c => c.status === 'ready');
|
|
83
97
|
log('');
|
|
84
|
-
log(
|
|
98
|
+
log(t(
|
|
99
|
+
`## 활성 (${ready.length}/${checks.length}): ${ready.map(c => c.id).join(', ') || '(없음)'}`,
|
|
100
|
+
`## Active (${ready.length}/${checks.length}): ${ready.map(c => c.id).join(', ') || '(none)'}`,
|
|
101
|
+
));
|
|
85
102
|
if (!ready.length) {
|
|
86
103
|
log('');
|
|
87
|
-
log(`💡 활성화
|
|
88
|
-
log(
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
104
|
+
log(t(`💡 활성화 방법:`, `💡 How to enable:`));
|
|
105
|
+
log(t(
|
|
106
|
+
` 1) CLI 설치 (예: \`npm i -g @openai/codex-cli\`, \`npm i -g @google/antigravity-cli\`)`,
|
|
107
|
+
` 1) Install a CLI (for example, \`npm i -g @openai/codex-cli\` or \`npm i -g @google/antigravity-cli\`)`,
|
|
108
|
+
));
|
|
109
|
+
log(t(
|
|
110
|
+
` 2) .env 또는 환경변수: LEERNESS_ENABLE_CODEX=1, LEERNESS_ENABLE_AGY=1`,
|
|
111
|
+
` 2) Set .env or environment variables: LEERNESS_ENABLE_CODEX=1, LEERNESS_ENABLE_AGY=1`,
|
|
112
|
+
));
|
|
113
|
+
log(t(` 3) \`leerness agents check\`로 재확인`, ` 3) Run \`leerness agents check\` to check again`));
|
|
114
|
+
log(t(
|
|
115
|
+
` 💡 1.9.157: 빌트인 외 CLI 추가: \`leerness provider add <id> --bin <cmd>\``,
|
|
116
|
+
` 💡 1.9.157: Add a CLI beyond the built-ins: \`leerness provider add <id> --bin <cmd>\``,
|
|
117
|
+
));
|
|
92
118
|
} else {
|
|
93
119
|
log('');
|
|
94
|
-
log(
|
|
95
|
-
|
|
120
|
+
log(t(
|
|
121
|
+
`💡 메인 에이전트가 sub-agent 분배 시 위 ${ready.length}개 CLI 활용 가능:`,
|
|
122
|
+
`💡 The primary agent can use the ${ready.length} enabled CLI(s) for sub-agent delegation:`,
|
|
123
|
+
));
|
|
124
|
+
log(t(
|
|
125
|
+
` \`leerness agents dispatch "<task>" --to <id>\` 로 프롬프트 전달`,
|
|
126
|
+
` Pass a prompt with \`leerness agents dispatch "<task>" --to <id>\``,
|
|
127
|
+
));
|
|
96
128
|
}
|
|
97
129
|
return;
|
|
98
130
|
}
|
package/lib/toggles.js
CHANGED
|
@@ -27,6 +27,17 @@ const TOGGLE_REGISTRY = {
|
|
|
27
27
|
'double-verify': { desc: '완료 전 재확인 지시문(같은 것을 다시 확인하라는 안내) — 끄면 안내만 생략된다 (gate/verify-claim 실행은 그대로)', affects: 'handoff · lens 안내' },
|
|
28
28
|
'full-reread': { desc: '매 세션 관련 문서 전체 재독 지시문 — 끄면 "필요할 때 읽어라" 로 바뀐다 (문서와 상태는 그대로 남는다)', affects: 'handoff 안내' },
|
|
29
29
|
};
|
|
30
|
+
const TOGGLE_REGISTRY_EN = {
|
|
31
|
+
'gate': { desc: 'completion gate (integrated verify+audit+scan+encoding+lazy checks)', affects: 'leerness gate' },
|
|
32
|
+
'lens': { desc: 'quality-lens self-questions (domain checks before claiming completion)', affects: 'leerness lens' },
|
|
33
|
+
'auto-graph': { desc: 'automatically refresh the ontology graph (leerness.html) on install/session-close', affects: 'install · session close' },
|
|
34
|
+
'delegation-brief': { desc: 'prepend the Leerness protocol brief when delegating to a background AI', affects: 'agents dispatch · agents multi' },
|
|
35
|
+
'bugfix-receipt': { desc: 'bugfix completion gate — tasks with a registered probe must pass it and record root cause/sibling scope before done (OFF by default)', affects: 'task update --status done · task sync --from' },
|
|
36
|
+
'difficulty-routing': { desc: 'route work by difficulty — allows --confirm for leerness agents route; high-risk work still requires --approved-by (OFF by default)', affects: 'leerness agents route --confirm' },
|
|
37
|
+
'workflow-distribute': { desc: 'the distribute (sub-agent) instruction in the session workflow; disabling it removes only the instruction, not evidence checks', affects: 'handoff · session-workflow guidance' },
|
|
38
|
+
'double-verify': { desc: 'the instruction to recheck work before completion; disabling it omits only the guidance, not gate/verify-claim execution', affects: 'handoff · lens guidance' },
|
|
39
|
+
'full-reread': { desc: 'the instruction to reread every related document each session; disabling it changes the wording to read when needed', affects: 'handoff guidance' },
|
|
40
|
+
};
|
|
30
41
|
const _hasToggle = (id) => Object.prototype.hasOwnProperty.call(TOGGLE_REGISTRY, id);
|
|
31
42
|
// P-0009 경계 계약: 토글로 끌 수 있는 것은 '지시문' 뿐이다. 아래 목록은 **어떤 토글 조합에서도** 살아 있어야 하며,
|
|
32
43
|
// 변이 테스트가 이 계약을 고정한다(계약이 깨지면 토글을 출하하지 않는다는 것이 승인 조건이었다).
|
|
@@ -100,6 +111,11 @@ function saveToggles(root, toggles) {
|
|
|
100
111
|
function toggleCmd(root, sub, id, val, deps = {}) {
|
|
101
112
|
const { has, VERSION } = deps;
|
|
102
113
|
root = absRoot(root);
|
|
114
|
+
const en = deps.uiLang === 'en';
|
|
115
|
+
const t = (ko, enText) => (en ? enText : ko);
|
|
116
|
+
const reasonText = reason => (en
|
|
117
|
+
? (reason === '최상위가 객체가 아닙니다' ? 'top level is not an object' : reason === 'JSON 파싱 실패' ? 'JSON parse failed' : reason)
|
|
118
|
+
: reason);
|
|
103
119
|
// 1.36.108 (T-0097): 변경 하위명령은 읽기~쓰기 전체를 락으로 직렬화한다 — clarify/referee/routing 과 같은 관례.
|
|
104
120
|
// 종전엔 이 모듈만 관례에서 빠져 있어 동시 `toggle set` 이 서로의 값을 덮었다(런타임 계측으로 실측: 락 X).
|
|
105
121
|
// 재진입 플래그(_locked)로 자기 재호출을 한 번만 감싼다.
|
|
@@ -111,17 +127,24 @@ function toggleCmd(root, sub, id, val, deps = {}) {
|
|
|
111
127
|
const cur = _chk.toggles;
|
|
112
128
|
if (!sub || sub === 'list') {
|
|
113
129
|
if (json) { log(JSON.stringify({ version: VERSION, toggles: cur, corrupt: !!_chk.corrupt, corruptReason: _chk.reason, registry: TOGGLE_REGISTRY }, null, 2)); return; }
|
|
114
|
-
log(`# leerness toggle — 기능 토글 (온톨로지 그래프 뷰 ⚙ 탭과 연동)`);
|
|
130
|
+
log(t(`# leerness toggle — 기능 토글 (온톨로지 그래프 뷰 ⚙ 탭과 연동)`, `# leerness toggle — feature toggles (synced with the ontology graph ⚙ tab)`));
|
|
115
131
|
// 손상 시 "그냥 OFF" 로 보이면 켜 뒀던 게이트가 꺼진 줄 모른다 — 사실을 먼저 알린다(헌트 #5).
|
|
116
|
-
if (_chk.corrupt) log(
|
|
132
|
+
if (_chk.corrupt) log(t(
|
|
133
|
+
` ⚠ toggles.json 손상(${_chk.reason}) — 아래는 **저장값이 아니라 기본값**입니다. 켜 두었던 토글이 꺼져 보일 수 있습니다: ${_togglesPath(root)}`,
|
|
134
|
+
` ⚠ toggles.json is corrupt (${reasonText(_chk.reason)}) — values below are **defaults, not stored values**. A previously enabled toggle may appear disabled: ${_togglesPath(root)}`,
|
|
135
|
+
));
|
|
117
136
|
for (const [k, meta] of Object.entries(TOGGLE_REGISTRY)) {
|
|
118
|
-
|
|
137
|
+
const shown = en ? TOGGLE_REGISTRY_EN[k] : meta;
|
|
138
|
+
log(` ${cur[k] ? '🟢 ON ' : '⚪ OFF'} ${k.padEnd(17)} ${shown.desc} [${shown.affects}]`);
|
|
119
139
|
}
|
|
120
|
-
log(
|
|
140
|
+
log(t(
|
|
141
|
+
`\n 변경: leerness toggle set <id> on|off · 그래프 뷰: leerness graph --html → leerness.html 의 ⚙ 탭`,
|
|
142
|
+
`\n Change: leerness toggle set <id> on|off · Graph view: leerness graph --html → the ⚙ tab in leerness.html`,
|
|
143
|
+
));
|
|
121
144
|
return;
|
|
122
145
|
}
|
|
123
146
|
if (sub === 'get') {
|
|
124
|
-
if (!_hasToggle(id)) { fail(`알 수 없는 토글: ${id} (가능: ${Object.keys(TOGGLE_REGISTRY).join(', ')})`); process.exitCode = 1; return; }
|
|
147
|
+
if (!_hasToggle(id)) { fail(t(`알 수 없는 토글: ${id} (가능: ${Object.keys(TOGGLE_REGISTRY).join(', ')})`, `Unknown toggle: ${id} (valid: ${Object.keys(TOGGLE_REGISTRY).join(', ')})`)); process.exitCode = 1; return; }
|
|
125
148
|
const payload = {
|
|
126
149
|
ok: true,
|
|
127
150
|
id,
|
|
@@ -135,29 +158,29 @@ function toggleCmd(root, sub, id, val, deps = {}) {
|
|
|
135
158
|
meta: TOGGLE_REGISTRY[id],
|
|
136
159
|
};
|
|
137
160
|
if (json) { log(JSON.stringify(payload, null, 2)); return payload; }
|
|
138
|
-
if (_chk.corrupt) log(` ⚠ toggles.json 손상(${_chk.reason}) — 아래 값은 저장값이 아니라 기본값입니다: ${_togglesPath(root)}`);
|
|
139
|
-
if (payload.unreadable) log(` ⚠ ${id} 저장값을 해석할 수 없어 기본값으로
|
|
140
|
-
log(`${payload.value ? '🟢 ON' : '⚪ OFF'} ${id} — ${TOGGLE_REGISTRY[id].desc} [${payload.source}]`);
|
|
161
|
+
if (_chk.corrupt) log(t(` ⚠ toggles.json 손상(${_chk.reason}) — 아래 값은 저장값이 아니라 기본값입니다: ${_togglesPath(root)}`, ` ⚠ toggles.json is corrupt (${reasonText(_chk.reason)}) — the value below is a default, not a stored value: ${_togglesPath(root)}`));
|
|
162
|
+
if (payload.unreadable) log(t(` ⚠ ${id} 저장값을 해석할 수 없어 기본값으로 표시합니다.`, ` ⚠ The stored value for ${id} is unreadable, so the default is shown.`));
|
|
163
|
+
log(`${payload.value ? '🟢 ON' : '⚪ OFF'} ${id} — ${(en ? TOGGLE_REGISTRY_EN[id] : TOGGLE_REGISTRY[id]).desc} [${payload.source}]`);
|
|
141
164
|
return payload;
|
|
142
165
|
}
|
|
143
166
|
if (sub === 'set') {
|
|
144
|
-
if (!_hasToggle(id)) { fail(`알 수 없는 토글: ${id} (가능: ${Object.keys(TOGGLE_REGISTRY).join(', ')})`); process.exitCode = 1; return; }
|
|
167
|
+
if (!_hasToggle(id)) { fail(t(`알 수 없는 토글: ${id} (가능: ${Object.keys(TOGGLE_REGISTRY).join(', ')})`, `Unknown toggle: ${id} (valid: ${Object.keys(TOGGLE_REGISTRY).join(', ')})`)); process.exitCode = 1; return; }
|
|
145
168
|
const on = String(val).toLowerCase();
|
|
146
|
-
if (on !== 'on' && on !== 'off') { fail(`값은 on|off (받음: ${val})`); process.exitCode = 1; return; }
|
|
169
|
+
if (on !== 'on' && on !== 'off') { fail(t(`값은 on|off (받음: ${val})`, `Value must be on|off (received: ${val})`)); process.exitCode = 1; return; }
|
|
147
170
|
// 1.36.49 (codex 6차 #5): 손상 toggles.json 위에 set 하면 loadToggles 의 기본값 폴백이 저장돼
|
|
148
171
|
// 무관한 토글(예: gate:false)이 무언 ON 으로 리셋됐다 — 변경 진입점은 fail-closed (1.36.28 스토어 손상 클래스).
|
|
149
172
|
const f = _togglesPath(root);
|
|
150
173
|
if (exists(f)) {
|
|
151
174
|
try { JSON.parse(read(f)); }
|
|
152
|
-
catch { fail(`toggles.json 손상(JSON 파싱 실패) — 덮어쓰기 거부: ${f}\n 복구하거나 삭제(전 토글 기본 ON 복원) 후
|
|
175
|
+
catch { fail(t(`toggles.json 손상(JSON 파싱 실패) — 덮어쓰기 거부: ${f}\n 복구하거나 삭제(전 토글 기본 ON 복원) 후 재시도`, `toggles.json is corrupt (JSON parse failed) — refusing to overwrite: ${f}\n Repair it or delete it to restore defaults, then retry`)); process.exitCode = 1; return; }
|
|
153
176
|
}
|
|
154
177
|
cur[id] = on === 'on';
|
|
155
178
|
saveToggles(root, cur);
|
|
156
179
|
if (json) { log(JSON.stringify({ ok: true, id, value: cur[id], toggles: cur }, null, 2)); return; }
|
|
157
|
-
ok(`toggle ${id} = ${on.toUpperCase()}${cur[id] ? '' : ' — 관련 명령이 스킵 동작으로 전환됩니다'}`);
|
|
180
|
+
ok(`toggle ${id} = ${on.toUpperCase()}${cur[id] ? '' : t(' — 관련 명령이 스킵 동작으로 전환됩니다', ' — related commands will switch to skip behavior')}`);
|
|
158
181
|
return;
|
|
159
182
|
}
|
|
160
|
-
fail(`알 수 없는 하위명령: ${sub} (가능: list, get, set)`); process.exitCode = 1;
|
|
183
|
+
fail(t(`알 수 없는 하위명령: ${sub} (가능: list, get, set)`, `Unknown subcommand: ${sub} (valid: list, get, set)`)); process.exitCode = 1;
|
|
161
184
|
}
|
|
162
185
|
|
|
163
186
|
module.exports = { TOGGLE_REGISTRY, SCAFFOLD_TOGGLES, EVIDENCE_COMMANDS, loadToggles, loadTogglesChecked, toggleOn, saveToggles, toggleCmd, _togglesPath, _coerceToggle };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leerness",
|
|
3
|
-
"version": "1.36.
|
|
3
|
+
"version": "1.36.180",
|
|
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",
|
|
@@ -48,17 +48,19 @@
|
|
|
48
48
|
"LICENSE"
|
|
49
49
|
],
|
|
50
50
|
"scripts": {
|
|
51
|
-
"test": "node ./scripts/lint.js && node ./scripts/workspace-dir-source-ratchet.js && node ./scripts/workspace-dir-migration-probe.js && node ./scripts/workspace-dir-lock-order-probe.js && node ./scripts/lock-probe.js && node ./scripts/platform-smoke.js && node ./scripts/verify-code-cross-runtime-probe.js && node ./scripts/release-runtime-probe.js && node ./scripts/which-shim-probe.js && node ./bin/leerness.js --version && node ./bin/leerness.js selftest && node ./scripts/e2e-core.js && node ./scripts/mutation-integrity-probe.js && node ./scripts/handoff-readonly-probe.js && node ./scripts/mcp-presence-probe.js && npm run test:commands && npm run test:installed && node ./scripts/e2e.js",
|
|
52
|
-
"test:core": "node ./scripts/lint.js && node ./scripts/workspace-dir-source-ratchet.js && node ./scripts/workspace-dir-migration-probe.js && node ./scripts/workspace-dir-lock-order-probe.js && node ./scripts/lock-probe.js && node ./scripts/platform-smoke.js && node ./scripts/verify-code-cross-runtime-probe.js && node ./scripts/release-runtime-probe.js && node ./scripts/which-shim-probe.js && node ./bin/leerness.js --version && node ./bin/leerness.js selftest && node ./scripts/e2e-core.js && node ./scripts/mutation-integrity-probe.js && node ./scripts/handoff-readonly-probe.js && node ./scripts/mcp-presence-probe.js",
|
|
51
|
+
"test": "node ./scripts/lint.js && node ./scripts/workspace-dir-source-ratchet.js && node ./scripts/workspace-dir-migration-probe.js && node ./scripts/workspace-dir-lock-order-probe.js && node ./scripts/lock-probe.js && node ./scripts/platform-smoke.js && node ./scripts/parent-detect-selftest-probe.js && node ./scripts/verify-code-cross-runtime-probe.js && node ./scripts/release-runtime-probe.js && node ./scripts/which-shim-probe.js && node ./scripts/i18n-priority-surface-probe.js && node ./bin/leerness.js --version && node ./bin/leerness.js selftest && node ./scripts/e2e-core.js && node ./scripts/mutation-integrity-probe.js && node ./scripts/handoff-readonly-probe.js && node ./scripts/mcp-presence-probe.js && npm run test:commands && npm run test:installed && node ./scripts/e2e.js",
|
|
52
|
+
"test:core": "node ./scripts/lint.js && node ./scripts/workspace-dir-source-ratchet.js && node ./scripts/workspace-dir-migration-probe.js && node ./scripts/workspace-dir-lock-order-probe.js && node ./scripts/lock-probe.js && node ./scripts/platform-smoke.js && node ./scripts/parent-detect-selftest-probe.js && node ./scripts/verify-code-cross-runtime-probe.js && node ./scripts/release-runtime-probe.js && node ./scripts/which-shim-probe.js && node ./scripts/i18n-priority-surface-probe.js && node ./bin/leerness.js --version && node ./bin/leerness.js selftest && node ./scripts/e2e-core.js && node ./scripts/mutation-integrity-probe.js && node ./scripts/handoff-readonly-probe.js && node ./scripts/mcp-presence-probe.js",
|
|
53
53
|
"test:commands": "node ./scripts/command-flags-probe.js && node ./scripts/dead-flags-probe.js && node ./scripts/false-claim-probe.js && node ./scripts/next-action-suggestion-probe.js && node ./scripts/claims-baseline-probe.js && node ./scripts/claims-baseline-concurrency-probe.js && node ./scripts/e2e-command-surface.js",
|
|
54
54
|
"test:next-actions": "node ./scripts/next-action-suggestion-probe.js",
|
|
55
55
|
"test:installed": "node ./scripts/installed-cleanroom-probe.js",
|
|
56
56
|
"test:handoff": "node ./scripts/handoff-readonly-probe.js",
|
|
57
|
-
"test:fast": "node ./scripts/lint.js && node ./scripts/workspace-dir-source-ratchet.js && node ./scripts/workspace-dir-migration-probe.js && node ./scripts/workspace-dir-lock-order-probe.js && node ./scripts/lock-probe.js && node ./scripts/platform-smoke.js && node ./scripts/release-runtime-probe.js && node ./scripts/which-shim-probe.js && node ./scripts/mutation-integrity-probe.js && node ./scripts/mcp-presence-probe.js && node ./scripts/false-claim-probe.js && node ./scripts/smoke.js",
|
|
57
|
+
"test:fast": "node ./scripts/lint.js && node ./scripts/workspace-dir-source-ratchet.js && node ./scripts/workspace-dir-migration-probe.js && node ./scripts/workspace-dir-lock-order-probe.js && node ./scripts/lock-probe.js && node ./scripts/platform-smoke.js && node ./scripts/parent-detect-selftest-probe.js && node ./scripts/release-runtime-probe.js && node ./scripts/which-shim-probe.js && node ./scripts/i18n-priority-surface-probe.js && node ./scripts/mutation-integrity-probe.js && node ./scripts/mcp-presence-probe.js && node ./scripts/false-claim-probe.js && node ./scripts/smoke.js",
|
|
58
58
|
"test:false-claims": "node ./scripts/false-claim-probe.js",
|
|
59
59
|
"test:workspace-lock-order": "node ./scripts/workspace-dir-lock-order-probe.js",
|
|
60
60
|
"test:mcp-presence": "node ./scripts/mcp-presence-probe.js",
|
|
61
61
|
"test:smoke": "node ./scripts/smoke.js",
|
|
62
|
+
"test:parent-detect": "node ./scripts/parent-detect-selftest-probe.js",
|
|
63
|
+
"test:i18n": "node ./scripts/i18n-priority-surface-probe.js",
|
|
62
64
|
"lint": "node ./scripts/lint.js",
|
|
63
65
|
"prepack": "node ./bin/leerness.js readme sync . && node ./bin/leerness.js --version"
|
|
64
66
|
},
|
package/scripts/e2e.js
CHANGED
|
@@ -11027,7 +11027,8 @@ total++;
|
|
|
11027
11027
|
// 1.36.112: 106 으로 조인다. `context budget` 을 손대는 김에 영어화해 5줄을 갚았다(111 → 106, 실측).
|
|
11028
11028
|
// 갚은 만큼만 낮춘다 — 여유를 남기면 그 안에서 조용히 썩는다(1.36.82 의 자기참조 가드에서 겪은 형태).
|
|
11029
11029
|
// 1.36.174: provider list 영어 모드의 요약/안내 두 줄을 영어화해 104로 조임(39개 명령 실측).
|
|
11030
|
-
|
|
11030
|
+
// 1.36.180: agents list(18) + insights(17) + toggle list(11) 우선 표면을 영어화해 58로 조임.
|
|
11031
|
+
const BASELINE = 58;
|
|
11031
11032
|
dbg.baseline = BASELINE;
|
|
11032
11033
|
dbg.withinRatchet = leaky <= BASELINE;
|
|
11033
11034
|
// 이번 라운드가 고친 표면은 **0 이어야** 한다 — 래칫과 별개로 회귀를 직접 막는다.
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const cp = require('child_process');
|
|
7
|
+
|
|
8
|
+
const CLI = path.resolve(__dirname, '..', 'bin', 'leerness.js');
|
|
9
|
+
const HANGUL = /[가-힣ㄱ-ㆎ]/;
|
|
10
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-i18n-priority-'));
|
|
11
|
+
const project = path.join(tmp, 'project');
|
|
12
|
+
|
|
13
|
+
const baseEnv = {
|
|
14
|
+
...process.env,
|
|
15
|
+
TMPDIR: tmp,
|
|
16
|
+
TEMP: tmp,
|
|
17
|
+
TMP: tmp,
|
|
18
|
+
LEERNESS_INTERNAL: '1',
|
|
19
|
+
LEERNESS_NO_BANNER: '1',
|
|
20
|
+
LEERNESS_NO_STALE_CHECK: '1',
|
|
21
|
+
LEERNESS_OFFLINE: '1',
|
|
22
|
+
};
|
|
23
|
+
// The English assertions must be driven by the project manifest written by
|
|
24
|
+
// `init --language en`; an inherited env override would make that path untested.
|
|
25
|
+
delete baseEnv.LEERNESS_LANG;
|
|
26
|
+
for (const key of [
|
|
27
|
+
'LEERNESS_ENABLE_CLAUDE', 'LEERNESS_ENABLE_CODEX', 'LEERNESS_ENABLE_AGY',
|
|
28
|
+
'LEERNESS_ENABLE_GROK', 'LEERNESS_ENABLE_OPENCODE', 'LEERNESS_ENABLE_QWEN',
|
|
29
|
+
'LEERNESS_ENABLE_AIDER', 'LEERNESS_ENABLE_GOOSE', 'LEERNESS_ENABLE_COPILOT',
|
|
30
|
+
'LEERNESS_ENABLE_OLLAMA',
|
|
31
|
+
]) baseEnv[key] = '0';
|
|
32
|
+
|
|
33
|
+
function run(args, env = baseEnv) {
|
|
34
|
+
return cp.spawnSync(process.execPath, [CLI, ...args], {
|
|
35
|
+
cwd: project,
|
|
36
|
+
env,
|
|
37
|
+
encoding: 'utf8',
|
|
38
|
+
timeout: 300000,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function outputOf(result) {
|
|
43
|
+
return String(result.stdout || '') + String(result.stderr || '');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function requireSuccess(label, result) {
|
|
47
|
+
const output = outputOf(result);
|
|
48
|
+
if (result.status !== 0 || !output.trim()) {
|
|
49
|
+
throw new Error(`${label} probe command failed or was silent (exit ${result.status}): ${output.slice(0, 500)}`);
|
|
50
|
+
}
|
|
51
|
+
return output;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function requireFailure(label, result) {
|
|
55
|
+
const output = outputOf(result);
|
|
56
|
+
if (result.status === 0 || !output.trim()) {
|
|
57
|
+
throw new Error(`${label} probe command unexpectedly succeeded or was silent (exit ${result.status}): ${output.slice(0, 500)}`);
|
|
58
|
+
}
|
|
59
|
+
return output;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function hangulLines(output) {
|
|
63
|
+
return String(output).split(/\r?\n/).filter(line => HANGUL.test(line));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
fs.mkdirSync(project, { recursive: true });
|
|
68
|
+
fs.writeFileSync(path.join(project, 'package.json'), '{"name":"i18n-probe","version":"0.1.0"}\n');
|
|
69
|
+
requireSuccess('init', run(['init', project, '--yes', '--language', 'en']));
|
|
70
|
+
|
|
71
|
+
requireSuccess('task add', run(['task', 'add', 'Implement the parser', '--path', project]));
|
|
72
|
+
requireSuccess('decision add', run(['decision', 'add', 'Use JSON storage', '--reason', 'simplest', '--path', project]));
|
|
73
|
+
requireSuccess('lesson save', run(['lesson', 'save', 'Keep outputs deterministic', '--tag', 'test', '--path', project]));
|
|
74
|
+
requireSuccess('plan add', run(['plan', 'add', 'Ship the parser', '--path', project]));
|
|
75
|
+
|
|
76
|
+
// Exercise the rejected-provider branch without executing an unsafe command.
|
|
77
|
+
// `provider add` creates the canonical store; the fixture then simulates a
|
|
78
|
+
// hand-edited/remote catalog that the production sanitizer must reject.
|
|
79
|
+
requireSuccess('provider add unsafe-bin', run(['provider', 'add', 'unsafe-bin', '--bin', 'missing-i18n-bin', '--path', project]));
|
|
80
|
+
requireSuccess('provider add unsafe-args', run(['provider', 'add', 'unsafe-args', '--bin', 'missing-i18n-args', '--path', project]));
|
|
81
|
+
const providersFile = path.join(project, '.leerness', 'providers.json');
|
|
82
|
+
const providersRaw = JSON.parse(fs.readFileSync(providersFile, 'utf8'));
|
|
83
|
+
const providers = Array.isArray(providersRaw) ? providersRaw : providersRaw.providers;
|
|
84
|
+
const unsafeBin = providers.find(p => p.id === 'unsafe-bin');
|
|
85
|
+
const unsafeArgs = providers.find(p => p.id === 'unsafe-args');
|
|
86
|
+
if (!unsafeBin || !unsafeArgs) throw new Error('provider rejection fixtures were not persisted');
|
|
87
|
+
unsafeBin.bin = 'unsafe provider bin';
|
|
88
|
+
unsafeArgs.versionArgs = ['--version', '&', 'never-run'];
|
|
89
|
+
fs.writeFileSync(providersFile, JSON.stringify(providersRaw, null, 2) + '\n');
|
|
90
|
+
|
|
91
|
+
const surfaces = [
|
|
92
|
+
['agents list', ['agents', 'list', '--path', project]],
|
|
93
|
+
['insights', ['insights', '--path', project]],
|
|
94
|
+
['insights workspace', ['insights', '--include', project, '--path', project]],
|
|
95
|
+
['toggle list', ['toggle', 'list', '--path', project]],
|
|
96
|
+
['toggle get', ['toggle', 'get', 'gate', '--path', project]],
|
|
97
|
+
['toggle set', ['toggle', 'set', 'gate', 'off', '--path', project]],
|
|
98
|
+
];
|
|
99
|
+
const leaks = [];
|
|
100
|
+
for (const [label, args] of surfaces) {
|
|
101
|
+
const output = requireSuccess(label, run(args));
|
|
102
|
+
const lines = hangulLines(output);
|
|
103
|
+
if (lines.length) leaks.push(`${label}=${lines.length} (${lines.slice(0, 2).join(' | ')})`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const koEnv = { ...baseEnv, LEERNESS_LANG: 'ko' };
|
|
107
|
+
for (const [label, args] of [
|
|
108
|
+
['agents list Korean control', ['agents', 'list', '--path', project]],
|
|
109
|
+
['insights Korean control', ['insights', '--path', project]],
|
|
110
|
+
['toggle list Korean control', ['toggle', 'list', '--path', project]],
|
|
111
|
+
]) {
|
|
112
|
+
const output = requireSuccess(label, run(args, koEnv));
|
|
113
|
+
if (!HANGUL.test(output)) throw new Error(`${label} lost Hangul; the probe cannot distinguish locales`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const unknownOutput = requireFailure('toggle unknown English error', run(['toggle', 'get', 'missing-toggle', '--path', project]));
|
|
117
|
+
const unknownLines = hangulLines(unknownOutput);
|
|
118
|
+
if (unknownLines.length) leaks.push(`toggle unknown error=${unknownLines.length} (${unknownLines.slice(0, 2).join(' | ')})`);
|
|
119
|
+
|
|
120
|
+
const toggleFile = path.join(project, '.leerness', 'toggles.json');
|
|
121
|
+
fs.writeFileSync(toggleFile, '{broken json\n');
|
|
122
|
+
const corruptList = requireSuccess('toggle corrupt list', run(['toggle', 'list', '--path', project]));
|
|
123
|
+
const corruptSet = requireFailure('toggle corrupt set', run(['toggle', 'set', 'gate', 'on', '--path', project]));
|
|
124
|
+
for (const [label, output] of [['toggle corrupt list', corruptList], ['toggle corrupt set', corruptSet]]) {
|
|
125
|
+
const lines = hangulLines(output);
|
|
126
|
+
if (lines.length) leaks.push(`${label}=${lines.length} (${lines.slice(0, 2).join(' | ')})`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Machine output remains locale-neutral in shape: do not expose the private
|
|
130
|
+
// English projection or replace the canonical registry payload.
|
|
131
|
+
fs.rmSync(toggleFile, { force: true });
|
|
132
|
+
const toggleJson = JSON.parse(requireSuccess('toggle JSON contract', run(['toggle', 'list', '--path', project, '--json'])));
|
|
133
|
+
if (!toggleJson.registry || !toggleJson.registry.gate || toggleJson.registry.gate.descEn !== undefined
|
|
134
|
+
|| !HANGUL.test(toggleJson.registry.gate.desc || '')) {
|
|
135
|
+
throw new Error('toggle JSON registry no longer exposes the canonical shape');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (leaks.length) throw new Error(`English UI leaked Hangul: ${leaks.join('; ')}`);
|
|
139
|
+
console.log('✓ English priority surfaces and edge paths contain no Hangul; 3/3 Korean controls and JSON shape remain intact');
|
|
140
|
+
} catch (error) {
|
|
141
|
+
console.error(`✗ ${error && error.message ? error.message : error}`);
|
|
142
|
+
process.exitCode = 1;
|
|
143
|
+
} finally {
|
|
144
|
+
try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {}
|
|
145
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// T-0091: the parent-detect selftest must be independent of a Leerness
|
|
5
|
+
// workspace above the OS temp directory, and partial setup must be cleaned if
|
|
6
|
+
// the second mkdtempSync call fails.
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
const cliPath = process.env.LEERNESS_PARENT_PROBE_CLI
|
|
13
|
+
? path.resolve(process.env.LEERNESS_PARENT_PROBE_CLI)
|
|
14
|
+
: path.resolve(__dirname, '..', 'bin', 'leerness.js');
|
|
15
|
+
const cli = require(cliPath);
|
|
16
|
+
const target = cli._selfTestCases().find(test => test.name.includes('parent detect (1.30.2 #157)'));
|
|
17
|
+
const sibling = cli._selfTestCases().find(test => test.name.includes('rule add flag/경로 break'));
|
|
18
|
+
|
|
19
|
+
if (!target || !sibling) {
|
|
20
|
+
process.stderr.write('parent-detect selftest probe failed: target or sibling case not found\n');
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const originalTempEnv = {};
|
|
25
|
+
for (const key of ['TMPDIR', 'TEMP', 'TMP']) originalTempEnv[key] = process.env[key];
|
|
26
|
+
|
|
27
|
+
const sandbox = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-parent-selftest-probe-'));
|
|
28
|
+
const setupDirs = [];
|
|
29
|
+
|
|
30
|
+
function restoreTempEnv() {
|
|
31
|
+
for (const [key, value] of Object.entries(originalTempEnv)) {
|
|
32
|
+
if (value === undefined) delete process.env[key];
|
|
33
|
+
else process.env[key] = value;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function probeSecondMkdtempCleanup(test) {
|
|
38
|
+
const originalMkdtempSync = fs.mkdtempSync;
|
|
39
|
+
let firstSetupDir = null;
|
|
40
|
+
let setupError = null;
|
|
41
|
+
let setupResult = null;
|
|
42
|
+
let setupCalls = 0;
|
|
43
|
+
fs.mkdtempSync = function injectedMkdtempSync(prefix, options) {
|
|
44
|
+
setupCalls++;
|
|
45
|
+
if (setupCalls === 2) {
|
|
46
|
+
const error = new Error('injected second mkdtempSync failure');
|
|
47
|
+
error.code = 'EACCES';
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
firstSetupDir = originalMkdtempSync.call(fs, prefix, options);
|
|
51
|
+
setupDirs.push(firstSetupDir);
|
|
52
|
+
return firstSetupDir;
|
|
53
|
+
};
|
|
54
|
+
try {
|
|
55
|
+
setupResult = test.run();
|
|
56
|
+
} catch (error) {
|
|
57
|
+
setupError = error;
|
|
58
|
+
} finally {
|
|
59
|
+
fs.mkdtempSync = originalMkdtempSync;
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
cleaned: !!firstSetupDir && !fs.existsSync(firstSetupDir),
|
|
63
|
+
observable: setupCalls === 2
|
|
64
|
+
&& ((setupError && setupError.code === 'EACCES') || setupResult === false),
|
|
65
|
+
calls: setupCalls,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const baselinePass = target.run() === true;
|
|
71
|
+
|
|
72
|
+
const contaminatedTemp = path.join(sandbox, 'tmp');
|
|
73
|
+
fs.mkdirSync(path.join(sandbox, '.leerness'), { recursive: true });
|
|
74
|
+
fs.mkdirSync(contaminatedTemp, { recursive: true });
|
|
75
|
+
process.env.TMPDIR = contaminatedTemp;
|
|
76
|
+
process.env.TEMP = contaminatedTemp;
|
|
77
|
+
process.env.TMP = contaminatedTemp;
|
|
78
|
+
const contaminatedAncestorPass = target.run() === true;
|
|
79
|
+
restoreTempEnv();
|
|
80
|
+
|
|
81
|
+
const targetSetup = probeSecondMkdtempCleanup(target);
|
|
82
|
+
const siblingSetup = probeSecondMkdtempCleanup(sibling);
|
|
83
|
+
const report = {
|
|
84
|
+
ok: baselinePass && contaminatedAncestorPass
|
|
85
|
+
&& targetSetup.cleaned && targetSetup.observable
|
|
86
|
+
&& siblingSetup.cleaned && siblingSetup.observable,
|
|
87
|
+
baselinePass,
|
|
88
|
+
contaminatedAncestorPass,
|
|
89
|
+
partialSetupCleaned: targetSetup.cleaned,
|
|
90
|
+
setupFailureObservable: targetSetup.observable,
|
|
91
|
+
setupCalls: targetSetup.calls,
|
|
92
|
+
siblingPartialSetupCleaned: siblingSetup.cleaned,
|
|
93
|
+
siblingSetupFailureObservable: siblingSetup.observable,
|
|
94
|
+
siblingSetupCalls: siblingSetup.calls,
|
|
95
|
+
};
|
|
96
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
97
|
+
if (!report.ok) process.exitCode = 1;
|
|
98
|
+
} finally {
|
|
99
|
+
restoreTempEnv();
|
|
100
|
+
for (const dir of setupDirs) {
|
|
101
|
+
try { fs.rmSync(dir, { recursive: true, force: true }); } catch {}
|
|
102
|
+
}
|
|
103
|
+
try { fs.rmSync(sandbox, { recursive: true, force: true }); } catch {}
|
|
104
|
+
}
|