leerness 1.30.0 → 1.32.0
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 +199 -0
- package/README.md +4 -4
- package/bin/leerness.js +314 -103
- package/lib/audit.js +17 -1
- package/lib/catalogs.js +23 -22
- package/lib/pure-utils.js +15 -4
- package/package.json +1 -1
- package/scripts/e2e.js +244 -7
package/bin/leerness.js
CHANGED
|
@@ -32,7 +32,7 @@ const { _evidenceQuality, _parseEvidenceStats, _shellGuardAnalyze, _claimFileInG
|
|
|
32
32
|
// 1.9.295 (UR-0025 4단계): 정적 데이터 카탈로그 모듈 분리 (비파괴, require-based).
|
|
33
33
|
const { CAPABILITY_SURFACE, POWERFUL_COMMANDS, ADAPTERS, REUSE_CATEGORIES, REUSE_CHECKLIST, _DEFAULT_PLATFORM_CONSTRAINTS, _DEFAULT_DOMAIN_CATALOG, _TOOL_CATALOG, _LSP_LANG_PATTERNS, OPTIMISM_PATTERNS, BUILT_IN_PERSONAS, STRINGS, BUILTIN_CATALOG, ROADMAP_STATUS_LABEL, ROADMAP_STATUS_COLOR, SECRET_PATTERNS, MERGE_OVERWRITE_FILES, MINIMAL_SKIP_KEYS, REQUIRED_WORKSPACE_FILES, KEYWORD_STOPWORDS, SKILL_CATALOG_PRESETS } = require('../lib/catalogs'); // 1.9.344/368/369 (UR-0025): catalog 분리 · 1.11.4 (UR-0007): _TOOL_CATALOG
|
|
34
34
|
|
|
35
|
-
const VERSION = '1.
|
|
35
|
+
const VERSION = '1.32.0';
|
|
36
36
|
|
|
37
37
|
// 1.9.290 (UR-0037, Codex gpt-5.5 #4 수렴): CLI 전용 부작용은 require 시 실행하지 않는다.
|
|
38
38
|
// 이전: warning listener 제거 / NODE_OPTIONS 변경 / chcp IIFE 가 top-level 즉시 실행 → require('harness') 시 호스트 프로세스 오염.
|
|
@@ -2863,7 +2863,7 @@ function _selfTestCases() {
|
|
|
2863
2863
|
{ name: 'get_project_context: MCP 시맨틱 verb 등록 + CLI context 디스패치 (UR-0031 1.9.292)', run: () => { const src = read(__filename); const mcpDef = require('../lib/mcp-tools').some(t => t.name === 'leerness_get_project_context'); const mcpCase = /case 'leerness_get_project_context':[\s\S]*?cliArgs = \['context'/.test(src); const cliDisp = /if \(cmd === 'context'\)\s+return contextCmd/.test(src); return typeof contextCmd === 'function' && mcpDef && mcpCase && cliDisp && _mcpToolCount() >= 80; } },
|
|
2864
2864
|
{ name: '_canonicalProgressHeader + idempotency auto-fix (근본 복제버그 fix 1.9.293)', run: () => { const h = _canonicalProgressHeader(); const headerOk = /leernessRole: progress-tracker/.test(h) && /\| ID \| Status \| Request \|/.test(h) && /\|---\|/.test(h); const src = read(__filename); const fnOk = typeof _autoFixIdempotency === 'function'; const noWholeTextFallback = /if \(idx < 0\) return _canonicalProgressHeader\(\);/.test(src); const driftWired = /_autoFixIdempotency\(root\)/.test(src) && /idempotency 중복/.test(src); return headerOk && fnOk && noWholeTextFallback && driftWired; } },
|
|
2865
2865
|
{ name: 'lib/role-catalog: ROLE/PROVIDER/ALIASES/PROMPTS 모듈 단일출처 분리 (UR-0025 1.9.294)', run: () => { const m = require('../lib/role-catalog'); return m.ROLE_CATALOG === ROLE_CATALOG && m._PROVIDER_MODEL_CATALOG === _PROVIDER_MODEL_CATALOG && m._ROLE_ALIASES === _ROLE_ALIASES && m._AGENT_ROLE_PROMPTS === _AGENT_ROLE_PROMPTS && Object.keys(m.ROLE_CATALOG).length === 7 && Object.keys(m._PROVIDER_MODEL_CATALOG).length === 10 && !/const ROLE_CATALOG = \{/.test(read(__filename)); } },
|
|
2866
|
-
{ name: 'lib/catalogs: CAPABILITY/ADAPTERS/REUSE 모듈 단일출처 분리 (UR-0025 1.9.295)', run: () => { const m = require('../lib/catalogs'); return m.CAPABILITY_SURFACE === CAPABILITY_SURFACE && m.ADAPTERS === ADAPTERS && m.REUSE_CATEGORIES === REUSE_CATEGORIES && m.REUSE_CHECKLIST === REUSE_CHECKLIST && m.POWERFUL_COMMANDS === POWERFUL_COMMANDS && Object.keys(m.CAPABILITY_SURFACE).length === 6 && !/const CAPABILITY_SURFACE = \{/.test(read(__filename)); } },
|
|
2866
|
+
{ name: 'lib/catalogs: CAPABILITY/ADAPTERS/REUSE 모듈 단일출처 분리 (UR-0025 1.9.295) + i18n en(1.31.3)', run: () => { const m = require('../lib/catalogs'); const _H = /[가-힣]/; const cs = Object.values(m.CAPABILITY_SURFACE); const i18nOk = cs.length === 6 && cs.every(v => typeof v.descEn === 'string' && v.descEn.length > 0 && !_H.test(v.descEn) && typeof v.optOutEn === 'string' && !_H.test(v.optOutEn)) && m.POWERFUL_COMMANDS.every(c => typeof c.noteEn === 'string' && c.noteEn.length > 0 && !_H.test(c.noteEn)); return m.CAPABILITY_SURFACE === CAPABILITY_SURFACE && m.ADAPTERS === ADAPTERS && m.REUSE_CATEGORIES === REUSE_CATEGORIES && m.REUSE_CHECKLIST === REUSE_CHECKLIST && m.POWERFUL_COMMANDS === POWERFUL_COMMANDS && Object.keys(m.CAPABILITY_SURFACE).length === 6 && i18nOk && !/const CAPABILITY_SURFACE = \{/.test(read(__filename)); } },
|
|
2867
2867
|
{ name: 'about: 정체성 verb(AI 운영 레이어) + MCP leerness_about 등록 (UR-0030 1.9.296)', run: () => { const id = _leernessIdentity(); const src = read(__filename); return typeof aboutCmd === 'function' && /운영 레이어/.test(id.identity) && id.layers.length === 5 && id.surface.mcpTools >= 81 && require('../lib/mcp-tools').some(t => t.name === 'leerness_about') && /case 'leerness_about':/.test(src) && /cmd === 'about' \|\| cmd === 'identity'/.test(src); } },
|
|
2868
2868
|
{ name: 'lib/mcp-tools: MCP 도구 정의 모듈 단일출처 (_mcpToolCount=모듈 length, Codex #5 영구해소) (UR-0025 1.9.297)', run: () => { const T = require('../lib/mcp-tools'); return Array.isArray(T) && T.length >= 81 && T.every(t => t.name && t.description && t.inputSchema) && T[0].name === 'leerness_handoff' && _mcpToolCount() === T.length && !/const TOOLS = \[/.test(read(__filename)); } },
|
|
2869
2869
|
{ name: 'writeUtf8: 원자적 쓰기(temp→rename) 손상방지 행위 (UR-0038 외부리뷰 / CV-5 행위화 1.9.366)', run: () => { if (typeof writeUtf8 !== 'function') return false; const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_wu_')); try { const f = path.join(tmp, 'sub', 'a.txt'); writeUtf8(f, '한글 UTF-8 내용'); const okContent = read(f) === '한글 UTF-8 내용'; const noTmpLeft = fs.readdirSync(path.dirname(f)).every(n => !n.includes('.tmp-')); return okContent && noTmpLeft; } finally { try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} } } },
|
|
@@ -2901,7 +2901,7 @@ function _selfTestCases() {
|
|
|
2901
2901
|
{ name: 'lib/pure-utils: project-brief config 분리(_BRIEF_FIELDS/_briefFilled) + 인라인 제거 (UR-0025 1.9.330)', run: () => { const m = require('../lib/pure-utils'); const cfgOk = Array.isArray(m._BRIEF_FIELDS) && m._BRIEF_FIELDS.length === 10 && m._BRIEF_FIELDS[0].key === 'intro'; const work = m._briefFilled({ intro: 'x', features: ['a'] }) === 2 && m._briefFilled({}) === 0; const src = read(__filename); const moved = m._briefFilled === _briefFilled && m._BRIEF_FIELDS === _BRIEF_FIELDS && !/^const _BRIEF_FIELDS = \[/m.test(src) && !/^function _briefFilled\(/m.test(src); return cfgOk && work && moved; } },
|
|
2902
2902
|
{ name: 'lib/pure-utils: brief 빌더 분리(_briefReadmeBlock/_briefBlueprint + BRIEF 마커, VERSION 주입) (UR-0025 1.9.331)', run: () => { const m = require('../lib/pure-utils'); const fnOk = typeof m._briefReadmeBlock === 'function' && typeof m._briefBlueprint === 'function' && m.BRIEF_START.includes('project-brief:start'); const b = { project: 'X', intro: 'i', features: ['f1'] }; const rb = m._briefReadmeBlock(b); const bp = m._briefBlueprint(b, '9.9.9'); const work = rb.includes(m.BRIEF_START) && rb.includes(m.BRIEF_END) && /f1/.test(rb) && /Blueprint/.test(bp) && /leerness v9\.9\.9/.test(bp); const src = read(__filename); const moved = m._briefBlueprint === _briefBlueprint && m.BRIEF_START === BRIEF_START && !/^function _briefReadmeBlock\(/m.test(src) && !/^function _briefBlueprint\(/m.test(src) && !/^const BRIEF_START =/m.test(src); return fnOk && work && moved; } },
|
|
2903
2903
|
{ name: 'lib/pure-utils: lessons.md 파서 분리(_parseLessonEntries) + 인라인 제거 (UR-0025 1.9.332)', run: () => { const m = require('../lib/pure-utils'); const r = m._parseLessonEntries('### 2026-06-05\n- Lesson: A\n- Tag: t\n\n### 2026-06-04\n- Lesson: B'); const work = r.length === 2 && r[0].text === 'A' && r[0].tag === 't' && r[1].tag === null && r[0].date === '2026-06-05'; const src = read(__filename); const moved = m._parseLessonEntries === _parseLessonEntries && !/^function _parseLessonEntries\(/m.test(src) && src.includes('_parseLessonEntries(read(mp))'); return work && moved; } },
|
|
2904
|
-
{ name: 'UR-0025 심층: constraints catalog→lib/catalogs + _matchConstraints→pure-utils 분리 (1.9.333)', run: () => { const c = require('../lib/catalogs'); const m = require('../lib/pure-utils'); const catOk = c._DEFAULT_PLATFORM_CONSTRAINTS && Object.keys(c._DEFAULT_PLATFORM_CONSTRAINTS.platforms).length === 6 && !!c._DEFAULT_PLATFORM_CONSTRAINTS.platforms.stripe; const r = m._matchConstraints(c._DEFAULT_PLATFORM_CONSTRAINTS, 'stripe 결제'); const work = r.matched.length === 1 && r.matched[0].platform === 'stripe' && r.totalPlatforms === 6 && m._matchConstraints(null, 'x').matched.length === 0; const src = read(__filename); const moved = _DEFAULT_PLATFORM_CONSTRAINTS === c._DEFAULT_PLATFORM_CONSTRAINTS && _matchConstraints === m._matchConstraints && !/const _DEFAULT_PLATFORM_CONSTRAINTS = \{/.test(src) && src.includes('_matchConstraints(_loadPlatformConstraints(root), text)'); return catOk && work && moved; } },
|
|
2904
|
+
{ name: 'UR-0025 심층: constraints catalog→lib/catalogs + _matchConstraints→pure-utils 분리 (1.9.333) + i18n en(1.31.2)', run: () => { const c = require('../lib/catalogs'); const m = require('../lib/pure-utils'); const catOk = c._DEFAULT_PLATFORM_CONSTRAINTS && Object.keys(c._DEFAULT_PLATFORM_CONSTRAINTS.platforms).length === 6 && !!c._DEFAULT_PLATFORM_CONSTRAINTS.platforms.stripe; const r = m._matchConstraints(c._DEFAULT_PLATFORM_CONSTRAINTS, 'stripe 결제'); const work = r.matched.length === 1 && r.matched[0].platform === 'stripe' && r.totalPlatforms === 6 && m._matchConstraints(null, 'x').matched.length === 0; const _H = /[가-힣]/; const enSug = (m._matchConstraints(c._DEFAULT_PLATFORM_CONSTRAINTS, 'generic api integration widget', 'en').suggestions || [])[0] || ''; const koSug = (m._matchConstraints(c._DEFAULT_PLATFORM_CONSTRAINTS, 'generic api integration widget', 'ko').suggestions || [])[0] || ''; const i18nOk = c._DEFAULT_PLATFORM_CONSTRAINTS.platforms.stripe.constraints.some(x => x.detailEn && !_H.test(x.detailEn)) && enSug.length > 0 && !_H.test(enSug) && _H.test(koSug); const src = read(__filename); const moved = _DEFAULT_PLATFORM_CONSTRAINTS === c._DEFAULT_PLATFORM_CONSTRAINTS && _matchConstraints === m._matchConstraints && !/const _DEFAULT_PLATFORM_CONSTRAINTS = \{/.test(src) && src.includes('_matchConstraints(_loadPlatformConstraints(root), text)'); return catOk && work && i18nOk && moved; } },
|
|
2905
2905
|
{ name: 'UR-0025 심층(Codex 위임·검증): intent domain catalog→lib/catalogs + _matchDomain→pure-utils 분리 (1.9.334)', run: () => { const c = require('../lib/catalogs'); const m = require('../lib/pure-utils'); const catOk = c._DEFAULT_DOMAIN_CATALOG && Object.keys(c._DEFAULT_DOMAIN_CATALOG.domains).length === 5 && !!c._DEFAULT_DOMAIN_CATALOG.domains.game; const r = m._matchDomain(c._DEFAULT_DOMAIN_CATALOG, 'unity 게임'); const work = r.domain === 'game' && Array.isArray(r.components) && m._matchDomain(c._DEFAULT_DOMAIN_CATALOG, 'zzz없음').domain === null && m._matchDomain(null, 'x').domain === null; const src = read(__filename); const moved = _DEFAULT_DOMAIN_CATALOG === c._DEFAULT_DOMAIN_CATALOG && _matchDomain === m._matchDomain && !/const _DEFAULT_DOMAIN_CATALOG = \{/.test(src) && src.includes('_matchDomain(_loadDomainCatalog(root), text)'); return catOk && work && moved; } },
|
|
2906
2906
|
{ name: 'UR-0025 심층: LSP catalog→lib/catalogs(_LSP_LANG_PATTERNS) + _detectLspLang/_matchLspSymbols→pure-utils 분리 (1.9.335)', run: () => { const c = require('../lib/catalogs'); const m = require('../lib/pure-utils'); const catOk = c._LSP_LANG_PATTERNS && Object.keys(c._LSP_LANG_PATTERNS).length === 5 && Array.isArray(c._LSP_LANG_PATTERNS.javascript); const langOk = m._detectLspLang('a.py') === 'python' && m._detectLspLang('b.go') === 'go' && m._detectLspLang('c.md') === 'javascript'; const sy = m._matchLspSymbols(c._LSP_LANG_PATTERNS, 'function alpha(){}\nclass Beta{}', 'javascript'); const work = sy.length === 2 && sy[0].name === 'alpha' && sy[0].kind === 'function' && sy[1].kind === 'class' && m._matchLspSymbols(null, 'x', 'javascript').length === 0; const src = read(__filename); const moved = _LSP_LANG_PATTERNS === c._LSP_LANG_PATTERNS && _detectLspLang === m._detectLspLang && _matchLspSymbols === m._matchLspSymbols && !/const _LSP_LANG_PATTERNS = \{/.test(src) && !/function _detectLspLang\(/.test(src); return catOk && langOk && work && moved; } },
|
|
2907
2907
|
{ name: 'UR-0025 심층(Codex 위임·검증): anti-laziness catalog→lib/catalogs(OPTIMISM_PATTERNS) + optimism 순수로직→pure-utils 분리 (1.9.336)', run: () => { const c = require('../lib/catalogs'); const m = require('../lib/pure-utils'); const catOk = Array.isArray(c.OPTIMISM_PATTERNS) && c.OPTIMISM_PATTERNS.length === 10 && c.OPTIMISM_PATTERNS[0].kind === 'API'; const ev = 'API 호출 완료, POST /users'; const sus = m._detectOptimism(c.OPTIMISM_PATTERNS, ev, 'function x(){}'); const conf = m._computeConfidence(c.OPTIMISM_PATTERNS, ev, 'function x(){}'); const work = sus.some(s => s.kind === 'API' && s.severity === 'high') && conf < 0.5 && m._computeConfidence(c.OPTIMISM_PATTERNS, '정리함', 'x') === 1 && m._detectOptimism(null, ev, 'x').length === 0 && m._extractUrlClaims('POST /a').length === 1 && m._verifyUrlClaim({ path: '/a' }, 'has /a') === true; const src = read(__filename); const moved = OPTIMISM_PATTERNS === c.OPTIMISM_PATTERNS && _puDetectOptimism === m._detectOptimism && !/const OPTIMISM_PATTERNS = \[/.test(src) && !/function _extractUrlClaims\(/.test(src); return catOk && work && moved; } },
|
|
@@ -2939,7 +2939,7 @@ function _selfTestCases() {
|
|
|
2939
2939
|
{ name: 'UR-0025: _parseArchiveBlocks/_parseSkillCatalog 순수 파서 모듈 분리 + 행위 (1.9.370)', run: () => { const m = require('../lib/pure-utils'); if (typeof _parseArchiveBlocks !== 'function' || typeof _parseSkillCatalog !== 'function') return false; const moved = m._parseArchiveBlocks === _parseArchiveBlocks && m._parseSkillCatalog === _parseSkillCatalog; const ab = _parseArchiveBlocks('## 제거 2026-01-01 (target: ' + '"T-1")\n### 헤더\n'); const abOk = ab.length === 1 && ab[0].date === '2026-01-01' && ab[0].target === 'T-1' && ab[0].originalHeader === '헤더'; const md = _parseSkillCatalog('- [nm](https://x/SKILL.md) — d', ''); const mdOk = md.length === 1 && md[0].name === 'nm' && md[0].format === 'markdown'; const js = _parseSkillCatalog('{' + '"skills":[{"id":"a","url":"u"}]}', ''); const jsOk = js.length === 1 && js[0].name === 'a' && js[0].format === 'json'; return moved && abOk && mdOk && jsOk; } },
|
|
2940
2940
|
{ name: 'UR-0073 Phase A: team 정의 레지스트리 (_renderTeamsMd + canonical load/save) 행위 (1.9.371)', run: () => { const m = require('../lib/pure-utils'); if (typeof teamCmd !== 'function' || typeof _renderTeamsMd !== 'function' || m._renderTeamsMd !== _renderTeamsMd) return false; const md = _renderTeamsMd([{ id: 't1', name: 'N', personas: ['security'], members: ['claude'], schedule: 'daily', status: 'active' }]); const mdOk = md.includes('## t1') && md.includes('security') && md.includes('daily') && md.includes('정의 전용'); const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_team_')); let rtOk = false; try { _saveTeams(tmp, [{ id: 'x', name: 'X', personas: [], members: [], schedule: 'manual', status: 'active' }]); const loaded = _loadTeams(tmp); rtOk = loaded.length === 1 && loaded[0].id === 'x' && fs.existsSync(path.join(tmp, '.harness', 'teams.json')) && fs.existsSync(path.join(tmp, '.harness', 'teams.md')); } finally { try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} } return mdOk && rtOk; } },
|
|
2941
2941
|
{ name: 'UR-0073 Phase B: _composeTeamPlan dry-run 실행 계획 (멤버별 dispatch, 실행 없음) 행위 (1.9.372)', run: () => { const m = require('../lib/pure-utils'); if (typeof _composeTeamPlan !== 'function' || m._composeTeamPlan !== _composeTeamPlan) return false; const team = { id: 'rev', name: 'R', purpose: 'PR 리뷰', personas: ['security', 'perf'], members: ['claude', 'codex'], schedule: 'manual' }; const p1 = _composeTeamPlan(team, '점검'); const ok1 = p1.steps.length === 2 && p1.task === '점검' && p1.steps[0].member === 'claude' && p1.steps[0].suggestedCommand.includes('agents dispatch') && p1.steps[0].suggestedCommand.includes('--to claude') && p1.steps[0].dispatchPrompt.includes('security'); const p2 = _composeTeamPlan(team, null); const ok2 = p2.task === 'PR 리뷰'; const p3 = _composeTeamPlan({ id: 'e', personas: [], members: [] }, 'x'); const ok3 = p3.steps.length === 0 && p3.memberCount === 0; return ok1 && ok2 && ok3; } },
|
|
2942
|
-
{ name: 'UR-0073 Phase C: _teamHandoffReminders 스케줄 알림 (비-manual·active 만, 실행 트리거 아님) 행위 (1.9.373)', run: () => { const m = require('../lib/pure-utils'); if (typeof _teamHandoffReminders !== 'function' || m._teamHandoffReminders !== _teamHandoffReminders) return false; const r = _teamHandoffReminders([{ id: 'rev', schedule: 'every-session', status: 'active', members: ['a', 'b'] }, { id: 'man', schedule: 'manual', status: 'active' }, { id: 'paused', schedule: 'daily', status: 'paused' }]);
|
|
2942
|
+
{ name: 'UR-0073 Phase C: _teamHandoffReminders 스케줄 알림 (비-manual·active 만, 실행 트리거 아님) 행위 (1.9.373) + i18n en(1.31.3)', run: () => { const m = require('../lib/pure-utils'); if (typeof _teamHandoffReminders !== 'function' || m._teamHandoffReminders !== _teamHandoffReminders) return false; const r = _teamHandoffReminders([{ id: 'rev', schedule: 'every-session', status: 'active', members: ['a', 'b'] }, { id: 'man', schedule: 'manual', status: 'active' }, { id: 'paused', schedule: 'daily', status: 'paused' }]); const behaviorOk = r.length === 1 && r[0].includes('rev') && r[0].includes('every-session') && r[0].includes('team preview rev') && !r.join('|').includes('man') && !r.join('|').includes('paused'); const _H = /[가-힣]/; const en = _teamHandoffReminders([{ id: 'rev', schedule: 'every-session', status: 'active', members: ['a', 'b'], review: true }], 'en')[0] || ''; const ko = _teamHandoffReminders([{ id: 'rev', schedule: 'every-session', status: 'active', members: ['a', 'b'], review: true }])[0] || ''; const i18nOk = !_H.test(en) && /2 members/.test(en) && /review needed/.test(en) && /preview:/.test(en) && _H.test(ko) && /2명/.test(ko) && /검수필요/.test(ko); return behaviorOk && i18nOk; } },
|
|
2943
2943
|
{ name: 'UR-0074: _cadenceAssessment 릴리스 빈도 평가 (임계값) 행위 (1.9.374)', run: () => { const m = require('../lib/pure-utils'); if (typeof _cadenceAssessment !== 'function' || m._cadenceAssessment !== _cadenceAssessment || typeof releaseCadenceCmd !== 'function') return false; return _cadenceAssessment(7, 1, 1).level === 'very-high' && _cadenceAssessment(3, 1, 1).level === 'high' && _cadenceAssessment(1, 1, 1).level === 'moderate' && _cadenceAssessment(0.2, 1, 1).level === 'healthy' && _cadenceAssessment(7, 1, 1).recommendation.length > 0; } },
|
|
2944
2944
|
{ name: 'UR-0084: _withLock 획득/재진입/해제 + maxWaitMs 하드닝(10s) 행위 (1.9.375)', run: () => { if (typeof _withLock !== 'function') return false; const src = read(__filename); const hardened = /maxWaitMs = opts\.maxWaitMs \|\| 10000/.test(src); const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_lock_')); try { const target = path.join(tmp, 'f.md'); let reentrant = false; const lockSeen = _withLock(target, () => { const exists = fs.existsSync(target + '.lock'); reentrant = (_withLock(target, () => 42) === 42); return exists; }); const cleaned = !fs.existsSync(target + '.lock'); return hardened && lockSeen === true && reentrant === true && cleaned; } finally { try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} } } },
|
|
2945
2945
|
{ name: 'UR-0073 Phase D: _teamDeployGate 이중 게이트 (dry-run 기본/env 게이트/실행) 행위 (1.9.376)', run: () => { const m = require('../lib/pure-utils'); if (typeof _teamDeployGate !== 'function' || m._teamDeployGate !== _teamDeployGate) return false; const team = { id: 'd', deployCommand: 'echo hi' }; const noCmd = _teamDeployGate({ id: 'x' }, { yes: true, envOn: true }).mode === 'no-command'; const dry = _teamDeployGate(team, { yes: false, envOn: true }).mode === 'dry-run'; const gated = _teamDeployGate(team, { yes: true, envOn: false }).mode === 'gated'; const exec = _teamDeployGate(team, { yes: true, envOn: true }).mode === 'execute'; return noCmd && dry && gated && exec; } },
|
|
@@ -2966,7 +2966,7 @@ function _selfTestCases() {
|
|
|
2966
2966
|
{ name: '6번째 외부평가/codex P1-A (UR-0098): install-safety 레시피 셸-무관 + hardeningNote (1.9.397)', run: () => { if (typeof installSafetyCmd !== 'function') return false; const save = process.argv; const _w = process.stdout.write; let out = ''; try { process.argv = ['node', 'h', 'install-safety', '--json']; process.stdout.write = s => { out += s; return true; }; installSafetyCmd({ json: true }); } catch {} finally { process.stdout.write = _w; process.argv = save; } let j; try { j = JSON.parse(out); } catch {} const noPosixPrefix = !!j && Array.isArray(j.safeInstall) && !j.safeInstall.some(x => /^npm_config_\w+=/.test(String(x).trim())); const crossShell = !!j && j.safeInstall.filter(x => String(x).includes('npx --yes')).length >= 2; const noteOk = !!j && typeof j.hardeningNote === 'string' && j.hardeningNote.includes('PowerShell'); return noPosixPrefix && crossShell && noteOk; } },
|
|
2967
2967
|
{ name: '6번째 외부평가/codex P1-C (UR-0099): --json 에러 경로 구조화 failJson + 와이어 (1.9.398)', run: () => { const io = require('../lib/io'); if (io.failJson !== failJson) return false; const _w = process.stdout.write; const saved = process.exitCode; let jOut = '', hOut = ''; let jExit = 0; try { process.stdout.write = s => { jOut += s; return true; }; process.exitCode = 0; failJson(true, 'tc', 'm'); jExit = process.exitCode; process.stdout.write = s => { hOut += s; return true; }; process.exitCode = 0; failJson(false, 'c', 'humanmsg'); } catch {} finally { process.stdout.write = _w; process.exitCode = saved; } let pj; try { pj = JSON.parse(jOut); } catch {} const jsonOk = !!pj && pj.ok === false && pj.code === 'tc' && pj.error === 'm' && jExit === 1; const humanOk = hOut.includes('✗') && hOut.includes('humanmsg') && !hOut.includes('{'); const src = read(__filename); const wired = src.includes("failJson(_j, 'missing_args'") && src.includes("failJson(_j, 'spec_not_found'"); return jsonOk && humanOk && wired; } },
|
|
2968
2968
|
{ name: '7번째 버그헌트 P1-A (UR-0104): 테이블셀 안전화 _cellSafe/_cellUnescape (파이프/개행 injection 차단) (1.9.399)', run: () => { const m = require('../lib/pure-utils'); if (m._cellSafe !== _cellSafe || m._cellUnescape !== _cellUnescape) return false; const safe = _cellSafe('fix | bug\nrow2'); const noRaw = !/(?<!\\)\|/.test(safe) && !/[\r\n]/.test(safe); const pipeRt = _cellUnescape(_cellSafe('a | b | c')) === 'a | b | c'; const nlGone = _cellSafe('a\nb') === 'a b'; const src = read(__filename); const wired = src.includes('_cellSafe(r.request)') && src.includes('_cellSafe(r.rule)'); return noRaw && pipeRt && nlGone && wired; } },
|
|
2969
|
-
{ name: '7번째 버그헌트 P1-B (UR-0105): verify-claim/optimism-check/honesty-check --json 에러 구조화 (1.9.400)', run: () => { const src = read(__filename); const vc = /function verifyClaimCmd[\s\S]{0,
|
|
2969
|
+
{ name: '7번째 버그헌트 P1-B (UR-0105): verify-claim/optimism-check/honesty-check --json 에러 구조화 (1.9.400)', run: () => { const src = read(__filename); const vc = /function verifyClaimCmd[\s\S]{0,700}?failJson\(_j, 'not_found'/.test(src); const oc = /function optimismCheckCmd[\s\S]{0,700}?failJson\(_j, 'not_found'/.test(src); const hc = /function honestyCheckCmd[\s\S]{0,900}?failJson\(has\('--json'\), 'not_found'/.test(src); return vc && oc && hc; } }, // 1.30.5: {0,400}→{0,700} (F4 가 missing_args 라인을 en/ko 로 늘려 not_found 가 창 밖)
|
|
2970
2970
|
{ name: '7번째 버그헌트 P1-C (UR-0106): 시크릿 FN — gitignore 부정(!) + placeholder substring 정밀화 (1.9.401)', run: () => { const m = require('../lib/pure-utils'); const gm = m._gitignoreMatch; const negOk = gm('*.example\n!.env.example', '.env.example') === false && gm('*.log', 'a.log') === true && gm('a.log\n!a.log', 'a.log') === false && gm('.env', '.env') === true; const ph = m._isPlaceholderSecret; const phOk = ph('sk-EXAMPLEab12cd34ef56gh78ij90kl') === false && ph('sk-proj-realKEYexample9988776655') === false && ph('your-key-here') === true && ph('changeme') === true && ph('example') === true && ph('xxxxxxxxxxxxxxxxxxxxxxxxxxxx') === true; return negOk && phOk; } },
|
|
2971
2971
|
{ name: '7번째 버그헌트 P1-A 잔여 (UR-0108): decisions/lessons MD projection 개행 주입 차단 _lineSafe (1.9.402)', run: () => { const m = require('../lib/pure-utils'); if (m._lineSafe !== _lineSafe) return false; const lsOk = _lineSafe('a\nb\r\nc') === 'a b c'; const md = m._renderDecisionsMd([{ date: '2026-06-07', title: 'real\n### 2099-01-01 — FAKE\n- Decision: forged', decision: 'd', reason: 'r' }]); const re = m._decisionsFromMd(md); const noInject = re.length === 1 && !/^### 2099-01-01 — FAKE/m.test(md); const lmd = m._renderLessonsMd([{ date: '2026-06-07', text: 'l1\n### FAKE\n- Lesson: x', tag: 't' }]); const lre = m._parseLessonEntries(lmd); const lNoInject = lre.length === 1; return lsOk && noInject && lNoInject; } },
|
|
2972
2972
|
{ name: '7번째 버그헌트 P2 (UR-0107): api-skill show/drop 에러 exit code 1 (1.9.403)', run: () => { const src = read(__filename); const showId = src.includes("api-skill show <id>')); process.exitCode = 1"); const dropId = src.includes("api-skill drop <id>')); process.exitCode = 1"); const addUrl = src.includes("api-skill add <url> [--direction") && src.includes('process.exitCode = 1'); return showId && dropId && addUrl; } },
|
|
@@ -3114,7 +3114,7 @@ function _selfTestCases() {
|
|
|
3114
3114
|
fs.mkdirSync(path.join(tmp, '.harness'), { recursive: true });
|
|
3115
3115
|
fs.writeFileSync(path.join(tmp, 'AGENTS.md'), '# x');
|
|
3116
3116
|
process.stdout.write = s => { out += s; return true; };
|
|
3117
|
-
m.audit(tmp, { json: true }, { VERSION, arg: (k, d) => d, has: f => f === '--json', planPath, readProgressRows, currentStatePath, handoffPath, envDiff, _readFeatureGraph, _matchAPISkills, _listAPISkills });
|
|
3117
|
+
m.audit(tmp, { json: true }, { VERSION, arg: (k, d) => d, has: f => f === '--json', planPath, readProgressRows, currentStatePath, handoffPath, envDiff, _readFeatureGraph, _matchAPISkills, _listAPISkills, _collectSecretFindings });
|
|
3118
3118
|
} catch (e) { out = 'ERR:' + e.message; } finally { process.stdout.write = _w; try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} }
|
|
3119
3119
|
try { const j = JSON.parse(out); behavOk = typeof j.healthy === 'boolean' && Array.isArray(j.findings); } catch {}
|
|
3120
3120
|
return expOk && delegated && movedToLib && behavOk;
|
|
@@ -3891,7 +3891,33 @@ function _selfTestCases() {
|
|
|
3891
3891
|
&& bin.includes('전체/기록/최신' + '화: leerness slash-commands');
|
|
3892
3892
|
return en && koPreserved;
|
|
3893
3893
|
} },
|
|
3894
|
-
{ name: '
|
|
3894
|
+
{ name: 'parent detect (1.30.2 #157): 상위 leerness 부모 탐지(행위) + 독립 null + assetCount', run: () => {
|
|
3895
|
+
const osx = require('os'); const fsx = require('fs');
|
|
3896
|
+
const base = fsx.mkdtempSync(path.join(osx.tmpdir(), 'leer-parent-st-'));
|
|
3897
|
+
const alone = fsx.mkdtempSync(path.join(osx.tmpdir(), 'leer-alone-st-'));
|
|
3898
|
+
try {
|
|
3899
|
+
fsx.mkdirSync(path.join(base, '.harness'), { recursive: true });
|
|
3900
|
+
fsx.writeFileSync(path.join(base, '.harness', 'design-system.md'), '# ds');
|
|
3901
|
+
const sub = path.join(base, 'sub'); fsx.mkdirSync(sub, { recursive: true });
|
|
3902
|
+
const found = _findParentWorkspace(sub);
|
|
3903
|
+
const standalone = _findParentWorkspace(alone);
|
|
3904
|
+
// 부모 탐지: 워크스페이스 .harness + assetCount≥1(design-system) · 독립: null · read-only(소스에 파일쓰기 없음 — adopt 미구현)
|
|
3905
|
+
const readOnly = /이 명령은 아무 파일도 쓰지 않는다/.test(read(__filename));
|
|
3906
|
+
return !!found && found.workspaceDir === '.harness' && found.assetCount >= 1 && standalone === null && readOnly;
|
|
3907
|
+
} finally { try { fsx.rmSync(base, { recursive: true, force: true }); } catch {}; try { fsx.rmSync(alone, { recursive: true, force: true }); } catch {} }
|
|
3908
|
+
} },
|
|
3909
|
+
{ name: 'VERSION 형식 (x.y.z)', run: () => /^\d+\.\d+\.\d+$/.test(VERSION) },
|
|
3910
|
+
{ name: 'VERSION ↔ package.json 일치 (1.30.2: 한쪽만 bump 하던 실수 2초 내 차단)', run: () => {
|
|
3911
|
+
// bin VERSION 과 package.json.version 이 어긋나면 배너/배포가 거짓 버전을 표기 → 즉시 fail.
|
|
3912
|
+
try { return require('../package.json').version === VERSION; } catch { return false; }
|
|
3913
|
+
} },
|
|
3914
|
+
{ name: 'parent adopt (1.30.3 #158): 게이트형 adopt(dry-run 기본/--apply) + 비파괴 참조파일 (소스 가드)', run: () => {
|
|
3915
|
+
const bin = read(__filename);
|
|
3916
|
+
// split-literals (self-reference trap 회피)
|
|
3917
|
+
const hasAdopt = bin.includes("'ado" + "pt'") && bin.includes('parent ado' + 'pt --apply');
|
|
3918
|
+
const nonDestructive = bin.includes('inherited-from-pa' + 'rent.md') && bin.includes('PARENT_LI' + 'NK.json');
|
|
3919
|
+
return hasAdopt && nonDestructive;
|
|
3920
|
+
} }
|
|
3895
3921
|
];
|
|
3896
3922
|
}
|
|
3897
3923
|
function selfTestCmd(opts = {}) {
|
|
@@ -4671,6 +4697,7 @@ function commandsCmd(root) {
|
|
|
4671
4697
|
{ cmd: 'update [--check|--yes|--force]', desc: '자가 업데이트' },
|
|
4672
4698
|
{ cmd: 'wakeup-interval get|set|auto|history|record', desc: 'adaptive wakeup (1.9.210)' },
|
|
4673
4699
|
{ cmd: 'workspace-dir get|guide', desc: '워크스페이스 디렉토리 (1.9.211)' },
|
|
4700
|
+
{ cmd: 'parent detect|adopt [--select <kinds>] [--apply]', desc: '상위 leerness 부모 탐지 + 자산 게이트형 adopt (1.30.2~3)' },
|
|
4674
4701
|
{ cmd: 'intent classify|expand|domains "<request>"', desc: '의도 파악 + scope (1.9.213)' },
|
|
4675
4702
|
{ cmd: 'constraints list|check|add', desc: '플랫폼/API 제약 (1.9.208)' },
|
|
4676
4703
|
{ cmd: 'provider list|add|remove|sync', desc: 'Provider Registry (1.9.157~160)' },
|
|
@@ -4786,8 +4813,8 @@ function _writePlatformConstraints(root, catalog) {
|
|
|
4786
4813
|
}
|
|
4787
4814
|
// 사용자 요청 텍스트에서 플랫폼 alias 매칭 → 적용 제약 목록 반환
|
|
4788
4815
|
// 1.9.333 (UR-0025 심층): 매칭 로직은 순수 _matchConstraints(catalog, text) (lib/pure-utils) — fs(load)는 여기서 주입.
|
|
4789
|
-
function _checkRequestConstraints(root, text) {
|
|
4790
|
-
return _matchConstraints(_loadPlatformConstraints(root), text);
|
|
4816
|
+
function _checkRequestConstraints(root, text, lang) {
|
|
4817
|
+
return _matchConstraints(_loadPlatformConstraints(root), text, lang);
|
|
4791
4818
|
}
|
|
4792
4819
|
|
|
4793
4820
|
// 1.9.209: pre-wake sub-agent audit (사용자 명시)
|
|
@@ -5202,6 +5229,36 @@ function _workspaceDirName(root) {
|
|
|
5202
5229
|
function _workspaceDirAbs(root) {
|
|
5203
5230
|
return path.join(root, _workspaceDirName(root));
|
|
5204
5231
|
}
|
|
5232
|
+
// 1.30.2 (#157 사용자명시, 하위 프로젝트 방향 — 외부AI+Claude 교차검토 → 방향 C "탐지+게이트"):
|
|
5233
|
+
// 현재 root 의 '상위' 디렉토리 중 가장 가까운 leerness 부모(.harness/ 또는 .leerness/ 보유)를 탐지(read-only).
|
|
5234
|
+
// 부모 자산(design-system/reuse-map/tone) 적용은 자동이 아니라 사용자 결정 게이트 — 이 함수는 '탐지만' 한다(적용 X).
|
|
5235
|
+
// FP/안전: root 자신 제외(dirname 부터 시작) + 깊이 상한(monorepo/깊은 트리에서 무한 상승 방지) + 실제 워크스페이스 디렉토리만 매칭.
|
|
5236
|
+
function _findParentWorkspace(root, opts = {}) {
|
|
5237
|
+
try {
|
|
5238
|
+
root = absRoot(root);
|
|
5239
|
+
const maxDepth = opts.maxDepth || 8;
|
|
5240
|
+
let cur = path.dirname(root);
|
|
5241
|
+
let prev = null, depth = 0;
|
|
5242
|
+
while (cur && cur !== prev && depth < maxDepth) {
|
|
5243
|
+
const hasHarness = exists(path.join(cur, '.harness'));
|
|
5244
|
+
const hasLeerness = exists(path.join(cur, '.leerness'));
|
|
5245
|
+
if (hasHarness || hasLeerness) {
|
|
5246
|
+
const wd = (hasLeerness && exists(path.join(cur, '.leerness', 'MIGRATED_FROM_HARNESS'))) ? '.leerness' : (hasHarness ? '.harness' : '.leerness');
|
|
5247
|
+
const wsAbs = path.join(cur, wd);
|
|
5248
|
+
const assets = {
|
|
5249
|
+
designSystem: exists(path.join(wsAbs, 'design-system.md')),
|
|
5250
|
+
reuseMap: exists(path.join(wsAbs, 'reuse-map.md')),
|
|
5251
|
+
agents: exists(path.join(cur, 'AGENTS.md')),
|
|
5252
|
+
skills: exists(path.join(wsAbs, 'skills')),
|
|
5253
|
+
};
|
|
5254
|
+
const assetCount = Object.values(assets).filter(Boolean).length;
|
|
5255
|
+
return { parentRoot: cur, workspaceDir: wd, workspaceAbs: wsAbs, assets, assetCount, depth: depth + 1 };
|
|
5256
|
+
}
|
|
5257
|
+
prev = cur; cur = path.dirname(cur); depth++;
|
|
5258
|
+
}
|
|
5259
|
+
} catch {}
|
|
5260
|
+
return null;
|
|
5261
|
+
}
|
|
5205
5262
|
// .harness → .leerness 마이그레이션 (copy + reference guide 생성)
|
|
5206
5263
|
function _migrateWorkspaceDir(root, opts = {}) {
|
|
5207
5264
|
const dryRun = opts.dryRun === true;
|
|
@@ -5723,9 +5780,9 @@ function constraintsCmd(root, sub, ...rest) {
|
|
|
5723
5780
|
const yel = s => isTty ? `\x1b[33m${s}\x1b[0m` : s;
|
|
5724
5781
|
const red = s => isTty ? `\x1b[31m${s}\x1b[0m` : s;
|
|
5725
5782
|
const dim = s => isTty ? `\x1b[2m${s}\x1b[0m` : s;
|
|
5783
|
+
const _L = _uiLang(root); const _t = (ko, en) => _L === 'en' ? en : ko; // 1.31.2 (UR-0010): function-level so list/check/add share it
|
|
5726
5784
|
|
|
5727
5785
|
if (!sub || sub === 'help' || sub === '--help') {
|
|
5728
|
-
const _L = _uiLang(root); const _t = (ko, en) => _L === 'en' ? en : ko; // 1.23.2 (UR-0010 Phase 7)
|
|
5729
5786
|
log(_t(`# leerness constraints (1.9.208) — 플랫폼/API 제약 사전 체크`, `# leerness constraints — pre-check platform/API constraints`));
|
|
5730
5787
|
log('');
|
|
5731
5788
|
log(_t(` list → 등록된 모든 플랫폼 catalog 출력 (--json 가능)`, ` list → list all registered platform catalogs (--json)`));
|
|
@@ -5743,11 +5800,13 @@ function constraintsCmd(root, sub, ...rest) {
|
|
|
5743
5800
|
log(` total platforms: ${Object.keys(catalog.platforms).length}`);
|
|
5744
5801
|
log('');
|
|
5745
5802
|
for (const [pid, plat] of Object.entries(catalog.platforms)) {
|
|
5746
|
-
|
|
5803
|
+
// 1.31.2: aliases are matchers (kept for matching); hide Hangul-only aliases from EN display
|
|
5804
|
+
const aliasList = _L === 'en' ? (plat.aliases || []).filter(a => !/[가-힣]/.test(a)) : (plat.aliases || []);
|
|
5805
|
+
log(grn(` 📦 ${pid}`) + dim(` aliases: ${aliasList.join(', ')}`));
|
|
5747
5806
|
log(dim(` docs: ${plat.docs || '-'}`));
|
|
5748
5807
|
for (const c of plat.constraints || []) {
|
|
5749
5808
|
const icon = c.kind === 'rate-limit' ? '🚦' : (c.kind === 'cost' ? '💰' : (c.kind === 'auth' ? '🔐' : '📋'));
|
|
5750
|
-
log(` ${icon} [${c.kind}] ${c.detail}`);
|
|
5809
|
+
log(` ${icon} [${c.kind}] ${_L === 'en' && c.detailEn ? c.detailEn : c.detail}`);
|
|
5751
5810
|
}
|
|
5752
5811
|
log('');
|
|
5753
5812
|
}
|
|
@@ -5757,32 +5816,32 @@ function constraintsCmd(root, sub, ...rest) {
|
|
|
5757
5816
|
if (sub === 'check') {
|
|
5758
5817
|
const text = rest.filter(x => !x.startsWith('-')).join(' ');
|
|
5759
5818
|
if (!text) { console.error('Usage: leerness constraints check "<request text>"'); process.exit(1); }
|
|
5760
|
-
const result = _checkRequestConstraints(root, text);
|
|
5819
|
+
const result = _checkRequestConstraints(root, text, _L);
|
|
5761
5820
|
if (has('--json')) { log(JSON.stringify({ query: text, ...result }, null, 2)); return; }
|
|
5762
5821
|
log(cyan(`# leerness constraints check (1.9.208)`));
|
|
5763
5822
|
log(` query: ${text.slice(0, 80)}${text.length > 80 ? '…' : ''}`);
|
|
5764
5823
|
log('');
|
|
5765
5824
|
if (result.matched.length === 0) {
|
|
5766
|
-
log(grn(` ✓ 매칭된 플랫폼 없음 (catalog ${result.totalPlatforms}종 검토 완료)`));
|
|
5825
|
+
log(grn(_t(` ✓ 매칭된 플랫폼 없음 (catalog ${result.totalPlatforms}종 검토 완료)`, ` ✓ no platform matched (reviewed ${result.totalPlatforms} catalog entries)`)));
|
|
5767
5826
|
if (result.suggestions.length) {
|
|
5768
5827
|
log('');
|
|
5769
|
-
log(yel(` 💡
|
|
5828
|
+
log(yel(_t(` 💡 제안:`, ` 💡 suggestions:`)));
|
|
5770
5829
|
result.suggestions.forEach(s => log(` • ${s}`));
|
|
5771
5830
|
}
|
|
5772
5831
|
return;
|
|
5773
5832
|
}
|
|
5774
|
-
log(red(` ⚠ ${result.matched.length}개 플랫폼 매칭 — 제약 사전 확인
|
|
5833
|
+
log(red(_t(` ⚠ ${result.matched.length}개 플랫폼 매칭 — 제약 사전 확인 필요:`, ` ⚠ ${result.matched.length} platform(s) matched — review constraints before building:`)));
|
|
5775
5834
|
log('');
|
|
5776
5835
|
for (const m of result.matched) {
|
|
5777
5836
|
log(grn(` 📦 ${m.platform}`) + dim(` (matched: "${m.matchedAlias}")`));
|
|
5778
5837
|
log(dim(` docs: ${m.docs || '-'}`));
|
|
5779
5838
|
for (const c of m.constraints || []) {
|
|
5780
5839
|
const icon = c.kind === 'rate-limit' ? '🚦' : (c.kind === 'cost' ? '💰' : (c.kind === 'auth' ? '🔐' : '📋'));
|
|
5781
|
-
log(` ${icon} [${c.kind}] ${c.detail}`);
|
|
5840
|
+
log(` ${icon} [${c.kind}] ${_L === 'en' && c.detailEn ? c.detailEn : c.detail}`);
|
|
5782
5841
|
}
|
|
5783
5842
|
log('');
|
|
5784
5843
|
}
|
|
5785
|
-
log(dim(` → 구현 전 위 제약을 반영한 설계 권장 (rate limiter / idempotency key / 비용 추정)`));
|
|
5844
|
+
log(dim(_t(` → 구현 전 위 제약을 반영한 설계 권장 (rate limiter / idempotency key / 비용 추정)`, ` → recommend designing with the above constraints before building (rate limiter / idempotency key / cost estimate)`)));
|
|
5786
5845
|
return;
|
|
5787
5846
|
}
|
|
5788
5847
|
|
|
@@ -5800,7 +5859,7 @@ function constraintsCmd(root, sub, ...rest) {
|
|
|
5800
5859
|
if (kind && detail) catalog.platforms[id].constraints.push({ kind: kind.trim(), detail });
|
|
5801
5860
|
_writePlatformConstraints(root, catalog);
|
|
5802
5861
|
if (has('--json')) { log(JSON.stringify(catalog.platforms[id], null, 2)); return; }
|
|
5803
|
-
log(grn(`✓ platform "${id}" 갱신 — constraints: ${catalog.platforms[id].constraints.length}`));
|
|
5862
|
+
log(grn(_t(`✓ platform "${id}" 갱신 — constraints: ${catalog.platforms[id].constraints.length}`, `✓ platform "${id}" updated — constraints: ${catalog.platforms[id].constraints.length}`)));
|
|
5804
5863
|
return;
|
|
5805
5864
|
}
|
|
5806
5865
|
|
|
@@ -6041,6 +6100,103 @@ function workspaceDirCmd(root, sub) {
|
|
|
6041
6100
|
process.exit(1);
|
|
6042
6101
|
}
|
|
6043
6102
|
|
|
6103
|
+
// 1.30.2 (#157 사용자명시): leerness parent — 상위 leerness 부모 프로젝트 탐지(read-only).
|
|
6104
|
+
// 방향 C(교차검토): 부모 자산을 '재사용 후보'로 AI 에게 노출만 하고, 톤/스타일 등 실제 적용은 사용자 결정 게이트(후속 adopt 명령).
|
|
6105
|
+
// intent expand(1.9.213) 안전 모델과 동일 철학 — 탐지/노출 ≠ 적용. 이 명령은 아무 파일도 쓰지 않는다.
|
|
6106
|
+
function parentCmd(root, sub) {
|
|
6107
|
+
root = absRoot(root);
|
|
6108
|
+
const uiLang = _uiLang(root);
|
|
6109
|
+
const t = (ko, en) => (uiLang === 'en' ? en : ko);
|
|
6110
|
+
const isTty = process.stdout && process.stdout.isTTY;
|
|
6111
|
+
const cyan = s => isTty ? `\x1b[36m${s}\x1b[0m` : s;
|
|
6112
|
+
const grn = s => isTty ? `\x1b[32m${s}\x1b[0m` : s;
|
|
6113
|
+
const dim = s => isTty ? `\x1b[2m${s}\x1b[0m` : s;
|
|
6114
|
+
const yel = s => isTty ? `\x1b[33m${s}\x1b[0m` : s;
|
|
6115
|
+
const red = s => isTty ? `\x1b[31m${s}\x1b[0m` : s;
|
|
6116
|
+
if (!sub || sub === 'detect') {
|
|
6117
|
+
const p = _findParentWorkspace(root);
|
|
6118
|
+
if (has('--json')) { log(JSON.stringify({ version: VERSION, root, parent: p, applied: false }, null, 2)); return; }
|
|
6119
|
+
log(cyan(`# leerness parent detect (1.30.2)`));
|
|
6120
|
+
if (!p) {
|
|
6121
|
+
log(dim(t(` 상위 leerness 부모 프로젝트 없음 — 독립 프로젝트입니다.`, ` no parent leerness project found — this is a standalone project.`)));
|
|
6122
|
+
return;
|
|
6123
|
+
}
|
|
6124
|
+
const mark = b => b ? '✓' : '✗';
|
|
6125
|
+
log(t(` 부모 프로젝트: ${grn(p.parentRoot)} (${p.workspaceDir}/, depth ${p.depth})`, ` parent project: ${grn(p.parentRoot)} (${p.workspaceDir}/, depth ${p.depth})`));
|
|
6126
|
+
log(t(` 재사용 가능 자산 ${p.assetCount}건: design-system=${mark(p.assets.designSystem)} reuse-map=${mark(p.assets.reuseMap)} AGENTS=${mark(p.assets.agents)} skills=${mark(p.assets.skills)}`,
|
|
6127
|
+
` reusable assets ${p.assetCount}: design-system=${mark(p.assets.designSystem)} reuse-map=${mark(p.assets.reuseMap)} AGENTS=${mark(p.assets.agents)} skills=${mark(p.assets.skills)}`));
|
|
6128
|
+
log('');
|
|
6129
|
+
log(yel(t(` ⚠ 자동 적용하지 않음 — 부모 자산(톤/스타일/디자인) 재사용은 사용자 결정 게이트입니다.`, ` ⚠ not auto-applied — reusing parent assets (tone/style/design) is a user decision.`)));
|
|
6130
|
+
log(dim(t(` 참고: leerness reuse find "<capability>" --path ${p.parentRoot} · 부모 design-system: ${path.join(p.workspaceAbs, 'design-system.md')}`,
|
|
6131
|
+
` ref: leerness reuse find "<capability>" --path ${p.parentRoot} · parent design-system: ${path.join(p.workspaceAbs, 'design-system.md')}`)));
|
|
6132
|
+
log(dim(t(` 재사용 적용(사용자 결정): leerness parent adopt --select design-system,reuse-map,conventions --apply`,
|
|
6133
|
+
` adopt (your decision): leerness parent adopt --select design-system,reuse-map,conventions --apply`)));
|
|
6134
|
+
return;
|
|
6135
|
+
}
|
|
6136
|
+
// 1.30.3 (#158): leerness parent adopt — 부모 자산을 자식-로컬 참조로 기록(게이트형 적용).
|
|
6137
|
+
// 사용자 결정 게이트: dry-run 기본, --apply(사용자 명시) 시에만 기록. 자식 design-system.md 무변경(비파괴, additive).
|
|
6138
|
+
if (sub === 'adopt') {
|
|
6139
|
+
const p = _findParentWorkspace(root);
|
|
6140
|
+
const apply = has('--apply');
|
|
6141
|
+
const selRaw = arg('--select', 'all');
|
|
6142
|
+
const allKinds = ['design-system', 'reuse-map', 'conventions'];
|
|
6143
|
+
const kinds = (selRaw === 'all') ? allKinds : String(selRaw).split(',').map(s => s.trim()).filter(Boolean);
|
|
6144
|
+
const wsAbs = _workspaceDirAbs(root);
|
|
6145
|
+
const inheritedPath = path.join(wsAbs, 'inherited-from-parent.md');
|
|
6146
|
+
const linkPath = path.join(wsAbs, 'PARENT_LINK.json');
|
|
6147
|
+
const cand = [];
|
|
6148
|
+
if (p) {
|
|
6149
|
+
if (kinds.includes('design-system') && p.assets.designSystem) cand.push({ kind: 'design-system', src: path.join(p.workspaceAbs, 'design-system.md') });
|
|
6150
|
+
if (kinds.includes('reuse-map') && p.assets.reuseMap) cand.push({ kind: 'reuse-map', src: path.join(p.workspaceAbs, 'reuse-map.md') });
|
|
6151
|
+
if (kinds.includes('conventions') && p.assets.agents) cand.push({ kind: 'conventions', src: path.join(p.parentRoot, 'AGENTS.md') });
|
|
6152
|
+
}
|
|
6153
|
+
// 1.30.3: --json 은 단일 객체만 출력 — apply(부모 존재) 경로는 자체 JSON(applied:true)을 내므로 여기선 제외(이중 JSON 방지).
|
|
6154
|
+
if (has('--json') && (!p || !apply)) {
|
|
6155
|
+
log(JSON.stringify({ version: VERSION, root, parent: p ? p.parentRoot : null, selected: kinds, candidates: cand.map(c => c.kind), apply, applied: false, inheritedPath: null }, null, 2));
|
|
6156
|
+
}
|
|
6157
|
+
if (!p) {
|
|
6158
|
+
if (!has('--json')) { log(cyan(`# leerness parent adopt (1.30.3)`)); log(dim(t(` 상위 leerness 부모 프로젝트 없음 — adopt 대상 없음.`, ` no parent leerness project — nothing to adopt.`))); }
|
|
6159
|
+
return;
|
|
6160
|
+
}
|
|
6161
|
+
if (!apply) {
|
|
6162
|
+
if (!has('--json')) {
|
|
6163
|
+
log(cyan(`# leerness parent adopt (1.30.3) [DRY-RUN]`));
|
|
6164
|
+
log(t(` 부모: ${grn(p.parentRoot)} · 선택: ${kinds.join(', ')}`, ` parent: ${grn(p.parentRoot)} · select: ${kinds.join(', ')}`));
|
|
6165
|
+
if (!cand.length) log(dim(t(` 적용 후보 없음 (부모에 선택 자산 없음).`, ` no candidates (parent lacks the selected assets).`)));
|
|
6166
|
+
else cand.forEach(c => log(t(` • ${c.kind} ← ${c.src}`, ` • ${c.kind} ← ${c.src}`)));
|
|
6167
|
+
log('');
|
|
6168
|
+
log(yel(t(` ⚠ DRY-RUN — 실제 적용하려면 \`leerness parent adopt --apply\` (사용자 명시 결정).`, ` ⚠ DRY-RUN — to apply, run \`leerness parent adopt --apply\` (explicit user decision).`)));
|
|
6169
|
+
log(dim(t(` 적용해도 자식 design-system.md 는 변경하지 않고, .harness/inherited-from-parent.md 에 '참조'로만 기록(비파괴).`,
|
|
6170
|
+
` even on apply, your design-system.md is NOT modified — parent assets are recorded as reference in .harness/inherited-from-parent.md.`)));
|
|
6171
|
+
}
|
|
6172
|
+
return;
|
|
6173
|
+
}
|
|
6174
|
+
// APPLY (사용자 명시): 자식-로컬 참조 파일 + 마커 기록 (비파괴 additive — 자식 원본 design-system.md/reuse-map.md 직접 변형 안 함)
|
|
6175
|
+
try {
|
|
6176
|
+
mkdirp(wsAbs);
|
|
6177
|
+
let body = `<!-- leerness:inherited-from-parent (1.30.3) — 사용자 명시 \`parent adopt --apply\`. 부모 자산을 '참조'로만 기록(자식 원본 무변경). -->\n`;
|
|
6178
|
+
body += `# ${t('부모 프로젝트 자산 (참조용)', 'Parent project assets (reference)')}\n\n`;
|
|
6179
|
+
body += `- ${t('부모', 'parent')}: ${p.parentRoot}\n- adopt: ${today()}\n- ${t('선택', 'select')}: ${kinds.join(', ')}\n\n`;
|
|
6180
|
+
for (const c of cand) { const content = exists(c.src) ? read(c.src) : ''; body += `## ${c.kind} (from ${c.src})\n\n${String(content).trim()}\n\n`; }
|
|
6181
|
+
writeUtf8(inheritedPath, body);
|
|
6182
|
+
writeUtf8(linkPath, JSON.stringify({ parentRoot: p.parentRoot, workspaceDir: p.workspaceDir, adoptedKinds: kinds, adoptedAt: today(), version: VERSION }, null, 2) + '\n');
|
|
6183
|
+
if (has('--json')) { log(JSON.stringify({ version: VERSION, root, parent: p.parentRoot, selected: kinds, candidates: cand.map(c => c.kind), apply: true, applied: true, inheritedPath }, null, 2)); }
|
|
6184
|
+
else {
|
|
6185
|
+
log(cyan(`# leerness parent adopt (1.30.3)`));
|
|
6186
|
+
log(grn(t(` ✓ adopt 완료 (${cand.length} 자산) — 자식 design-system.md 무변경(참조로만 기록).`, ` ✓ adopted (${cand.length} assets) — your design-system.md unchanged (recorded as reference only).`)));
|
|
6187
|
+
log(dim(` ${t('참조', 'reference')}: ${inheritedPath}`));
|
|
6188
|
+
log(dim(` ${t('마커', 'marker')}: ${linkPath}`));
|
|
6189
|
+
}
|
|
6190
|
+
} catch (e) {
|
|
6191
|
+
if (!has('--json')) log(red(t(` ✗ adopt 실패: ${e.message}`, ` ✗ adopt failed: ${e.message}`)));
|
|
6192
|
+
process.exitCode = 1;
|
|
6193
|
+
}
|
|
6194
|
+
return;
|
|
6195
|
+
}
|
|
6196
|
+
console.error(t(`Usage: leerness parent [detect|adopt] [--select <kinds>] [--apply] [--json]`, `Usage: leerness parent [detect|adopt] [--select <kinds>] [--apply] [--json]`));
|
|
6197
|
+
process.exit(1);
|
|
6198
|
+
}
|
|
6199
|
+
|
|
6044
6200
|
// 1.9.211: leerness migrate-workspace-dir — .harness → .leerness 마이그레이션 (사용자 명시)
|
|
6045
6201
|
function migrateWorkspaceDirCmd(root) {
|
|
6046
6202
|
root = absRoot(root);
|
|
@@ -7555,12 +7711,19 @@ function lessonSave(root, text) {
|
|
|
7555
7711
|
if (!text) return fail('lesson text required. 예: leerness lesson save "JWT는 refresh token도 짧게 (15분 권장)"');
|
|
7556
7712
|
const tag = arg('--tag', '');
|
|
7557
7713
|
// 1.9.406 (8번째 버그헌트, UR-0110): RMW 락 직렬화 — 동시 lesson save lost-update 방지(UR-0043 패턴).
|
|
7714
|
+
// 1.30.4 (14th리뷰 F5): task/rule add 와 일관된 dedup — 동일 text 존재 시 skip(--force 우회). 종전엔 무조건 append(중복 누적).
|
|
7715
|
+
let _skipped = false;
|
|
7558
7716
|
_withLock(lessonsJsonPath(root), () => {
|
|
7559
7717
|
const all = _loadLessons(root);
|
|
7718
|
+
if (!has('--force') && all.some(l => l && l.text === text)) { _skipped = true; return; }
|
|
7560
7719
|
all.push({ date: today(), text, tag: tag || null });
|
|
7561
7720
|
_saveLessons(root, all);
|
|
7562
7721
|
});
|
|
7563
7722
|
// 1.9.413 (6th외부평가 codex P2, UR-0101): --json 구조화 출력(데이터 이미 영속).
|
|
7723
|
+
if (_skipped) {
|
|
7724
|
+
if (has('--json')) { log(JSON.stringify({ ok: true, skipped: true, text, tag: tag || null })); return; }
|
|
7725
|
+
ok(`lesson exists (skip): ${text.slice(0, 40)} (--force 로 추가)`); return;
|
|
7726
|
+
}
|
|
7564
7727
|
if (has('--json')) { log(JSON.stringify({ ok: true, text, tag: tag || null })); return; }
|
|
7565
7728
|
ok(`lesson saved`);
|
|
7566
7729
|
_autoRoadmap(absRoot(root), 'data-change');
|
|
@@ -7643,8 +7806,11 @@ function decisionAdd(root, title) {
|
|
|
7643
7806
|
const impact = arg('--impact', '');
|
|
7644
7807
|
// 1.9.339 (UR-0053): canonical JSON write path — 기존 항목(JSON 우선, 없으면 MD backfill) 로드 후 추가, JSON+MD projection 동시 저장.
|
|
7645
7808
|
// 1.9.406 (8번째 버그헌트, UR-0110): RMW 락 직렬화 — 동시 decision add lost-update 방지(UR-0043 패턴).
|
|
7809
|
+
// 1.30.4 (14th리뷰 F5): task/rule add 와 일관된 dedup — 동일 title 존재 시 skip(--force 우회). 종전엔 무조건 append(중복 누적).
|
|
7810
|
+
let _skipped = false;
|
|
7646
7811
|
_withLock(decisionsJsonPath(root), () => {
|
|
7647
7812
|
const all = _loadDecisions(root);
|
|
7813
|
+
if (!has('--force') && all.some(d => d && (d.title === title || d.decision === title))) { _skipped = true; return; }
|
|
7648
7814
|
all.push({
|
|
7649
7815
|
date: today(), title,
|
|
7650
7816
|
decision: title,
|
|
@@ -7655,6 +7821,10 @@ function decisionAdd(root, title) {
|
|
|
7655
7821
|
_saveDecisions(root, all);
|
|
7656
7822
|
});
|
|
7657
7823
|
// 1.9.413 (6th외부평가 codex P2, UR-0101): --json 구조화 출력(데이터 이미 영속).
|
|
7824
|
+
if (_skipped) {
|
|
7825
|
+
if (has('--json')) { log(JSON.stringify({ ok: true, skipped: true, title })); return; }
|
|
7826
|
+
ok(`decision exists (skip): ${title} (--force 로 추가)`); return;
|
|
7827
|
+
}
|
|
7658
7828
|
if (has('--json')) { log(JSON.stringify({ ok: true, title })); return; }
|
|
7659
7829
|
ok(`decision added: ${title}`);
|
|
7660
7830
|
// 1.9.43+ handoff lessons 회수 흐름과 자동 통합 (decisions.md fuzzy 매칭됨)
|
|
@@ -7830,6 +8000,9 @@ function installSafetyCmd(opts = {}) {
|
|
|
7830
8000
|
const deps = Object.keys(pkg.dependencies || {});
|
|
7831
8001
|
const scripts = pkg.scripts || {};
|
|
7832
8002
|
const installHooks = ['preinstall', 'install', 'postinstall'].filter(h => scripts[h]);
|
|
8003
|
+
// 1.31.1 (UR-0010): install-safety 출력 영어 opt-in (한국어 기본). safeInstall 의 npx --yes 토큰 + hardeningNote 의 PowerShell 토큰은 양 언어 공통(셸-무관 가드 보존).
|
|
8004
|
+
const uiLang = _uiLang(arg('--path', process.cwd()));
|
|
8005
|
+
const t = (ko, en) => (uiLang === 'en' ? en : ko);
|
|
7833
8006
|
const profile = {
|
|
7834
8007
|
version: VERSION,
|
|
7835
8008
|
runtimeDeps: deps.length,
|
|
@@ -7843,22 +8016,23 @@ function installSafetyCmd(opts = {}) {
|
|
|
7843
8016
|
// 타 패키지 대비 일반 하드닝은 셸별로 hardeningNote 참조.
|
|
7844
8017
|
safeInstall: [
|
|
7845
8018
|
'git checkout -b chore/leerness-update',
|
|
7846
|
-
'npx --yes leerness@latest migrate plan --path . # 임시폴더 비교(읽기 전용, 셸 무관)',
|
|
8019
|
+
t('npx --yes leerness@latest migrate plan --path . # 임시폴더 비교(읽기 전용, 셸 무관)', 'npx --yes leerness@latest migrate plan --path . # read-only temp-folder compare (shell-agnostic)'),
|
|
7847
8020
|
'npx --yes leerness@latest update --yes --path .',
|
|
7848
|
-
'git diff # 변경 검토 후 커밋 또는 롤백',
|
|
8021
|
+
t('git diff # 변경 검토 후 커밋 또는 롤백', 'git diff # review the diff, then commit or roll back'),
|
|
7849
8022
|
],
|
|
7850
|
-
hardeningNote: 'leerness 자체는 0 deps / 0 install-script 라 설치/업데이트 시 임의코드 실행 없음. 일반 npx 하드닝(타 패키지 대비)은 셸별 — bash: npm_config_ignore_scripts=true npx ... · PowerShell: $env:npm_config_ignore_scripts=1; npx ... · cmd: set npm_config_ignore_scripts=1 && npx ...',
|
|
8023
|
+
hardeningNote: t('leerness 자체는 0 deps / 0 install-script 라 설치/업데이트 시 임의코드 실행 없음. 일반 npx 하드닝(타 패키지 대비)은 셸별 — bash: npm_config_ignore_scripts=true npx ... · PowerShell: $env:npm_config_ignore_scripts=1; npx ... · cmd: set npm_config_ignore_scripts=1 && npx ...',
|
|
8024
|
+
'leerness itself has 0 deps / 0 install-script, so no arbitrary code runs on install/update. General npx hardening (for other packages) is shell-specific — bash: npm_config_ignore_scripts=true npx ... · PowerShell: $env:npm_config_ignore_scripts=1; npx ... · cmd: set npm_config_ignore_scripts=1 && npx ...'),
|
|
7851
8025
|
};
|
|
7852
8026
|
if (opts.json) { log(JSON.stringify(profile, null, 2)); return; }
|
|
7853
|
-
log(`# leerness install-safety (1.9.359) — 설치 안전
|
|
7854
|
-
log(` 버전: ${VERSION} · Node: ${profile.node || '(미지정)'}`);
|
|
7855
|
-
log(` 런타임 의존성: ${deps.length === 0 ? '0건 (외부 패키지 없음 — 공급망 노출 0)' : deps.length + '건 — ' + deps.join(', ')}`);
|
|
7856
|
-
log(` install-time 스크립트: ${installHooks.length === 0 ? '없음 (preinstall/install/postinstall 0 — 설치 시 임의코드 실행 없음)' : installHooks.join(', ')}`);
|
|
7857
|
-
log(` 동작 방식: offline-first (설치 시 네트워크/빌드 불필요, 단일 bin + lib)`);
|
|
7858
|
-
log(`\n 안전 설치 워크플로 (검토 후 적용):`);
|
|
8027
|
+
log(t(`# leerness install-safety (1.9.359) — 설치 안전 프로필`, `# leerness install-safety — install safety profile`));
|
|
8028
|
+
log(t(` 버전: ${VERSION} · Node: ${profile.node || '(미지정)'}`, ` version: ${VERSION} · Node: ${profile.node || '(unspecified)'}`));
|
|
8029
|
+
log(t(` 런타임 의존성: ${deps.length === 0 ? '0건 (외부 패키지 없음 — 공급망 노출 0)' : deps.length + '건 — ' + deps.join(', ')}`, ` runtime deps: ${deps.length === 0 ? '0 (no external packages — zero supply-chain exposure)' : deps.length + ' — ' + deps.join(', ')}`));
|
|
8030
|
+
log(t(` install-time 스크립트: ${installHooks.length === 0 ? '없음 (preinstall/install/postinstall 0 — 설치 시 임의코드 실행 없음)' : installHooks.join(', ')}`, ` install-time scripts: ${installHooks.length === 0 ? 'none (0 preinstall/install/postinstall — no arbitrary code on install)' : installHooks.join(', ')}`));
|
|
8031
|
+
log(t(` 동작 방식: offline-first (설치 시 네트워크/빌드 불필요, 단일 bin + lib)`, ` how it works: offline-first (no network/build on install, single bin + lib)`));
|
|
8032
|
+
log(t(`\n 안전 설치 워크플로 (검토 후 적용):`, `\n Safe install workflow (review then apply):`));
|
|
7859
8033
|
profile.safeInstall.forEach((s, i) => log(` ${i + 1}. ${s}`));
|
|
7860
8034
|
log(`\n ⓘ ${profile.hardeningNote}`); // 1.9.397 (UR-0098): 셸별 하드닝 노트
|
|
7861
|
-
if (installHooks.length > 0) warn('install-time 스크립트 존재 — 위 ignore-scripts safe-install 권장');
|
|
8035
|
+
if (installHooks.length > 0) warn(t('install-time 스크립트 존재 — 위 ignore-scripts safe-install 권장', 'install-time scripts present — use the ignore-scripts safe-install above'));
|
|
7862
8036
|
}
|
|
7863
8037
|
function debug(root) {
|
|
7864
8038
|
root = absRoot(root); let warnings = 0, failures = 0;
|
|
@@ -7872,7 +8046,7 @@ function debug(root) {
|
|
|
7872
8046
|
|
|
7873
8047
|
const _audit = require('../lib/audit');
|
|
7874
8048
|
// 1.9.421 (UR-0025/UR-0125 큰 핸들러 모듈화 6번째): audit → lib/audit.js (DI 위임, thin wrapper)
|
|
7875
|
-
function audit(root, opts = {}) { return _audit.audit(root, opts, { VERSION, arg, has, planPath, readProgressRows, currentStatePath, handoffPath, envDiff, _readFeatureGraph, _matchAPISkills, _listAPISkills }); }
|
|
8049
|
+
function audit(root, opts = {}) { return _audit.audit(root, opts, { VERSION, arg, has, planPath, readProgressRows, currentStatePath, handoffPath, envDiff, _readFeatureGraph, _matchAPISkills, _listAPISkills, _collectSecretFindings }); }
|
|
7876
8050
|
|
|
7877
8051
|
// 1.9.312 (UR-0050, 설치리뷰 3중수렴): secret 스캐너 현대 키 패턴 보강.
|
|
7878
8052
|
// 배경: 기존 OpenAI 패턴 `sk-[A-Za-z0-9]{32,}` 은 하이픈에서 끊겨 sk-proj-/sk-svcacct- (modern 프로젝트/서비스 키)를 놓침.
|
|
@@ -8488,6 +8662,16 @@ function handoff(root) {
|
|
|
8488
8662
|
const parts = [];
|
|
8489
8663
|
// 1.20.3 (UR-0010 Phase 2): 헤드라인 항목 라벨 UI 언어 적용 (영어 opt-in, 한국어 기본). 블록 1회 해석.
|
|
8490
8664
|
const _L = _uiLang(root); const t = (ko, en) => (_L === 'en' ? en : ko);
|
|
8665
|
+
// 1.30.2 (#157): 상위 leerness 부모 프로젝트 탐지 → AI 가 세션 시작 즉시 인지(재사용 후보). read-only — 자동 적용 X(사용자 결정 게이트). 상세: leerness parent detect
|
|
8666
|
+
try {
|
|
8667
|
+
const _pw = _findParentWorkspace(root);
|
|
8668
|
+
if (_pw && _pw.assetCount > 0) {
|
|
8669
|
+
const _adopted = exists(path.join(_workspaceDirAbs(root), 'PARENT_LINK.json')); // 1.30.3: adopt 여부 반영
|
|
8670
|
+
parts.push(_adopted
|
|
8671
|
+
? t(`🔗 부모 프로젝트 (${_pw.assetCount} 자산·adopted)`, `🔗 parent project (${_pw.assetCount} assets, adopted)`)
|
|
8672
|
+
: t(`🔗 부모 프로젝트 (${_pw.assetCount} 자산·미적용)`, `🔗 parent project (${_pw.assetCount} assets, not applied)`));
|
|
8673
|
+
}
|
|
8674
|
+
} catch {}
|
|
8491
8675
|
// 1) drift level (가벼운 check)
|
|
8492
8676
|
try {
|
|
8493
8677
|
const r = cp.spawnSync(process.execPath, [__filename, 'drift', 'check', root, '--json'],
|
|
@@ -9348,24 +9532,25 @@ function handoff(root) {
|
|
|
9348
9532
|
const red = s => isTty ? `\x1b[31m${s}\x1b[0m` : s;
|
|
9349
9533
|
const dim = s => isTty ? `\x1b[2m${s}\x1b[0m` : s;
|
|
9350
9534
|
const grn = s => isTty ? `\x1b[32m${s}\x1b[0m` : s;
|
|
9535
|
+
const _t = (ko, en) => (_uiLang(root) === 'en' ? en : ko); // 1.30.5 (#156 F3): 로컬 i18n (이 블록 9536 에 t 화살표 파라미터가 있어 _t 로 명명)
|
|
9351
9536
|
if (gapInfo.hasLast) {
|
|
9352
9537
|
// 마지막 handoff 시점 알고 있음 — 정확한 측정 (1.9.199)
|
|
9353
9538
|
if (gapInfo.isLong) {
|
|
9354
9539
|
// 60분+ → 강한 알림
|
|
9355
|
-
log(red(`## ⏰ ScheduleWakeup miss 강한 의심 (1.9.199) — 이전 handoff ${gapInfo.gapMin}분
|
|
9356
|
-
log(dim(` R-0001 영구 룰 (25분) 대비 ${Math.floor(gapInfo.gapMin/25)}× 초과 — 시스템 sleep / wakeup 누락
|
|
9357
|
-
log(dim(` → 회복: 사용자가 "다음 라운드" 입력 또는 leerness rule list 로 룰
|
|
9358
|
-
log(dim(` → handoff 이력: ${(gapInfo.history || []).slice(-3).map(t => t.slice(11, 19)).join(' → ')}`));
|
|
9540
|
+
log(red(_t(`## ⏰ ScheduleWakeup miss 강한 의심 (1.9.199) — 이전 handoff ${gapInfo.gapMin}분 전`, `## ⏰ ScheduleWakeup miss strongly suspected — last handoff ${gapInfo.gapMin} min ago`)));
|
|
9541
|
+
log(dim(_t(` R-0001 영구 룰 (25분) 대비 ${Math.floor(gapInfo.gapMin/25)}× 초과 — 시스템 sleep / wakeup 누락 확실`, ` ${Math.floor(gapInfo.gapMin/25)}× over the R-0001 rule (25 min) — system sleep / missed wakeup`)));
|
|
9542
|
+
log(dim(_t(` → 회복: 사용자가 "다음 라운드" 입력 또는 leerness rule list 로 룰 확인`, ` → recover: user types "next round" or check rules via leerness rule list`)));
|
|
9543
|
+
log(dim(_t(` → handoff 이력: ${(gapInfo.history || []).slice(-3).map(t => t.slice(11, 19)).join(' → ')}`, ` → handoff history: ${(gapInfo.history || []).slice(-3).map(t => t.slice(11, 19)).join(' → ')}`)));
|
|
9359
9544
|
log('');
|
|
9360
9545
|
} else if (gapInfo.isMiss) {
|
|
9361
9546
|
// 35~60분 → 의심
|
|
9362
|
-
log(yel(`## ⏰ ScheduleWakeup 지연 (1.9.199) — 이전 handoff ${gapInfo.gapMin}분 전 (룰: 25분)`));
|
|
9363
|
-
log(dim(` ±10분 buffer 초과 — wakeup 한 cycle 누락
|
|
9547
|
+
log(yel(_t(`## ⏰ ScheduleWakeup 지연 (1.9.199) — 이전 handoff ${gapInfo.gapMin}분 전 (룰: 25분)`, `## ⏰ ScheduleWakeup delayed — last handoff ${gapInfo.gapMin} min ago (rule: 25 min)`)));
|
|
9548
|
+
log(dim(_t(` ±10분 buffer 초과 — wakeup 한 cycle 누락 가능성`, ` beyond the ±10 min buffer — a wakeup cycle may have been missed`)));
|
|
9364
9549
|
log('');
|
|
9365
9550
|
} else if (gapInfo.gapMin >= 0 && gapInfo.gapMin <= 30) {
|
|
9366
9551
|
// 정상 범위 (handoff_history.length >= 2 일 때만 의미 있음 — 첫 진입 제외)
|
|
9367
9552
|
if ((gapInfo.history || []).length >= 2) {
|
|
9368
|
-
log(dim(` ✓ ScheduleWakeup cycle 정상 (gap ${gapInfo.gapMin}분, 룰 25분 — 1.9.199)`));
|
|
9553
|
+
log(dim(_t(` ✓ ScheduleWakeup cycle 정상 (gap ${gapInfo.gapMin}분, 룰 25분 — 1.9.199)`, ` ✓ ScheduleWakeup cycle healthy (gap ${gapInfo.gapMin} min, rule 25 min)`)));
|
|
9369
9554
|
}
|
|
9370
9555
|
}
|
|
9371
9556
|
} else {
|
|
@@ -9375,9 +9560,9 @@ function handoff(root) {
|
|
|
9375
9560
|
const ageMs = Date.now() - fs.statSync(tlp).mtimeMs;
|
|
9376
9561
|
const ageMin = Math.floor(ageMs / 60000);
|
|
9377
9562
|
if (ageMin >= 60) {
|
|
9378
|
-
const label = ageMin < 120 ? `${ageMin}
|
|
9379
|
-
log(yel(`## ⏰ ScheduleWakeup miss 의심 (1.9.196 fallback) — task-log 마지막 ${label}
|
|
9380
|
-
log(dim(` 1.9.199 last-handoff.json 첫 기록 — 다음 handoff 부터 정확
|
|
9563
|
+
const label = ageMin < 120 ? _t(`${ageMin}분`, `${ageMin}min`) : (ageMin < 1440 ? _t(`${Math.floor(ageMin/60)}시간`, `${Math.floor(ageMin/60)}h`) : _t(`${Math.floor(ageMin/1440)}일`, `${Math.floor(ageMin/1440)}d`));
|
|
9564
|
+
log(yel(_t(`## ⏰ ScheduleWakeup miss 의심 (1.9.196 fallback) — task-log 마지막 ${label} 전`, `## ⏰ ScheduleWakeup miss suspected (fallback) — task-log last modified ${label} ago`)));
|
|
9565
|
+
log(dim(_t(` 1.9.199 last-handoff.json 첫 기록 — 다음 handoff 부터 정확 측정`, ` first last-handoff.json record — precise measurement from the next handoff`)));
|
|
9381
9566
|
log('');
|
|
9382
9567
|
}
|
|
9383
9568
|
}
|
|
@@ -9392,6 +9577,7 @@ function handoff(root) {
|
|
|
9392
9577
|
const isTtyMd = process.stdout && process.stdout.isTTY;
|
|
9393
9578
|
const mdCy = s => isTtyMd ? `\x1b[36m${s}\x1b[0m` : s;
|
|
9394
9579
|
const mdDim = s => isTtyMd ? `\x1b[2m${s}\x1b[0m` : s;
|
|
9580
|
+
const t = (ko, en) => (_uiLang(root) === 'en' ? en : ko); // 1.30.5 (#156 F3): 로컬 t()
|
|
9395
9581
|
const cutoff = Date.now() - 24 * 60 * 60 * 1000;
|
|
9396
9582
|
const deltas = [];
|
|
9397
9583
|
// tasks: progress-tracker.md row Updated 컬럼 기반 (간단 휴리스틱 — mtime이 24h내면 표시)
|
|
@@ -9432,7 +9618,7 @@ function handoff(root) {
|
|
|
9432
9618
|
const pp = planPath(root);
|
|
9433
9619
|
if (exists(pp) && fs.statSync(pp).mtimeMs > cutoff) {
|
|
9434
9620
|
// M-XXXX 중 line이 24h내 추가됐는지 정확히는 어려움 — mtime 24h내면 "plan: 변경됨"으로 표시
|
|
9435
|
-
deltas.push('plan: 변경됨');
|
|
9621
|
+
deltas.push(t('plan: 변경됨', 'plan: changed'));
|
|
9436
9622
|
}
|
|
9437
9623
|
} catch {}
|
|
9438
9624
|
// rules: rule add 후 mtime 24h내
|
|
@@ -9444,8 +9630,8 @@ function handoff(root) {
|
|
|
9444
9630
|
}
|
|
9445
9631
|
} catch {}
|
|
9446
9632
|
if (deltas.length) {
|
|
9447
|
-
log(mdCy(`🆕 최근 24h 메모리 변동 (1.9.121): ${deltas.join(' · ')}`));
|
|
9448
|
-
log(mdDim(` → 상세: leerness memory status --json`));
|
|
9633
|
+
log(mdCy(t(`🆕 최근 24h 메모리 변동 (1.9.121): ${deltas.join(' · ')}`, `🆕 memory changes in last 24h: ${deltas.join(' · ')}`)));
|
|
9634
|
+
log(mdDim(t(` → 상세: leerness memory status --json`, ` → details: leerness memory status --json`)));
|
|
9449
9635
|
log('');
|
|
9450
9636
|
}
|
|
9451
9637
|
} catch {}
|
|
@@ -9573,8 +9759,13 @@ function handoff(root) {
|
|
|
9573
9759
|
const _Lsec = _uiLang(root);
|
|
9574
9760
|
const t = (ko, en) => (_Lsec === 'en' ? en : ko);
|
|
9575
9761
|
const envExists = exists(path.join(root, '.env'));
|
|
9762
|
+
const issues = [];
|
|
9763
|
+
// 0) 1.30.1 (14th 외부리뷰 F2): 커밋된 plaintext 시크릿을 보안 요약에 노출 — headline '🚨 시크릿 N건' 과 일관.
|
|
9764
|
+
// envExists 무관(소스에 커밋된 시크릿은 .env 없어도 위험). gitignored 는 _collectSecretFindings 가 committed 에서 제외.
|
|
9765
|
+
let committedSecrets = [];
|
|
9766
|
+
try { committedSecrets = _collectSecretFindings(root).committed || []; } catch {}
|
|
9767
|
+
if (committedSecrets.length) issues.push(t(`커밋된 시크릿 ${committedSecrets.length}건 (소스 노출)`, `${committedSecrets.length} committed secret(s) (exposed in source)`));
|
|
9576
9768
|
if (envExists) {
|
|
9577
|
-
const issues = [];
|
|
9578
9769
|
// 1) env diff
|
|
9579
9770
|
try {
|
|
9580
9771
|
const d = envDiff(root);
|
|
@@ -9589,41 +9780,43 @@ function handoff(root) {
|
|
|
9589
9780
|
const missing = SECRET_PATTERNS.filter(p => !giLines.some(l => l === p || l === '/' + p));
|
|
9590
9781
|
if (missing.length) issues.push(t(`.gitignore 시크릿 누락 ${missing.length}건`, `.gitignore missing secret patterns ${missing.length}`));
|
|
9591
9782
|
} catch {}
|
|
9592
|
-
|
|
9593
|
-
|
|
9594
|
-
|
|
9595
|
-
|
|
9596
|
-
|
|
9597
|
-
|
|
9598
|
-
|
|
9599
|
-
|
|
9600
|
-
|
|
9601
|
-
|
|
9602
|
-
|
|
9603
|
-
|
|
9604
|
-
|
|
9605
|
-
|
|
9606
|
-
|
|
9607
|
-
|
|
9608
|
-
|
|
9609
|
-
|
|
9610
|
-
|
|
9611
|
-
|
|
9612
|
-
|
|
9613
|
-
|
|
9614
|
-
|
|
9615
|
-
|
|
9616
|
-
|
|
9617
|
-
|
|
9618
|
-
}
|
|
9619
|
-
log(dim(t(` ⚠ 자동 회복
|
|
9783
|
+
}
|
|
9784
|
+
if (issues.length) {
|
|
9785
|
+
const isTty = process.stdout && process.stdout.isTTY;
|
|
9786
|
+
const red = s => isTty ? `\x1b[31m${s}\x1b[0m` : s;
|
|
9787
|
+
const dim = s => isTty ? `\x1b[2m${s}\x1b[0m` : s;
|
|
9788
|
+
const yel = s => isTty ? `\x1b[33m${s}\x1b[0m` : s;
|
|
9789
|
+
log('');
|
|
9790
|
+
log(red(t(`## 🔒 보안 요약 (1.9.76) — ${issues.length}건 주의`, `## 🔒 Security summary — ${issues.length} to review`)));
|
|
9791
|
+
for (const i of issues) log(dim(` ⚠ ${i}`));
|
|
9792
|
+
// 1.30.1 (F2): 커밋된 시크릿 파일 위치 노출 (값 snippet 은 미출력 — handoff 로그로의 시크릿 유출 방지)
|
|
9793
|
+
for (const f of committedSecrets.slice(0, 4)) log(dim(` • ${f.file}:${f.line} (${f.name})`));
|
|
9794
|
+
log(dim(t(` → 자동 수정: leerness audit --fix · 상세: leerness scan secrets / leerness env check`, ` → auto-fix: leerness audit --fix · details: leerness scan secrets / leerness env check`)));
|
|
9795
|
+
// 1.9.80: critical 수준 (.gitignore에 .env 자체 누락) 시 자동 회복 옵션
|
|
9796
|
+
const giText2 = exists(path.join(root, '.gitignore')) ? read(path.join(root, '.gitignore')) : '';
|
|
9797
|
+
const giLines2 = giText2.split('\n').map(l => l.trim());
|
|
9798
|
+
const envInGitignore = giLines2.includes('.env') || giLines2.includes('/.env');
|
|
9799
|
+
if (envExists && !envInGitignore) {
|
|
9800
|
+
// .env 자체 누락 → 최우선 위험
|
|
9801
|
+
log(yel(t(` 🚨 CRITICAL (1.9.80): .env가 .gitignore에 없습니다! 시크릿 노출 위험 — 즉시 \`leerness audit --fix\` 권장.`, ` 🚨 CRITICAL: .env is not in .gitignore! secret-exposure risk — run \`leerness audit --fix\` now.`)));
|
|
9802
|
+
// LEERNESS_AUTO_SECURITY_FIX=1 자동 실행 옵션
|
|
9803
|
+
if (process.env.LEERNESS_AUTO_SECURITY_FIX === '1') {
|
|
9804
|
+
try {
|
|
9805
|
+
const r = cp.spawnSync(process.execPath, [__filename, 'audit', root, '--fix'],
|
|
9806
|
+
{ encoding: 'utf8', timeout: 15000, env: { ...process.env, LEERNESS_INTERNAL: '1', LEERNESS_NO_PROMPT: '1', LEERNESS_NO_DRIFT_CHECK: '1' } });
|
|
9807
|
+
if (r.status === 0) {
|
|
9808
|
+
log(dim(t(` ✓ 자동 회복 (LEERNESS_AUTO_SECURITY_FIX=1): audit --fix 완료`, ` ✓ auto-recovered (LEERNESS_AUTO_SECURITY_FIX=1): audit --fix done`)));
|
|
9809
|
+
} else {
|
|
9810
|
+
log(dim(t(` ⚠ 자동 회복 실패 (exit ${r.status}) — 수동 \`leerness audit --fix\` 권장`, ` ⚠ auto-recovery failed (exit ${r.status}) — run \`leerness audit --fix\` manually`)));
|
|
9620
9811
|
}
|
|
9621
|
-
}
|
|
9622
|
-
log(dim(t(`
|
|
9812
|
+
} catch (e) {
|
|
9813
|
+
log(dim(t(` ⚠ 자동 회복 예외: ${e.message}`, ` ⚠ auto-recovery error: ${e.message}`)));
|
|
9623
9814
|
}
|
|
9815
|
+
} else {
|
|
9816
|
+
log(dim(t(` 💡 자동 실행 옵션: LEERNESS_AUTO_SECURITY_FIX=1 leerness handoff .`, ` 💡 auto-fix option: LEERNESS_AUTO_SECURITY_FIX=1 leerness handoff .`)));
|
|
9624
9817
|
}
|
|
9625
|
-
log('');
|
|
9626
9818
|
}
|
|
9819
|
+
log('');
|
|
9627
9820
|
}
|
|
9628
9821
|
} catch {}
|
|
9629
9822
|
}
|
|
@@ -9656,27 +9849,29 @@ function handoff(root) {
|
|
|
9656
9849
|
const cy = s => isTty ? `\x1b[36m${s}\x1b[0m` : s;
|
|
9657
9850
|
const b = s => isTty ? `\x1b[1m${s}\x1b[0m` : s;
|
|
9658
9851
|
const d = s => isTty ? `\x1b[2m${s}\x1b[0m` : s;
|
|
9852
|
+
const t = (ko, en) => (_uiLang(root) === 'en' ? en : ko); // 1.30.5 (#156 F3): headline t() 스코프 밖 — 로컬 t() (1.29.1 교훈)
|
|
9659
9853
|
log('');
|
|
9660
|
-
log(cy('## 🛠 세션 워크플로 6단계 (1.9.39+, AI 하네스 엔지니어링)'));
|
|
9661
|
-
log(d(' 상세: ') + cy('.harness/session-workflow.md'));
|
|
9662
|
-
log(` 1. ${b('요청 분석')} handoff(이미 완료) · drift check · 모호하면
|
|
9663
|
-
log(` 2. ${b('계획 수립')} plan add / TodoWrite · reuse-map으로 기존 자원
|
|
9664
|
-
log(` 3. ${b('업무 분배')} agents list/recommend · 작업유형별 sub-agent
|
|
9665
|
-
log(` 4. ${b('sub-agent 작업')} 파일 경로 격리 · mtime 검증 의무 · 자체
|
|
9666
|
-
log(` 5. ${b('종합 검증')} contract verify · verify-claim --run-tests · review --persona`);
|
|
9667
|
-
log(` 6. ${b('세션 마감')} session close · audit --fix · usage stats`);
|
|
9668
|
-
log(d(' 끄려면: --no-workflow-guide 또는 LEERNESS_NO_WORKFLOW_GUIDE=1'));
|
|
9854
|
+
log(cy(t('## 🛠 세션 워크플로 6단계 (1.9.39+, AI 하네스 엔지니어링)', '## 🛠 Session workflow — 6 steps (AI harness engineering)')));
|
|
9855
|
+
log(d(t(' 상세: ', ' details: ')) + cy('.harness/session-workflow.md'));
|
|
9856
|
+
log(` 1. ${b(t('요청 분석', 'Analyze request'))} ${t('handoff(이미 완료) · drift check · 모호하면 명확화', 'handoff (done) · drift check · clarify if ambiguous')}`);
|
|
9857
|
+
log(` 2. ${b(t('계획 수립', 'Plan'))} ${t('plan add / TodoWrite · reuse-map으로 기존 자원 우선', 'plan add / TodoWrite · prefer existing via reuse-map')}`);
|
|
9858
|
+
log(` 3. ${b(t('업무 분배', 'Distribute'))} ${t('agents list/recommend · 작업유형별 sub-agent 매핑', 'agents list/recommend · map sub-agents by task type')}`);
|
|
9859
|
+
log(` 4. ${b(t('sub-agent 작업', 'sub-agent work'))} ${t('파일 경로 격리 · mtime 검증 의무 · 자체 테스트', 'isolate file paths · verify mtime · self-test')}`);
|
|
9860
|
+
log(` 5. ${b(t('종합 검증', 'Verify'))} ${t('contract verify · verify-claim --run-tests · review --persona', 'contract verify · verify-claim --run-tests · review --persona')}`);
|
|
9861
|
+
log(` 6. ${b(t('세션 마감', 'Close'))} ${t('session close · audit --fix · usage stats', 'session close · audit --fix · usage stats')}`);
|
|
9862
|
+
log(d(t(' 끄려면: --no-workflow-guide 또는 LEERNESS_NO_WORKFLOW_GUIDE=1', ' to disable: --no-workflow-guide or LEERNESS_NO_WORKFLOW_GUIDE=1')));
|
|
9669
9863
|
log('');
|
|
9670
9864
|
}
|
|
9671
9865
|
// 1.9.373 (UR-0073 Phase C): 에이전트 팀 스케줄 알림 — 비-manual 팀이 정의돼 있으면 미리보기(dry-run) 안내. 실행 없음. opt-out.
|
|
9672
9866
|
if (!has('--no-team-reminders') && !has('--compact') && !has('--quiet') && process.env.LEERNESS_NO_TEAM_REMINDERS !== '1') {
|
|
9673
9867
|
try {
|
|
9674
|
-
const _teamRem = _teamHandoffReminders(_loadTeams(root));
|
|
9868
|
+
const _teamRem = _teamHandoffReminders(_loadTeams(root), _uiLang(root));
|
|
9675
9869
|
if (_teamRem.length) {
|
|
9870
|
+
const t = (ko, en) => (_uiLang(root) === 'en' ? en : ko); // 1.30.5 (#156 F3): 로컬 t()
|
|
9676
9871
|
log('');
|
|
9677
|
-
log('## 🤝 에이전트 팀 스케줄 (UR-0073 Phase C) — 정의 전용 · 자동 실행 X');
|
|
9872
|
+
log(t('## 🤝 에이전트 팀 스케줄 (UR-0073 Phase C) — 정의 전용 · 자동 실행 X', '## 🤝 Agent team schedule (UR-0073) — definitions only · no auto-run'));
|
|
9678
9873
|
_teamRem.forEach(r => log(' ' + r));
|
|
9679
|
-
log(' ⓘ 미리보기는 dry-run. 실행은 제안 명령 검토 후 직접 · 끄려면 --no-team-reminders 또는 LEERNESS_NO_TEAM_REMINDERS=1');
|
|
9874
|
+
log(t(' ⓘ 미리보기는 dry-run. 실행은 제안 명령 검토 후 직접 · 끄려면 --no-team-reminders 또는 LEERNESS_NO_TEAM_REMINDERS=1', ' ⓘ preview is dry-run. run it yourself after reviewing the suggested command · disable: --no-team-reminders or LEERNESS_NO_TEAM_REMINDERS=1'));
|
|
9680
9875
|
}
|
|
9681
9876
|
} catch {}
|
|
9682
9877
|
}
|
|
@@ -10180,7 +10375,7 @@ function _vcImplIsEmpty(body) {
|
|
|
10180
10375
|
function verifyClaimCmd(root, taskId) {
|
|
10181
10376
|
root = absRoot(root);
|
|
10182
10377
|
const _j = has('--json'); // 1.9.400 (7번째 버그헌트 P1-B, UR-0105): --json 에러도 구조화
|
|
10183
|
-
if (!taskId) return failJson(_j, 'missing_args', 'verify-claim <T-ID> 필요. 예: leerness verify-claim T-0008');
|
|
10378
|
+
if (!taskId) return failJson(_j, 'missing_args', _uiLang(root) === 'en' ? 'verify-claim <T-ID> required. ex: leerness verify-claim T-0008' : 'verify-claim <T-ID> 필요. 예: leerness verify-claim T-0008'); // 1.30.5 (#156 F4)
|
|
10184
10379
|
const rows = readProgressRows(root);
|
|
10185
10380
|
const row = rows.find(r => r.id === taskId);
|
|
10186
10381
|
if (!row) return failJson(_j, 'not_found', `progress-tracker.md에 ${taskId} 없음.`);
|
|
@@ -11064,7 +11259,7 @@ function honestyCheckCmd(root, arg1) {
|
|
|
11064
11259
|
function optimismCheckCmd(root, taskId) {
|
|
11065
11260
|
root = absRoot(root || process.cwd());
|
|
11066
11261
|
const _j = has('--json'); // 1.9.400 (7번째 버그헌트 P1-B, UR-0105): --json 에러도 구조화
|
|
11067
|
-
if (!taskId) return failJson(_j, 'missing_args', 'optimism-check <T-ID> 필요. 예: leerness optimism-check T-0001');
|
|
11262
|
+
if (!taskId) return failJson(_j, 'missing_args', _uiLang(root) === 'en' ? 'optimism-check <T-ID> required. ex: leerness optimism-check T-0001' : 'optimism-check <T-ID> 필요. 예: leerness optimism-check T-0001'); // 1.30.5 (#156 F4 twin)
|
|
11068
11263
|
const rows = readProgressRows(root);
|
|
11069
11264
|
const row = rows.find(r => r.id === taskId);
|
|
11070
11265
|
if (!row) return failJson(_j, 'not_found', `progress-tracker.md에 ${taskId} 없음.`);
|
|
@@ -17224,33 +17419,36 @@ function rolesCmd(root, sub, ...args) {
|
|
|
17224
17419
|
// 무엇을 할 수 있고 어떻게 끄는지를 명시적으로 공개해 신뢰도를 높인다. SECURITY.md 와 동일 출처.
|
|
17225
17420
|
// CAPABILITY_SURFACE / POWERFUL_COMMANDS → lib/catalogs.js (1.9.295 UR-0025 4단계)
|
|
17226
17421
|
function capabilitiesCmd(root, opts = {}) {
|
|
17422
|
+
const _L = _uiLang(root); const _t = (ko, en) => _L === 'en' ? en : ko; // 1.31.3 (UR-0010)
|
|
17227
17423
|
if (opts.json) {
|
|
17228
17424
|
log(JSON.stringify({ version: VERSION, surface: CAPABILITY_SURFACE, powerfulCommands: POWERFUL_COMMANDS,
|
|
17229
|
-
principles:
|
|
17425
|
+
principles: _L === 'en'
|
|
17426
|
+
? ['0 runtime dependencies', 'no postinstall', 'no external LLM/API/CLI auto-call without user consent', 'auto-backup before changes']
|
|
17427
|
+
: ['0 런타임 의존성', 'postinstall 없음', '사용자 동의 없이 외부 LLM/API/CLI 자동 호출 안 함', '변경 전 자동 백업'] }, null, 2));
|
|
17230
17428
|
return;
|
|
17231
17429
|
}
|
|
17232
17430
|
const isTty = process.stdout && process.stdout.isTTY;
|
|
17233
17431
|
const cy = s => isTty ? `\x1b[36m${s}\x1b[0m` : s;
|
|
17234
17432
|
const dm = s => isTty ? `\x1b[2m${s}\x1b[0m` : s;
|
|
17235
17433
|
const riskMark = r => r === 'high' ? '🔴 high' : r === 'medium' ? '🟡 medium' : '🟢 low';
|
|
17236
|
-
log(cy(`# leerness capabilities (1.9.272) — 권한·보안 표면
|
|
17237
|
-
log(dm(` leerness 는 권한이 큰 CLI 하네스입니다. 아래가 할 수 있는 전부이며, 각 항목의 opt-out 을 함께
|
|
17434
|
+
log(cy(_t(`# leerness capabilities (1.9.272) — 권한·보안 표면 공개`, `# leerness capabilities — permission/security surface disclosure`)));
|
|
17435
|
+
log(dm(_t(` leerness 는 권한이 큰 CLI 하네스입니다. 아래가 할 수 있는 전부이며, 각 항목의 opt-out 을 함께 표기합니다.`, ` leerness is a high-privilege CLI harness. Below is everything it can do, with the opt-out for each item.`)));
|
|
17238
17436
|
log('');
|
|
17239
|
-
log(`##
|
|
17240
|
-
log(` ✓ 런타임 의존성 0 · postinstall 없음 · 변경 전 자동
|
|
17241
|
-
log(` ✓ 사용자 동의 없이 외부 LLM/API/CLI 를 자동 호출하지
|
|
17437
|
+
log(_t(`## 원칙`, `## Principles`));
|
|
17438
|
+
log(_t(` ✓ 런타임 의존성 0 · postinstall 없음 · 변경 전 자동 백업`, ` ✓ 0 runtime dependencies · no postinstall · auto-backup before changes`));
|
|
17439
|
+
log(_t(` ✓ 사용자 동의 없이 외부 LLM/API/CLI 를 자동 호출하지 않음`, ` ✓ never auto-calls an external LLM/API/CLI without user consent`));
|
|
17242
17440
|
log('');
|
|
17243
|
-
log(`## 권한
|
|
17441
|
+
log(_t(`## 권한 표면`, `## Permission surface`));
|
|
17244
17442
|
for (const [k, v] of Object.entries(CAPABILITY_SURFACE)) {
|
|
17245
17443
|
log(` ${riskMark(v.risk)} ${cy(k)}`);
|
|
17246
|
-
log(` ${v.desc}`);
|
|
17247
|
-
log(dm(` opt-out: ${v.optOut}`));
|
|
17444
|
+
log(` ${_L === 'en' && v.descEn ? v.descEn : v.desc}`);
|
|
17445
|
+
log(dm(` opt-out: ${_L === 'en' && v.optOutEn ? v.optOutEn : v.optOut}`));
|
|
17248
17446
|
}
|
|
17249
17447
|
log('');
|
|
17250
|
-
log(`## ⚠ 주의해서 쓸 명령 (회사/운영 코드)`);
|
|
17251
|
-
for (const c of POWERFUL_COMMANDS) log(` • ${c.cmd.padEnd(22)} ${dm(c.note)}`);
|
|
17448
|
+
log(_t(`## ⚠ 주의해서 쓸 명령 (회사/운영 코드)`, `## ⚠ Commands to use with care (company/production code)`));
|
|
17449
|
+
for (const c of POWERFUL_COMMANDS) log(` • ${c.cmd.padEnd(22)} ${dm(_L === 'en' && c.noteEn ? c.noteEn : c.note)}`);
|
|
17252
17450
|
log('');
|
|
17253
|
-
log(dm(` 전체 정책: SECURITY.md · 기계 판독: leerness capabilities --json · 권한 등급: leerness policy`));
|
|
17451
|
+
log(dm(_t(` 전체 정책: SECURITY.md · 기계 판독: leerness capabilities --json · 권한 등급: leerness policy`, ` full policy: SECURITY.md · machine-readable: leerness capabilities --json · permission tiers: leerness policy`)));
|
|
17254
17452
|
}
|
|
17255
17453
|
|
|
17256
17454
|
// ===== 1.9.281 (UR-0034, GPT-5.5 범용 하네스): 권한 등급(permission tiers) =====
|
|
@@ -20204,13 +20402,19 @@ async function main() {
|
|
|
20204
20402
|
if (cmd === 'wakeup-interval') return wakeupIntervalCmd(arg('--path', process.cwd()), args[1], args[2]);
|
|
20205
20403
|
// 1.9.211: leerness workspace-dir <get|guide> — 현재 워크스페이스 디렉토리 / AI 참조 가이드 (사용자 명시)
|
|
20206
20404
|
if (cmd === 'workspace-dir') return workspaceDirCmd(arg('--path', process.cwd()), args[1]);
|
|
20405
|
+
if (cmd === 'parent') return parentCmd(arg('--path', process.cwd()), args[1]);
|
|
20207
20406
|
// 1.9.211: leerness migrate-workspace-dir — .harness → .leerness 마이그레이션 (사용자 명시)
|
|
20208
20407
|
if (cmd === 'migrate-workspace-dir') return migrateWorkspaceDirCmd(arg('--path', process.cwd()));
|
|
20209
20408
|
// 1.9.212: leerness idempotency audit — 멱등성 위반 탐지 (사용자 명시)
|
|
20210
20409
|
if (cmd === 'idempotency') return idempotencyCmd(arg('--path', process.cwd()), args[1]);
|
|
20211
20410
|
// 1.9.213: leerness intent <classify|expand|domains> — intent inference + scope expansion (사용자 명시)
|
|
20212
20411
|
if (cmd === 'intent') return intentCmd(arg('--path', process.cwd()), args[1], ...args.slice(2));
|
|
20213
|
-
if (cmd === 'rule' && args[1] === 'add')
|
|
20412
|
+
if (cmd === 'rule' && args[1] === 'add') { // 1.9.426: flag/경로 break(_parseAddTitle) · 1.9.445 (UR-0151): positional path 지원(제목과 분리)
|
|
20413
|
+
const _desc = _parseAddTitle(args, 2);
|
|
20414
|
+
// 1.30.4 (14th리뷰 F6): 빈 입력 시 --json 에서도 구조화 JSON(task/decision add 와 일관). 종전엔 ruleAdd 내부 fail() 가 평문 출력.
|
|
20415
|
+
if (!_desc) { failJson(has('--json'), 'empty_title', 'rule add "<설명>" 필요 (빈/경로-only 거부)'); return process.exit(process.exitCode || 1); }
|
|
20416
|
+
return ruleAdd(arg('--path', null) || _taskPositionalPath(args, 2) || process.cwd(), _desc);
|
|
20417
|
+
}
|
|
20214
20418
|
if (cmd === 'rule' && args[1] === 'list') return ruleList(arg('--path', process.cwd()));
|
|
20215
20419
|
if (cmd === 'rule' && args[1] === 'remove') return ruleRemove(arg('--path', process.cwd()), args[2]);
|
|
20216
20420
|
if (cmd === 'rule' && args[1] === 'pause') return rulePause(arg('--path', process.cwd()), args[2]);
|
|
@@ -20218,6 +20422,8 @@ async function main() {
|
|
|
20218
20422
|
if (cmd === 'rule' && args[1] === 'stop') return ruleStop(arg('--path', process.cwd()));
|
|
20219
20423
|
if (cmd === 'rule' && args[1] === 'resume-all') return ruleResumeAll(arg('--path', process.cwd()));
|
|
20220
20424
|
if (cmd === 'rule' && args[1] === 'verify') return ruleVerifyCmd(arg('--path', process.cwd()));
|
|
20425
|
+
// 1.30.4 (14th리뷰 F7): rule 하위명령 미매칭 시 잘못된 토큰 명시 + usage(종전엔 top-level 'unknown_command: rule' 로 유효 부모명을 오인 표기).
|
|
20426
|
+
if (cmd === 'rule') { failJson(has('--json'), 'unknown_subcommand', `알 수 없는 rule 하위명령: ${args[1] || '(없음)'} — leerness rule add|list|remove|pause|resume|stop|resume-all|verify`); return process.exit(process.exitCode || 1); }
|
|
20221
20427
|
if (cmd === 'release' && args[1] === 'bump') return releaseBump(args[2] || arg('--path', process.cwd()));
|
|
20222
20428
|
if (cmd === 'release' && args[1] === 'note') return releaseNote(arg('--path', process.cwd()), args.slice(2).filter(x => !x.startsWith('-')).join(' '));
|
|
20223
20429
|
if (cmd === 'release' && args[1] === 'publish') return releasePublish(args[2] || arg('--path', process.cwd()));
|
|
@@ -20272,6 +20478,8 @@ async function main() {
|
|
|
20272
20478
|
if (sub==='relink') return taskRelink(root);
|
|
20273
20479
|
if (sub==='sync') return taskSyncCmd(root);
|
|
20274
20480
|
if (sub==='export') return taskExportCmd(root);
|
|
20481
|
+
// 1.30.4 (14th리뷰 F7): 미매칭 하위명령 시 잘못된 토큰을 명시 + usage(종전엔 top-level 'unknown_command: task' 로 유효 부모명을 오인 표기).
|
|
20482
|
+
failJson(has('--json'), 'unknown_subcommand', `알 수 없는 task 하위명령: ${sub} — leerness task list|add|update|drop|fix-evidence|relink|sync|export`); return process.exit(process.exitCode || 1);
|
|
20275
20483
|
}
|
|
20276
20484
|
// 1.9.114: memory status — Memory Write Surface 5종 통합 상태
|
|
20277
20485
|
if (cmd === 'memory' && args[1] === 'status') {
|
|
@@ -20299,7 +20507,10 @@ async function main() {
|
|
|
20299
20507
|
if (args[i].startsWith('--') || /^([A-Za-z]:[\\/]|\/|\.\.?[\\/])/.test(args[i])) break;
|
|
20300
20508
|
textParts.push(args[i]);
|
|
20301
20509
|
}
|
|
20302
|
-
|
|
20510
|
+
const _text = textParts.join(' ');
|
|
20511
|
+
// 1.30.4 (14th리뷰 F6): 빈 입력 시 --json 에서도 구조화 JSON(task/decision add 와 일관). 종전엔 lessonSave 내부 fail() 가 평문 출력.
|
|
20512
|
+
if (!_text) { failJson(has('--json'), 'empty_text', 'lesson save "<text>" 필요 (빈/경로-only 거부)'); return process.exit(process.exitCode || 1); }
|
|
20513
|
+
return lessonSave(root, _text);
|
|
20303
20514
|
}
|
|
20304
20515
|
if (sub === 'list') {
|
|
20305
20516
|
return lessonListCmd(root, { json: has('--json') });
|