leerness 1.36.172 → 1.36.174
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 +301 -53
- package/lib/agent-registry.js +10 -10
- package/lib/mcp-tools.js +2 -2
- package/lib/toggles.js +34 -9
- package/package.json +2 -2
- package/scripts/dead-flags-probe.js +365 -0
- package/scripts/e2e-core.js +30 -2
- package/scripts/e2e.js +2 -1
package/bin/leerness.js
CHANGED
|
@@ -48,7 +48,7 @@ const {
|
|
|
48
48
|
migrateLegacyWorkspace,
|
|
49
49
|
} = require('../lib/workspace-dir');
|
|
50
50
|
|
|
51
|
-
const VERSION = '1.36.
|
|
51
|
+
const VERSION = '1.36.174';
|
|
52
52
|
|
|
53
53
|
// MCP lifecycle 주소 표식은 현재 CLI 호출 한 번에만 유효하다. CLI bootstrap에서 즉시 env에서
|
|
54
54
|
// 떼어 두어 `--no-record`/hook처럼 presence 기록 함수에 도달하지 않는 경로도 후속 child에 유출하지 않는다.
|
|
@@ -718,7 +718,7 @@ for (const _bf of _BRIEF_FIELDS) { if (_bf && _bf.flag) _VALUE_FLAGS.add('--' +
|
|
|
718
718
|
// 판정에만 쓰이고 파싱 동작에는 관여하지 않으므로, 넓게 잡아도 안전하다(과다 포함 = 경고 억제).
|
|
719
719
|
const _BOOL_FLAGS = new Set(['--ai','--all','--all-apps','--baseline','--all-presets','--allow-empty','--allow-unrelated','--apply','--audit','--auto','--auto-apply-delivered','--auto-cleanup-branches','--auto-fix','--auto-fix-bom','--auto-fix-encoding','--auto-main-push','--auto-recover','--auto-track','--banner','--bench','--build','--bundle-only','--claims','--close','--compact','--detect','--dry-run','--dry-run-npm','--embedding','--enforce','--fail-on-candidates','--fail-on-violation','--force','--gh-pages','--gh-release','--git-push','--global','--guide','--help','--html','--include-code','--interactive','--json','--last','--lenient','--major','--minimal','--minor','--no-animate','--no-auto-roadmap','--no-auto-update','--no-banner','--no-brainstorm-hits','--no-cache','--no-context-inject','--no-crawl','--no-drift-check','--no-enforce','--no-env','--no-env-detect','--no-feature-impact','--no-headline','--no-init-check','--no-interactive-select','--no-lazy-warn','--no-lessons','--no-longterm-recall','--no-mcp','--no-mem-delta','--no-multiagent-hint','--no-next-actions','--no-npm','--no-official-skills','--no-on-every-change','--no-pre-wake','--no-preserve-link','--no-readme-sync','--no-record','--no-repl','--no-review','--no-save','--no-security-check','--no-security-summary','--no-setup-agents','--no-skill-suggest','--no-stale-check','--no-suggest','--no-synonyms','--no-team-reminders','--no-wakeup-miss','--no-workflow-guide','--no-write','--npm-publish','--offline','--on-every-change','--pack','--parent-migrate','--patch','--publish-npm','--pulse','--quiet','--record','--refresh','--remove','--repair','--repl','--require-evidence','--run-tests','--skeleton','--skip-verify','--strict','--strict-claims','--strict-elements','--strict-exit','--suggest','--version','--writeback','--yes',
|
|
720
720
|
// 소스에 리터럴로만 존재해 소비 형태를 기계 추출하지 못한 것들 — 실재하는 플래그이므로 오타로 오인하면 안 된다
|
|
721
|
-
'--commands','--decision','--errors','--
|
|
721
|
+
'--commands','--decision','--errors','--fix','--list','--notes','--print','--raw','--select-all','--skip-git-repo-check','--tests','--verbose','--yolo',
|
|
722
722
|
// lib/*.js 의 부울 소비자 — bin/ 만 훑었다면 `agents route --confirm` 같은 실재 문법에 오탐 경고를 냈을 것이다
|
|
723
723
|
'--confirm','--dispatch','--execute','--ladder','--log','--multi','--no-env-check','--no-feature-check','--no-gitignore-check','--no-npm-audit','--no-secret-scan','--readonly','--write']);
|
|
724
724
|
// 1.36.103: 하위명령이 '선택' 인 명령에서 `<cmd> <경로>` 의 경로가 하위명령 자리로 읽히던 것 차단.
|
|
@@ -1791,7 +1791,8 @@ async function resolveInstallOptions(root, opts = {}) {
|
|
|
1791
1791
|
// 1.9.151: CLI 에이전트 활성화 — 복수 선택 (사용자 명시 요청)
|
|
1792
1792
|
// _selectMany 로 Space 토글, a 전체, n 해제, Enter 확정. 선택된 에이전트들만 .env.example에 LEERNESS_ENABLE_* 활성화.
|
|
1793
1793
|
let agentsOptIn = null; // string[] (다중) 또는 'none' (선택 안함)
|
|
1794
|
-
|
|
1794
|
+
const skipAgentSetup = opts.noSetupAgents === true || has('--no-setup-agents');
|
|
1795
|
+
if (shouldAsk && !opts._skipAgentsPrompt && !skipAgentSetup) {
|
|
1795
1796
|
// 1.9.206: 선택된 언어로 prompt 표시
|
|
1796
1797
|
const _agLang = lang === 'en' ? 'en' : 'ko';
|
|
1797
1798
|
if (useInteractive) {
|
|
@@ -1830,7 +1831,7 @@ async function resolveInstallOptions(root, opts = {}) {
|
|
|
1830
1831
|
// 1.18.3 (사용자 명시): 설치 직후 REPL agent 모드 진입 문항 제거 — REPL 은 완성도가 올라가면 그때 구현 예정.
|
|
1831
1832
|
// 수동 진입(`leerness agent`)은 그대로 유지.
|
|
1832
1833
|
const startRepl = false;
|
|
1833
|
-
return { lang, skills, agentsOptIn, permissionMode, startRepl };
|
|
1834
|
+
return { lang, skills, agentsOptIn, skipAgentSetup, permissionMode, startRepl };
|
|
1834
1835
|
}
|
|
1835
1836
|
|
|
1836
1837
|
async function install(root, opts = {}) {
|
|
@@ -1891,6 +1892,7 @@ async function install(root, opts = {}) {
|
|
|
1891
1892
|
const list = Array.isArray(resolved.agentsOptIn) ? resolved.agentsOptIn.join(', ') : String(resolved.agentsOptIn);
|
|
1892
1893
|
log(`Agents 활성화: ${list}`);
|
|
1893
1894
|
}
|
|
1895
|
+
if (resolved.skipAgentSetup) log(`Agents 설정 보존: --no-setup-agents (기존 활성화 값을 읽거나 덮어쓰지 않음)`);
|
|
1894
1896
|
if (resolved.permissionMode) log(`Agent 권한 모드: ${resolved.permissionMode} (1.9.174 — REPL에서 \`:permissions extended|full\` 로 즉시 변경 가능)`);
|
|
1895
1897
|
// 1.9.10: 스킬 카탈로그 출처 안내
|
|
1896
1898
|
// 1.9.184 (사용자 명시): leerness-skillpack 미사용 정책 — 안내 메시지 제거. builtin catalog 만 사용.
|
|
@@ -2062,7 +2064,16 @@ async function install(root, opts = {}) {
|
|
|
2062
2064
|
}
|
|
2063
2065
|
// 1.9.187: 비시크릿 LEERNESS_* 설정 → .leerness/leerness-config.json (AI 가시성)
|
|
2064
2066
|
try {
|
|
2065
|
-
|
|
2067
|
+
const leernessConfig = {
|
|
2068
|
+
LEERNESS_SKILL_DISCOVER_URL: '',
|
|
2069
|
+
LEERNESS_SKILL_AUTO_DISCOVER: '0',
|
|
2070
|
+
LEERNESS_SKILL_AUTO_INSTALL: '0',
|
|
2071
|
+
LEERNESS_SKILL_AUTO_PRESETS: 'vercel,anthropic'
|
|
2072
|
+
};
|
|
2073
|
+
// Explicit opt-out means preserve, not "select none". Omitting these keys
|
|
2074
|
+
// lets _writeLeernessConfig retain existing values and keeps a fresh init
|
|
2075
|
+
// free of an implicit ten-provider decision.
|
|
2076
|
+
if (!resolved.skipAgentSetup) Object.assign(leernessConfig, {
|
|
2066
2077
|
LEERNESS_OLLAMA_BASE_URL: enable('ollama') ? 'http://localhost:11434' : '',
|
|
2067
2078
|
LEERNESS_OLLAMA_MODEL: '',
|
|
2068
2079
|
LEERNESS_ENABLE_CLAUDE: enable('claude') ? '1' : '0',
|
|
@@ -2075,11 +2086,8 @@ async function install(root, opts = {}) {
|
|
|
2075
2086
|
LEERNESS_ENABLE_GOOSE: enable('goose') ? '1' : '0',
|
|
2076
2087
|
LEERNESS_ENABLE_COPILOT: enable('copilot') ? '1' : '0',
|
|
2077
2088
|
LEERNESS_ENABLE_OLLAMA: enable('ollama') ? '1' : '0',
|
|
2078
|
-
LEERNESS_SKILL_DISCOVER_URL: '',
|
|
2079
|
-
LEERNESS_SKILL_AUTO_DISCOVER: '0',
|
|
2080
|
-
LEERNESS_SKILL_AUTO_INSTALL: '0',
|
|
2081
|
-
LEERNESS_SKILL_AUTO_PRESETS: 'vercel,anthropic'
|
|
2082
2089
|
});
|
|
2090
|
+
_writeLeernessConfig(root, leernessConfig);
|
|
2083
2091
|
} catch (e) {
|
|
2084
2092
|
warn(`.leerness/leerness-config.json 생성 실패 (계속 진행): ${e.message}`);
|
|
2085
2093
|
}
|
|
@@ -4340,7 +4348,7 @@ function _selfTestCases() {
|
|
|
4340
4348
|
const s = read(__filename);
|
|
4341
4349
|
const forceGuard = s.includes("const effForce = opts.force && !_USER_STATE.has(f);") && s.includes("'.leerness/progress-tracker.md', '.leerness/plan.md', '.leerness/task-log.md'");
|
|
4342
4350
|
const skillGuard = s.includes('유효하지 않은 skill name (path traversal/경로 문자 차단)') && s.includes('이미 설치된 skill: ${skillId} — 내용이 다릅니다');
|
|
4343
|
-
const settingsGuard = s.includes('settings.local.json 이 손상돼(JSON
|
|
4351
|
+
const settingsGuard = s.includes('settings.local.json 이 손상돼(JSON 파싱/형상 실패) hook 설치를 중단');
|
|
4344
4352
|
// Workspace migration fails closed on links/junctions; it never follows
|
|
4345
4353
|
// them and never reports a partial migration as success.
|
|
4346
4354
|
let symlinkGuard = false;
|
|
@@ -4779,8 +4787,10 @@ function _selfTestCases() {
|
|
|
4779
4787
|
const jsonContract = hookBody.includes("hookEventName: 'SessionStart'") && hookBody.includes('additionalContext');
|
|
4780
4788
|
const optOut = hookBody.includes("has('--no-context-inject')") && hookBody.includes("LEERNESS_NO_CONTEXT_INJECT");
|
|
4781
4789
|
const selfLabel = hookBody.includes('위는 요약'); // 1.9.272 투명성: 요약임을 자기 라벨링
|
|
4782
|
-
const matcherCompact =
|
|
4783
|
-
|
|
4790
|
+
const matcherCompact = _AUTO_UPDATE_CONTEXT_HOOK.matcher === 'startup|clear|compact'
|
|
4791
|
+
&& _AUTO_UPDATE_CONTEXT_HOOK.command === 'leerness hook session-start'; // 실측 결함(compaction) 커버
|
|
4792
|
+
const idempotent = _exactHook({ matcher: 'startup|clear|compact', command: 'leerness hook session-start' }, _AUTO_UPDATE_CONTEXT_HOOK)
|
|
4793
|
+
&& !_exactHook({ matcher: 'startup', command: 'echo leerness hook session-start' }, _AUTO_UPDATE_CONTEXT_HOOK);
|
|
4784
4794
|
return surface && zeroWrite && noHandoffCall && jsonContract && optOut && selfLabel && matcherCompact && idempotent;
|
|
4785
4795
|
} },
|
|
4786
4796
|
{ name: '경로/EOL/형태 견고성 4종 (1.36.21, 전수 sweep P2): handoff·env detect 없는경로 가드 + CRLF plan tasks + migrate 오타설치 차단 + user-requests 미인식 쓰기차단 — 소스가드 + 순수 행위', run: () => {
|
|
@@ -4956,7 +4966,35 @@ function _selfTestCases() {
|
|
|
4956
4966
|
{ name: 'CV-1/UR-0076: arg() --path=값 파싱 + _resolveRoot(--path>positional>cwd) 행위', run: () => { if (typeof _resolveRoot !== 'function') return false; const save = process.argv; try { process.argv = ['node', 'h', 'context', '--path=/tmp/eqform']; const eq = arg('--path', null) === '/tmp/eqform'; process.argv = ['node', 'h', 'context', 'X', '--path', '/tmp/flag']; const flagWins = _resolveRoot('X') === '/tmp/flag'; process.argv = ['node', 'h', 'context', '/tmp/pos']; const posWins = _resolveRoot('/tmp/pos') === '/tmp/pos'; process.argv = ['node', 'h', 'context']; const cwdFb = _resolveRoot(undefined) === process.cwd(); return eq && flagWins && posWins && cwdFb; } finally { process.argv = save; } } },
|
|
4957
4967
|
{ name: 'CV-4/UR-0079: _pruneArchives archive retention (최신 keep 유지, 오래된 prune) 행위', run: () => { if (typeof _pruneArchives !== 'function') return false; const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_prune_')); try { const adir = path.join(tmp, '.leerness', 'archive'); fs.mkdirSync(adir, { recursive: true }); for (let i = 0; i < 5; i++) fs.mkdirSync(path.join(adir, 'leerness-1.9.' + i + '-stamp')); const pruned = _pruneArchives(tmp, 2); const left = fs.readdirSync(adir).filter(n => /^leerness-/.test(n)).length; return pruned === 3 && left === 2; } finally { try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} } } },
|
|
4958
4968
|
{ name: 'CV-7/UR-0082: commands 카탈로그 + help 에 누락 명령군 등재 (표면 drift 가드)', run: () => { const src = read(__filename); const ci = src.indexOf('function commandsCmd'); const hi = src.indexOf('function help('); if (ci < 0 || hi < 0) return false; const cbody = src.slice(ci, ci + 8000); const hbody = src.slice(hi, hi + 7000); const must = ['verify-code', 'contract verify', 'install-safety', 'feature add', 'creds list', 'incident list', 'webhook serve', 'deploy auto', 'runs list', 'permissions list', 'whats-new', 'migrate audit']; return must.every(c => cbody.includes(c)) && hbody.includes('install-safety') && hbody.includes('feature add'); } },
|
|
4959
|
-
{ name: 'UR-0083(4th외부평가 9.3): auto-update hook 비침투 (update --quiet 모드 + hook --check --quiet + 업그레이드)', run: () => {
|
|
4969
|
+
{ name: 'UR-0083(4th외부평가 9.3): auto-update hook 비침투 (update --quiet 모드 + hook --check --quiet + 업그레이드)', run: () => {
|
|
4970
|
+
const src = read(__filename);
|
|
4971
|
+
const quietMode = /const quiet = !!opts\.quiet \|\| has\('--quiet'\)/.test(src);
|
|
4972
|
+
const exactness = _exactHook({ matcher: '*', command: 'leerness update --check --quiet' }, _AUTO_UPDATE_CHECK_HOOK)
|
|
4973
|
+
&& !_exactHook({ matcher: '*', command: 'echo leerness update --check --quiet' }, _AUTO_UPDATE_CHECK_HOOK);
|
|
4974
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_auto_update_self_'));
|
|
4975
|
+
try {
|
|
4976
|
+
mkdirp(path.join(tmp, '.claude'));
|
|
4977
|
+
writeUtf8(path.join(tmp, '.claude', 'settings.local.json'), JSON.stringify({ hooks: { SessionStart: [
|
|
4978
|
+
{ matcher: '*', command: 'leerness update --check' },
|
|
4979
|
+
] } }, null, 2) + '\n');
|
|
4980
|
+
const installed = cp.spawnSync(process.execPath, [__filename, 'auto-update', 'install', tmp], { encoding: 'utf8', timeout: 20000 });
|
|
4981
|
+
const settings = JSON.parse(read(path.join(tmp, '.claude', 'settings.local.json')));
|
|
4982
|
+
const hooks = settings.hooks.SessionStart || [];
|
|
4983
|
+
const upgraded = installed.status === 0
|
|
4984
|
+
&& !hooks.some(h => h && h.command === 'leerness update --check')
|
|
4985
|
+
&& hooks.some(h => _exactHook(h, _AUTO_UPDATE_CHECK_HOOK))
|
|
4986
|
+
&& hooks.some(h => _exactHook(h, _AUTO_UPDATE_CONTEXT_HOOK))
|
|
4987
|
+
&& _validUpdateSlashCommand(path.join(tmp, '.claude', 'commands', 'update.md'));
|
|
4988
|
+
const settingsFile = path.join(tmp, '.claude', 'settings.local.json');
|
|
4989
|
+
const malformedBytes = '{"hooks":{"SessionStart":[42]}}';
|
|
4990
|
+
writeUtf8(settingsFile, malformedBytes);
|
|
4991
|
+
const malformedStatus = cp.spawnSync(process.execPath, [__filename, 'auto-update', 'status', tmp, '--json'], { encoding: 'utf8', timeout: 20000 });
|
|
4992
|
+
const malformedInstall = cp.spawnSync(process.execPath, [__filename, 'auto-update', 'install', tmp], { encoding: 'utf8', timeout: 20000 });
|
|
4993
|
+
const malformedRejected = malformedStatus.status !== 0 && malformedInstall.status !== 0
|
|
4994
|
+
&& read(settingsFile) === malformedBytes;
|
|
4995
|
+
return quietMode && exactness && upgraded && malformedRejected;
|
|
4996
|
+
} finally { try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} }
|
|
4997
|
+
} },
|
|
4960
4998
|
{ name: 'CV-6/UR-0081: 시크릿 스캐너 FP/FN — _isPlaceholderSecret + _looksSecretLike 행위', run: () => { if (typeof _isPlaceholderSecret !== 'function' || typeof _looksSecretLike !== 'function') return false; const fp = _isPlaceholderSecret('change-me') && _isPlaceholderSecret('your-api-key-here') && _isPlaceholderSecret('<token>') && _isPlaceholderSecret('') && !_isPlaceholderSecret('hunter2realpass'); const fn = _looksSecretLike('secret123') && _looksSecretLike('a'.repeat(24)) && !_looksSecretLike('processEnv') && !_looksSecretLike('reqBodyPassword'); return fp && fn; } },
|
|
4961
4999
|
{ name: 'UR-0025: _mergeLines/_mergeEnvLines 순수 코어 모듈 분리 + 행위 (1.9.367)', run: () => { if (typeof _mergeLines !== 'function' || typeof _mergeEnvLines !== 'function') return false; const m = require('../lib/pure-utils'); const moved = m._mergeLines === _mergeLines && m._mergeEnvLines === _mergeEnvLines; const ml = _mergeLines('a\n', ['a', 'b']) === 'a\nb\n'; const meKeep = _mergeEnvLines('FOO=keep\n', ['FOO=new']) === 'FOO=keep\n'; const meAdd = _mergeEnvLines('FOO=keep\n', ['BAR=add']).includes('BAR=add'); return moved && ml && meKeep && meAdd; } },
|
|
4962
5000
|
{ name: 'UR-0025: _mergeReadmeSection/_managedMerge + MERGE_OVERWRITE_FILES 모듈 분리 + 행위 (1.9.368)', run: () => { const m = require('../lib/pure-utils'); const c = require('../lib/catalogs'); if (typeof _mergeReadmeSection !== 'function' || typeof _managedMerge !== 'function') return false; const moved = m._mergeReadmeSection === _mergeReadmeSection && m._managedMerge === _managedMerge && MERGE_OVERWRITE_FILES === c.MERGE_OVERWRITE_FILES; const rd = _mergeReadmeSection('', 'BLOCK', '<s>', '<e>') === '# Project\n\nBLOCK\n'; const mm = _managedMerge('a.md', 'NEW', 'OLD', '.h', new Set()).includes('migration-preserved'); const ow = _managedMerge('.leerness/manifest.json', 'NEW', 'OLD', '.h', c.MERGE_OVERWRITE_FILES) === 'NEW'; const same = _managedMerge('a.md', 'X', 'X', '.h', new Set()) === 'X'; return moved && rd && mm && ow && same; } },
|
|
@@ -6696,7 +6734,7 @@ function _selfTestCases() {
|
|
|
6696
6734
|
// 수신 변수 매칭에 단어 경계는 필수 — 없으면 `s.includes` 가 `tps.includes` 에 걸려 오탐이 난다(자체 실측).
|
|
6697
6735
|
const litRe = /(\w+)\.includes\((['"`])((?:\\.|(?!\2)[\s\S])*?)\2\)/g;
|
|
6698
6736
|
const codeish = (L) => /[(){}=;]|=>|\|\||&&/.test(L) && /[a-zA-Z_$]{3,}/.test(L);
|
|
6699
|
-
|
|
6737
|
+
const offenders = [];
|
|
6700
6738
|
for (const body of chunks) {
|
|
6701
6739
|
if (body.includes(MARK)) continue; // 이 메타가드 자신 (표식은 소스에 그대로 존재)
|
|
6702
6740
|
if (!/read\(__filename\)/.test(body)) continue;
|
|
@@ -6711,13 +6749,18 @@ function _selfTestCases() {
|
|
|
6711
6749
|
// (검수 High#2) 1차판이 따옴표까지 싸잡아 제외해 16개 가드가 무방비가 됐다 — 실측 우회 재현됨.
|
|
6712
6750
|
if (lm[2] === '`' && L.includes('${')) continue;
|
|
6713
6751
|
const haystack = binVars.has(lm[1]) ? binOutside : outside;
|
|
6714
|
-
if (!haystack.includes(L)) {
|
|
6752
|
+
if (!haystack.includes(L)) {
|
|
6753
|
+
const caseName = (/\{ name: ['"]([^'"]+)/.exec(body) || [])[1] || 'unknown selftest';
|
|
6754
|
+
offenders.push(`${caseName}: ${L.slice(0, 120)}`);
|
|
6755
|
+
break;
|
|
6756
|
+
}
|
|
6715
6757
|
}
|
|
6716
6758
|
}
|
|
6717
6759
|
// 1.36.83: 잔여 부채 **0** — 1.36.82 가 baseline 12 로 유예했던 것을 이 라운드에서 전부 수리했다
|
|
6718
6760
|
// (각 가드는 지키던 동작을 실제로 깨뜨렸을 때 실패함을 변이로 증명). 이제 하나라도 생기면 즉시 실패한다.
|
|
6719
6761
|
// 유예 수치를 남겨두면 그 안에서 조용히 썩으므로, 갚은 뒤에는 반드시 0 으로 조인다.
|
|
6720
|
-
|
|
6762
|
+
if (offenders.length) throw new Error(`stale source guard: ${offenders.join(' | ')}`);
|
|
6763
|
+
return true;
|
|
6721
6764
|
} },
|
|
6722
6765
|
{ name: '동봉 문서의 낡을 수치 주장 금지 (1.36.82): README/docs 의 MCP 도구·selftest 케이스 수가 실측과 불일치 시 실패 (메타가드)', run: () => {
|
|
6723
6766
|
// 사고: docs/interoperability.md 가 "86 도구"(3릴리스 낡음), README.ko.md 가 "selftest 210 케이스"(실제 335) 를
|
|
@@ -8937,8 +8980,10 @@ async function apiSkillCmd(root, sub) {
|
|
|
8937
8980
|
while (i < argv.length) {
|
|
8938
8981
|
const a = argv[i];
|
|
8939
8982
|
if (a.startsWith('--')) {
|
|
8940
|
-
//
|
|
8941
|
-
|
|
8983
|
+
// 값 플래그만 다음 token을 소비한다. 종전에는 `--no-crawl <url>`의
|
|
8984
|
+
// boolean flag가 URL까지 삼켜 위치에 따라 missing_args가 나왔다.
|
|
8985
|
+
const name = a.split('=')[0];
|
|
8986
|
+
if (_VALUE_FLAGS.has(name) && !a.includes('=') && i + 1 < argv.length && !argv[i + 1].startsWith('--')) i += 2;
|
|
8942
8987
|
else i += 1;
|
|
8943
8988
|
continue;
|
|
8944
8989
|
}
|
|
@@ -8990,7 +9035,9 @@ async function apiSkillCmd(root, sub) {
|
|
|
8990
9035
|
_hlog(cy(`# leerness api-skill add (1.9.245, UR-0015)`));
|
|
8991
9036
|
_hlog(` URL: ${url}`);
|
|
8992
9037
|
if (direction) _hlog(` 방향: ${direction}`);
|
|
8993
|
-
_hlog(dm(
|
|
9038
|
+
_hlog(dm(noCrawl
|
|
9039
|
+
? ` Fetch only (--no-crawl: related links are not requested)...`
|
|
9040
|
+
: ` Fetch + crawl (depth=1, same-domain, max 10)...`));
|
|
8994
9041
|
try {
|
|
8995
9042
|
const doc = await _collectAPIDoc(url, { direction, noCrawl });
|
|
8996
9043
|
const skillName = name || doc.title || new URL(url).hostname;
|
|
@@ -9011,7 +9058,7 @@ async function apiSkillCmd(root, sub) {
|
|
|
9011
9058
|
_hlog(dm(` title: ${doc.title || '(none)'}`));
|
|
9012
9059
|
_hlog(dm(` related links: ${(doc.related_links || []).length}`));
|
|
9013
9060
|
_hlog(dm(` text size: ${doc.text.length} chars`));
|
|
9014
|
-
if (_j) log(JSON.stringify({ ok: true, id, name: skillName, url, related_count: (doc.related_links || []).length }, null, 2));
|
|
9061
|
+
if (_j) log(JSON.stringify({ ok: true, id, name: skillName, url, crawl: !noCrawl, related_count: (doc.related_links || []).length }, null, 2));
|
|
9015
9062
|
} catch (e) {
|
|
9016
9063
|
_hlog(rd(`✗ Fetch 실패: ${e.message}`));
|
|
9017
9064
|
// 1.9.245: --skeleton fallback — Cloudflare/WAF 차단된 URL 도 빈 .md 골격 생성 → 사용자가 수동 채움
|
|
@@ -9038,9 +9085,9 @@ async function apiSkillCmd(root, sub) {
|
|
|
9038
9085
|
_hlog(yl(`⚠ 빈 골격 생성 (--skeleton fallback): .leerness/api-skills/${id}.md`));
|
|
9039
9086
|
_hlog(dm(` → 직접 ${url} 내용을 복사해서 .md 파일에 채워넣어주세요`));
|
|
9040
9087
|
_hlog(dm(` → 이후 leerness handoff 가 매칭 시 참조됩니다`));
|
|
9041
|
-
if (_j) log(JSON.stringify({ ok: true, id, skeleton: true, url, name: skillName, fetchError: e.message }, null, 2));
|
|
9088
|
+
if (_j) log(JSON.stringify({ ok: true, id, skeleton: true, url, name: skillName, crawl: !noCrawl, related_count: 0, fetchError: e.message }, null, 2));
|
|
9042
9089
|
} else {
|
|
9043
|
-
if (_j) log(JSON.stringify({ ok: false, code: 'fetch_failed', error: e.message, url }, null, 2)); // 실패도 구조화(종전: --json 에 사람용 텍스트만)
|
|
9090
|
+
if (_j) log(JSON.stringify({ ok: false, code: 'fetch_failed', error: e.message, url, crawl: !noCrawl }, null, 2)); // 실패도 구조화(종전: --json 에 사람용 텍스트만)
|
|
9044
9091
|
process.exitCode = 1;
|
|
9045
9092
|
}
|
|
9046
9093
|
}
|
|
@@ -10619,7 +10666,37 @@ function _p0103JsonOk() {
|
|
|
10619
10666
|
// ④ --json 이 아예 구현되지 않아 사람용 텍스트를 내던 표면(게시본 3/30) — 계약 스윕
|
|
10620
10667
|
const sweepBad = [['consistency', 'check', '--json'], ['agent-mode', 'tick', '--json'], ['graph', '--json']]
|
|
10621
10668
|
.filter(a => !parse(R(a)));
|
|
10622
|
-
|
|
10669
|
+
// ⑤ Memory DELETE 성공 경로도 같은 계약을 지킨다. 종전에는 mutation은 끝냈지만
|
|
10670
|
+
// stdout은 비고 사람용 ✓ 문구만 stderr에 남아 JSON 소비자가 성공할 때만 깨졌다(T-0093).
|
|
10671
|
+
const taskAdded = parse(R(['task', 'add', 'json-delete-task', '--json']));
|
|
10672
|
+
R(['decision', 'add', 'json-delete-decision']);
|
|
10673
|
+
R(['lesson', 'save', 'json-delete-lesson']);
|
|
10674
|
+
const ruleAdded = parse(R(['rule', 'add', 'json-delete-rule', '--trigger', 'every-session', '--json']));
|
|
10675
|
+
R(['plan', 'add', 'json-delete-plan']);
|
|
10676
|
+
if (!taskAdded?.id || !ruleAdded?.id) return false;
|
|
10677
|
+
fs.mkdirSync(path.join(proj, '.leerness', 'cache'), { recursive: true });
|
|
10678
|
+
fs.writeFileSync(path.join(proj, '.leerness', 'cache', 'auto-roadmap.json'), JSON.stringify({ enabled: true, onEveryChange: true }));
|
|
10679
|
+
const liveEnv = Object.assign({}, process.env, { LEERNESS_OFFLINE: '1' });
|
|
10680
|
+
delete liveEnv.LEERNESS_NO_AUTO_ROADMAP;
|
|
10681
|
+
const RLive = (a) => cp2.spawnSync(process.execPath, [__filename, ...a], { cwd: proj, encoding: 'utf8', timeout: 180000, env: liveEnv });
|
|
10682
|
+
const deleteArgs = [
|
|
10683
|
+
['task', 'drop', taskAdded.id, '--json'],
|
|
10684
|
+
['decision', 'drop', 'json-delete-decision', '--json'],
|
|
10685
|
+
['lesson', 'drop', 'json-delete-lesson', '--json'],
|
|
10686
|
+
['rule', 'remove', ruleAdded.id, '--json'],
|
|
10687
|
+
['plan', 'remove', 'json-delete-plan', '--json'],
|
|
10688
|
+
];
|
|
10689
|
+
const graphPath = path.join(proj, 'leerness.html');
|
|
10690
|
+
const deleteResults = [];
|
|
10691
|
+
let roadmapOk = true;
|
|
10692
|
+
for (const args of deleteArgs) {
|
|
10693
|
+
fs.rmSync(graphPath, { force: true });
|
|
10694
|
+
deleteResults.push(RLive(args));
|
|
10695
|
+
roadmapOk = roadmapOk && fs.existsSync(graphPath);
|
|
10696
|
+
}
|
|
10697
|
+
const deleteOk = deleteResults.every(r => r.status === 0 && !(r.stderr || '').trim() && parse(r)?.ok === true)
|
|
10698
|
+
&& roadmapOk;
|
|
10699
|
+
return failuresOk && successOk && keepOk && sweepBad.length === 0 && deleteOk;
|
|
10623
10700
|
} catch { return false; } finally { try { fs.rmSync(arena, { recursive: true, force: true }); } catch { /* 정리 실패는 판정과 무관 */ } }
|
|
10624
10701
|
}
|
|
10625
10702
|
// 1.36.103: 값-플래그 등록 완전성 + 오타 가시성 회귀 가드.
|
|
@@ -11933,7 +12010,7 @@ function _inferScopeExpansion(text, root) {
|
|
|
11933
12010
|
result.expansionCandidates = [];
|
|
11934
12011
|
result.note = `intent: precise — 명시 요청만 진행, AI 추론 확장 비활성 (사용자 의도 보호)`;
|
|
11935
12012
|
} else if (classify.intent === 'broad') {
|
|
11936
|
-
result.note = `intent: broad — ${result.expansionCandidates.length} 확장 후보 제시
|
|
12013
|
+
result.note = `intent: broad — ${result.expansionCandidates.length} 확장 후보 제시(dry-run); 선택한 후보는 leerness task add 로 별도 등록`;
|
|
11937
12014
|
} else {
|
|
11938
12015
|
result.note = `intent: default — 명시 ${result.explicitMentions.length}건만 우선, 확장 후보 ${result.expansionCandidates.length}건 검토용`;
|
|
11939
12016
|
}
|
|
@@ -14688,9 +14765,9 @@ function planProgress(root, opts = {}) {
|
|
|
14688
14765
|
// Memory Surface DELETE 5종 완전 완성 (task drop / decision drop / lesson drop / rule remove / plan remove)
|
|
14689
14766
|
function planRemoveCmd(root, target) {
|
|
14690
14767
|
root = absRoot(root);
|
|
14691
|
-
if (!target) return
|
|
14768
|
+
if (!target) return failJson(has('--json'), 'missing_target', 'plan remove <M-XXXX|title-substring> 필요 — 매칭되는 milestone 블록을 제거하고 .leerness/plan.archive.md에 보존');
|
|
14692
14769
|
const pp = planPath(root);
|
|
14693
|
-
if (!exists(pp)) return
|
|
14770
|
+
if (!exists(pp)) return failJson(has('--json'), 'plan_not_found', 'plan.md 없음');
|
|
14694
14771
|
let removed = 0;
|
|
14695
14772
|
let disappeared = false;
|
|
14696
14773
|
// 1.36.152 (T-0115): plan restore/add와 archive를 공유한다. 락 안에서 다시 읽어야 새 milestone을 덮지 않는다.
|
|
@@ -14730,8 +14807,13 @@ function planRemoveCmd(root, target) {
|
|
|
14730
14807
|
if (removed === 0) return;
|
|
14731
14808
|
writeUtf8(pp, kept.join('\n'));
|
|
14732
14809
|
});
|
|
14733
|
-
if (disappeared) return
|
|
14810
|
+
if (disappeared) return failJson(has('--json'), 'plan_not_found', 'plan.md 없음');
|
|
14734
14811
|
if (removed === 0) return failJson(has('--json'), 'milestone_not_found', `매칭 milestone 없음: "${target}"`); // codex P2: --json 에러 구조화
|
|
14812
|
+
if (has('--json')) {
|
|
14813
|
+
_autoRoadmap(absRoot(root), 'data-change', { quiet: true });
|
|
14814
|
+
log(JSON.stringify({ ok: true, surface: 'plan', action: 'remove', target, removedCount: removed, archive: '.leerness/plan.archive.md' }, null, 2));
|
|
14815
|
+
return;
|
|
14816
|
+
}
|
|
14735
14817
|
ok(`milestone removed: ${removed}건 (보존: .leerness/plan.archive.md)`);
|
|
14736
14818
|
_autoRoadmap(absRoot(root), 'data-change');
|
|
14737
14819
|
}
|
|
@@ -14997,10 +15079,12 @@ const _STRICT_COMMAND_FLAGS = {
|
|
|
14997
15079
|
'verify-claim baseline show': { allowed: [], globals: [..._COMMON_COMMAND_FLAGS, '--json'], usage: 'verify-claim baseline show [--json]' },
|
|
14998
15080
|
'verify-code': { allowed: ['--build', '--bench', '--strict'], globals: [..._COMMON_COMMAND_FLAGS], usage: 'verify-code [path] [--build] [--bench] [--strict]' },
|
|
14999
15081
|
'contract verify': { allowed: ['--allow-empty'], globals: [..._COMMON_COMMAND_FLAGS, '--json'], usage: 'contract verify <spec.md> <impl.js> [--allow-empty] [--json]' },
|
|
15082
|
+
'intent expand': { allowed: [], globals: [..._COMMON_COMMAND_FLAGS, '--json'], usage: 'intent expand "<request>" [--json]' },
|
|
15000
15083
|
};
|
|
15001
15084
|
function _validateCommandFlags(cmd, args) {
|
|
15002
15085
|
let route = cmd === 'contract' && args[1] === 'verify' ? 'contract verify' : cmd;
|
|
15003
15086
|
if (cmd === 'verify-claim' && args[1] === 'baseline') route = `verify-claim baseline ${args[2] || ''}`.trim();
|
|
15087
|
+
if (cmd === 'intent' && args[1] === 'expand') route = 'intent expand';
|
|
15004
15088
|
const cfg = _STRICT_COMMAND_FLAGS[route];
|
|
15005
15089
|
if (!cfg) return true;
|
|
15006
15090
|
return _rejectUnknownFlags(cfg.allowed, cfg.usage, { globals: cfg.globals });
|
|
@@ -15065,12 +15149,19 @@ function taskUpdate(root, id) {
|
|
|
15065
15149
|
}
|
|
15066
15150
|
function taskDrop(root, id) {
|
|
15067
15151
|
if (!_requireInit(root, 'task drop')) return; // 1.9.396 (6번째 외부평가/codex P1-B): init 가드
|
|
15068
|
-
if (!id) return
|
|
15152
|
+
if (!id) return failJson(has('--json'), 'missing_id', 'id required');
|
|
15069
15153
|
const rows = readProgressRows(root);
|
|
15070
15154
|
// 1.9.396 (6번째 외부평가/codex P1-B): 없는 task drop 시 가짜 row(request undefined) 생성 = 데이터 손상 → task update 와 동일하게 존재 확인 후 fail(no-op).
|
|
15071
15155
|
if (!rows.find(r => r.id === id)) { failJson(has('--json'), 'task_not_found', `task ${id} not found in progress-tracker.md`); return; } // codex P2: --json 에러 구조화
|
|
15072
15156
|
const _tdEn = _uiLang(root) === 'en'; // 1.36.63 (검수 #2)
|
|
15073
|
-
|
|
15157
|
+
const evidence = arg('--reason', _tdEn ? 'dropped by user request' : '사용자 요청으로 제외');
|
|
15158
|
+
const nextAction = _tdEn ? 'none' : '없음';
|
|
15159
|
+
upsertProgress(root, { id, status: 'dropped', evidence, nextAction });
|
|
15160
|
+
if (has('--json')) {
|
|
15161
|
+
_autoRoadmap(absRoot(root), 'data-change', { quiet: true });
|
|
15162
|
+
log(JSON.stringify({ ok: true, surface: 'tasks', action: 'drop', id, status: 'dropped', evidence, nextAction }, null, 2));
|
|
15163
|
+
return;
|
|
15164
|
+
}
|
|
15074
15165
|
ok(`task dropped: ${id}`);
|
|
15075
15166
|
_autoRoadmap(absRoot(root), 'data-change');
|
|
15076
15167
|
}
|
|
@@ -15385,7 +15476,7 @@ function lessonDropCmd(root, target) {
|
|
|
15385
15476
|
// 1.36.140 (재검수 P3): 락/쓰기 헬퍼가 부모를 만들기 때문에, 오타 난 `--path` 로 실패하면서도
|
|
15386
15477
|
// 그 경로와 `.leerness/` 를 만들었다(1.36.139 는 안 만들었다 — 대조군). 없으면 아무것도 만들지 않는다.
|
|
15387
15478
|
if (!exists(root)) return failJson(has('--json'), 'path_not_found', `경로가 없습니다: ${root} — 디렉토리를 새로 만들지 않았습니다`);
|
|
15388
|
-
if (!target) return
|
|
15479
|
+
if (!target) return failJson(has('--json'), 'missing_target', 'lesson drop <date|text-substring> 필요. 예: leerness lesson drop "2026-05-20" 또는 leerness lesson drop "JWT"');
|
|
15389
15480
|
// archive 보존 — lessons.archive.md 에 projection MD 블록 형태로 추가
|
|
15390
15481
|
const archivePath = path.join(root, '.leerness/lessons.archive.md');
|
|
15391
15482
|
// 1.36.140 (재검수 P1, 재현): 종전엔 락 **밖**에서 매칭한 뒤 락 안에서 `${date}|${text}` 복합 키로
|
|
@@ -15417,6 +15508,11 @@ function lessonDropCmd(root, target) {
|
|
|
15417
15508
|
});
|
|
15418
15509
|
if (loadErr === 'no_lessons') return failJson(has('--json'), 'no_lessons', 'lessons 없음'); // codex P2: --json 에러 구조화
|
|
15419
15510
|
if (loadErr === 'lesson_not_found' || !removed.length) return failJson(has('--json'), 'lesson_not_found', `매칭 lesson 없음: "${target}"`);
|
|
15511
|
+
if (has('--json')) {
|
|
15512
|
+
_autoRoadmap(absRoot(root), 'data-change', { quiet: true });
|
|
15513
|
+
log(JSON.stringify({ ok: true, surface: 'lessons', action: 'drop', target, removedCount: removed.length, archive: '.leerness/lessons.archive.md' }, null, 2));
|
|
15514
|
+
return;
|
|
15515
|
+
}
|
|
15420
15516
|
ok(`lesson dropped: ${removed.length}건 (보존: .leerness/lessons.archive.md)`);
|
|
15421
15517
|
_autoRoadmap(absRoot(root), 'data-change');
|
|
15422
15518
|
}
|
|
@@ -15494,7 +15590,7 @@ function decisionDropCmd(root, target) {
|
|
|
15494
15590
|
// 1.36.140 (재검수 P3): 락/쓰기 헬퍼가 부모를 만들기 때문에, 오타 난 `--path` 로 실패하면서도
|
|
15495
15591
|
// 그 경로와 `.leerness/` 를 만들었다(1.36.139 는 안 만들었다 — 대조군). 없으면 아무것도 만들지 않는다.
|
|
15496
15592
|
if (!exists(root)) return failJson(has('--json'), 'path_not_found', `경로가 없습니다: ${root} — 디렉토리를 새로 만들지 않았습니다`);
|
|
15497
|
-
if (!target) return
|
|
15593
|
+
if (!target) return failJson(has('--json'), 'missing_target', 'decision drop <date|title-substring> 필요. 예: leerness decision drop "2026-05-20" 또는 leerness decision drop "PostgreSQL"');
|
|
15498
15594
|
// 1.9.339 (UR-0053): canonical JSON 기준 drop (date 또는 title substring 매칭) — JSON+MD projection 동시 갱신.
|
|
15499
15595
|
const archivePath = path.join(root, '.leerness/decisions.archive.md');
|
|
15500
15596
|
// 1.36.138 (동시성 감사 실측): `decision add` 는 락 안인데 **drop 은 락 밖**이었다 —
|
|
@@ -15527,6 +15623,11 @@ function decisionDropCmd(root, target) {
|
|
|
15527
15623
|
});
|
|
15528
15624
|
if (loadErr === 'no_decisions') return failJson(has('--json'), 'no_decisions', 'decisions 없음'); // codex P2: --json 에러 구조화
|
|
15529
15625
|
if (loadErr === 'decision_not_found' || !removed.length) return failJson(has('--json'), 'decision_not_found', `매칭 decision 없음: "${target}"`);
|
|
15626
|
+
if (has('--json')) {
|
|
15627
|
+
_autoRoadmap(absRoot(root), 'data-change', { quiet: true });
|
|
15628
|
+
log(JSON.stringify({ ok: true, surface: 'decisions', action: 'drop', target, removedCount: removed.length, archive: '.leerness/decisions.archive.md' }, null, 2));
|
|
15629
|
+
return;
|
|
15630
|
+
}
|
|
15530
15631
|
ok(`decision dropped: ${removed.length}건 (보존: .leerness/decisions.archive.md)`);
|
|
15531
15632
|
_autoRoadmap(absRoot(root), 'data-change');
|
|
15532
15633
|
}
|
|
@@ -20241,6 +20342,7 @@ function _allProviders(root) {
|
|
|
20241
20342
|
versionArgs: _uvaOk ? _uva : _fallbackVa,
|
|
20242
20343
|
...((_uva && !_uvaOk) ? { versionArgsRejectedReason: `providers.json 의 versionArgs 에 셸 메타문자가 있어 실행하지 않았습니다: ${JSON.stringify(_uva).slice(0, 120)}` } : {}),
|
|
20243
20344
|
desc: u.desc || (_bi && _bi.desc) || `(user) ${u.id}`,
|
|
20345
|
+
descEn: u.desc || (_bi && _bi.descEn) || (_bi && _bi.desc) || `(user) ${u.id}`,
|
|
20244
20346
|
installHint: u.installHint || (_bi && _bi.installHint) || '',
|
|
20245
20347
|
installCmd: u.installCmd || (_bi && _bi.installCmd) || '',
|
|
20246
20348
|
...(_inherited ? { authCheck: _inherited } : {}),
|
|
@@ -20277,22 +20379,36 @@ function providerCmd(root, sub, ...args) {
|
|
|
20277
20379
|
total: all.length,
|
|
20278
20380
|
builtin: EXTERNAL_AGENTS.length,
|
|
20279
20381
|
user: userList.length,
|
|
20280
|
-
providers: all.map(p => ({
|
|
20382
|
+
providers: all.map(p => ({
|
|
20383
|
+
id: p.id,
|
|
20384
|
+
bin: p.bin,
|
|
20385
|
+
envFlag: p.envFlag,
|
|
20386
|
+
versionArgs: Array.isArray(p.versionArgs) ? p.versionArgs.slice() : [],
|
|
20387
|
+
desc: p.desc || '',
|
|
20388
|
+
installHint: p.installHint || '',
|
|
20389
|
+
source: userIds.has(p.id) ? 'user' : 'builtin'
|
|
20390
|
+
}))
|
|
20281
20391
|
}, null, 2));
|
|
20282
20392
|
return;
|
|
20283
20393
|
}
|
|
20394
|
+
const en = _uiLang(root) === 'en';
|
|
20395
|
+
const cell = v => String(v == null ? '' : v).replace(/\r?\n/g, ' ').replace(/\|/g, '\\|');
|
|
20284
20396
|
log(`# leerness provider list (1.9.157)`);
|
|
20285
|
-
log(
|
|
20397
|
+
log(en
|
|
20398
|
+
? `Total ${all.length} (${EXTERNAL_AGENTS.length} built-in + ${userList.length} user-defined)`
|
|
20399
|
+
: `총 ${all.length}개 (빌트인 ${EXTERNAL_AGENTS.length} + 사용자 ${userList.length})`);
|
|
20286
20400
|
log('');
|
|
20287
|
-
log(`| id | source | bin | envFlag |`);
|
|
20288
|
-
log(
|
|
20401
|
+
log(`| id | source | bin | envFlag | versionArgs | description |`);
|
|
20402
|
+
log(`|---|---|---|---|---|---|`);
|
|
20289
20403
|
for (const p of all) {
|
|
20290
20404
|
const src = userIds.has(p.id) ? (EXTERNAL_AGENTS.some(b => b.id === p.id) ? 'user(override)' : 'user') : 'builtin';
|
|
20291
|
-
log(`| ${p.id} | ${src} | ${p.bin} | ${p.envFlag} |`);
|
|
20405
|
+
log(`| ${cell(p.id)} | ${src} | ${cell(p.bin)} | ${cell(p.envFlag)} | ${cell((p.versionArgs || []).join(' '))} | ${cell(en ? (p.descEn || p.desc) : p.desc)} |`);
|
|
20292
20406
|
}
|
|
20293
20407
|
if (!userList.length) {
|
|
20294
20408
|
log('');
|
|
20295
|
-
log(
|
|
20409
|
+
log(en
|
|
20410
|
+
? `💡 Add a user-defined provider: leerness provider add <id> --bin <cmd> [--env-flag F] [--version-args ARGS] [--desc D]`
|
|
20411
|
+
: `💡 사용자 정의 provider 추가: leerness provider add <id> --bin <cmd> [--env-flag F] [--version-args ARGS] [--desc D]`);
|
|
20296
20412
|
}
|
|
20297
20413
|
return;
|
|
20298
20414
|
}
|
|
@@ -21120,6 +21236,14 @@ function _tryInstallAgent(agent) {
|
|
|
21120
21236
|
// 1.9.34: 방향키/스페이스 multi-select 도입 (LEERNESS_NO_INTERACTIVE=1 → 기존 yes/no 폴백)
|
|
21121
21237
|
async function setupAgentsCmd(root, opts = {}) {
|
|
21122
21238
|
root = absRoot(root || process.cwd());
|
|
21239
|
+
// T-0094: init 의 agent 선택을 건너뛰는 opt-out. standalone 에 붙어도
|
|
21240
|
+
// 플래그를 경로로 오독하거나 설정 화면을 띄우지 않는 명시적 no-op 으로 유지한다.
|
|
21241
|
+
if (has('--no-setup-agents')) {
|
|
21242
|
+
const payload = { ok: true, skipped: true, reason: 'no_setup_agents', root };
|
|
21243
|
+
if (has('--json')) log(JSON.stringify(payload, null, 2));
|
|
21244
|
+
else log(`⏭ 외부 AI CLI 설정 건너뜀 (--no-setup-agents): ${root}`);
|
|
21245
|
+
return payload;
|
|
21246
|
+
}
|
|
21123
21247
|
_loadEnvFile(root);
|
|
21124
21248
|
_loadEnvFile(path.join(root, '..'));
|
|
21125
21249
|
const envPath = path.join(root, '.env');
|
|
@@ -22555,7 +22679,8 @@ function _autoRoadmapConfig(root) {
|
|
|
22555
22679
|
function _saveAutoRoadmapConfig(root, cfg) {
|
|
22556
22680
|
writeUtf8(_autoRoadmapConfigPath(root), JSON.stringify(cfg, null, 2) + '\n');
|
|
22557
22681
|
}
|
|
22558
|
-
function _autoRoadmap(root, trigger) {
|
|
22682
|
+
function _autoRoadmap(root, trigger, opts = {}) {
|
|
22683
|
+
const quiet = !!(opts && opts.quiet);
|
|
22559
22684
|
try {
|
|
22560
22685
|
if (process.env.LEERNESS_NO_AUTO_ROADMAP === '1') return false;
|
|
22561
22686
|
if (!exists(path.join(root, '.leerness'))) return false;
|
|
@@ -22569,10 +22694,10 @@ function _autoRoadmap(root, trigger) {
|
|
|
22569
22694
|
const outFile = path.resolve(cfg.outFile || path.join(root, 'leerness.html'));
|
|
22570
22695
|
const s = _graph.graphHtmlCmd(root, { _roadmapData, _loadDecisions, _loadLessons, _parseFeatureGraph, _loadToggles: _tgl.loadToggles, _toggleRegistry: _tgl.TOGGLE_REGISTRY, _loadTechProfile: _tech.loadTechProfile, quiet: true }, outFile);
|
|
22571
22696
|
const _en = _uiLang(root) === 'en';
|
|
22572
|
-
log(_en ? `✓ ontology graph auto-updated (${trigger}) — ${rel(root, outFile)} (graph+roadmap+toggles)` : `✓ 온톨로지 그래프 자동 갱신 (${trigger}) — ${rel(root, outFile)} (그래프+로드맵+토글 탭)`);
|
|
22697
|
+
if (!quiet) log(_en ? `✓ ontology graph auto-updated (${trigger}) — ${rel(root, outFile)} (graph+roadmap+toggles)` : `✓ 온톨로지 그래프 자동 갱신 (${trigger}) — ${rel(root, outFile)} (그래프+로드맵+토글 탭)`);
|
|
22573
22698
|
return true;
|
|
22574
22699
|
} catch (e) {
|
|
22575
|
-
warn((_uiLang(root) === 'en' ? 'roadmap auto-update failed: ' : 'roadmap 자동 갱신 실패: ') + (e && e.message ? e.message : e));
|
|
22700
|
+
if (!quiet) warn((_uiLang(root) === 'en' ? 'roadmap auto-update failed: ' : 'roadmap 자동 갱신 실패: ') + (e && e.message ? e.message : e));
|
|
22576
22701
|
return false;
|
|
22577
22702
|
}
|
|
22578
22703
|
}
|
|
@@ -22850,7 +22975,7 @@ function _updateRules(root, mutate) {
|
|
|
22850
22975
|
|
|
22851
22976
|
function ruleRemove(root, id) {
|
|
22852
22977
|
root = absRoot(root);
|
|
22853
|
-
if (!id) return
|
|
22978
|
+
if (!id) return failJson(has('--json'), 'missing_id', 'id required');
|
|
22854
22979
|
let removed = null;
|
|
22855
22980
|
const res = _updateRules(root, (rules) => {
|
|
22856
22981
|
const i = rules.findIndex(r => r.id === id);
|
|
@@ -22863,7 +22988,13 @@ function ruleRemove(root, id) {
|
|
|
22863
22988
|
return removed;
|
|
22864
22989
|
});
|
|
22865
22990
|
if (res.error) return failJson(has('--json'), 'rule_not_found', `rule not found: ${id}`); // codex P2: --json 에러 구조화
|
|
22991
|
+
if (has('--json')) {
|
|
22992
|
+
_autoRoadmap(root, 'data-change', { quiet: true });
|
|
22993
|
+
log(JSON.stringify({ ok: true, surface: 'rules', action: 'remove', id, removedCount: 1, archive: '.leerness/rules.archive.md' }, null, 2));
|
|
22994
|
+
return;
|
|
22995
|
+
}
|
|
22866
22996
|
ok(`rule removed: ${id} (보존: .leerness/rules.archive.md)`);
|
|
22997
|
+
_autoRoadmap(root, 'data-change');
|
|
22867
22998
|
}
|
|
22868
22999
|
|
|
22869
23000
|
function rulePause(root, id) {
|
|
@@ -24522,6 +24653,47 @@ async function updateCmd(root, opts = {}) {
|
|
|
24522
24653
|
ok('update complete');
|
|
24523
24654
|
}
|
|
24524
24655
|
|
|
24656
|
+
const _AUTO_UPDATE_CHECK_HOOK = Object.freeze({ matcher: '*', command: 'leerness update --check --quiet' });
|
|
24657
|
+
const _AUTO_UPDATE_CONTEXT_HOOK = Object.freeze({ matcher: 'startup|clear|compact', command: 'leerness hook session-start' });
|
|
24658
|
+
function _exactHook(hook, expected) {
|
|
24659
|
+
return !!(hook && typeof hook.command === 'string' && typeof hook.matcher === 'string' &&
|
|
24660
|
+
hook.command.trim() === expected.command && hook.matcher.trim() === expected.matcher);
|
|
24661
|
+
}
|
|
24662
|
+
function _validUpdateSlashCommand(file) {
|
|
24663
|
+
if (!exists(file)) return false;
|
|
24664
|
+
try {
|
|
24665
|
+
const content = read(file);
|
|
24666
|
+
return /^\s*!leerness\s+update\s+--yes\s*$/m.test(content) &&
|
|
24667
|
+
/^\s*!leerness\s+update\s+--check\s*$/m.test(content);
|
|
24668
|
+
} catch { return false; }
|
|
24669
|
+
}
|
|
24670
|
+
function _validAutoUpdateSettingsShape(settings) {
|
|
24671
|
+
if (!settings || typeof settings !== 'object' || Array.isArray(settings)) return false;
|
|
24672
|
+
const own = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key);
|
|
24673
|
+
if (own(settings, 'hooks') && (!settings.hooks || typeof settings.hooks !== 'object' || Array.isArray(settings.hooks))) return false;
|
|
24674
|
+
if (settings.hooks && own(settings.hooks, 'SessionStart')) {
|
|
24675
|
+
if (!Array.isArray(settings.hooks.SessionStart)) return false;
|
|
24676
|
+
// Preserve foreign/modern hook objects, but never treat scalar/array entries or
|
|
24677
|
+
// mistyped fields as a writable settings document. Otherwise install's filter
|
|
24678
|
+
// silently retains the bad entry and rewrites the user's file around it.
|
|
24679
|
+
const validHookEntry = hook => {
|
|
24680
|
+
if (!hook || typeof hook !== 'object' || Array.isArray(hook)) return false;
|
|
24681
|
+
if (own(hook, 'matcher') && typeof hook.matcher !== 'string') return false;
|
|
24682
|
+
if (own(hook, 'command') && typeof hook.command !== 'string') return false;
|
|
24683
|
+
if (own(hook, 'type') && typeof hook.type !== 'string') return false;
|
|
24684
|
+
if (own(hook, 'hooks')) {
|
|
24685
|
+
if (!Array.isArray(hook.hooks)) return false;
|
|
24686
|
+
if (!hook.hooks.every(nested => nested && typeof nested === 'object' && !Array.isArray(nested)
|
|
24687
|
+
&& (!own(nested, 'command') || typeof nested.command === 'string')
|
|
24688
|
+
&& (!own(nested, 'type') || typeof nested.type === 'string'))) return false;
|
|
24689
|
+
}
|
|
24690
|
+
return true;
|
|
24691
|
+
};
|
|
24692
|
+
if (!settings.hooks.SessionStart.every(validHookEntry)) return false;
|
|
24693
|
+
}
|
|
24694
|
+
return true;
|
|
24695
|
+
}
|
|
24696
|
+
|
|
24525
24697
|
function autoUpdateInstall(root) {
|
|
24526
24698
|
root = absRoot(root);
|
|
24527
24699
|
const settingsDir = path.join(root, '.claude');
|
|
@@ -24531,25 +24703,28 @@ function autoUpdateInstall(root) {
|
|
|
24531
24703
|
// 1.36.33 (codex 3차 #3, 1.36.28 손상-스토어 클래스): 손상 settings 를 빈 객체로 오인해 hooks-only 로 덮어쓰면
|
|
24532
24704
|
// 사용자의 permissions 등 무관 설정이 유실된다. 파싱 실패 시 원본 보존 + 중단.
|
|
24533
24705
|
if (exists(settingsFile)) {
|
|
24534
|
-
try {
|
|
24535
|
-
|
|
24706
|
+
try {
|
|
24707
|
+
settings = JSON.parse(read(settingsFile));
|
|
24708
|
+
if (!_validAutoUpdateSettingsShape(settings)) throw new Error('settings schema invalid');
|
|
24709
|
+
} catch { fail(`.claude/settings.local.json 이 손상돼(JSON 파싱/형상 실패) hook 설치를 중단합니다 — 파일을 복구한 뒤 재시도: ${settingsFile}`); process.exitCode = 1; return; }
|
|
24536
24710
|
}
|
|
24537
24711
|
settings.hooks = settings.hooks || {};
|
|
24538
24712
|
// 1.9.1 P1: legacy 'leerness-plus update' hook 자동 제거 (이전 fork 시절 잔재).
|
|
24539
24713
|
let removedLegacy = 0, upgradedQuiet = 0;
|
|
24540
24714
|
settings.hooks.SessionStart = (settings.hooks.SessionStart || []).filter(h => {
|
|
24541
|
-
|
|
24715
|
+
const command = h && typeof h.command === 'string' ? h.command.trim() : '';
|
|
24716
|
+
if (/^leerness-plus\s+update(?:\s|$)/.test(command)) { removedLegacy++; return false; }
|
|
24542
24717
|
// 1.9.364 (4번째 외부평가 9.3): 비-quiet 'leerness update --check' 를 quiet 버전으로 업그레이드 (기존 설치 매세션 노이즈 제거)
|
|
24543
|
-
if (
|
|
24718
|
+
if (command === 'leerness update --check') { upgradedQuiet++; return false; }
|
|
24544
24719
|
return true;
|
|
24545
24720
|
});
|
|
24546
|
-
if (!settings.hooks.SessionStart.some(h => h
|
|
24547
|
-
settings.hooks.SessionStart.push({
|
|
24721
|
+
if (!settings.hooks.SessionStart.some(h => _exactHook(h, _AUTO_UPDATE_CHECK_HOOK))) {
|
|
24722
|
+
settings.hooks.SessionStart.push({ ..._AUTO_UPDATE_CHECK_HOOK });
|
|
24548
24723
|
}
|
|
24549
24724
|
// 1.36.22: 2번째 SessionStart hook — 컨텍스트 주입. matcher 에 compact 를 포함하는 게 핵심(실측 결함: compaction/resume 후
|
|
24550
24725
|
// 에이전트가 세션시작 handoff 의례를 건너뜀). 기존 설치는 이 멱등 push 로 자동 취득(1.9.364 업그레이드 패턴 재사용).
|
|
24551
|
-
if (!settings.hooks.SessionStart.some(h => h
|
|
24552
|
-
settings.hooks.SessionStart.push({
|
|
24726
|
+
if (!settings.hooks.SessionStart.some(h => _exactHook(h, _AUTO_UPDATE_CONTEXT_HOOK))) {
|
|
24727
|
+
settings.hooks.SessionStart.push({ ..._AUTO_UPDATE_CONTEXT_HOOK });
|
|
24553
24728
|
}
|
|
24554
24729
|
writeUtf8(settingsFile, JSON.stringify(settings, null, 2) + '\n');
|
|
24555
24730
|
// 1.36.62 (검수 #1): en 프로젝트의 /update 를 한국어로 되덮던 것 — 언어 인지 렌더
|
|
@@ -24571,6 +24746,71 @@ function autoUpdateInstall(root) {
|
|
|
24571
24746
|
log(` ⓘ 제거: ${rel(root, settingsFile)} 의 hooks.SessionStart 항목 삭제 · 설치 시 끄기: leerness init . --no-auto-update`);
|
|
24572
24747
|
}
|
|
24573
24748
|
|
|
24749
|
+
// T-0094: help/command catalog 에 공개돼 있던 read-only 상태 표면. 설치 명령과 같은
|
|
24750
|
+
// 세 구성요소(update check hook, context hook, /update command)를 보고하며 파일은 만들거나 고치지 않는다.
|
|
24751
|
+
function autoUpdateStatus(root) {
|
|
24752
|
+
root = absRoot(root || process.cwd());
|
|
24753
|
+
if (exists(root) && !fs.statSync(root).isDirectory()) {
|
|
24754
|
+
const payload = { ok: false, code: 'path_not_directory', root, error: `프로젝트 경로가 디렉토리가 아닙니다: ${root}` };
|
|
24755
|
+
if (has('--json')) log(JSON.stringify(payload, null, 2));
|
|
24756
|
+
else fail(payload.error);
|
|
24757
|
+
process.exitCode = 1;
|
|
24758
|
+
return payload;
|
|
24759
|
+
}
|
|
24760
|
+
const settingsFile = path.join(root, '.claude', 'settings.local.json');
|
|
24761
|
+
const slashFile = path.join(root, '.claude', 'commands', 'update.md');
|
|
24762
|
+
const base = {
|
|
24763
|
+
ok: true,
|
|
24764
|
+
root,
|
|
24765
|
+
settingsFile,
|
|
24766
|
+
exists: exists(settingsFile),
|
|
24767
|
+
valid: true,
|
|
24768
|
+
installed: false,
|
|
24769
|
+
hooks: { updateCheck: false, updateQuiet: false, contextInjection: false },
|
|
24770
|
+
slashCommand: _validUpdateSlashCommand(slashFile),
|
|
24771
|
+
};
|
|
24772
|
+
if (!base.exists) {
|
|
24773
|
+
if (has('--json')) log(JSON.stringify(base, null, 2));
|
|
24774
|
+
else {
|
|
24775
|
+
log(`# leerness auto-update status`);
|
|
24776
|
+
log(` ⚪ 미설치 — ${settingsFile} 없음`);
|
|
24777
|
+
log(` 설치: leerness auto-update install ${root}`);
|
|
24778
|
+
}
|
|
24779
|
+
return base;
|
|
24780
|
+
}
|
|
24781
|
+
let settings;
|
|
24782
|
+
try {
|
|
24783
|
+
settings = JSON.parse(read(settingsFile));
|
|
24784
|
+
if (!_validAutoUpdateSettingsShape(settings)) throw new Error('settings schema invalid');
|
|
24785
|
+
}
|
|
24786
|
+
catch {
|
|
24787
|
+
const payload = { ...base, ok: false, valid: false, code: 'settings_corrupt', error: `.claude/settings.local.json JSON 파싱/형상 실패: ${settingsFile}` };
|
|
24788
|
+
if (has('--json')) log(JSON.stringify(payload, null, 2));
|
|
24789
|
+
else fail(`auto-update 상태 확인 실패 — settings JSON 손상(파일은 변경하지 않음): ${settingsFile}`);
|
|
24790
|
+
process.exitCode = 1;
|
|
24791
|
+
return payload;
|
|
24792
|
+
}
|
|
24793
|
+
const sessionStart = settings && settings.hooks && Array.isArray(settings.hooks.SessionStart)
|
|
24794
|
+
? settings.hooks.SessionStart : [];
|
|
24795
|
+
base.hooks.updateQuiet = sessionStart.some(h => _exactHook(h, _AUTO_UPDATE_CHECK_HOOK));
|
|
24796
|
+
base.hooks.updateCheck = base.hooks.updateQuiet || sessionStart.some(h =>
|
|
24797
|
+
h && typeof h.command === 'string' && typeof h.matcher === 'string' &&
|
|
24798
|
+
h.command.trim() === 'leerness update --check' && h.matcher.trim() === _AUTO_UPDATE_CHECK_HOOK.matcher);
|
|
24799
|
+
base.hooks.contextInjection = sessionStart.some(h => _exactHook(h, _AUTO_UPDATE_CONTEXT_HOOK));
|
|
24800
|
+
// install이 생성하는 정확한 훅(`--quiet` 포함)이 아니면 일부 설치로 보고한다.
|
|
24801
|
+
base.installed = base.hooks.updateCheck && base.hooks.updateQuiet && base.hooks.contextInjection && base.slashCommand;
|
|
24802
|
+
if (has('--json')) log(JSON.stringify(base, null, 2));
|
|
24803
|
+
else {
|
|
24804
|
+
log(`# leerness auto-update status`);
|
|
24805
|
+
log(` ${base.installed ? '🟢 설치됨' : '🟡 일부 설치'} — ${settingsFile}`);
|
|
24806
|
+
log(` ${base.hooks.updateCheck ? '✓' : '✗'} update check hook${base.hooks.updateCheck && !base.hooks.updateQuiet ? ' (⚠ --quiet 없음)' : ''}`);
|
|
24807
|
+
log(` ${base.hooks.contextInjection ? '✓' : '✗'} context injection hook`);
|
|
24808
|
+
log(` ${base.slashCommand ? '✓' : '✗'} /update command (${slashFile})`);
|
|
24809
|
+
if (!base.installed) log(` 복구/설치: leerness auto-update install ${root}`);
|
|
24810
|
+
}
|
|
24811
|
+
return base;
|
|
24812
|
+
}
|
|
24813
|
+
|
|
24574
24814
|
// 1.9.151: ViewWork hook 제거 (사용자 명시 — leerness와 무관한 외부 도구)
|
|
24575
24815
|
// 이전 1.9.0~1.9.150 에서 .viewwork/ 디렉토리에 hook 으로 이벤트 기록했으나, ViewWork는 leerness 의존 산출물이
|
|
24576
24816
|
// 아닌 별도 도구임. 사용자가 직접 ViewWork 를 사용하지 않으면 leerness 가 이를 강제할 이유가 없음.
|
|
@@ -25610,6 +25850,8 @@ function _mcpToCliArgs(name, args, targetPath) {
|
|
|
25610
25850
|
cliArgs = ['api-skill', args.sub || 'list', '--path', targetPath, '--json'];
|
|
25611
25851
|
if (args.sub === 'add' && args.url) cliArgs.push('--url', args.url);
|
|
25612
25852
|
if (args.sub === 'add' && args.direction) cliArgs.push('--direction', args.direction);
|
|
25853
|
+
if (args.sub === 'add' && args.name) cliArgs.push('--name', args.name);
|
|
25854
|
+
if (args.sub === 'add' && args.noCrawl === true) cliArgs.push('--no-crawl');
|
|
25613
25855
|
if ((args.sub === 'show' || args.sub === 'drop') && args.id) cliArgs.push('--id', args.id);
|
|
25614
25856
|
if (args.sub === 'match' && args.query) cliArgs.push('--query', args.query);
|
|
25615
25857
|
break;
|
|
@@ -30218,12 +30460,12 @@ Korean-first by default; this English help shows with --language en or LEERNESS_
|
|
|
30218
30460
|
Usage: leerness <command> [path] [options]
|
|
30219
30461
|
|
|
30220
30462
|
SETUP & UPDATE
|
|
30221
|
-
init [path] [--language auto|ko|en] [--skills recommended|all|a,b] [--minimal] [--yes]
|
|
30463
|
+
init [path] [--language auto|ko|en] [--skills recommended|all|a,b] [--minimal] [--yes] [--no-setup-agents]
|
|
30222
30464
|
Install the .leerness/ workspace into your project
|
|
30223
30465
|
migrate [path] [--dry-run] [--force] Non-destructive cross-version migration
|
|
30224
30466
|
migrate audit|apply|plan [path] [--json] Diagnose / backfill / compare migration
|
|
30225
30467
|
update [path] [--check|--yes|--force] Auto-detect version + migrate
|
|
30226
|
-
auto-update install [path]
|
|
30468
|
+
auto-update install|status [path] Install or inspect the auto-update hook (status is read-only)
|
|
30227
30469
|
path-setup [--apply] Register the leerness CLI on PATH
|
|
30228
30470
|
|
|
30229
30471
|
STATUS & DIAGNOSTICS
|
|
@@ -30821,7 +31063,12 @@ async function main() {
|
|
|
30821
31063
|
return await install(arg('--path', _msub || process.cwd()), { force:has('--force'), dry:has('--dry-run'), migration:true }); // 1.9.355 (UR-0075): --path 지원
|
|
30822
31064
|
}
|
|
30823
31065
|
if (cmd === 'update') return await updateCmd(arg('--path', args[1] || process.cwd()), { checkOnly: has('--check'), yes: has('--yes'), force: has('--force') }); // 1.9.355 (UR-0075): --path 지원
|
|
30824
|
-
if (cmd === 'auto-update'
|
|
31066
|
+
if (cmd === 'auto-update') {
|
|
31067
|
+
if (args[1] === 'install') return autoUpdateInstall(_resolveRoot(args[2]));
|
|
31068
|
+
if (args[1] === 'status') return autoUpdateStatus(_resolveRoot(args[2]));
|
|
31069
|
+
failJson(has('--json'), 'unknown_subcommand', `알 수 없는 auto-update 하위명령: ${args[1] || '(없음)'} — leerness auto-update install|status [path]`);
|
|
31070
|
+
return;
|
|
31071
|
+
}
|
|
30825
31072
|
if (cmd === 'status') return status(arg('--path', args[1] || process.cwd()));
|
|
30826
31073
|
if (cmd === 'verify') return verify(arg('--path', args[1] || process.cwd()));
|
|
30827
31074
|
if (cmd === 'debug') return debug(arg('--path', args[1] || process.cwd()));
|
|
@@ -30957,7 +31204,8 @@ async function main() {
|
|
|
30957
31204
|
if (cmd === 'health') return healthCmd(args[1] || arg('--path', process.cwd()));
|
|
30958
31205
|
if (cmd === 'whats-new') return whatsNewCmd(args[1] || arg('--path', process.cwd()));
|
|
30959
31206
|
if (cmd === 'reuse' && args[1] === 'autodetect') return reuseAutodetectCmd(args[2] || arg('--path', process.cwd()));
|
|
30960
|
-
if (cmd === 'setup-agents'
|
|
31207
|
+
if (cmd === 'setup-agents') return await setupAgentsCmd(_resolveRoot(args[1]));
|
|
31208
|
+
if (cmd === 'setup' && args[1] === 'agents') return await setupAgentsCmd(_resolveRoot(args[2]));
|
|
30961
31209
|
if (cmd === 'session' && args[1] === 'close') return sessionClose(_resolveRoot(args[2]), { json: has('--json') });
|
|
30962
31210
|
// 1.9.151: viewwork 명령 제거 (사용자 명시 — leerness 와 무관). session close 의 viewworkEmit 콜도 함께 제거.
|
|
30963
31211
|
if (cmd === 'ci' && (args[1] === 'init' || args[1] == null)) return ciInitCmd(absRoot(_resolveRoot(args[2])), { force: has('--force'), json: has('--json') }); // 1.9.444 (UR-0152): CI gate 워크플로 생성
|