leerness 1.36.185 → 1.36.187
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 +17 -0
- package/README.ko.md +5 -0
- package/README.md +14 -4
- package/bin/leerness.js +291 -57
- package/docs/e2e-observation-contract.md +51 -0
- package/docs/runtime-compatibility-api.md +183 -0
- package/docs/state-paths-api.md +92 -0
- package/docs/state-scopes.md +266 -0
- package/docs/worktree-runtime-migration.md +278 -0
- package/lib/claims-baseline.js +1 -1
- package/lib/file-leases.js +2 -2
- package/lib/git.js +64 -15
- package/lib/io.js +9 -3
- package/lib/migrate.js +1 -1
- package/lib/preview-serve.js +5 -1
- package/lib/pure-utils.js +20 -0
- package/lib/role-fallback.js +3 -3
- package/lib/role-store.js +2 -2
- package/lib/runtime-layout.js +395 -0
- package/lib/runtime-writes.js +166 -0
- package/lib/session-close.js +1 -1
- package/lib/state-git.js +138 -0
- package/lib/state-inspect.js +39 -0
- package/lib/state-inventory.js +69 -0
- package/lib/state-paths.js +59 -0
- package/lib/workspace-dir.js +34 -10
- package/package.json +10 -6
- package/scripts/command-flags-probe.js +1 -1
- package/scripts/dead-flags-probe.js +1 -1
- package/scripts/e2e-child-diagnostics-probe.js +143 -0
- package/scripts/e2e-child-diagnostics.js +33 -0
- package/scripts/e2e-temp-scope-probe.js +127 -0
- package/scripts/e2e.js +108 -27
- package/scripts/encoding-selftest-probe.js +222 -0
- package/scripts/false-claim-probe.js +34 -3
- package/scripts/file-lease-probe.js +5 -2
- package/scripts/legacy-runtime-negative-probe.js +72 -0
- package/scripts/mutation-integrity-probe.js +3 -0
- package/scripts/role-fallback-probe.js +67 -6
- package/scripts/runtime-admission-probe.js +282 -0
- package/scripts/runtime-git-write-probe.js +116 -0
- package/scripts/runtime-layout-probe.js +1198 -0
- package/scripts/runtime-repl-probe.js +271 -0
- package/scripts/runtime-replacement-probe.js +190 -0
- package/scripts/runtime-write-probe.js +458 -0
- package/scripts/selftest-cleanup-probe.js +168 -0
- package/scripts/state-inspect-cli-probe.js +209 -0
- package/scripts/state-scopes-probe.js +590 -0
- package/scripts/workspace-dir-lock-order-probe.js +310 -35
- package/scripts/workspace-dir-migration-probe.js +17 -2
- package/scripts/workspace-selection-probe.js +230 -0
package/bin/leerness.js
CHANGED
|
@@ -10,6 +10,7 @@ const { resolvePortableLaunch, spawnPortable, spawnPortableSync } = require('../
|
|
|
10
10
|
const { scanShellScriptsEncoding, applyShellScriptUtf8Bom } = require('../lib/shell-encoding');
|
|
11
11
|
const os = require('os'); // 1.9.178: _publishToNpm 에서 os.tmpdir() 사용 (전역 import)
|
|
12
12
|
const readline = require('readline');
|
|
13
|
+
const { withRuntimeWrites, withRuntimePathWrite, assertCurrentRuntimeWrite, projectWriter } = require('../lib/runtime-writes');
|
|
13
14
|
// 1.9.274 (UR-0025 1단계): 순수 유틸 함수 모듈 분리 (require-based, 비파괴). selftest 7종이 동작 검증.
|
|
14
15
|
const { _isSecretKey, _isPlaceholderSecret, _looksSecretLike, redactSecrets, stripDefaultIgnorables, _mergeLines, _mergeEnvLines, _mergeReadmeSection, _managedMerge, _parseSkillsValue, _parseArchiveBlocks, _parseSkillCatalog, _renderTeamsMd, _composeTeamPlan, _teamHandoffReminders, _cadenceAssessment, _teamDeployGate, _renderWorkspaceReferenceGuide, _memorySurface, _renderPulseLine, compareVer, parseHarnessVersion, _classifyCJK, _riskLabel, _detectSystemLang, _parseSlashFromHelp,
|
|
15
16
|
PERMISSION_TIERS, _tierRank, _requiredTier, _policyAllows, _resolveNpmTag, _mcpJsonContent, _newRunRecord,
|
|
@@ -29,7 +30,7 @@ const { _isSecretKey, _isPlaceholderSecret, _looksSecretLike, redactSecrets, str
|
|
|
29
30
|
_migrationGuideText, _parseContractSpec, _gitignoreMatch,
|
|
30
31
|
_featureGraphTemplate, _parseFeatureGraph, _nextFeatureId, _featureBlock, _featureImpactBfs,
|
|
31
32
|
_parseChangelogBetween, _cellSafe, _cellUnescape, _lineSafe, _milestoneSurface, _recentChangesAggregate, _parseLimit, _parseAddTitle, _parseImplExports, _taskPositionalPath, _completionClaimAllowed, _minorKey, _shouldPublishNpm,
|
|
32
|
-
_matchTool, _parsePackageJsonDeps, _parseRequirementsTxt, _buildGlossary, _renderGlossaryMd, _briefUnfilled, _planGoalUnfilled, _draftAnchors, _replaceMdSection, _mdSectionBody, _blocksWithOffset, _decisionBlocksWithOffset } = require('../lib/pure-utils'); // 1.9.318~1.11.4 (UR-0025/.../0007 glossary): 순수 유틸 모듈 분리 · 1.36.36 anchors
|
|
33
|
+
_matchTool, _parsePackageJsonDeps, _parseRequirementsTxt, _buildGlossary, _renderGlossaryMd, GLOSSARY_START, GLOSSARY_END, _briefUnfilled, _planGoalUnfilled, _draftAnchors, _replaceMdSection, _mdSectionBody, _blocksWithOffset, _decisionBlocksWithOffset } = require('../lib/pure-utils'); // 1.9.318~1.11.4 (UR-0025/.../0007 glossary): 순수 유틸 모듈 분리 · 1.36.36 anchors
|
|
33
34
|
// 1.9.304 (UR-0025): 순수 분석/검증 함수 모듈 분리.
|
|
34
35
|
const { _evidenceQuality, _parseEvidenceStats, _shellGuardAnalyze, _claimFileInGit, _epistemicHonestyCheck } = require('../lib/analyzers');
|
|
35
36
|
// 1.9.295 (UR-0025 4단계): 정적 데이터 카탈로그 모듈 분리 (비파괴, require-based).
|
|
@@ -49,7 +50,7 @@ const {
|
|
|
49
50
|
migrateLegacyWorkspace,
|
|
50
51
|
} = require('../lib/workspace-dir');
|
|
51
52
|
|
|
52
|
-
const VERSION = '1.36.
|
|
53
|
+
const VERSION = '1.36.187';
|
|
53
54
|
|
|
54
55
|
// MCP lifecycle 주소 표식은 현재 CLI 호출 한 번에만 유효하다. CLI bootstrap에서 즉시 env에서
|
|
55
56
|
// 떼어 두어 `--no-record`/hook처럼 presence 기록 함수에 도달하지 않는 경로도 후속 child에 유출하지 않는다.
|
|
@@ -190,8 +191,8 @@ function _loadSkillCatalog() {
|
|
|
190
191
|
|
|
191
192
|
// 1.9.341 (UR-0025 심층): BUILTIN_CATALOG (9 내장 스킬 catalog) 는 lib/catalogs.js 로 이전 (import).
|
|
192
193
|
|
|
193
|
-
//
|
|
194
|
-
|
|
194
|
+
// Catalog initialization is below the parser declarations so metadata-only
|
|
195
|
+
// inspection can skip irrelevant skillpack discovery (including npm root -g).
|
|
195
196
|
|
|
196
197
|
const routes = {
|
|
197
198
|
planning: { read: ['.leerness/plan.md','.leerness/progress-tracker.md','.leerness/project-brief.md','.leerness/current-state.md','.leerness/guideline.md'], update: ['.leerness/plan.md','.leerness/progress-tracker.md','.leerness/current-state.md','.leerness/session-handoff.md'] },
|
|
@@ -323,23 +324,30 @@ function _ensureLockHeartbeat() {
|
|
|
323
324
|
const worker = new Worker(`
|
|
324
325
|
'use strict';
|
|
325
326
|
const fs = require('fs');
|
|
326
|
-
const { parentPort } = require('worker_threads');
|
|
327
|
+
const { parentPort, workerData } = require('worker_threads');
|
|
328
|
+
const { withRuntimeWrites } = require(workerData.runtimeWrites);
|
|
327
329
|
const locks = new Map();
|
|
328
|
-
function touch(ownerPath,
|
|
330
|
+
function touch(ownerPath, entry) {
|
|
329
331
|
try {
|
|
330
332
|
const owner = JSON.parse(fs.readFileSync(ownerPath, 'utf8'));
|
|
331
|
-
if (!owner || owner.token !== token || owner.lease !== 'worker-v1') { locks.delete(ownerPath); return; }
|
|
332
|
-
const
|
|
333
|
+
if (!owner || owner.token !== entry.token || owner.lease !== 'worker-v1') { locks.delete(ownerPath); return; }
|
|
334
|
+
const apply = index => index < entry.roots.length
|
|
335
|
+
? withRuntimeWrites(entry.roots[index], () => apply(index + 1))
|
|
336
|
+
: fs.utimesSync(ownerPath, new Date(), new Date());
|
|
337
|
+
apply(0);
|
|
333
338
|
} catch { locks.delete(ownerPath); }
|
|
334
339
|
}
|
|
335
340
|
parentPort.on('message', (m) => {
|
|
336
341
|
if (!m || typeof m.ownerPath !== 'string') return;
|
|
337
|
-
if (m.op === 'add' && typeof m.token === 'string') {
|
|
342
|
+
if (m.op === 'add' && typeof m.token === 'string') {
|
|
343
|
+
const entry = { token: m.token, roots: Array.isArray(m.roots) ? m.roots : [] };
|
|
344
|
+
locks.set(m.ownerPath, entry); touch(m.ownerPath, entry);
|
|
345
|
+
}
|
|
338
346
|
else if (m.op === 'remove') locks.delete(m.ownerPath);
|
|
339
347
|
});
|
|
340
348
|
const timer = setInterval(() => { for (const [p, t] of locks) touch(p, t); }, 1000);
|
|
341
349
|
if (timer.unref) timer.unref();
|
|
342
|
-
`, { eval: true });
|
|
350
|
+
`, { eval: true, workerData: { runtimeWrites: require.resolve('../lib/runtime-writes') } });
|
|
343
351
|
worker.on('error', () => { _lockHeartbeatWorker = null; _lockHeartbeatUnavailable = true; });
|
|
344
352
|
worker.on('exit', (code) => { _lockHeartbeatWorker = null; if (code !== 0) _lockHeartbeatUnavailable = true; });
|
|
345
353
|
worker.unref();
|
|
@@ -348,7 +356,7 @@ function _ensureLockHeartbeat() {
|
|
|
348
356
|
} catch { _lockHeartbeatUnavailable = true; return false; }
|
|
349
357
|
}
|
|
350
358
|
function _lockHeartbeat(op, ownerPath, token) {
|
|
351
|
-
try { if (_lockHeartbeatWorker) _lockHeartbeatWorker.postMessage({ op, ownerPath, token }); } catch {}
|
|
359
|
+
try { if (_lockHeartbeatWorker) _lockHeartbeatWorker.postMessage({ op, ownerPath, token, roots: require('../lib/runtime-writes').runtimeWriteRoots() }); } catch {}
|
|
352
360
|
}
|
|
353
361
|
function _newLockOwner(lease) {
|
|
354
362
|
const hostId = _lockHostIdentity();
|
|
@@ -420,6 +428,9 @@ function _reclaimStaleLock(lockPath, staleMs) {
|
|
|
420
428
|
// 경고를 못 전달하는 것은 감수한다 — 그 때문에 성공을 실패로 보고하는 것은 감수하지 않는다.
|
|
421
429
|
for (const _st of [process.stderr, process.stdout]) { try { _st.on('error', (e) => { if (!e || e.code !== 'EPIPE') return; }); } catch {} }
|
|
422
430
|
function _withLock(targetPath, fn, opts = {}) {
|
|
431
|
+
return withRuntimePathWrite(targetPath, () => _withLockBody(targetPath, fn, opts));
|
|
432
|
+
}
|
|
433
|
+
function _withLockBody(targetPath, fn, opts = {}) {
|
|
423
434
|
const lockPath = targetPath + '.lock';
|
|
424
435
|
// 1.36.144 (T-0117): dry-run 이면 **락을 잡지 않는다** — 쓰기가 어차피 가드에 막히므로 지킬 것이 없고,
|
|
425
436
|
// 락 디렉토리를 만드는 것만으로 "변경 없음" 이 거짓이 된다(실측: `state *` 8건이 빈 `.leerness/` 를 남겼다).
|
|
@@ -611,7 +622,7 @@ function _withLock(targetPath, fn, opts = {}) {
|
|
|
611
622
|
throw e;
|
|
612
623
|
};
|
|
613
624
|
let out;
|
|
614
|
-
try { out = fn(); }
|
|
625
|
+
try { assertCurrentRuntimeWrite(targetPath); out = fn(); }
|
|
615
626
|
catch (e) { return releaseAndRethrow(e); }
|
|
616
627
|
let then = null;
|
|
617
628
|
try { then = out && out.then; }
|
|
@@ -882,8 +893,13 @@ function _errorUiLang(root, command) {
|
|
|
882
893
|
const parsed = nonFlagArgs();
|
|
883
894
|
const resolvedCommand = command || parsed[0];
|
|
884
895
|
if (resolvedCommand === 'mode' && has('--json')) return 'ko';
|
|
896
|
+
if (resolvedCommand === 'state' && ['inspect', 'compatibility'].includes(parsed[1])) return _inspectionUiLang();
|
|
885
897
|
return _uiLang(resolvedCommand === 'mode' ? _modeRoot(parsed, root) : root);
|
|
886
898
|
}
|
|
899
|
+
function _inspectionUiLang() {
|
|
900
|
+
const language = String(arg('--language', process.env.LEERNESS_LANG || 'ko')).toLowerCase();
|
|
901
|
+
return language === 'en' ? 'en' : 'ko';
|
|
902
|
+
}
|
|
887
903
|
// ko/en 쌍에서 해석된 UI 언어로 선택 (Phase 1: 첫 화면 한정 사용).
|
|
888
904
|
function _tx(lang, ko, en) { return lang === 'en' ? en : ko; }
|
|
889
905
|
function fm(role, readWhen, updateWhen, body, lang = 'ko') {
|
|
@@ -4252,6 +4268,33 @@ function _selftestFixture(lang) {
|
|
|
4252
4268
|
return dst;
|
|
4253
4269
|
}
|
|
4254
4270
|
|
|
4271
|
+
// Only these in-process diagnostics own this cleanup path. Windows can keep a
|
|
4272
|
+
// child-process cwd open briefly; native rmSync retry options do not reliably
|
|
4273
|
+
// wait for that case on supported Node versions. Never hide exhausted cleanup.
|
|
4274
|
+
function _stRemoveInProcessFixture(target) {
|
|
4275
|
+
const tempRoot = fs.realpathSync.native(os.tmpdir());
|
|
4276
|
+
const initial = fs.lstatSync(target, { bigint: true });
|
|
4277
|
+
const canonical = fs.realpathSync.native(target);
|
|
4278
|
+
const invalid = () => Object.assign(new Error('Selftest fixture cleanup identity changed'), { code: 'E_SELFTEST_FIXTURE_CHANGED' });
|
|
4279
|
+
if (!initial.isDirectory() || initial.isSymbolicLink() || path.dirname(canonical) !== tempRoot
|
|
4280
|
+
|| !/^__leerness_(anchor_redraft|next_action|addjson)_[A-Za-z0-9]{6}$/.test(path.basename(canonical))) throw invalid();
|
|
4281
|
+
const deadline = process.hrtime.bigint() + 2500000000n;
|
|
4282
|
+
const waitCell = new Int32Array(new SharedArrayBuffer(4));
|
|
4283
|
+
for (;;) {
|
|
4284
|
+
let current;
|
|
4285
|
+
try { current = fs.lstatSync(canonical, { bigint: true }); }
|
|
4286
|
+
catch (error) { if (error.code === 'ENOENT') return; throw error; }
|
|
4287
|
+
if (!current.isDirectory() || current.isSymbolicLink() || current.dev !== initial.dev || current.ino !== initial.ino
|
|
4288
|
+
|| fs.realpathSync.native(canonical) !== canonical || fs.realpathSync.native(os.tmpdir()) !== tempRoot) throw invalid();
|
|
4289
|
+
try { fs.rmSync(canonical, { recursive: true, force: true }); return; }
|
|
4290
|
+
catch (error) {
|
|
4291
|
+
const remaining = Number(deadline - process.hrtime.bigint()) / 1e6;
|
|
4292
|
+
if (!['EPERM', 'EBUSY', 'ENOTEMPTY'].includes(error.code) || remaining <= 0) throw error;
|
|
4293
|
+
Atomics.wait(waitCell, 0, 0, Math.min(100, remaining));
|
|
4294
|
+
}
|
|
4295
|
+
}
|
|
4296
|
+
}
|
|
4297
|
+
|
|
4255
4298
|
// 1.9.258: leerness selftest — 설치된 leerness 바이너리의 코어 순수 함수 자가 검증.
|
|
4256
4299
|
// 1.9.255~257 에서 export 한 보안/정확성/인코딩-핵심 함수를 실제 호출해 무결성 확인.
|
|
4257
4300
|
// 사용자/CI 가 "내 leerness 가 정상인가?" 를 검증 (행위 검사가 포함돼 환경에 따라 수십 초 소요).
|
|
@@ -4643,7 +4686,7 @@ function _selfTestCases() {
|
|
|
4643
4686
|
&& rb.includes('새 정체성 신호') && rp.includes('새 정체성 신호');
|
|
4644
4687
|
} catch {} finally {
|
|
4645
4688
|
process.argv = savedArgv; process.stdout.write = savedWrite;
|
|
4646
|
-
|
|
4689
|
+
_stRemoveInProcessFixture(redraw);
|
|
4647
4690
|
}
|
|
4648
4691
|
if (!(phraseOk && redraftOk)) return false;
|
|
4649
4692
|
// 무신호 시 발명 금지
|
|
@@ -4711,7 +4754,7 @@ function _selfTestCases() {
|
|
|
4711
4754
|
&& migratedProgress.command === 'leerness task list --path .'
|
|
4712
4755
|
&& migrated.every(a => typeof a.actionKey === 'string' && a.actionKey.length > 0);
|
|
4713
4756
|
} catch { return false; }
|
|
4714
|
-
finally {
|
|
4757
|
+
finally { _stRemoveInProcessFixture(arena); }
|
|
4715
4758
|
} },
|
|
4716
4759
|
{ name: 'managed 파일 force 병합 (1.36.37, 1.9.441 드리프트 실측): writeIfSafe 가 --force 여도 managed 는 라인-diff 병합 — CLAUDE 커스텀 보존 (행위검사)', run: () => {
|
|
4717
4760
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_wif_'));
|
|
@@ -4767,7 +4810,12 @@ function _selfTestCases() {
|
|
|
4767
4810
|
const s = read(__filename);
|
|
4768
4811
|
const tagOk = s.includes("'--list', 'v*'") && !s.includes("'--list', 'v1.9" + ".*'");
|
|
4769
4812
|
const celebOk = (s.match(/이 저장소 계보의 릴리스 태그 이력 없음/g) || []).length >= 2;
|
|
4770
|
-
const
|
|
4813
|
+
const priorArgv = process.argv;
|
|
4814
|
+
let usageOk;
|
|
4815
|
+
try {
|
|
4816
|
+
process.argv = ['node', 'leerness', 'scan', 'secrets', tmp];
|
|
4817
|
+
usageOk = _runtimeProjectRoot(process.argv.slice(2), 'scan') === absRoot(tmp);
|
|
4818
|
+
} finally { process.argv = priorArgv; }
|
|
4771
4819
|
const cutoffWired = s.includes('_retroAggregate(root, cutoff)');
|
|
4772
4820
|
return filterOk && sigOk && tagOk && celebOk && usageOk && cutoffWired;
|
|
4773
4821
|
} },
|
|
@@ -5284,6 +5332,7 @@ function _selfTestCases() {
|
|
|
5284
5332
|
{ name: '8번째 버그헌트 (UR-0112): _parseSkillMd CRLF/CR 줄바꿈 정규화 (Windows SKILL.md meta 소실 차단) (1.9.408)', run: () => { const m = require('../lib/pure-utils'); const lf = m._parseSkillMd('---\nname: s\ndescription: d\n---\nbody'); const crlf = m._parseSkillMd('---\r\nname: s\r\ndescription: d\r\n---\r\nbody'); const cr = m._parseSkillMd('---\rname: s\rdescription: d\r---\rbody'); const bom = m._parseSkillMd('---\r\nname: s\r\n---\r\nbody'); return lf.meta.name === 's' && crlf.meta.name === 's' && crlf.meta.description === 'd' && cr.meta.name === 's' && bom.meta.name === 's'; } },
|
|
5285
5333
|
{ name: '8번째 버그헌트 (UR-0113): env encoding-check --apply 셸/플랫폼/모호 인코딩 안전계약 (1.9.409/T-0022)', run: () => {
|
|
5286
5334
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_bom_'));
|
|
5335
|
+
const priorExit = process.exitCode;
|
|
5287
5336
|
try {
|
|
5288
5337
|
const sh = path.join(tmp, 's.sh');
|
|
5289
5338
|
const ps = path.join(tmp, 's.ps1');
|
|
@@ -5291,12 +5340,12 @@ function _selfTestCases() {
|
|
|
5291
5340
|
fs.writeFileSync(sh, '#!/bin/bash\n# 한글\necho hi\n');
|
|
5292
5341
|
fs.writeFileSync(ps, 'Write-Host "😀"\n');
|
|
5293
5342
|
fs.writeFileSync(ambiguous, Buffer.from([0x41, 0xC2, 0xA1, 0x42]));
|
|
5294
|
-
const save = process.argv; let out = ''; const _w = process.stdout.write;
|
|
5343
|
+
const save = process.argv; let out = ''; let thrown = null; const _w = process.stdout.write;
|
|
5295
5344
|
try {
|
|
5296
5345
|
process.argv = ['node', 'h', 'env', 'encoding-check', '--path', tmp, '--apply', '--json'];
|
|
5297
5346
|
process.stdout.write = s => { out += s; return true; };
|
|
5298
5347
|
envCmd(tmp, 'encoding-check');
|
|
5299
|
-
} catch {} finally { process.stdout.write = _w; process.argv = save; }
|
|
5348
|
+
} catch (error) { thrown = error; } finally { process.stdout.write = _w; process.argv = save; }
|
|
5300
5349
|
const shBuf = fs.readFileSync(sh);
|
|
5301
5350
|
const psBuf = fs.readFileSync(ps);
|
|
5302
5351
|
const ambiguousBuf = fs.readFileSync(ambiguous);
|
|
@@ -5304,8 +5353,24 @@ function _selfTestCases() {
|
|
|
5304
5353
|
const platformOk = process.platform === 'win32'
|
|
5305
5354
|
? psBuf.subarray(0, 3).equals(Buffer.from([0xEF, 0xBB, 0xBF]))
|
|
5306
5355
|
: psBuf.equals(Buffer.from('Write-Host "😀"\n'));
|
|
5307
|
-
|
|
5308
|
-
|
|
5356
|
+
const ambiguousOk = ambiguousBuf.equals(Buffer.from([0x41, 0xC2, 0xA1, 0x42]));
|
|
5357
|
+
if (thrown || !shNoBom || !platformOk || !ambiguousOk) {
|
|
5358
|
+
let report;
|
|
5359
|
+
try { report = JSON.parse(out); } catch { report = null; }
|
|
5360
|
+
// selftest는 실패 판정을 유지하고, 폐기하던 bounded 원인만 전달한다.
|
|
5361
|
+
const detail = {
|
|
5362
|
+
shNoBom, platformOk, ambiguousOk, exit: process.exitCode == null ? null : process.exitCode,
|
|
5363
|
+
thrown: thrown ? { code: String(thrown.code || 'error').slice(0, 80), message: String(thrown.message || '').slice(0, 400) } : null,
|
|
5364
|
+
scanErrors: report && Array.isArray(report.scanErrors) ? report.scanErrors.slice(0, 4).map(item => String(item.code || item.error || item).slice(0, 180)) : [],
|
|
5365
|
+
applied: report && Array.isArray(report.applied) ? report.applied.slice(0, 4).map(item => ({ file: path.basename(String(item.file || '')).slice(0, 80), action: String(item.action || '').slice(0, 80), code: String(item.code || '').slice(0, 80), error: String(item.error || '').slice(0, 300) })) : [],
|
|
5366
|
+
};
|
|
5367
|
+
throw new Error(`UR-0113 encoding contract failed: ${JSON.stringify(detail).slice(0, 3600)}`);
|
|
5368
|
+
}
|
|
5369
|
+
return true;
|
|
5370
|
+
} finally {
|
|
5371
|
+
process.exitCode = priorExit;
|
|
5372
|
+
try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {}
|
|
5373
|
+
}
|
|
5309
5374
|
} },
|
|
5310
5375
|
{ name: '8번째 버그헌트 (UR-0114): absRoot 비문자열(--path 값없음 boolean true) → cwd 폴백(raw TypeError 차단) (1.9.410)', run: () => { const io = require('../lib/io'); const cwd = process.cwd(); const tBool = io.absRoot(true) === cwd; const tEmpty = io.absRoot('') === cwd; const tUndef = io.absRoot(undefined) === cwd; const tSpace = io.absRoot(' ') === cwd; const tReal = io.absRoot(os.tmpdir()) === path.resolve(os.tmpdir()); return tBool && tEmpty && tUndef && tSpace && tReal; } },
|
|
5311
5376
|
{ name: '8번째 버그헌트 (UR-0115): lazy detect --auto-track 단일 RMW 배치(O(T×N)→O(N+T)) (1.9.411)', run: () => { const src = read(__filename); const batched = src.includes("8번째 버그헌트, UR-0115") && /has\('--auto-track'\)[\s\S]{0,500}?_withLock\(progressPath\(root\), \(\) => \{[\s\S]{0,1200}?writeProgressRows/.test(src); const noPerTodoUpsert = !/for \(const t of newTodos\) \{\s*const id = nextId\(root, 'T'\);/.test(src); return batched && noPerTodoUpsert; } },
|
|
@@ -5331,7 +5396,10 @@ function _selfTestCases() {
|
|
|
5331
5396
|
l = cap(['node', 'h', 'lesson', 'save', 'JSON계약 L', '--json', '--tag', 'tg'], () => lessonSave(tmp, 'JSON계약 L'));
|
|
5332
5397
|
r = cap(['node', 'h', 'rule', 'add', 'JSON계약 R', '--json', '--trigger', 'every-session'], () => ruleAdd(tmp, 'JSON계약 R'));
|
|
5333
5398
|
rDup = cap(['node', 'h', 'rule', 'add', 'JSON계약 R', '--json', '--trigger', 'every-session'], () => ruleAdd(tmp, 'JSON계약 R'));
|
|
5334
|
-
} catch (e) { return false; } finally {
|
|
5399
|
+
} catch (e) { return false; } finally {
|
|
5400
|
+
process.stdout.write = _w; process.argv = save; process.exitCode = savedExit;
|
|
5401
|
+
_stRemoveInProcessFixture(tmp);
|
|
5402
|
+
}
|
|
5335
5403
|
const taskJ = !!t && t.ok === true && /^T-\d{4}/.test(String(t.id)) && t.status === 'requested' && t.request === 'JSON계약 T';
|
|
5336
5404
|
const decJ = !!d && d.ok === true && d.title === 'JSON계약 D';
|
|
5337
5405
|
const lesJ = !!l && l.ok === true && l.text === 'JSON계약 L' && l.tag === 'tg';
|
|
@@ -5983,7 +6051,10 @@ function _selfTestCases() {
|
|
|
5983
6051
|
{ name: '14th 버그헌트 (UR-0181): MCP _bumpMcpUsage 를 unknown-tool 검증 후로 이동(unknown tool 임의경로 쓰기 차단) (1.12.2)', run: () => {
|
|
5984
6052
|
const src = read(__filename);
|
|
5985
6053
|
// _bumpMcpUsage 호출이 unknown-tool 가드(cliArgs === null return) "뒤"에 위치 — generic 서버 path-타게팅은 유지(취약점 아님), unknown tool 쓰기만 차단.
|
|
5986
|
-
|
|
6054
|
+
const dispatch = src.slice(src.indexOf('function ' + 'mcpServeCmd('));
|
|
6055
|
+
const unknownGuard = dispatch.indexOf('if (cliArgs === null) return send');
|
|
6056
|
+
const usageWrite = dispatch.indexOf('_bumpMcpUsage(targetPath, name)');
|
|
6057
|
+
return unknownGuard >= 0 && usageWrite > unknownGuard;
|
|
5987
6058
|
} },
|
|
5988
6059
|
{ name: '14th 버그헌트 P2/P3 (UR-0182/0183): lazy TODO 파일별 추적 + session close 완료 정직성 advisory (1.12.3)', run: () => {
|
|
5989
6060
|
const src = read(__filename);
|
|
@@ -10840,6 +10911,9 @@ function _p0014SecretBaselineOk() {
|
|
|
10840
10911
|
const d3 = path.join(d, 'workspace-dir-migration');
|
|
10841
10912
|
const canonicalDir = path.join(d3, '.leerness');
|
|
10842
10913
|
fs.mkdirSync(canonicalDir, { recursive: true });
|
|
10914
|
+
// This is a migrated workspace, not an unowned directory containing an
|
|
10915
|
+
// arbitrary fixture file. Establish the same ownership marker as migration.
|
|
10916
|
+
fs.writeFileSync(path.join(canonicalDir, 'HARNESS_VERSION'), VERSION + '\n');
|
|
10843
10917
|
const migratedRel = '.leerness/migrated-fixture.test.js';
|
|
10844
10918
|
const migratedFile = path.join(d3, migratedRel);
|
|
10845
10919
|
const oldKey = 'sk-proj-MigratedFixtureKey1234567890abcd';
|
|
@@ -11487,6 +11561,8 @@ function commandsCmd(root) {
|
|
|
11487
11561
|
{ cmd: 'roles list|set|unset|catalog|suggest|verify|validate', desc: '모델별 역할 부여 + fail-closed 저장소 검증 — 1.9.270/T-0171', descEn: 'assign roles and validate the role store fail-closed — 1.9.270/T-0171' },
|
|
11488
11562
|
{ cmd: 'capabilities [--json]', desc: '권한·보안 표면 공개 (무엇을 하는지 + opt-out + 주의 명령) — 1.9.272', descEn: 'disclose the permission/security surface (what it does + opt-outs + commands to watch) — 1.9.272' },
|
|
11489
11563
|
{ cmd: 'state show|start|record|verify|handoff', desc: '.leerness/ JSON 상태 substrate (에이전트 간 인수인계 표준) — 1.9.278', descEn: '.leerness/ JSON state substrate (cross-agent handoff standard) — 1.9.278' },
|
|
11564
|
+
{ cmd: 'state inspect [path] [--json]', desc: '5-scope 현재/제안 경로와 기존 상태 목록 — 읽기 전용, 이동·활성화 없음', descEn: 'five-scope current/proposed paths and legacy inventory — read-only, no migration or activation' },
|
|
11565
|
+
{ cmd: 'state compatibility [path] [--json]', desc: '관측한 runtime layout의 쓰기 호환성 — 읽기 전용, 이동·활성화 없음', descEn: 'observed runtime-layout write compatibility — read-only, no migration or activation' },
|
|
11490
11566
|
{ cmd: 'adapter <tool>|list [--dry-run]', desc: '도구별 지침/.mcp.json 선택 생성 (claude/cursor/codex/goose/...) — 1.9.280', descEn: 'generate per-tool instructions/.mcp.json selectively (claude/cursor/codex/goose/...) — 1.9.280' },
|
|
11491
11567
|
{ cmd: 'ci init [path] [--force]', desc: 'PR 마다 leerness gate 실행하는 GitHub Actions 워크플로 생성 (.github/workflows/leerness-gate.yml) — 1.9.444', descEn: 'create a GitHub Actions workflow that runs leerness gate on every PR (.github/workflows/leerness-gate.yml) — 1.9.444' },
|
|
11492
11568
|
{ cmd: 'export|prompt --target <agent>', desc: 'adapter 별칭 — 도구별 지침/계약 파일 생성 (claude/cursor/codex/agents-md/...) — 1.9.448', descEn: 'adapter alias — generate per-tool instruction/contract files (claude/cursor/codex/agents-md/...) — 1.9.448' },
|
|
@@ -12181,6 +12257,7 @@ const _OBSERVATION_ONLY_SUBCOMMANDS = new Map([
|
|
|
12181
12257
|
['lease', new Set(['list', 'check'])],
|
|
12182
12258
|
['roles', new Set(['validate'])],
|
|
12183
12259
|
['role', new Set(['validate'])],
|
|
12260
|
+
['state', new Set(['inspect', 'compatibility'])],
|
|
12184
12261
|
]);
|
|
12185
12262
|
function _cliMutationClass(args, cmd) {
|
|
12186
12263
|
if (cmd === 'mcp') return 'observation-only'; // server startup must not mutate its incidental cwd
|
|
@@ -12195,6 +12272,88 @@ function _cliMutationClass(args, cmd) {
|
|
|
12195
12272
|
}
|
|
12196
12273
|
function _mustRemainReadOnly(args, cmd) { return _cliMutationClass(args, cmd) === 'observation-only'; }
|
|
12197
12274
|
|
|
12275
|
+
// 시작 bookkeeping도 실제 handler의 project target을 사용한다. 제목/출력 파일을
|
|
12276
|
+
// 디렉터리라는 이유만으로 project로 추측하거나, 명시 target 옆의 cwd를 쓰지 않는다.
|
|
12277
|
+
function _runtimeProjectRoot(args, cmd) {
|
|
12278
|
+
const sub = args[1];
|
|
12279
|
+
const cwd = process.cwd();
|
|
12280
|
+
const explicit = arg('--path', null);
|
|
12281
|
+
const at = index => typeof args[index] === 'string' && !args[index].startsWith('-') ? args[index] : null;
|
|
12282
|
+
const choose = (positional, positionalFirst = false) => absRoot(positionalFirst
|
|
12283
|
+
? positional || explicit || cwd : explicit || positional || cwd);
|
|
12284
|
+
const global = new Set(['mcp', 'about', 'identity', 'help', 'which', 'doctor', 'selftest', 'self-test',
|
|
12285
|
+
'install-safety', 'lens', 'route', 'path', 'path-setup']);
|
|
12286
|
+
if (global.has(cmd) || (cmd === 'adapter' && has('--global')) || !_isKnownTopLevelCommand(cmd)) return null;
|
|
12287
|
+
if (cmd === 'mode') {
|
|
12288
|
+
const root = _modeRoot(args);
|
|
12289
|
+
// Invalid mode targets already have a locale-independent path diagnostic.
|
|
12290
|
+
// They cannot own bookkeeping or a workspace, so leave validation to it.
|
|
12291
|
+
try { if (exists(root) && !fs.statSync(root).isDirectory()) return null; } catch {}
|
|
12292
|
+
return root;
|
|
12293
|
+
}
|
|
12294
|
+
|
|
12295
|
+
// These handlers own files rather than a project target.
|
|
12296
|
+
if (cmd === 'contract' && sub === 'verify') return null;
|
|
12297
|
+
if ((cmd === 'scan' && sub === 'secrets') || (cmd === 'encoding' && sub === 'check')) {
|
|
12298
|
+
const root = choose(at(2));
|
|
12299
|
+
try { if (exists(root) && !fs.statSync(root).isDirectory()) return null; } catch {}
|
|
12300
|
+
return root;
|
|
12301
|
+
}
|
|
12302
|
+
if (cmd === 'self' && sub === 'migrate') return null;
|
|
12303
|
+
if (cmd === 'migrate') {
|
|
12304
|
+
if (has('--guide') || sub === 'guide') return null;
|
|
12305
|
+
return choose(['audit', 'apply', 'plan'].includes(sub) || has('--audit') ? at(2) : at(1));
|
|
12306
|
+
}
|
|
12307
|
+
// install's first positional is a SKILL.md / URL / source directory.
|
|
12308
|
+
if (cmd === 'install') return choose();
|
|
12309
|
+
// plan's positional contracts are intentionally different by subcommand.
|
|
12310
|
+
if (cmd === 'plan') {
|
|
12311
|
+
if (sub === 'init') return choose(at(2), true);
|
|
12312
|
+
return choose(sub === 'list' ? at(2) : null);
|
|
12313
|
+
}
|
|
12314
|
+
if (cmd === 'decision' && sub === 'list') return choose(at(2));
|
|
12315
|
+
if (cmd === 'context' && sub === 'budget') return choose(_taskPositionalPath(args, 2));
|
|
12316
|
+
if (cmd === 'parent') return choose(args.slice(2).find(value => value && !value.startsWith('-')));
|
|
12317
|
+
if (cmd === 'memory' && sub === 'archive' && args[2] === 'list') return choose(at(3));
|
|
12318
|
+
if (cmd === 'skill') {
|
|
12319
|
+
if (sub === 'list') return choose(at(2));
|
|
12320
|
+
if (sub === 'impact') return choose(at(2), true);
|
|
12321
|
+
if (sub === 'lint') return choose(at(2) && /^([A-Za-z]:[\\/]|\/|\.\.?[\\/])/.test(at(2)) ? at(2) : null);
|
|
12322
|
+
return choose();
|
|
12323
|
+
}
|
|
12324
|
+
// Keep invalid operands for the handler to diagnose without bookkeeping.
|
|
12325
|
+
if (cmd === 'bugfix') {
|
|
12326
|
+
if (!sub || sub === 'list') return choose(_taskPositionalPath(args, sub ? 2 : 1));
|
|
12327
|
+
if (['start', 'receipt', 'drop'].includes(sub)) {
|
|
12328
|
+
const trailing = args.slice(3).filter(Boolean);
|
|
12329
|
+
if (!trailing.length) return choose();
|
|
12330
|
+
if (trailing.length !== 1 || explicit || !/^([A-Za-z]:[\\/]|\\\\|\/|\.\.?[\\/])/.test(trailing[0])) return null;
|
|
12331
|
+
return choose(trailing[0]);
|
|
12332
|
+
}
|
|
12333
|
+
return choose();
|
|
12334
|
+
}
|
|
12335
|
+
|
|
12336
|
+
if (['health', 'whats-new', 'benchmark'].includes(cmd)) return choose(at(1), true);
|
|
12337
|
+
if (['drift', 'usage', 'adapter', 'glossary', 'env'].includes(cmd)
|
|
12338
|
+
|| (cmd === 'reuse' && sub === 'autodetect')
|
|
12339
|
+
|| (cmd === 'release' && ['bump', 'publish', 'pack', 'sync-main', 'cleanup', 'channel'].includes(sub))) return choose(at(2), true);
|
|
12340
|
+
|
|
12341
|
+
const first = new Set(['init', 'update', 'status', 'verify', 'debug', 'audit', 'check',
|
|
12342
|
+
'handoff', 'reuse-map', 'gate', 'verify-code', 'retro', 'insights', 'sessions', 'context',
|
|
12343
|
+
'round-history', 'milestones', 'pulse', 'migrate-workspace-dir', 'graph', 'setup-agents']);
|
|
12344
|
+
if (first.has(cmd) || (cmd === 'roadmap' && sub !== 'auto')) return choose(at(1));
|
|
12345
|
+
const second = new Set(['lazy', 'hook', 'session', 'self', 'readme', 'consistency', 'auto-update']);
|
|
12346
|
+
const secondRoutes = new Set(['release cadence', 'team list', 'feature list', 'runs list',
|
|
12347
|
+
'setup agents', 'ci init', 'ci ', 'memory status', 'ui consistency']);
|
|
12348
|
+
if (second.has(cmd) || secondRoutes.has(`${cmd} ${sub || ''}`)) return choose(at(2));
|
|
12349
|
+
const shapedFirst = new Set(['lessons', 'resume', 'session-resume', 'anchors', 'tech', 'library',
|
|
12350
|
+
'dashboard', 'commands', 'py-check', 'capabilities', 'security-surface']);
|
|
12351
|
+
if (shapedFirst.has(cmd)) return choose(_taskPositionalPath(args, 1));
|
|
12352
|
+
const shapedSecond = new Set(['task', 'decision', 'lesson', 'requests', 'enforce', 'integrity', 'feature']);
|
|
12353
|
+
if (shapedSecond.has(cmd) || (cmd === 'rule' && sub === 'add')) return choose(_taskPositionalPath(args, 2));
|
|
12354
|
+
return choose();
|
|
12355
|
+
}
|
|
12356
|
+
|
|
12198
12357
|
function _autoMigrateLegacyWorkspace(args, cmd) {
|
|
12199
12358
|
if (process.env.LEERNESS_NO_AUTO_WORKSPACE_MIGRATION === '1') return true;
|
|
12200
12359
|
if (has('--dry-run') || cmd === 'migrate-workspace-dir' || cmd === 'workspace-dir') return true;
|
|
@@ -12202,18 +12361,14 @@ function _autoMigrateLegacyWorkspace(args, cmd) {
|
|
|
12202
12361
|
// Invalid commands must be observationally pure. In particular, a typo in
|
|
12203
12362
|
// a legacy project may not rename project memory before reporting the typo.
|
|
12204
12363
|
if (!_isKnownTopLevelCommand(cmd)) return true;
|
|
12205
|
-
const
|
|
12206
|
-
const
|
|
12207
|
-
if (typeof explicit === 'string' && explicit.trim()) candidates.push(explicit);
|
|
12208
|
-
else candidates.push(process.cwd());
|
|
12364
|
+
const targetRoot = _runtimeProjectRoot(args, cmd);
|
|
12365
|
+
const candidates = targetRoot ? [targetRoot] : [];
|
|
12209
12366
|
// Never interpret arbitrary command content as a project to mutate. Topics,
|
|
12210
12367
|
// titles and output paths may all be path-shaped (for example
|
|
12211
12368
|
// `brainstorm ./legacy-design`); scanning every positional token used to
|
|
12212
12369
|
// migrate those unrelated directories before command dispatch. Positional
|
|
12213
|
-
// project targets
|
|
12214
|
-
//
|
|
12215
|
-
// global `--path` option. When it is present, the unrelated process cwd is
|
|
12216
|
-
// not another migration target.
|
|
12370
|
+
// project targets follow the same selector as command dispatch. The unrelated
|
|
12371
|
+
// process cwd is never added as another target when a project is specified.
|
|
12217
12372
|
const seen = new Set();
|
|
12218
12373
|
for (const candidate of candidates) {
|
|
12219
12374
|
let root;
|
|
@@ -12238,19 +12393,8 @@ function _autoMigrateLegacyWorkspace(args, cmd) {
|
|
|
12238
12393
|
}
|
|
12239
12394
|
|
|
12240
12395
|
function _guardLinkedWorkspaceTargets(args) {
|
|
12241
|
-
const
|
|
12242
|
-
const
|
|
12243
|
-
if (typeof explicit === 'string' && explicit.trim()) candidates.push(explicit);
|
|
12244
|
-
// Positional project arguments vary by command. Inspecting an existing
|
|
12245
|
-
// directory is read-only, so include every such token rather than risk a
|
|
12246
|
-
// hardcoded writer following `<project>/.leerness` outside the project.
|
|
12247
|
-
for (const token of (args || []).slice(1)) {
|
|
12248
|
-
if (typeof token !== 'string' || token.startsWith('-')) continue;
|
|
12249
|
-
try {
|
|
12250
|
-
const candidate = absRoot(token);
|
|
12251
|
-
if (exists(candidate) && fs.statSync(candidate).isDirectory()) candidates.push(candidate);
|
|
12252
|
-
} catch {}
|
|
12253
|
-
}
|
|
12396
|
+
const root = _runtimeProjectRoot(args, args[0] || 'init');
|
|
12397
|
+
const candidates = root ? [root] : [];
|
|
12254
12398
|
const seen = new Set();
|
|
12255
12399
|
for (const candidate of candidates) {
|
|
12256
12400
|
let state;
|
|
@@ -15563,6 +15707,8 @@ const _STRICT_COMMAND_FLAGS = {
|
|
|
15563
15707
|
'lease list': { allowed: ['--session', '--all'], globals: [..._COMMON_COMMAND_FLAGS, '--json'], usage: 'lease list [--session <key>] [--all] [--json]' },
|
|
15564
15708
|
'lease check': { allowed: ['--session'], globals: [..._COMMON_COMMAND_FLAGS, '--json'], usage: 'lease check <file> [--session <key>] [--json]' },
|
|
15565
15709
|
'roles validate': { allowed: [], globals: [..._COMMON_COMMAND_FLAGS, '--json'], usage: 'roles validate [--path .] [--json]' },
|
|
15710
|
+
'state inspect': { allowed: [], globals: [..._COMMON_COMMAND_FLAGS, '--json'], usage: 'state inspect [path] [--path <path>] [--json]' },
|
|
15711
|
+
'state compatibility': { allowed: [], globals: [..._COMMON_COMMAND_FLAGS, '--json'], usage: 'state compatibility [path] [--path <path>] [--json]' },
|
|
15566
15712
|
'roles set': { allowed: ['--provider', '--to', '--model', '--model-family', '--policy', '--fallback-policy', '--candidate', '--candidate-family', '--remove-candidate', '--clear-candidates', '--persona', '--force'], globals: [..._COMMON_COMMAND_FLAGS, '--json'], usage: 'roles set <role> --provider <id> [--model <id>] [--policy strict|balanced|continuity]' },
|
|
15567
15713
|
'roles unset': { allowed: [], globals: [..._COMMON_COMMAND_FLAGS, '--json'], usage: 'roles unset <role> [--path .] [--json]' },
|
|
15568
15714
|
'roles suggest': { allowed: ['--apply'], globals: [..._COMMON_COMMAND_FLAGS, '--json'], usage: 'roles suggest [--apply] [--path .] [--json]' },
|
|
@@ -15644,6 +15790,13 @@ function _validateRolesCommandShape(cmd, args) {
|
|
|
15644
15790
|
}
|
|
15645
15791
|
|
|
15646
15792
|
function _validateCommandFlags(cmd, args) {
|
|
15793
|
+
if (cmd === 'state' && ['inspect', 'compatibility'].includes(args[1])) {
|
|
15794
|
+
if (!_rejectDuplicateValueFlags(['--path'])) return false;
|
|
15795
|
+
if (args.length > 3) {
|
|
15796
|
+
failJson(has('--json'), 'too_many_arguments', `Usage: state ${args[1]} [path] [--path <path>] [--json]`);
|
|
15797
|
+
return false;
|
|
15798
|
+
}
|
|
15799
|
+
}
|
|
15647
15800
|
if (cmd === 'lease' && !_rejectDuplicateValueFlags(['--session', '--ttl', '--note', '--path'])) return false;
|
|
15648
15801
|
if (cmd === 'agents' && args[1] === 'route'
|
|
15649
15802
|
&& !_rejectDuplicateValueFlags(['--tier', '--approved-by', '--reason', '--path'])) return false;
|
|
@@ -15656,6 +15809,7 @@ function _validateCommandFlags(cmd, args) {
|
|
|
15656
15809
|
if (cmd === 'intent' && args[1] === 'expand') route = 'intent expand';
|
|
15657
15810
|
if (cmd === 'lease') route = `lease ${args[1] == null ? 'list' : args[1]}`;
|
|
15658
15811
|
if (roleSub) route = `roles ${roleSub === 'remove' || roleSub === 'rm' ? 'unset' : roleSub}`;
|
|
15812
|
+
if (cmd === 'state' && ['inspect', 'compatibility'].includes(args[1])) route = `state ${args[1]}`;
|
|
15659
15813
|
const cfg = _STRICT_COMMAND_FLAGS[route];
|
|
15660
15814
|
if (!cfg) return true;
|
|
15661
15815
|
return _rejectUnknownFlags(cfg.allowed, cfg.usage, { globals: cfg.globals });
|
|
@@ -25790,6 +25944,9 @@ function _bumpUsageLocked(root, cmdName) {
|
|
|
25790
25944
|
|
|
25791
25945
|
// 1.9.70: MCP tools/call 자동 사용 통계 — 도구별 호출 카운트
|
|
25792
25946
|
function _bumpMcpUsage(root, toolName) {
|
|
25947
|
+
return withRuntimeWrites(root, () => _bumpMcpUsageBody(root, toolName));
|
|
25948
|
+
}
|
|
25949
|
+
function _bumpMcpUsageBody(root, toolName) {
|
|
25793
25950
|
try {
|
|
25794
25951
|
// 1.36.76 (9차 헌트 #2a): read-only 호출조차 텔레메트리가 대상 프로젝트에 디렉토리·파일을 만들었다
|
|
25795
25952
|
// (존재하지 않는 대상까지 통째로 생성). 통계는 이미 초기화된 하네스에만 기록 — 읽기가 대상을 변형하지 않는다.
|
|
@@ -27157,7 +27314,11 @@ function mcpServeCmd(root) {
|
|
|
27157
27314
|
const _mcpReadOnly = !!(_toolDef && _toolDef.requiredTier === 'read-only');
|
|
27158
27315
|
// A read-only tool may not mutate the target merely to count itself.
|
|
27159
27316
|
// Writable tools retain existing per-project MCP telemetry.
|
|
27160
|
-
if (!_mcpReadOnly) {
|
|
27317
|
+
if (!_mcpReadOnly) {
|
|
27318
|
+
require('../lib/runtime-layout').assertRuntimeWriteAllowed(targetPath);
|
|
27319
|
+
try { _bumpMcpUsage(targetPath, name); }
|
|
27320
|
+
catch (error) { if (error.code === 'E_RUNTIME_LAYOUT_INCOMPATIBLE') throw error; }
|
|
27321
|
+
}
|
|
27161
27322
|
// 1.9.288 (Codex gpt-5.5 리뷰 #1 수렴): MCP 도구도 policy enforce 적용 — read-only enforce 시 write 도구 차단.
|
|
27162
27323
|
// 이전: _policyEnforce 는 agents multi --execute 한 곳뿐 → MCP state_start 등이 정책 우회하고 .leerness 기록.
|
|
27163
27324
|
// cliArgs(실제 실행 명령) 로 required tier 판정 → enforce ON 이고 초과 시 JSON-RPC error 반환(실행 안 함).
|
|
@@ -28893,6 +29054,29 @@ function _updateRun(root, id, mutator) {
|
|
|
28893
29054
|
}
|
|
28894
29055
|
// 1.9.328 (UR-0025): _splitList → lib/pure-utils.js 로 이동 (순수 콤마 리스트 분할, require 사용).
|
|
28895
29056
|
|
|
29057
|
+
// Inspect is intentionally independent of all legacy state loaders and writers.
|
|
29058
|
+
function stateInspectCmd(root) {
|
|
29059
|
+
try {
|
|
29060
|
+
const { inspectState, formatStateInspection } = require('../lib/state-inspect');
|
|
29061
|
+
const report = inspectState(root);
|
|
29062
|
+
if (has('--json')) return log(JSON.stringify(report, null, 2));
|
|
29063
|
+
// Do not read a project manifest merely to render a metadata-only diagnostic.
|
|
29064
|
+
return log(formatStateInspection(report, _inspectionUiLang()));
|
|
29065
|
+
} catch (error) {
|
|
29066
|
+
const known = error.name === 'StatePathError' || error.name === 'WorkspaceDirectoryError';
|
|
29067
|
+
return failJson(has('--json'), known ? error.code : 'state_inspection_failed',
|
|
29068
|
+
known ? error.message : 'State inspection failed; no files were changed.');
|
|
29069
|
+
}
|
|
29070
|
+
}
|
|
29071
|
+
|
|
29072
|
+
function stateCompatibilityCmd(root) {
|
|
29073
|
+
const report = require('../lib/runtime-layout').inspectRuntimeCompatibility(root);
|
|
29074
|
+
if (has('--json')) log(JSON.stringify(report, null, 2));
|
|
29075
|
+
else log(`Runtime compatibility: ${report.writeDisposition} (${report.reasonCode})\nScope: ${report.scope}\nWriter protocol: ${report.supportedWriterProtocol}\nActivation: unsupported; legacy storage is unchanged.`);
|
|
29076
|
+
if (!report.compatible) process.exitCode = 1;
|
|
29077
|
+
return report;
|
|
29078
|
+
}
|
|
29079
|
+
|
|
28896
29080
|
// leerness state <show|start|record|verify|handoff>
|
|
28897
29081
|
// 1.9.292 (UR-0031): get_project_context — 외부 에이전트 온보딩용 단일 집약 컨텍스트.
|
|
28898
29082
|
// 1콜로 현재 작업/미답 요청/최근 결정/활성 룰/next-actions/memory/프로젝트 의도를 구조화 회수.
|
|
@@ -30401,6 +30585,7 @@ async function _agentRepl(root, opts) {
|
|
|
30401
30585
|
};
|
|
30402
30586
|
return new Promise(resolve => {
|
|
30403
30587
|
rl.on('line', async (line) => {
|
|
30588
|
+
try { await withRuntimeWrites(root, async () => {
|
|
30404
30589
|
const input = line.trim();
|
|
30405
30590
|
_lastCycleLines = 0; // 1.9.189: 사용자 입력 시 cycle overwrite 추적 reset
|
|
30406
30591
|
if (!input) { promptWithStatus(); return; }
|
|
@@ -30529,8 +30714,14 @@ async function _agentRepl(root, opts) {
|
|
|
30529
30714
|
} catch {}
|
|
30530
30715
|
}
|
|
30531
30716
|
promptWithStatus();
|
|
30717
|
+
}, { fresh: true }); }
|
|
30718
|
+
catch (error) { fail(error.message); rl.close(); resolve(); }
|
|
30719
|
+
});
|
|
30720
|
+
rl.on('close', () => {
|
|
30721
|
+
try { withRuntimeWrites(root, saveSession, { fresh: true }); }
|
|
30722
|
+
catch (error) { fail(error.message); }
|
|
30723
|
+
resolve();
|
|
30532
30724
|
});
|
|
30533
|
-
rl.on('close', () => { saveSession(); resolve(); });
|
|
30534
30725
|
});
|
|
30535
30726
|
}
|
|
30536
30727
|
|
|
@@ -31793,6 +31984,8 @@ SETUP & UPDATE
|
|
|
31793
31984
|
|
|
31794
31985
|
STATUS & DIAGNOSTICS
|
|
31795
31986
|
status [path] Install status (files present)
|
|
31987
|
+
state inspect [path] [--json] Read-only five-scope paths/inventory; no migration or runtime activation
|
|
31988
|
+
state compatibility [path] [--json] Read-only observed-layout write admission; no activation
|
|
31796
31989
|
health [path] | doctor [--json] health: project state · doctor: CLI install/environment (no path arg)
|
|
31797
31990
|
verify [path] Required-file verification
|
|
31798
31991
|
which [--json] Resolve current binary/version (npm cache conflicts)
|
|
@@ -31874,6 +32067,8 @@ function help() {
|
|
|
31874
32067
|
log(`Leerness v${VERSION}\n\nUsage:\n leerness init [path] [--language auto|ko|en] [--skills recommended|all|a,b]\n leerness migrate [path] [--dry-run] [--force]\n leerness update [path] [--check|--yes|--force|--from <tarball>]\n leerness auto-update install [path]\n leerness status [path]\n leerness verify [path]\n leerness debug [path]\n leerness audit [path]\n leerness check [path]\n leerness scan secrets [path]\n leerness encoding check [path]\n leerness lazy detect [path]\n leerness memory search "query" [--limit 5]\n leerness handoff [path] [--all-apps] [--include p1,p2] [--since 24h|3d] [--compact] [--json] # 1.9.17-22 워크스페이스 (--compact: LLM 시스템 프롬프트용 1줄 요약)\n leerness orchestrate "<목표>" [--agents N] [--model qwen2.5:7b-instruct] [--retry-on-fail K] # 1.9.22 Ollama opt-in (LEERNESS_OLLAMA_BASE_URL 필요)\n leerness llm-bench record --score N --model X [--label L] [--tokens T] # 1.9.22 LLM 벤치 히스토리 누적\n leerness deps <capability> [--run-tests] [--json] # 1.9.24 depends-on 역방향 추적 + 자동 회귀 sweep\n leerness memory search "키" [--include-code] # 1.9.25 소스 코드 본문도 검색 (모순 감지 핵심)\n leerness brainstorm "주제" [--include-code] # 1.9.25 코드 본문 hits 포함\n leerness register-pending "<요청>" [--agent X] [--note Y] # 1.9.25 다중 세션 in-progress 즉시 등록\n leerness optimism-check <T-ID> [--json] # 1.9.26/27 낙관적 표시 감지 (1.9.27: 10 카테고리 + URL/메서드 매핑 + 신뢰도 점수)\n leerness persona list|show <id>|add <id> # 1.9.29 페르소나 카탈로그 (보안/성능/UX/testing/docs 5종 내장)\n leerness review <file> --persona <id1,id2,...> # 1.9.29 도메인 페르소나 리뷰 프롬프트 자동 생성\n leerness agents list|check|quota # 외부 AI CLI 다축 가용성 확인\n leerness agents resolve \"<task>\" --role <role> [--preset strict|balanced|continuity] # 역할 유지 + 대체 선택지\n leerness agents fallback provider|session|direct|hold \"<task>\" --role <role> [--session-provider P --session-model M --session-model-family F] # 명시 선택·기록\n leerness agents record completed|failed|reviewed|validated \"<summary>\" [--task T-ID --role R --to P --model M --model-family F --agent P --agent-model M --agent-model-family F --evidence E]\n leerness agents history [--limit 20] # 실행자·검수자·결과 provenance\n leerness agents dispatch \"<task>\" --to <id> # 활성 CLI 대상 실행 명령 생성 (실 호출 X, 사용자 실행)\n leerness agents multi "<task>" [--only c1,c2] [--write] [--execute] [--timeout 60] # 1.9.152/156 활성 N개 일괄 dispatch (--execute: 실 spawn + consensus)\n leerness provider list|add|remove [args] # 1.9.157 Provider Registry — 사용자 정의 CLI provider 동적 추가 (OpenRouter/Bedrock 흡수)\n leerness agents dispatch "<task>" --multi # 1.9.152 multi 모드 alias (또는 --to all)\n leerness setup-agents [path] [--yes|--no-setup-agents] # 1.9.32 sub-agent CLI 인터랙티브 설정 (.env + 미설치 자동 설치)\n leerness init [path] [--no-stale-check] # 1.9.33 npx 캐시 함정 — 옛 버전 자동 경고 (끄려면 --no-stale-check)\n leerness which [--json] # 1.9.164 진단: 현재 실행 경로/버전 + npm 캐시 + PATH 후보 (구버전 충돌 해결)\n leerness selftest [--json] # 1.9.258 코어 함수 무결성 자가 검증 (설치 손상/부분설치 감지, CI 친화 exit 1)\n leerness shell-guard "<command>" [--json] # 1.9.260 터미널 명령 셸 호환성 린터 (PowerShell 5.1 && 미지원 등 실행 전 감지, UR-0020)\n leerness shell-guard --record --cmd "..." --exit N # 1.9.260 실패한 터미널 명령 기록 → 다음 분석 시 회수\n leerness path-setup [--apply] [--json] # 1.9.254 leerness CLI PATH 자동 등록 (npm global bin 미등록 시)\n leerness web check|screenshot|extract <url> [--out file.png] [--selector "css"] # 1.9.165 playwright bridge (opt-in: npm i -g playwright + permissions.browser)\n leerness pc check|click|type|screenshot [--x N --y N] [--text "s"] [--out f.png] # 1.9.166 robotjs/nut-tree bridge (opt-in: npm i -g robotjs + permissions.mouse/keyboard, ⚠ full 모드 권장)\n leerness lsp check|symbols|references <file/name> [--in dir] [--json] # 1.9.167 LSP 어댑터 MVP (typescript opt-in + regex fallback, 코드 인텔리전스)\n leerness review-request "<request>" [--json] # 1.9.176 사용자 요청 사전 검토 (충돌/재사용/효율/권장 단계 — 사용자 명시)\n leerness contract verify <spec.md> <impl.js> [--json] # 1.9.35 명세 ↔ 구현 일치 검사 (함수/필드)\n leerness reuse autodetect [path] [--apply] [--json] # 1.9.35 src/*.js의 module.exports → reuse-map 후보 등록\n leerness audit [path] [--fix] # 1.9.35 --fix: session-handoff/current-state 자동 갱신\n leerness verify-claim <T-ID> ... [--strict-claims] # 1.9.26 verify-claim에 낙관적 표시 자동 검사 통합
|
|
31875
32068
|
leerness lens [${_lensDomainList()}] [--json] # 1.18.3/1.36.97 분야별 자기질문 품질 렌즈 (database·contract·recovery·observability 심화 · axes 8축 경량 — 완료 선언 전 자가 점검)\n leerness library [show|page] [path] [--json] [--ai] # 1.36.98 재사용 인벤토리 — 컴포넌트·디자인 토큰 추출 (page: 오프라인 HTML · --ai: 에이전트용 압축)\n leerness reuse-map [path] [--all-apps] [--include p1,p2] [--strict-elements] [--json] # 1.9.18 중복/잠재중복/depends-on\n leerness verify-claim <T-ID> [--path .] [--run-tests] [--json] # 1.9.18-20 evidence 자동 검증 (1.9.20: scenes/scripts 등 도메인 폴더 + jest/mocha 파싱)\n leerness verify-code [path] [--build] [--bench] # 1.9.20 --bench: scripts.bench 추가 실행 + evidence 누적\n leerness session close [path]\n leerness route <task-type>\n leerness self check [path]\n leerness readme sync [path]\n leerness consistency check [path]\n leerness consistency merge-design-guide [path]\n leerness plan show|init|add|drop|progress|sync [args]\n leerness task list|add|update|drop|fix-evidence|relink [args]\n leerness skill list|info <name>\n leerness skill learn <id> --doc <url> --command "..." --capability "..." [--note ...]\n leerness skill use <id> [--note ...]\n leerness skill optimize <id> --before "..." --after "..." [--note ...]\n leerness skill remove <id>\n leerness skill consolidate [--threshold 0.3]\n leerness gate [path] # verify+audit+scan+encoding+lazy
|
|
31876
32069
|
leerness lease acquire|release|list|check [file|lease-id] [--session KEY] [--ttl SEC] [--json] # exact-file opt-in 협업 lease
|
|
32070
|
+
leerness state inspect [path] [--json] # 5-scope 경로/목록 읽기 전용 진단 — 이동·활성화 없음
|
|
32071
|
+
leerness state compatibility [path] [--json] # 관측 layout 쓰기 호환성 읽기 전용 진단 — 활성화 없음
|
|
31877
32072
|
leerness retro [path] [--days 7] [--all-apps] [--include p1,p2] [--json] # 회고 (1.9.13~1.9.16)
|
|
31878
32073
|
leerness insights [path] [--all-apps] [--include p1,p2] [--json] # 누적 통계 (1.9.13~1.9.16)
|
|
31879
32074
|
leerness brainstorm "<주제>" [--all-apps] [--include p1,p2] [--json] # 브레인스토밍 (1.9.13~1.9.16)
|
|
@@ -31898,7 +32093,7 @@ function help() {
|
|
|
31898
32093
|
leerness release publish [--dry-run] [--pack] [--git-push] [--gh-release] [--gh-pages] [--gh-pages-src file] [--npm-publish] [--auto] # 통합 배포 (1.9.8 + 1.9.10)\n leerness impact <target> [--all] # 변경 전 영향 분석 (기본 strong, --all로 weak 포함)\n leerness reuse find <query> # 기존 자원 검색 (재귀 안내)\n leerness reuse register <name> --where <p> --kind component|hook|util|api [--note ...]\n leerness ui consistency [path] [--strict] [--fail-on-violation]\n leerness graph [path] [--out <file>] # mermaid 의존성 그래프\n leerness graph [path] --html [--out <file>] # 온톨로지 그래프 HTML(leerness.html) 자동생성 — 노드 클릭으로 하네스 조회 (1.34.3)\n leerness guide [target] # impact + reuse + ui consistency 통합 가이드\n leerness migrate audit|apply|plan [path] [--json] [--yes] # 크로스버전 마이그레이션 진단/적용(canonical 백필)/플랜(임시폴더 비교) (UR-0075, 1.9.356~358)\n leerness migrate --guide # AI 에이전트용 크로스버전 마이그레이션 가이드 (1.9.355)\n leerness install-safety [--json] # 설치 안전 프로필 — 0 런타임 deps / 0 install-script (1.9.359)\n leerness capabilities [--json] # 권한·보안 표면 공개 (1.9.272)\n leerness feature add|link|impact|list|show # 기능 그래프(feature-graph) 추적\n leerness permissions list|set # agent 권한 모드 (1.9.174)\n leerness creds list|register|check|refresh # 크리덴셜 메타 추적 (값 미저장)\n leerness incident list|show|handle · webhook serve · deploy auto · runs list|show # 운영(ops)\n leerness whats-new [path] # 최근 버전 변경 요약\n leerness team list|add|show|remove|preview|deploy <id> [--personas a,b --members claude,codex --schedule every-session --task "..." --deploy "<배포명령>" --yes] # 에이전트 팀 정의/미리보기/배포 — UR-0073 A~D, opt-in · 배포는 2중게이트(--yes + LEERNESS_TEAM_DEPLOY=1)\n leerness release channel|cadence [path] [--json] # 릴리스 채널 정책 + 빈도 진단 (UR-0074 케이던스 가시화, 1.9.275/374)\n leerness commands [--json] # 전체 명령 전수 목록 (누락 없이 이 명령으로 확인)\n`);
|
|
31899
32094
|
}
|
|
31900
32095
|
|
|
31901
|
-
async function main() {
|
|
32096
|
+
async function main(runtimeEntered = false) {
|
|
31902
32097
|
const args = nonFlagArgs(); const cmd = args[0] || 'init';
|
|
31903
32098
|
if (has('--version') || has('-v')) {
|
|
31904
32099
|
// 1.9.32: --version은 순수 버전만 (CI/script 친화). 배너는 --banner 시.
|
|
@@ -31910,6 +32105,20 @@ async function main() {
|
|
|
31910
32105
|
if (!_rejectUnregisteredFlags(cmd)) return;
|
|
31911
32106
|
if (!_rejectMalformedFlagForms(cmd, args)) return;
|
|
31912
32107
|
if (!_validateCommandFlags(cmd, args)) return;
|
|
32108
|
+
// Exact early dispatch is both a no-write boundary and a latency boundary:
|
|
32109
|
+
// no workspace migration/guard rescans, usage locks, presence, or stale checks.
|
|
32110
|
+
if (cmd === 'state' && args[1] === 'inspect') {
|
|
32111
|
+
const explicit = arg('--path', null);
|
|
32112
|
+
return stateInspectCmd(explicit !== null ? explicit : args[2] === undefined ? process.cwd() : args[2]);
|
|
32113
|
+
}
|
|
32114
|
+
if (cmd === 'state' && args[1] === 'compatibility') {
|
|
32115
|
+
const explicit = arg('--path', null);
|
|
32116
|
+
return stateCompatibilityCmd(explicit !== null ? explicit : args[2] === undefined ? process.cwd() : args[2]);
|
|
32117
|
+
}
|
|
32118
|
+
const runtimeRoot = _runtimeProjectRoot(args, cmd);
|
|
32119
|
+
if (!runtimeEntered && runtimeRoot && !_mustRemainReadOnly(args, cmd)) {
|
|
32120
|
+
return withRuntimeWrites(runtimeRoot, () => main(true));
|
|
32121
|
+
}
|
|
31913
32122
|
// 값이 존재하지만 의미가 잘못된 --keep 도 legacy migration/write 전에 거부한다.
|
|
31914
32123
|
// preview serve 의 --keep 은 route별 boolean 계약이므로 숫자 검증 대상이 아니다.
|
|
31915
32124
|
if (!_commandBooleanValueFlags(cmd, args[1]).has('--keep')) {
|
|
@@ -32038,16 +32247,10 @@ async function main() {
|
|
|
32038
32247
|
// 1.36.104: 'mcp' 를 제외한다. MCP 서버는 프로젝트 명령이 아니라 상주 프로세스라, 서버의 cwd 가 우연히
|
|
32039
32248
|
// leerness 프로젝트면 거기에 `mcp: N` 이 쌓였다 — 사용자가 그 프로젝트에서 쓴 적 없는 '명령' 이 통계에 뜬다
|
|
32040
32249
|
// (실측: 대상 프로젝트는 "사용 기록 없음", 무관한 cwd 는 "총 3회 호출"). 도구별 귀속은 _bumpMcpUsage 가 이미 한다.
|
|
32041
|
-
if (process.env.LEERNESS_INTERNAL !== '1' && !_mustRemainReadOnly(args, cmd) && cmd !== 'usage' && cmd !== 'init' && cmd !== 'migrate' && cmd !== 'mcp' && cmd !== '--version' && cmd !== '--help') {
|
|
32250
|
+
if (runtimeRoot && process.env.LEERNESS_INTERNAL !== '1' && !_mustRemainReadOnly(args, cmd) && cmd !== 'usage' && cmd !== 'init' && cmd !== 'migrate' && cmd !== 'mcp' && cmd !== '--version' && cmd !== '--help') {
|
|
32042
32251
|
try {
|
|
32043
32252
|
// 1.9.352 (UR-0069 외부리뷰): usage 루트 — --path 우선, 없으면 args[1] 이 .leerness 보유 디렉토리일 때만 path(positional 보존), 아니면 cwd. (이전: args[1] 무조건 path 가정 → subcommand[decision add 등] root=cwd/add → .leerness 못 찾아 미집계)
|
|
32044
|
-
const
|
|
32045
|
-
let root;
|
|
32046
|
-
if (_pathArg) root = absRoot(_pathArg);
|
|
32047
|
-
else if (args[1] && !args[1].startsWith('-') && exists(path.join(absRoot(args[1]), '.leerness'))) root = absRoot(args[1]);
|
|
32048
|
-
// 1.36.38 (codex 4차 #9): subcommand 형(예: memory status <B>)은 대상 경로가 args[2] — 종전엔 cwd(A)에 오귀속됐다.
|
|
32049
|
-
else if (args[2] && !args[2].startsWith('-') && exists(path.join(absRoot(args[2]), '.leerness'))) root = absRoot(args[2]);
|
|
32050
|
-
else root = absRoot(process.cwd());
|
|
32253
|
+
const root = runtimeRoot;
|
|
32051
32254
|
if (exists(path.join(root, '.leerness'))) _bumpUsage(root, cmd);
|
|
32052
32255
|
} catch {}
|
|
32053
32256
|
}
|
|
@@ -33091,6 +33294,14 @@ async function main() {
|
|
|
33091
33294
|
return;
|
|
33092
33295
|
}
|
|
33093
33296
|
|
|
33297
|
+
const skillCatalog = (() => {
|
|
33298
|
+
const args = nonFlagArgs();
|
|
33299
|
+
if (require.main === module && args[0] === 'state' && ['inspect', 'compatibility'].includes(args[1])) {
|
|
33300
|
+
return _withBuiltinSource(BUILTIN_CATALOG);
|
|
33301
|
+
}
|
|
33302
|
+
return _loadSkillCatalog();
|
|
33303
|
+
})();
|
|
33304
|
+
|
|
33094
33305
|
// 1.9.4 B: main 종료 후 exitCode를 명시적으로 process.exit으로 강제 (셸/wrapper 차 무시).
|
|
33095
33306
|
// 1.9.255 (UR-0019 2단계): require.main 가드 — `require('harness.js')` 시 main() 미실행 (init 부작용 차단 + 내부 함수 단위 테스트 가능).
|
|
33096
33307
|
// CLI 직접 실행 (node harness.js / npx leerness) 시에만 main() 호출. 기존 동작 100% 보존.
|
|
@@ -33098,6 +33309,26 @@ if (require.main === module) {
|
|
|
33098
33309
|
main()
|
|
33099
33310
|
.then(() => { if (process.exitCode && process.exitCode !== 0) process.exit(process.exitCode); })
|
|
33100
33311
|
.catch(err => {
|
|
33312
|
+
if (err && err.code === 'E_RUNTIME_LAYOUT_INCOMPATIBLE') {
|
|
33313
|
+
const blockedArgs = nonFlagArgs();
|
|
33314
|
+
const reason = String(err.reasonCode || '');
|
|
33315
|
+
// 기존 workspace 오류의 기계 소비자 계약은 유지한다. 새 layout 오류만
|
|
33316
|
+
// runtime_layout_incompatible로 확장하고, 어느 경우에도 writer를 재실행하지 않는다.
|
|
33317
|
+
if (blockedArgs[0] === 'migrate-workspace-dir' && /^workspace_dir_/.test(reason)) {
|
|
33318
|
+
const current = inspectWorkspace(_runtimeProjectRoot(blockedArgs, blockedArgs[0]));
|
|
33319
|
+
const blockedReason = reason === 'workspace_dir_symlink' && current.canonical.isSymbolicLink
|
|
33320
|
+
? 'canonical-workspace-symlink' : reason.replace(/_/g, '-');
|
|
33321
|
+
const report = { ok: false, blocked: true, blockedReason, errors: [err.message] };
|
|
33322
|
+
if (process.argv.includes('--json')) log(JSON.stringify(report, null, 2)); else fail(err.message);
|
|
33323
|
+
process.exit(1);
|
|
33324
|
+
}
|
|
33325
|
+
if (/^workspace_dir_/.test(reason)) {
|
|
33326
|
+
failJson(process.argv.includes('--json'), reason, err.message);
|
|
33327
|
+
process.exit(1);
|
|
33328
|
+
}
|
|
33329
|
+
failJson(process.argv.includes('--json'), 'runtime_layout_incompatible', err.message);
|
|
33330
|
+
process.exit(1);
|
|
33331
|
+
}
|
|
33101
33332
|
// 1.36.144 (T-0117): dry-run 인데 쓰려다 멈춘 것은 크래시가 아니라 **계약 위반 보고**다.
|
|
33102
33333
|
if (err && err.code === 'E_DRY_RUN_WRITE') {
|
|
33103
33334
|
failJson(process.argv.includes('--json'), 'dry_run_unsupported',
|
|
@@ -33135,7 +33366,7 @@ if (require.main === module) {
|
|
|
33135
33366
|
// _isSecretKey (시크릿 차단, 보안), compareVer (버전 비교, stale-check 정확성), parseHarnessVersion (버전 파싱).
|
|
33136
33367
|
module.exports = {
|
|
33137
33368
|
VERSION,
|
|
33138
|
-
_cliMutationClass, _mustRemainReadOnly,
|
|
33369
|
+
_cliMutationClass, _mustRemainReadOnly, _runtimeProjectRoot,
|
|
33139
33370
|
_npmGlobalBin, _dirInPath, _leernessResolvable, _pathDiagnose, _registerPath,
|
|
33140
33371
|
_winPathPsScript, _unixPathBlock, pathSetupCmd,
|
|
33141
33372
|
_isSecretKey, compareVer, parseHarnessVersion,
|
|
@@ -33146,7 +33377,7 @@ module.exports = {
|
|
|
33146
33377
|
// 1.9.260: shell-guard — 셸 호환성 린터 (UR-0020) 순수 분석 함수 — 단위 테스트
|
|
33147
33378
|
_shellGuardAnalyze, _detectShellCtx, shellGuardCmd,
|
|
33148
33379
|
// 1.9.263: shell 실패 메모리 + 환경 버전 변동 (UR-0020 3단계) — handoff 통합 단위 테스트
|
|
33149
|
-
_shellFailuresPath, _loadShellFailures, _recordShellFailure, _shellEnvDrift,
|
|
33380
|
+
_shellFailuresPath, _loadShellFailures, _recordShellFailure: projectWriter(_recordShellFailure), _shellEnvDrift,
|
|
33150
33381
|
// 1.9.265: CLI 에이전트 슬래시 명령어 레지스트리 (UR-0021 1단계) — 단위 테스트
|
|
33151
33382
|
AGENT_SLASH_COMMANDS, _agentSlashFile, _loadAgentSlashCommands, _recordAgentSlashCommands, _agentSlashHint, slashCommandsCmd,
|
|
33152
33383
|
_parseSlashFromHelp, _probeAgentSlash, _refreshAgentSlashCommands,
|
|
@@ -33164,9 +33395,12 @@ module.exports = {
|
|
|
33164
33395
|
// 1.9.276: init --minimal 파일 필터 (GPT-5.5 2차 리뷰) — 단위 테스트
|
|
33165
33396
|
MINIMAL_SKIP_KEYS, coreFiles,
|
|
33166
33397
|
// 1.9.278: .leerness/ 상태 스키마 (UR-0032, GPT-5.5 범용 하네스) — 단위 테스트
|
|
33167
|
-
_newRunRecord, _leernessStateDir, _loadLeernessState, _saveLeernessState, _loadRun, _saveRun,
|
|
33398
|
+
_newRunRecord, _leernessStateDir, _loadLeernessState, _saveLeernessState, _loadRun, _saveRun,
|
|
33399
|
+
stateCmd: projectWriter(stateCmd), stateCompatibilityCmd,
|
|
33400
|
+
_withLock, _recordRun: projectWriter(_recordRun),
|
|
33401
|
+
_bumpMcpUsage,
|
|
33168
33402
|
// 1.9.280: adapter (UR-0033) — 단위 테스트
|
|
33169
|
-
ADAPTERS, _mcpJsonContent, _mergeMcpJson, adapterCmd,
|
|
33403
|
+
ADAPTERS, _mcpJsonContent, _mergeMcpJson, adapterCmd: projectWriter(adapterCmd),
|
|
33170
33404
|
// 1.9.281: 권한 등급 (UR-0034) — 단위 테스트
|
|
33171
33405
|
PERMISSION_TIERS, _tierRank, _requiredTier, _policyAllows, _loadPolicy, _savePolicy, _policyEnforce, policyCmd,
|
|
33172
33406
|
// 1.9.285: 외부 OSS 재사용 게이트 (UR-0023) — 단위 테스트
|