leerness 1.29.0 → 1.31.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 +219 -0
- package/README.md +4 -4
- package/bin/leerness.js +332 -78
- package/lib/audit.js +17 -1
- package/package.json +1 -1
- package/scripts/e2e.js +225 -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.31.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') 시 호스트 프로세스 오염.
|
|
@@ -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;
|
|
@@ -3851,7 +3851,73 @@ function _selfTestCases() {
|
|
|
3851
3851
|
const koPreserved = dg.includes('설치/환경 진단') && dg.includes('## npm 환경') && dg.includes('버전별 헤드라인'); // ko 인자 보존(내부호출/e2e)
|
|
3852
3852
|
return injected && en && koPreserved;
|
|
3853
3853
|
} },
|
|
3854
|
-
{ name: '
|
|
3854
|
+
{ name: 'Phase 10d (1.29.1): handoff 보안 요약 섹션 영어/한국어 보존 (소스 가드)', run: () => {
|
|
3855
|
+
const bin = read(__filename);
|
|
3856
|
+
// split-literals so this guard's own strings are not contiguous matches (self-reference trap)
|
|
3857
|
+
const en = bin.includes('Securit' + 'y summary —')
|
|
3858
|
+
&& bin.includes('auto-recover' + 'ed (LEERNESS_AUTO_SECURITY_FIX')
|
|
3859
|
+
&& bin.includes('auto-fix optio' + 'n: LEERNESS_AUTO_SECURITY_FIX');
|
|
3860
|
+
const koPreserved = bin.includes('보안 ' + '요약 (1.9.76)')
|
|
3861
|
+
&& bin.includes('자동 실행 ' + '옵션: LEERNESS_AUTO_SECURITY_FIX');
|
|
3862
|
+
return en && koPreserved;
|
|
3863
|
+
} },
|
|
3864
|
+
{ name: 'Phase 10e (1.29.2): handoff env-detect 블록 영어/한국어 보존 (소스 가드)', run: () => {
|
|
3865
|
+
const bin = read(__filename);
|
|
3866
|
+
// split-literals (self-reference trap 회피)
|
|
3867
|
+
const en = bin.includes('Runtime environ' + 'ment: ⚠')
|
|
3868
|
+
&& bin.includes('change(s) detec' + 'ted')
|
|
3869
|
+
&& bin.includes('→ detail' + 's: leerness env detect');
|
|
3870
|
+
const koPreserved = bin.includes('실행 환경 (1.9.145): ⚠ PATH 누' + '락')
|
|
3871
|
+
&& bin.includes('→ 상' + '세: leerness env detect');
|
|
3872
|
+
return en && koPreserved;
|
|
3873
|
+
} },
|
|
3874
|
+
{ name: 'Phase 10f (1.29.3): handoff shell-guard 블록 영어/한국어 보존 (소스 가드)', run: () => {
|
|
3875
|
+
const bin = read(__filename);
|
|
3876
|
+
// split-literals (self-reference trap 회피)
|
|
3877
|
+
const en = bin.includes('Terminal shell gua' + 'rd (UR-0020)')
|
|
3878
|
+
&& bin.includes('review past shell fail' + 'ures')
|
|
3879
|
+
&& bin.includes('check before running a comm' + 'and: leerness shell-guard');
|
|
3880
|
+
const koPreserved = bin.includes('터미널 셸 가' + '드 (1.9.263, UR-0020)')
|
|
3881
|
+
&& bin.includes('과거 셸 실패 기록 재검' + '토 권장');
|
|
3882
|
+
return en && koPreserved;
|
|
3883
|
+
} },
|
|
3884
|
+
{ name: 'Phase 10g (1.29.4): handoff CLI 에이전트 슬래시 블록 영어/한국어 보존 (소스 가드)', run: () => {
|
|
3885
|
+
const bin = read(__filename);
|
|
3886
|
+
// split-literals (self-reference trap 회피)
|
|
3887
|
+
const en = bin.includes('CLI agent slash comma' + 'nds (UR-0021)')
|
|
3888
|
+
&& bin.includes("active agent(s) — use each one'" + 's slash commands')
|
|
3889
|
+
&& bin.includes('full list / record / refr' + 'esh: leerness slash-commands');
|
|
3890
|
+
const koPreserved = bin.includes('CLI 에이전트 슬래시 명' + '령 (1.9.265~266, UR-0021)')
|
|
3891
|
+
&& bin.includes('전체/기록/최신' + '화: leerness slash-commands');
|
|
3892
|
+
return en && koPreserved;
|
|
3893
|
+
} },
|
|
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
|
+
} }
|
|
3855
3921
|
];
|
|
3856
3922
|
}
|
|
3857
3923
|
function selfTestCmd(opts = {}) {
|
|
@@ -4631,6 +4697,7 @@ function commandsCmd(root) {
|
|
|
4631
4697
|
{ cmd: 'update [--check|--yes|--force]', desc: '자가 업데이트' },
|
|
4632
4698
|
{ cmd: 'wakeup-interval get|set|auto|history|record', desc: 'adaptive wakeup (1.9.210)' },
|
|
4633
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)' },
|
|
4634
4701
|
{ cmd: 'intent classify|expand|domains "<request>"', desc: '의도 파악 + scope (1.9.213)' },
|
|
4635
4702
|
{ cmd: 'constraints list|check|add', desc: '플랫폼/API 제약 (1.9.208)' },
|
|
4636
4703
|
{ cmd: 'provider list|add|remove|sync', desc: 'Provider Registry (1.9.157~160)' },
|
|
@@ -5162,6 +5229,36 @@ function _workspaceDirName(root) {
|
|
|
5162
5229
|
function _workspaceDirAbs(root) {
|
|
5163
5230
|
return path.join(root, _workspaceDirName(root));
|
|
5164
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
|
+
}
|
|
5165
5262
|
// .harness → .leerness 마이그레이션 (copy + reference guide 생성)
|
|
5166
5263
|
function _migrateWorkspaceDir(root, opts = {}) {
|
|
5167
5264
|
const dryRun = opts.dryRun === true;
|
|
@@ -6001,6 +6098,103 @@ function workspaceDirCmd(root, sub) {
|
|
|
6001
6098
|
process.exit(1);
|
|
6002
6099
|
}
|
|
6003
6100
|
|
|
6101
|
+
// 1.30.2 (#157 사용자명시): leerness parent — 상위 leerness 부모 프로젝트 탐지(read-only).
|
|
6102
|
+
// 방향 C(교차검토): 부모 자산을 '재사용 후보'로 AI 에게 노출만 하고, 톤/스타일 등 실제 적용은 사용자 결정 게이트(후속 adopt 명령).
|
|
6103
|
+
// intent expand(1.9.213) 안전 모델과 동일 철학 — 탐지/노출 ≠ 적용. 이 명령은 아무 파일도 쓰지 않는다.
|
|
6104
|
+
function parentCmd(root, sub) {
|
|
6105
|
+
root = absRoot(root);
|
|
6106
|
+
const uiLang = _uiLang(root);
|
|
6107
|
+
const t = (ko, en) => (uiLang === 'en' ? en : ko);
|
|
6108
|
+
const isTty = process.stdout && process.stdout.isTTY;
|
|
6109
|
+
const cyan = s => isTty ? `\x1b[36m${s}\x1b[0m` : s;
|
|
6110
|
+
const grn = s => isTty ? `\x1b[32m${s}\x1b[0m` : s;
|
|
6111
|
+
const dim = s => isTty ? `\x1b[2m${s}\x1b[0m` : s;
|
|
6112
|
+
const yel = s => isTty ? `\x1b[33m${s}\x1b[0m` : s;
|
|
6113
|
+
const red = s => isTty ? `\x1b[31m${s}\x1b[0m` : s;
|
|
6114
|
+
if (!sub || sub === 'detect') {
|
|
6115
|
+
const p = _findParentWorkspace(root);
|
|
6116
|
+
if (has('--json')) { log(JSON.stringify({ version: VERSION, root, parent: p, applied: false }, null, 2)); return; }
|
|
6117
|
+
log(cyan(`# leerness parent detect (1.30.2)`));
|
|
6118
|
+
if (!p) {
|
|
6119
|
+
log(dim(t(` 상위 leerness 부모 프로젝트 없음 — 독립 프로젝트입니다.`, ` no parent leerness project found — this is a standalone project.`)));
|
|
6120
|
+
return;
|
|
6121
|
+
}
|
|
6122
|
+
const mark = b => b ? '✓' : '✗';
|
|
6123
|
+
log(t(` 부모 프로젝트: ${grn(p.parentRoot)} (${p.workspaceDir}/, depth ${p.depth})`, ` parent project: ${grn(p.parentRoot)} (${p.workspaceDir}/, depth ${p.depth})`));
|
|
6124
|
+
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)}`,
|
|
6125
|
+
` 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)}`));
|
|
6126
|
+
log('');
|
|
6127
|
+
log(yel(t(` ⚠ 자동 적용하지 않음 — 부모 자산(톤/스타일/디자인) 재사용은 사용자 결정 게이트입니다.`, ` ⚠ not auto-applied — reusing parent assets (tone/style/design) is a user decision.`)));
|
|
6128
|
+
log(dim(t(` 참고: leerness reuse find "<capability>" --path ${p.parentRoot} · 부모 design-system: ${path.join(p.workspaceAbs, 'design-system.md')}`,
|
|
6129
|
+
` ref: leerness reuse find "<capability>" --path ${p.parentRoot} · parent design-system: ${path.join(p.workspaceAbs, 'design-system.md')}`)));
|
|
6130
|
+
log(dim(t(` 재사용 적용(사용자 결정): leerness parent adopt --select design-system,reuse-map,conventions --apply`,
|
|
6131
|
+
` adopt (your decision): leerness parent adopt --select design-system,reuse-map,conventions --apply`)));
|
|
6132
|
+
return;
|
|
6133
|
+
}
|
|
6134
|
+
// 1.30.3 (#158): leerness parent adopt — 부모 자산을 자식-로컬 참조로 기록(게이트형 적용).
|
|
6135
|
+
// 사용자 결정 게이트: dry-run 기본, --apply(사용자 명시) 시에만 기록. 자식 design-system.md 무변경(비파괴, additive).
|
|
6136
|
+
if (sub === 'adopt') {
|
|
6137
|
+
const p = _findParentWorkspace(root);
|
|
6138
|
+
const apply = has('--apply');
|
|
6139
|
+
const selRaw = arg('--select', 'all');
|
|
6140
|
+
const allKinds = ['design-system', 'reuse-map', 'conventions'];
|
|
6141
|
+
const kinds = (selRaw === 'all') ? allKinds : String(selRaw).split(',').map(s => s.trim()).filter(Boolean);
|
|
6142
|
+
const wsAbs = _workspaceDirAbs(root);
|
|
6143
|
+
const inheritedPath = path.join(wsAbs, 'inherited-from-parent.md');
|
|
6144
|
+
const linkPath = path.join(wsAbs, 'PARENT_LINK.json');
|
|
6145
|
+
const cand = [];
|
|
6146
|
+
if (p) {
|
|
6147
|
+
if (kinds.includes('design-system') && p.assets.designSystem) cand.push({ kind: 'design-system', src: path.join(p.workspaceAbs, 'design-system.md') });
|
|
6148
|
+
if (kinds.includes('reuse-map') && p.assets.reuseMap) cand.push({ kind: 'reuse-map', src: path.join(p.workspaceAbs, 'reuse-map.md') });
|
|
6149
|
+
if (kinds.includes('conventions') && p.assets.agents) cand.push({ kind: 'conventions', src: path.join(p.parentRoot, 'AGENTS.md') });
|
|
6150
|
+
}
|
|
6151
|
+
// 1.30.3: --json 은 단일 객체만 출력 — apply(부모 존재) 경로는 자체 JSON(applied:true)을 내므로 여기선 제외(이중 JSON 방지).
|
|
6152
|
+
if (has('--json') && (!p || !apply)) {
|
|
6153
|
+
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));
|
|
6154
|
+
}
|
|
6155
|
+
if (!p) {
|
|
6156
|
+
if (!has('--json')) { log(cyan(`# leerness parent adopt (1.30.3)`)); log(dim(t(` 상위 leerness 부모 프로젝트 없음 — adopt 대상 없음.`, ` no parent leerness project — nothing to adopt.`))); }
|
|
6157
|
+
return;
|
|
6158
|
+
}
|
|
6159
|
+
if (!apply) {
|
|
6160
|
+
if (!has('--json')) {
|
|
6161
|
+
log(cyan(`# leerness parent adopt (1.30.3) [DRY-RUN]`));
|
|
6162
|
+
log(t(` 부모: ${grn(p.parentRoot)} · 선택: ${kinds.join(', ')}`, ` parent: ${grn(p.parentRoot)} · select: ${kinds.join(', ')}`));
|
|
6163
|
+
if (!cand.length) log(dim(t(` 적용 후보 없음 (부모에 선택 자산 없음).`, ` no candidates (parent lacks the selected assets).`)));
|
|
6164
|
+
else cand.forEach(c => log(t(` • ${c.kind} ← ${c.src}`, ` • ${c.kind} ← ${c.src}`)));
|
|
6165
|
+
log('');
|
|
6166
|
+
log(yel(t(` ⚠ DRY-RUN — 실제 적용하려면 \`leerness parent adopt --apply\` (사용자 명시 결정).`, ` ⚠ DRY-RUN — to apply, run \`leerness parent adopt --apply\` (explicit user decision).`)));
|
|
6167
|
+
log(dim(t(` 적용해도 자식 design-system.md 는 변경하지 않고, .harness/inherited-from-parent.md 에 '참조'로만 기록(비파괴).`,
|
|
6168
|
+
` even on apply, your design-system.md is NOT modified — parent assets are recorded as reference in .harness/inherited-from-parent.md.`)));
|
|
6169
|
+
}
|
|
6170
|
+
return;
|
|
6171
|
+
}
|
|
6172
|
+
// APPLY (사용자 명시): 자식-로컬 참조 파일 + 마커 기록 (비파괴 additive — 자식 원본 design-system.md/reuse-map.md 직접 변형 안 함)
|
|
6173
|
+
try {
|
|
6174
|
+
mkdirp(wsAbs);
|
|
6175
|
+
let body = `<!-- leerness:inherited-from-parent (1.30.3) — 사용자 명시 \`parent adopt --apply\`. 부모 자산을 '참조'로만 기록(자식 원본 무변경). -->\n`;
|
|
6176
|
+
body += `# ${t('부모 프로젝트 자산 (참조용)', 'Parent project assets (reference)')}\n\n`;
|
|
6177
|
+
body += `- ${t('부모', 'parent')}: ${p.parentRoot}\n- adopt: ${today()}\n- ${t('선택', 'select')}: ${kinds.join(', ')}\n\n`;
|
|
6178
|
+
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`; }
|
|
6179
|
+
writeUtf8(inheritedPath, body);
|
|
6180
|
+
writeUtf8(linkPath, JSON.stringify({ parentRoot: p.parentRoot, workspaceDir: p.workspaceDir, adoptedKinds: kinds, adoptedAt: today(), version: VERSION }, null, 2) + '\n');
|
|
6181
|
+
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)); }
|
|
6182
|
+
else {
|
|
6183
|
+
log(cyan(`# leerness parent adopt (1.30.3)`));
|
|
6184
|
+
log(grn(t(` ✓ adopt 완료 (${cand.length} 자산) — 자식 design-system.md 무변경(참조로만 기록).`, ` ✓ adopted (${cand.length} assets) — your design-system.md unchanged (recorded as reference only).`)));
|
|
6185
|
+
log(dim(` ${t('참조', 'reference')}: ${inheritedPath}`));
|
|
6186
|
+
log(dim(` ${t('마커', 'marker')}: ${linkPath}`));
|
|
6187
|
+
}
|
|
6188
|
+
} catch (e) {
|
|
6189
|
+
if (!has('--json')) log(red(t(` ✗ adopt 실패: ${e.message}`, ` ✗ adopt failed: ${e.message}`)));
|
|
6190
|
+
process.exitCode = 1;
|
|
6191
|
+
}
|
|
6192
|
+
return;
|
|
6193
|
+
}
|
|
6194
|
+
console.error(t(`Usage: leerness parent [detect|adopt] [--select <kinds>] [--apply] [--json]`, `Usage: leerness parent [detect|adopt] [--select <kinds>] [--apply] [--json]`));
|
|
6195
|
+
process.exit(1);
|
|
6196
|
+
}
|
|
6197
|
+
|
|
6004
6198
|
// 1.9.211: leerness migrate-workspace-dir — .harness → .leerness 마이그레이션 (사용자 명시)
|
|
6005
6199
|
function migrateWorkspaceDirCmd(root) {
|
|
6006
6200
|
root = absRoot(root);
|
|
@@ -7515,12 +7709,19 @@ function lessonSave(root, text) {
|
|
|
7515
7709
|
if (!text) return fail('lesson text required. 예: leerness lesson save "JWT는 refresh token도 짧게 (15분 권장)"');
|
|
7516
7710
|
const tag = arg('--tag', '');
|
|
7517
7711
|
// 1.9.406 (8번째 버그헌트, UR-0110): RMW 락 직렬화 — 동시 lesson save lost-update 방지(UR-0043 패턴).
|
|
7712
|
+
// 1.30.4 (14th리뷰 F5): task/rule add 와 일관된 dedup — 동일 text 존재 시 skip(--force 우회). 종전엔 무조건 append(중복 누적).
|
|
7713
|
+
let _skipped = false;
|
|
7518
7714
|
_withLock(lessonsJsonPath(root), () => {
|
|
7519
7715
|
const all = _loadLessons(root);
|
|
7716
|
+
if (!has('--force') && all.some(l => l && l.text === text)) { _skipped = true; return; }
|
|
7520
7717
|
all.push({ date: today(), text, tag: tag || null });
|
|
7521
7718
|
_saveLessons(root, all);
|
|
7522
7719
|
});
|
|
7523
7720
|
// 1.9.413 (6th외부평가 codex P2, UR-0101): --json 구조화 출력(데이터 이미 영속).
|
|
7721
|
+
if (_skipped) {
|
|
7722
|
+
if (has('--json')) { log(JSON.stringify({ ok: true, skipped: true, text, tag: tag || null })); return; }
|
|
7723
|
+
ok(`lesson exists (skip): ${text.slice(0, 40)} (--force 로 추가)`); return;
|
|
7724
|
+
}
|
|
7524
7725
|
if (has('--json')) { log(JSON.stringify({ ok: true, text, tag: tag || null })); return; }
|
|
7525
7726
|
ok(`lesson saved`);
|
|
7526
7727
|
_autoRoadmap(absRoot(root), 'data-change');
|
|
@@ -7603,8 +7804,11 @@ function decisionAdd(root, title) {
|
|
|
7603
7804
|
const impact = arg('--impact', '');
|
|
7604
7805
|
// 1.9.339 (UR-0053): canonical JSON write path — 기존 항목(JSON 우선, 없으면 MD backfill) 로드 후 추가, JSON+MD projection 동시 저장.
|
|
7605
7806
|
// 1.9.406 (8번째 버그헌트, UR-0110): RMW 락 직렬화 — 동시 decision add lost-update 방지(UR-0043 패턴).
|
|
7807
|
+
// 1.30.4 (14th리뷰 F5): task/rule add 와 일관된 dedup — 동일 title 존재 시 skip(--force 우회). 종전엔 무조건 append(중복 누적).
|
|
7808
|
+
let _skipped = false;
|
|
7606
7809
|
_withLock(decisionsJsonPath(root), () => {
|
|
7607
7810
|
const all = _loadDecisions(root);
|
|
7811
|
+
if (!has('--force') && all.some(d => d && (d.title === title || d.decision === title))) { _skipped = true; return; }
|
|
7608
7812
|
all.push({
|
|
7609
7813
|
date: today(), title,
|
|
7610
7814
|
decision: title,
|
|
@@ -7615,6 +7819,10 @@ function decisionAdd(root, title) {
|
|
|
7615
7819
|
_saveDecisions(root, all);
|
|
7616
7820
|
});
|
|
7617
7821
|
// 1.9.413 (6th외부평가 codex P2, UR-0101): --json 구조화 출력(데이터 이미 영속).
|
|
7822
|
+
if (_skipped) {
|
|
7823
|
+
if (has('--json')) { log(JSON.stringify({ ok: true, skipped: true, title })); return; }
|
|
7824
|
+
ok(`decision exists (skip): ${title} (--force 로 추가)`); return;
|
|
7825
|
+
}
|
|
7618
7826
|
if (has('--json')) { log(JSON.stringify({ ok: true, title })); return; }
|
|
7619
7827
|
ok(`decision added: ${title}`);
|
|
7620
7828
|
// 1.9.43+ handoff lessons 회수 흐름과 자동 통합 (decisions.md fuzzy 매칭됨)
|
|
@@ -7832,7 +8040,7 @@ function debug(root) {
|
|
|
7832
8040
|
|
|
7833
8041
|
const _audit = require('../lib/audit');
|
|
7834
8042
|
// 1.9.421 (UR-0025/UR-0125 큰 핸들러 모듈화 6번째): audit → lib/audit.js (DI 위임, thin wrapper)
|
|
7835
|
-
function audit(root, opts = {}) { return _audit.audit(root, opts, { VERSION, arg, has, planPath, readProgressRows, currentStatePath, handoffPath, envDiff, _readFeatureGraph, _matchAPISkills, _listAPISkills }); }
|
|
8043
|
+
function audit(root, opts = {}) { return _audit.audit(root, opts, { VERSION, arg, has, planPath, readProgressRows, currentStatePath, handoffPath, envDiff, _readFeatureGraph, _matchAPISkills, _listAPISkills, _collectSecretFindings }); }
|
|
7836
8044
|
|
|
7837
8045
|
// 1.9.312 (UR-0050, 설치리뷰 3중수렴): secret 스캐너 현대 키 패턴 보강.
|
|
7838
8046
|
// 배경: 기존 OpenAI 패턴 `sk-[A-Za-z0-9]{32,}` 은 하이픈에서 끊겨 sk-proj-/sk-svcacct- (modern 프로젝트/서비스 키)를 놓침.
|
|
@@ -8448,6 +8656,16 @@ function handoff(root) {
|
|
|
8448
8656
|
const parts = [];
|
|
8449
8657
|
// 1.20.3 (UR-0010 Phase 2): 헤드라인 항목 라벨 UI 언어 적용 (영어 opt-in, 한국어 기본). 블록 1회 해석.
|
|
8450
8658
|
const _L = _uiLang(root); const t = (ko, en) => (_L === 'en' ? en : ko);
|
|
8659
|
+
// 1.30.2 (#157): 상위 leerness 부모 프로젝트 탐지 → AI 가 세션 시작 즉시 인지(재사용 후보). read-only — 자동 적용 X(사용자 결정 게이트). 상세: leerness parent detect
|
|
8660
|
+
try {
|
|
8661
|
+
const _pw = _findParentWorkspace(root);
|
|
8662
|
+
if (_pw && _pw.assetCount > 0) {
|
|
8663
|
+
const _adopted = exists(path.join(_workspaceDirAbs(root), 'PARENT_LINK.json')); // 1.30.3: adopt 여부 반영
|
|
8664
|
+
parts.push(_adopted
|
|
8665
|
+
? t(`🔗 부모 프로젝트 (${_pw.assetCount} 자산·adopted)`, `🔗 parent project (${_pw.assetCount} assets, adopted)`)
|
|
8666
|
+
: t(`🔗 부모 프로젝트 (${_pw.assetCount} 자산·미적용)`, `🔗 parent project (${_pw.assetCount} assets, not applied)`));
|
|
8667
|
+
}
|
|
8668
|
+
} catch {}
|
|
8451
8669
|
// 1) drift level (가벼운 check)
|
|
8452
8670
|
try {
|
|
8453
8671
|
const r = cp.spawnSync(process.execPath, [__filename, 'drift', 'check', root, '--json'],
|
|
@@ -8806,23 +9024,26 @@ function handoff(root) {
|
|
|
8806
9024
|
const hasFailures = sf.failures && sf.failures.length > 0;
|
|
8807
9025
|
const hasDrift = drift && drift.changes && drift.changes.length > 0;
|
|
8808
9026
|
if (hasFailures || hasDrift) {
|
|
9027
|
+
// 1.29.3: headline t() 스코프 밖 — 로컬 t() (1.29.1 교훈: 없으면 ReferenceError 가 try 에 삼켜져 블록 증발)
|
|
9028
|
+
const _Lsh = _uiLang(root);
|
|
9029
|
+
const t = (ko, en) => (_Lsh === 'en' ? en : ko);
|
|
8809
9030
|
const isTty8 = process.stdout && process.stdout.isTTY;
|
|
8810
9031
|
const yl8 = s => isTty8 ? `\x1b[33m${s}\x1b[0m` : s;
|
|
8811
9032
|
const dm8 = s => isTty8 ? `\x1b[2m${s}\x1b[0m` : s;
|
|
8812
9033
|
const cy8 = s => isTty8 ? `\x1b[36m${s}\x1b[0m` : s;
|
|
8813
9034
|
log('');
|
|
8814
|
-
log(cy8(`## 🐚 터미널 셸 가드 (1.9.263, UR-0020)`));
|
|
9035
|
+
log(cy8(t(`## 🐚 터미널 셸 가드 (1.9.263, UR-0020)`, `## 🐚 Terminal shell guard (UR-0020)`)));
|
|
8815
9036
|
if (hasDrift) {
|
|
8816
|
-
log(yl8(` ⚠ 환경 버전 변동 — 과거 셸 실패 기록 재검토
|
|
9037
|
+
log(yl8(t(` ⚠ 환경 버전 변동 — 과거 셸 실패 기록 재검토 권장:`, ` ⚠ Environment version changed — review past shell failures:`)));
|
|
8817
9038
|
drift.changes.forEach(ch => log(dm8(` ${ch.what}: ${ch.from} → ${ch.to}`)));
|
|
8818
9039
|
}
|
|
8819
9040
|
if (hasFailures) {
|
|
8820
|
-
log(dm8(` 최근 셸 실패 ${sf.failures.length}건 (최대 3 표시):`));
|
|
9041
|
+
log(dm8(t(` 최근 셸 실패 ${sf.failures.length}건 (최대 3 표시):`, ` ${sf.failures.length} recent shell failure(s) (showing up to 3):`)));
|
|
8821
9042
|
sf.failures.slice(-3).reverse().forEach(f => {
|
|
8822
9043
|
const rules = (f.issues && f.issues.length) ? ` [${f.issues.join(',')}]` : '';
|
|
8823
9044
|
log(dm8(` • ${(f.cmd || '').slice(0, 50)} (exit=${f.exitCode}, ${f.shell})${rules}`));
|
|
8824
9045
|
});
|
|
8825
|
-
log(dm8(` → 명령 실행 전 점검: leerness shell-guard "<command>"`));
|
|
9046
|
+
log(dm8(t(` → 명령 실행 전 점검: leerness shell-guard "<command>"`, ` → check before running a command: leerness shell-guard "<command>"`)));
|
|
8826
9047
|
}
|
|
8827
9048
|
}
|
|
8828
9049
|
} catch {}
|
|
@@ -8835,20 +9056,23 @@ function handoff(root) {
|
|
|
8835
9056
|
return v && v !== '0' && String(v).toLowerCase() !== 'false';
|
|
8836
9057
|
});
|
|
8837
9058
|
if (enabledAgents.length > 0) {
|
|
9059
|
+
// 1.29.4: headline t() 스코프 밖 — 로컬 t() (1.29.1 교훈)
|
|
9060
|
+
const _Lag = _uiLang(root);
|
|
9061
|
+
const t = (ko, en) => (_Lag === 'en' ? en : ko);
|
|
8838
9062
|
const isTtyS = process.stdout && process.stdout.isTTY;
|
|
8839
9063
|
const cyS = s => isTtyS ? `\x1b[36m${s}\x1b[0m` : s;
|
|
8840
9064
|
const dmS = s => isTtyS ? `\x1b[2m${s}\x1b[0m` : s;
|
|
8841
9065
|
log('');
|
|
8842
|
-
log(cyS(`## 🤖 CLI 에이전트 슬래시 명령 (1.9.265~266, UR-0021)`));
|
|
8843
|
-
log(dmS(` 활성 에이전트 ${enabledAgents.length}개 — sub-agent 호출 시 각자 슬래시 명령
|
|
9066
|
+
log(cyS(t(`## 🤖 CLI 에이전트 슬래시 명령 (1.9.265~266, UR-0021)`, `## 🤖 CLI agent slash commands (UR-0021)`)));
|
|
9067
|
+
log(dmS(t(` 활성 에이전트 ${enabledAgents.length}개 — sub-agent 호출 시 각자 슬래시 명령 사용:`, ` ${enabledAgents.length} active agent(s) — use each one's slash commands when invoking a sub-agent:`)));
|
|
8844
9068
|
for (const a of enabledAgents) {
|
|
8845
9069
|
const hint = _agentSlashHint(root, a.id);
|
|
8846
9070
|
if (hint && hint.commands.length) {
|
|
8847
9071
|
const top = hint.commands.slice(0, 8).map(c => c.cmd).join(' ');
|
|
8848
|
-
log(dmS(` ${a.id.padEnd(8)} ${top}${hint.invoke === 'subcommand' ? ' (하위명령)' : ''}`));
|
|
9072
|
+
log(dmS(` ${a.id.padEnd(8)} ${top}${hint.invoke === 'subcommand' ? t(' (하위명령)', ' (subcommand)') : ''}`));
|
|
8849
9073
|
}
|
|
8850
9074
|
}
|
|
8851
|
-
log(dmS(` → 전체/기록/최신화: leerness slash-commands [agent] [--record]`));
|
|
9075
|
+
log(dmS(t(` → 전체/기록/최신화: leerness slash-commands [agent] [--record]`, ` → full list / record / refresh: leerness slash-commands [agent] [--record]`)));
|
|
8852
9076
|
}
|
|
8853
9077
|
} catch {}
|
|
8854
9078
|
|
|
@@ -9302,24 +9526,25 @@ function handoff(root) {
|
|
|
9302
9526
|
const red = s => isTty ? `\x1b[31m${s}\x1b[0m` : s;
|
|
9303
9527
|
const dim = s => isTty ? `\x1b[2m${s}\x1b[0m` : s;
|
|
9304
9528
|
const grn = s => isTty ? `\x1b[32m${s}\x1b[0m` : s;
|
|
9529
|
+
const _t = (ko, en) => (_uiLang(root) === 'en' ? en : ko); // 1.30.5 (#156 F3): 로컬 i18n (이 블록 9536 에 t 화살표 파라미터가 있어 _t 로 명명)
|
|
9305
9530
|
if (gapInfo.hasLast) {
|
|
9306
9531
|
// 마지막 handoff 시점 알고 있음 — 정확한 측정 (1.9.199)
|
|
9307
9532
|
if (gapInfo.isLong) {
|
|
9308
9533
|
// 60분+ → 강한 알림
|
|
9309
|
-
log(red(`## ⏰ ScheduleWakeup miss 강한 의심 (1.9.199) — 이전 handoff ${gapInfo.gapMin}분
|
|
9310
|
-
log(dim(` R-0001 영구 룰 (25분) 대비 ${Math.floor(gapInfo.gapMin/25)}× 초과 — 시스템 sleep / wakeup 누락
|
|
9311
|
-
log(dim(` → 회복: 사용자가 "다음 라운드" 입력 또는 leerness rule list 로 룰
|
|
9312
|
-
log(dim(` → handoff 이력: ${(gapInfo.history || []).slice(-3).map(t => t.slice(11, 19)).join(' → ')}`));
|
|
9534
|
+
log(red(_t(`## ⏰ ScheduleWakeup miss 강한 의심 (1.9.199) — 이전 handoff ${gapInfo.gapMin}분 전`, `## ⏰ ScheduleWakeup miss strongly suspected — last handoff ${gapInfo.gapMin} min ago`)));
|
|
9535
|
+
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`)));
|
|
9536
|
+
log(dim(_t(` → 회복: 사용자가 "다음 라운드" 입력 또는 leerness rule list 로 룰 확인`, ` → recover: user types "next round" or check rules via leerness rule list`)));
|
|
9537
|
+
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(' → ')}`)));
|
|
9313
9538
|
log('');
|
|
9314
9539
|
} else if (gapInfo.isMiss) {
|
|
9315
9540
|
// 35~60분 → 의심
|
|
9316
|
-
log(yel(`## ⏰ ScheduleWakeup 지연 (1.9.199) — 이전 handoff ${gapInfo.gapMin}분 전 (룰: 25분)`));
|
|
9317
|
-
log(dim(` ±10분 buffer 초과 — wakeup 한 cycle 누락
|
|
9541
|
+
log(yel(_t(`## ⏰ ScheduleWakeup 지연 (1.9.199) — 이전 handoff ${gapInfo.gapMin}분 전 (룰: 25분)`, `## ⏰ ScheduleWakeup delayed — last handoff ${gapInfo.gapMin} min ago (rule: 25 min)`)));
|
|
9542
|
+
log(dim(_t(` ±10분 buffer 초과 — wakeup 한 cycle 누락 가능성`, ` beyond the ±10 min buffer — a wakeup cycle may have been missed`)));
|
|
9318
9543
|
log('');
|
|
9319
9544
|
} else if (gapInfo.gapMin >= 0 && gapInfo.gapMin <= 30) {
|
|
9320
9545
|
// 정상 범위 (handoff_history.length >= 2 일 때만 의미 있음 — 첫 진입 제외)
|
|
9321
9546
|
if ((gapInfo.history || []).length >= 2) {
|
|
9322
|
-
log(dim(` ✓ ScheduleWakeup cycle 정상 (gap ${gapInfo.gapMin}분, 룰 25분 — 1.9.199)`));
|
|
9547
|
+
log(dim(_t(` ✓ ScheduleWakeup cycle 정상 (gap ${gapInfo.gapMin}분, 룰 25분 — 1.9.199)`, ` ✓ ScheduleWakeup cycle healthy (gap ${gapInfo.gapMin} min, rule 25 min)`)));
|
|
9323
9548
|
}
|
|
9324
9549
|
}
|
|
9325
9550
|
} else {
|
|
@@ -9329,9 +9554,9 @@ function handoff(root) {
|
|
|
9329
9554
|
const ageMs = Date.now() - fs.statSync(tlp).mtimeMs;
|
|
9330
9555
|
const ageMin = Math.floor(ageMs / 60000);
|
|
9331
9556
|
if (ageMin >= 60) {
|
|
9332
|
-
const label = ageMin < 120 ? `${ageMin}
|
|
9333
|
-
log(yel(`## ⏰ ScheduleWakeup miss 의심 (1.9.196 fallback) — task-log 마지막 ${label}
|
|
9334
|
-
log(dim(` 1.9.199 last-handoff.json 첫 기록 — 다음 handoff 부터 정확
|
|
9557
|
+
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`));
|
|
9558
|
+
log(yel(_t(`## ⏰ ScheduleWakeup miss 의심 (1.9.196 fallback) — task-log 마지막 ${label} 전`, `## ⏰ ScheduleWakeup miss suspected (fallback) — task-log last modified ${label} ago`)));
|
|
9559
|
+
log(dim(_t(` 1.9.199 last-handoff.json 첫 기록 — 다음 handoff 부터 정확 측정`, ` first last-handoff.json record — precise measurement from the next handoff`)));
|
|
9335
9560
|
log('');
|
|
9336
9561
|
}
|
|
9337
9562
|
}
|
|
@@ -9346,6 +9571,7 @@ function handoff(root) {
|
|
|
9346
9571
|
const isTtyMd = process.stdout && process.stdout.isTTY;
|
|
9347
9572
|
const mdCy = s => isTtyMd ? `\x1b[36m${s}\x1b[0m` : s;
|
|
9348
9573
|
const mdDim = s => isTtyMd ? `\x1b[2m${s}\x1b[0m` : s;
|
|
9574
|
+
const t = (ko, en) => (_uiLang(root) === 'en' ? en : ko); // 1.30.5 (#156 F3): 로컬 t()
|
|
9349
9575
|
const cutoff = Date.now() - 24 * 60 * 60 * 1000;
|
|
9350
9576
|
const deltas = [];
|
|
9351
9577
|
// tasks: progress-tracker.md row Updated 컬럼 기반 (간단 휴리스틱 — mtime이 24h내면 표시)
|
|
@@ -9386,7 +9612,7 @@ function handoff(root) {
|
|
|
9386
9612
|
const pp = planPath(root);
|
|
9387
9613
|
if (exists(pp) && fs.statSync(pp).mtimeMs > cutoff) {
|
|
9388
9614
|
// M-XXXX 중 line이 24h내 추가됐는지 정확히는 어려움 — mtime 24h내면 "plan: 변경됨"으로 표시
|
|
9389
|
-
deltas.push('plan: 변경됨');
|
|
9615
|
+
deltas.push(t('plan: 변경됨', 'plan: changed'));
|
|
9390
9616
|
}
|
|
9391
9617
|
} catch {}
|
|
9392
9618
|
// rules: rule add 후 mtime 24h내
|
|
@@ -9398,8 +9624,8 @@ function handoff(root) {
|
|
|
9398
9624
|
}
|
|
9399
9625
|
} catch {}
|
|
9400
9626
|
if (deltas.length) {
|
|
9401
|
-
log(mdCy(`🆕 최근 24h 메모리 변동 (1.9.121): ${deltas.join(' · ')}`));
|
|
9402
|
-
log(mdDim(` → 상세: leerness memory status --json`));
|
|
9627
|
+
log(mdCy(t(`🆕 최근 24h 메모리 변동 (1.9.121): ${deltas.join(' · ')}`, `🆕 memory changes in last 24h: ${deltas.join(' · ')}`)));
|
|
9628
|
+
log(mdDim(t(` → 상세: leerness memory status --json`, ` → details: leerness memory status --json`)));
|
|
9403
9629
|
log('');
|
|
9404
9630
|
}
|
|
9405
9631
|
} catch {}
|
|
@@ -9489,6 +9715,9 @@ function handoff(root) {
|
|
|
9489
9715
|
// 첫 실행에선 자동 캡처 (silent), 이후엔 변동/누락 시에만 노출.
|
|
9490
9716
|
if (!has('--no-env-detect') && !has('--compact') && !has('--quiet') && process.env.LEERNESS_NO_ENV_DETECT !== '1') {
|
|
9491
9717
|
try {
|
|
9718
|
+
// 1.29.2: headline t() 스코프 밖 — 로컬 t() (없으면 ReferenceError 가 try 에 삼켜져 env-detect 블록이 사라짐, 1.29.1 교훈)
|
|
9719
|
+
const _Lenv = _uiLang(root);
|
|
9720
|
+
const t = (ko, en) => (_Lenv === 'en' ? en : ko);
|
|
9492
9721
|
const isTtyEd = process.stdout && process.stdout.isTTY;
|
|
9493
9722
|
const edCy = s => isTtyEd ? `\x1b[35m${s}\x1b[0m` : s; // magenta
|
|
9494
9723
|
const edDim = s => isTtyEd ? `\x1b[2m${s}\x1b[0m` : s;
|
|
@@ -9502,14 +9731,14 @@ function handoff(root) {
|
|
|
9502
9731
|
} else if (diff.changes.length || (diff.missing && diff.missing.length)) {
|
|
9503
9732
|
// 변동/누락 알림
|
|
9504
9733
|
if (diff.missing && diff.missing.length) {
|
|
9505
|
-
log(edCy(`🖥 실행 환경 (1.9.145): ⚠ PATH 누락 ${diff.missing.length}건 — npm run 시 실패
|
|
9734
|
+
log(edCy(t(`🖥 실행 환경 (1.9.145): ⚠ PATH 누락 ${diff.missing.length}건 — npm run 시 실패 가능`, `🖥 Runtime environment: ⚠ ${diff.missing.length} PATH tool(s) missing — npm run may fail`)));
|
|
9506
9735
|
for (const m of diff.missing.slice(0, 3)) log(edDim(` • ${m.command} (used by: npm run ${m.usedBy})`));
|
|
9507
9736
|
}
|
|
9508
9737
|
if (diff.changes.length) {
|
|
9509
|
-
log(edCy(`🖥 실행 환경 (1.9.145): 변동 ${diff.changes.length}건
|
|
9738
|
+
log(edCy(t(`🖥 실행 환경 (1.9.145): 변동 ${diff.changes.length}건 감지`, `🖥 Runtime environment: ${diff.changes.length} change(s) detected`)));
|
|
9510
9739
|
for (const c of diff.changes.slice(0, 3)) log(edDim(` • ${c}`));
|
|
9511
9740
|
}
|
|
9512
|
-
log(edDim(` → 상세: leerness env detect . --json`));
|
|
9741
|
+
log(edDim(t(` → 상세: leerness env detect . --json`, ` → details: leerness env detect . --json`)));
|
|
9513
9742
|
log('');
|
|
9514
9743
|
// 갱신 (다음 비교 baseline)
|
|
9515
9744
|
try { writeUtf8(snapPath, JSON.stringify(curr, null, 2) + '\n'); } catch {}
|
|
@@ -9520,13 +9749,21 @@ function handoff(root) {
|
|
|
9520
9749
|
// 매 세션 시작 시 AI가 보안 위험을 즉시 인지. --no-security-summary 또는 --compact로 끄기
|
|
9521
9750
|
if (!has('--no-security-summary') && !has('--compact') && !has('--quiet')) {
|
|
9522
9751
|
try {
|
|
9752
|
+
// 1.29.1: 이 블록은 headline의 t() 스코프 밖 — 로컬 t() 정의 (없으면 ReferenceError가 try에 삼켜져 보안 요약 전체가 사라짐)
|
|
9753
|
+
const _Lsec = _uiLang(root);
|
|
9754
|
+
const t = (ko, en) => (_Lsec === 'en' ? en : ko);
|
|
9523
9755
|
const envExists = exists(path.join(root, '.env'));
|
|
9756
|
+
const issues = [];
|
|
9757
|
+
// 0) 1.30.1 (14th 외부리뷰 F2): 커밋된 plaintext 시크릿을 보안 요약에 노출 — headline '🚨 시크릿 N건' 과 일관.
|
|
9758
|
+
// envExists 무관(소스에 커밋된 시크릿은 .env 없어도 위험). gitignored 는 _collectSecretFindings 가 committed 에서 제외.
|
|
9759
|
+
let committedSecrets = [];
|
|
9760
|
+
try { committedSecrets = _collectSecretFindings(root).committed || []; } catch {}
|
|
9761
|
+
if (committedSecrets.length) issues.push(t(`커밋된 시크릿 ${committedSecrets.length}건 (소스 노출)`, `${committedSecrets.length} committed secret(s) (exposed in source)`));
|
|
9524
9762
|
if (envExists) {
|
|
9525
|
-
const issues = [];
|
|
9526
9763
|
// 1) env diff
|
|
9527
9764
|
try {
|
|
9528
9765
|
const d = envDiff(root);
|
|
9529
|
-
if (d.inEnvOnly.length) issues.push(`.env→.env.example 누락 ${d.inEnvOnly.length}
|
|
9766
|
+
if (d.inEnvOnly.length) issues.push(t(`.env→.env.example 누락 ${d.inEnvOnly.length}건`, `.env→.env.example missing ${d.inEnvOnly.length}`));
|
|
9530
9767
|
} catch {}
|
|
9531
9768
|
// 2) gitignore 시크릿 패턴
|
|
9532
9769
|
try {
|
|
@@ -9535,43 +9772,45 @@ function handoff(root) {
|
|
|
9535
9772
|
const giLines = giText.split('\n').map(l => l.trim());
|
|
9536
9773
|
const SECRET_PATTERNS = ['.env', '.env.local', '.env.production', '.env.*.local', '*.pem', 'credentials.json'];
|
|
9537
9774
|
const missing = SECRET_PATTERNS.filter(p => !giLines.some(l => l === p || l === '/' + p));
|
|
9538
|
-
if (missing.length) issues.push(`.gitignore 시크릿 누락 ${missing.length}
|
|
9775
|
+
if (missing.length) issues.push(t(`.gitignore 시크릿 누락 ${missing.length}건`, `.gitignore missing secret patterns ${missing.length}`));
|
|
9539
9776
|
} catch {}
|
|
9540
|
-
|
|
9541
|
-
|
|
9542
|
-
|
|
9543
|
-
|
|
9544
|
-
|
|
9545
|
-
|
|
9546
|
-
|
|
9547
|
-
|
|
9548
|
-
|
|
9549
|
-
|
|
9550
|
-
|
|
9551
|
-
|
|
9552
|
-
|
|
9553
|
-
|
|
9554
|
-
|
|
9555
|
-
|
|
9556
|
-
|
|
9557
|
-
|
|
9558
|
-
|
|
9559
|
-
|
|
9560
|
-
|
|
9561
|
-
|
|
9562
|
-
|
|
9563
|
-
|
|
9564
|
-
|
|
9565
|
-
|
|
9566
|
-
}
|
|
9567
|
-
log(dim(` ⚠ 자동 회복
|
|
9777
|
+
}
|
|
9778
|
+
if (issues.length) {
|
|
9779
|
+
const isTty = process.stdout && process.stdout.isTTY;
|
|
9780
|
+
const red = s => isTty ? `\x1b[31m${s}\x1b[0m` : s;
|
|
9781
|
+
const dim = s => isTty ? `\x1b[2m${s}\x1b[0m` : s;
|
|
9782
|
+
const yel = s => isTty ? `\x1b[33m${s}\x1b[0m` : s;
|
|
9783
|
+
log('');
|
|
9784
|
+
log(red(t(`## 🔒 보안 요약 (1.9.76) — ${issues.length}건 주의`, `## 🔒 Security summary — ${issues.length} to review`)));
|
|
9785
|
+
for (const i of issues) log(dim(` ⚠ ${i}`));
|
|
9786
|
+
// 1.30.1 (F2): 커밋된 시크릿 파일 위치 노출 (값 snippet 은 미출력 — handoff 로그로의 시크릿 유출 방지)
|
|
9787
|
+
for (const f of committedSecrets.slice(0, 4)) log(dim(` • ${f.file}:${f.line} (${f.name})`));
|
|
9788
|
+
log(dim(t(` → 자동 수정: leerness audit --fix · 상세: leerness scan secrets / leerness env check`, ` → auto-fix: leerness audit --fix · details: leerness scan secrets / leerness env check`)));
|
|
9789
|
+
// 1.9.80: critical 수준 (.gitignore에 .env 자체 누락) 시 자동 회복 옵션
|
|
9790
|
+
const giText2 = exists(path.join(root, '.gitignore')) ? read(path.join(root, '.gitignore')) : '';
|
|
9791
|
+
const giLines2 = giText2.split('\n').map(l => l.trim());
|
|
9792
|
+
const envInGitignore = giLines2.includes('.env') || giLines2.includes('/.env');
|
|
9793
|
+
if (envExists && !envInGitignore) {
|
|
9794
|
+
// .env 자체 누락 → 최우선 위험
|
|
9795
|
+
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.`)));
|
|
9796
|
+
// LEERNESS_AUTO_SECURITY_FIX=1 자동 실행 옵션
|
|
9797
|
+
if (process.env.LEERNESS_AUTO_SECURITY_FIX === '1') {
|
|
9798
|
+
try {
|
|
9799
|
+
const r = cp.spawnSync(process.execPath, [__filename, 'audit', root, '--fix'],
|
|
9800
|
+
{ encoding: 'utf8', timeout: 15000, env: { ...process.env, LEERNESS_INTERNAL: '1', LEERNESS_NO_PROMPT: '1', LEERNESS_NO_DRIFT_CHECK: '1' } });
|
|
9801
|
+
if (r.status === 0) {
|
|
9802
|
+
log(dim(t(` ✓ 자동 회복 (LEERNESS_AUTO_SECURITY_FIX=1): audit --fix 완료`, ` ✓ auto-recovered (LEERNESS_AUTO_SECURITY_FIX=1): audit --fix done`)));
|
|
9803
|
+
} else {
|
|
9804
|
+
log(dim(t(` ⚠ 자동 회복 실패 (exit ${r.status}) — 수동 \`leerness audit --fix\` 권장`, ` ⚠ auto-recovery failed (exit ${r.status}) — run \`leerness audit --fix\` manually`)));
|
|
9568
9805
|
}
|
|
9569
|
-
}
|
|
9570
|
-
log(dim(`
|
|
9806
|
+
} catch (e) {
|
|
9807
|
+
log(dim(t(` ⚠ 자동 회복 예외: ${e.message}`, ` ⚠ auto-recovery error: ${e.message}`)));
|
|
9571
9808
|
}
|
|
9809
|
+
} else {
|
|
9810
|
+
log(dim(t(` 💡 자동 실행 옵션: LEERNESS_AUTO_SECURITY_FIX=1 leerness handoff .`, ` 💡 auto-fix option: LEERNESS_AUTO_SECURITY_FIX=1 leerness handoff .`)));
|
|
9572
9811
|
}
|
|
9573
|
-
log('');
|
|
9574
9812
|
}
|
|
9813
|
+
log('');
|
|
9575
9814
|
}
|
|
9576
9815
|
} catch {}
|
|
9577
9816
|
}
|
|
@@ -9604,16 +9843,17 @@ function handoff(root) {
|
|
|
9604
9843
|
const cy = s => isTty ? `\x1b[36m${s}\x1b[0m` : s;
|
|
9605
9844
|
const b = s => isTty ? `\x1b[1m${s}\x1b[0m` : s;
|
|
9606
9845
|
const d = s => isTty ? `\x1b[2m${s}\x1b[0m` : s;
|
|
9846
|
+
const t = (ko, en) => (_uiLang(root) === 'en' ? en : ko); // 1.30.5 (#156 F3): headline t() 스코프 밖 — 로컬 t() (1.29.1 교훈)
|
|
9607
9847
|
log('');
|
|
9608
|
-
log(cy('## 🛠 세션 워크플로 6단계 (1.9.39+, AI 하네스 엔지니어링)'));
|
|
9609
|
-
log(d(' 상세: ') + cy('.harness/session-workflow.md'));
|
|
9610
|
-
log(` 1. ${b('요청 분석')} handoff(이미 완료) · drift check · 모호하면
|
|
9611
|
-
log(` 2. ${b('계획 수립')} plan add / TodoWrite · reuse-map으로 기존 자원
|
|
9612
|
-
log(` 3. ${b('업무 분배')} agents list/recommend · 작업유형별 sub-agent
|
|
9613
|
-
log(` 4. ${b('sub-agent 작업')} 파일 경로 격리 · mtime 검증 의무 · 자체
|
|
9614
|
-
log(` 5. ${b('종합 검증')} contract verify · verify-claim --run-tests · review --persona`);
|
|
9615
|
-
log(` 6. ${b('세션 마감')} session close · audit --fix · usage stats`);
|
|
9616
|
-
log(d(' 끄려면: --no-workflow-guide 또는 LEERNESS_NO_WORKFLOW_GUIDE=1'));
|
|
9848
|
+
log(cy(t('## 🛠 세션 워크플로 6단계 (1.9.39+, AI 하네스 엔지니어링)', '## 🛠 Session workflow — 6 steps (AI harness engineering)')));
|
|
9849
|
+
log(d(t(' 상세: ', ' details: ')) + cy('.harness/session-workflow.md'));
|
|
9850
|
+
log(` 1. ${b(t('요청 분석', 'Analyze request'))} ${t('handoff(이미 완료) · drift check · 모호하면 명확화', 'handoff (done) · drift check · clarify if ambiguous')}`);
|
|
9851
|
+
log(` 2. ${b(t('계획 수립', 'Plan'))} ${t('plan add / TodoWrite · reuse-map으로 기존 자원 우선', 'plan add / TodoWrite · prefer existing via reuse-map')}`);
|
|
9852
|
+
log(` 3. ${b(t('업무 분배', 'Distribute'))} ${t('agents list/recommend · 작업유형별 sub-agent 매핑', 'agents list/recommend · map sub-agents by task type')}`);
|
|
9853
|
+
log(` 4. ${b(t('sub-agent 작업', 'sub-agent work'))} ${t('파일 경로 격리 · mtime 검증 의무 · 자체 테스트', 'isolate file paths · verify mtime · self-test')}`);
|
|
9854
|
+
log(` 5. ${b(t('종합 검증', 'Verify'))} ${t('contract verify · verify-claim --run-tests · review --persona', 'contract verify · verify-claim --run-tests · review --persona')}`);
|
|
9855
|
+
log(` 6. ${b(t('세션 마감', 'Close'))} ${t('session close · audit --fix · usage stats', 'session close · audit --fix · usage stats')}`);
|
|
9856
|
+
log(d(t(' 끄려면: --no-workflow-guide 또는 LEERNESS_NO_WORKFLOW_GUIDE=1', ' to disable: --no-workflow-guide or LEERNESS_NO_WORKFLOW_GUIDE=1')));
|
|
9617
9857
|
log('');
|
|
9618
9858
|
}
|
|
9619
9859
|
// 1.9.373 (UR-0073 Phase C): 에이전트 팀 스케줄 알림 — 비-manual 팀이 정의돼 있으면 미리보기(dry-run) 안내. 실행 없음. opt-out.
|
|
@@ -9621,10 +9861,11 @@ function handoff(root) {
|
|
|
9621
9861
|
try {
|
|
9622
9862
|
const _teamRem = _teamHandoffReminders(_loadTeams(root));
|
|
9623
9863
|
if (_teamRem.length) {
|
|
9864
|
+
const t = (ko, en) => (_uiLang(root) === 'en' ? en : ko); // 1.30.5 (#156 F3): 로컬 t()
|
|
9624
9865
|
log('');
|
|
9625
|
-
log('## 🤝 에이전트 팀 스케줄 (UR-0073 Phase C) — 정의 전용 · 자동 실행 X');
|
|
9866
|
+
log(t('## 🤝 에이전트 팀 스케줄 (UR-0073 Phase C) — 정의 전용 · 자동 실행 X', '## 🤝 Agent team schedule (UR-0073) — definitions only · no auto-run'));
|
|
9626
9867
|
_teamRem.forEach(r => log(' ' + r));
|
|
9627
|
-
log(' ⓘ 미리보기는 dry-run. 실행은 제안 명령 검토 후 직접 · 끄려면 --no-team-reminders 또는 LEERNESS_NO_TEAM_REMINDERS=1');
|
|
9868
|
+
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'));
|
|
9628
9869
|
}
|
|
9629
9870
|
} catch {}
|
|
9630
9871
|
}
|
|
@@ -10128,7 +10369,7 @@ function _vcImplIsEmpty(body) {
|
|
|
10128
10369
|
function verifyClaimCmd(root, taskId) {
|
|
10129
10370
|
root = absRoot(root);
|
|
10130
10371
|
const _j = has('--json'); // 1.9.400 (7번째 버그헌트 P1-B, UR-0105): --json 에러도 구조화
|
|
10131
|
-
if (!taskId) return failJson(_j, 'missing_args', 'verify-claim <T-ID> 필요. 예: leerness verify-claim T-0008');
|
|
10372
|
+
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)
|
|
10132
10373
|
const rows = readProgressRows(root);
|
|
10133
10374
|
const row = rows.find(r => r.id === taskId);
|
|
10134
10375
|
if (!row) return failJson(_j, 'not_found', `progress-tracker.md에 ${taskId} 없음.`);
|
|
@@ -11012,7 +11253,7 @@ function honestyCheckCmd(root, arg1) {
|
|
|
11012
11253
|
function optimismCheckCmd(root, taskId) {
|
|
11013
11254
|
root = absRoot(root || process.cwd());
|
|
11014
11255
|
const _j = has('--json'); // 1.9.400 (7번째 버그헌트 P1-B, UR-0105): --json 에러도 구조화
|
|
11015
|
-
if (!taskId) return failJson(_j, 'missing_args', 'optimism-check <T-ID> 필요. 예: leerness optimism-check T-0001');
|
|
11256
|
+
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)
|
|
11016
11257
|
const rows = readProgressRows(root);
|
|
11017
11258
|
const row = rows.find(r => r.id === taskId);
|
|
11018
11259
|
if (!row) return failJson(_j, 'not_found', `progress-tracker.md에 ${taskId} 없음.`);
|
|
@@ -20152,13 +20393,19 @@ async function main() {
|
|
|
20152
20393
|
if (cmd === 'wakeup-interval') return wakeupIntervalCmd(arg('--path', process.cwd()), args[1], args[2]);
|
|
20153
20394
|
// 1.9.211: leerness workspace-dir <get|guide> — 현재 워크스페이스 디렉토리 / AI 참조 가이드 (사용자 명시)
|
|
20154
20395
|
if (cmd === 'workspace-dir') return workspaceDirCmd(arg('--path', process.cwd()), args[1]);
|
|
20396
|
+
if (cmd === 'parent') return parentCmd(arg('--path', process.cwd()), args[1]);
|
|
20155
20397
|
// 1.9.211: leerness migrate-workspace-dir — .harness → .leerness 마이그레이션 (사용자 명시)
|
|
20156
20398
|
if (cmd === 'migrate-workspace-dir') return migrateWorkspaceDirCmd(arg('--path', process.cwd()));
|
|
20157
20399
|
// 1.9.212: leerness idempotency audit — 멱등성 위반 탐지 (사용자 명시)
|
|
20158
20400
|
if (cmd === 'idempotency') return idempotencyCmd(arg('--path', process.cwd()), args[1]);
|
|
20159
20401
|
// 1.9.213: leerness intent <classify|expand|domains> — intent inference + scope expansion (사용자 명시)
|
|
20160
20402
|
if (cmd === 'intent') return intentCmd(arg('--path', process.cwd()), args[1], ...args.slice(2));
|
|
20161
|
-
if (cmd === 'rule' && args[1] === 'add')
|
|
20403
|
+
if (cmd === 'rule' && args[1] === 'add') { // 1.9.426: flag/경로 break(_parseAddTitle) · 1.9.445 (UR-0151): positional path 지원(제목과 분리)
|
|
20404
|
+
const _desc = _parseAddTitle(args, 2);
|
|
20405
|
+
// 1.30.4 (14th리뷰 F6): 빈 입력 시 --json 에서도 구조화 JSON(task/decision add 와 일관). 종전엔 ruleAdd 내부 fail() 가 평문 출력.
|
|
20406
|
+
if (!_desc) { failJson(has('--json'), 'empty_title', 'rule add "<설명>" 필요 (빈/경로-only 거부)'); return process.exit(process.exitCode || 1); }
|
|
20407
|
+
return ruleAdd(arg('--path', null) || _taskPositionalPath(args, 2) || process.cwd(), _desc);
|
|
20408
|
+
}
|
|
20162
20409
|
if (cmd === 'rule' && args[1] === 'list') return ruleList(arg('--path', process.cwd()));
|
|
20163
20410
|
if (cmd === 'rule' && args[1] === 'remove') return ruleRemove(arg('--path', process.cwd()), args[2]);
|
|
20164
20411
|
if (cmd === 'rule' && args[1] === 'pause') return rulePause(arg('--path', process.cwd()), args[2]);
|
|
@@ -20166,6 +20413,8 @@ async function main() {
|
|
|
20166
20413
|
if (cmd === 'rule' && args[1] === 'stop') return ruleStop(arg('--path', process.cwd()));
|
|
20167
20414
|
if (cmd === 'rule' && args[1] === 'resume-all') return ruleResumeAll(arg('--path', process.cwd()));
|
|
20168
20415
|
if (cmd === 'rule' && args[1] === 'verify') return ruleVerifyCmd(arg('--path', process.cwd()));
|
|
20416
|
+
// 1.30.4 (14th리뷰 F7): rule 하위명령 미매칭 시 잘못된 토큰 명시 + usage(종전엔 top-level 'unknown_command: rule' 로 유효 부모명을 오인 표기).
|
|
20417
|
+
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); }
|
|
20169
20418
|
if (cmd === 'release' && args[1] === 'bump') return releaseBump(args[2] || arg('--path', process.cwd()));
|
|
20170
20419
|
if (cmd === 'release' && args[1] === 'note') return releaseNote(arg('--path', process.cwd()), args.slice(2).filter(x => !x.startsWith('-')).join(' '));
|
|
20171
20420
|
if (cmd === 'release' && args[1] === 'publish') return releasePublish(args[2] || arg('--path', process.cwd()));
|
|
@@ -20220,6 +20469,8 @@ async function main() {
|
|
|
20220
20469
|
if (sub==='relink') return taskRelink(root);
|
|
20221
20470
|
if (sub==='sync') return taskSyncCmd(root);
|
|
20222
20471
|
if (sub==='export') return taskExportCmd(root);
|
|
20472
|
+
// 1.30.4 (14th리뷰 F7): 미매칭 하위명령 시 잘못된 토큰을 명시 + usage(종전엔 top-level 'unknown_command: task' 로 유효 부모명을 오인 표기).
|
|
20473
|
+
failJson(has('--json'), 'unknown_subcommand', `알 수 없는 task 하위명령: ${sub} — leerness task list|add|update|drop|fix-evidence|relink|sync|export`); return process.exit(process.exitCode || 1);
|
|
20223
20474
|
}
|
|
20224
20475
|
// 1.9.114: memory status — Memory Write Surface 5종 통합 상태
|
|
20225
20476
|
if (cmd === 'memory' && args[1] === 'status') {
|
|
@@ -20247,7 +20498,10 @@ async function main() {
|
|
|
20247
20498
|
if (args[i].startsWith('--') || /^([A-Za-z]:[\\/]|\/|\.\.?[\\/])/.test(args[i])) break;
|
|
20248
20499
|
textParts.push(args[i]);
|
|
20249
20500
|
}
|
|
20250
|
-
|
|
20501
|
+
const _text = textParts.join(' ');
|
|
20502
|
+
// 1.30.4 (14th리뷰 F6): 빈 입력 시 --json 에서도 구조화 JSON(task/decision add 와 일관). 종전엔 lessonSave 내부 fail() 가 평문 출력.
|
|
20503
|
+
if (!_text) { failJson(has('--json'), 'empty_text', 'lesson save "<text>" 필요 (빈/경로-only 거부)'); return process.exit(process.exitCode || 1); }
|
|
20504
|
+
return lessonSave(root, _text);
|
|
20251
20505
|
}
|
|
20252
20506
|
if (sub === 'list') {
|
|
20253
20507
|
return lessonListCmd(root, { json: has('--json') });
|