leerness 1.36.85 → 1.36.88
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 +70 -0
- package/README.md +4 -4
- package/bin/leerness.js +349 -183
- package/lib/bugfix.js +335 -0
- package/lib/mcp-tools.js +4 -4
- package/lib/toggles.js +31 -6
- package/package.json +1 -1
- package/scripts/e2e.js +119 -12
package/bin/leerness.js
CHANGED
|
@@ -34,7 +34,7 @@ const { CAPABILITY_SURFACE, POWERFUL_COMMANDS, ADAPTERS, REUSE_CATEGORIES, REUSE
|
|
|
34
34
|
const { tokenizeForRank: _tokenizeForRank, expandQuery: _expandQuery, scoreHits: _scoreHits, suggestTerms: _suggestTerms } = require('../lib/search-core'); // 1.36.23: memory search 랭킹 코어(순수·0-deps)
|
|
35
35
|
const { findCorruptedStateJson: _findCorruptedStateJson } = require('../lib/state-integrity'); // 1.36.1 (클린룸 리뷰 FN): .harness/*.json 상태 무결성 (audit/health/check 공유)
|
|
36
36
|
|
|
37
|
-
const VERSION = '1.36.
|
|
37
|
+
const VERSION = '1.36.88';
|
|
38
38
|
|
|
39
39
|
// 1.9.290 (UR-0037, Codex gpt-5.5 #4 수렴): CLI 전용 부작용은 require 시 실행하지 않는다.
|
|
40
40
|
// 이전: warning listener 제거 / NODE_OPTIONS 변경 / chcp IIFE 가 top-level 즉시 실행 → require('harness') 시 호스트 프로세스 오염.
|
|
@@ -238,7 +238,7 @@ function _resolveRoot(positional) {
|
|
|
238
238
|
}
|
|
239
239
|
function nonFlagArgs() {
|
|
240
240
|
const out = [];
|
|
241
|
-
const withValue = new Set(['--language','--skills','--path','--status','--progress','--goal','--reason','--next','--target','--token-env','--package','--out','--from','--to','--repo','--id','--note','--evidence','--query','--limit','--action','--agent','--tool','--doc','--command','--capability','--before','--after','--display','--threshold','--trigger','--check','--set','--min-score','--include','--days','--gh-pages-src','--roadmap','--since','--agents','--model','--timeout','--retry-on-fail','--label','--score','--tokens','--alternatives','--impact','--tag','--surface','--depends-on','--affects','--co-changes-with','--files','--branch','--remote','--task-add','--next-action','--role','--provider','--env-var','--deploy','--token-lifetime-hours','--port','--secret','--keep','--shell','--ps-version','--done-when','--test-cmd','--require-referee','--referee','--expect-bad','--good','--bad','--static-dir','--dev-url']); // 1.14.2 (UR-0032): --done-when 값이 positional 로 누출돼 milestone 제목에 흡수되던 것 차단. 1.17.2 (UR-0045): --test-cmd 동일 원칙(신규 value-flag 는 반드시 여기 등록). 1.36.82 (검수 High#3): --require-referee 미등록으로 `gate --require-referee <id>` 의 값이 positional 경로로 흡수돼 root 가 <cwd>/<id> 가 됐다 — 툴이 안내하는 문법 그대로 쓰면 깨졌다. 1.36.80/81 신규 value-flag 도 소급 등록.
|
|
241
|
+
const withValue = new Set(['--language','--skills','--path','--status','--progress','--goal','--reason','--next','--target','--token-env','--package','--out','--from','--to','--repo','--id','--note','--evidence','--query','--limit','--action','--agent','--tool','--doc','--command','--capability','--before','--after','--display','--threshold','--trigger','--check','--set','--min-score','--include','--days','--gh-pages-src','--roadmap','--since','--agents','--model','--timeout','--retry-on-fail','--label','--score','--tokens','--alternatives','--impact','--tag','--surface','--depends-on','--affects','--co-changes-with','--files','--branch','--remote','--task-add','--next-action','--role','--provider','--env-var','--deploy','--token-lifetime-hours','--port','--secret','--keep','--shell','--ps-version','--done-when','--test-cmd','--require-referee','--referee','--expect-bad','--good','--bad','--static-dir','--dev-url','--repro','--root-cause','--siblings','--siblings-na','--previous-gap']); // 1.14.2 (UR-0032): --done-when 값이 positional 로 누출돼 milestone 제목에 흡수되던 것 차단. 1.17.2 (UR-0045): --test-cmd 동일 원칙(신규 value-flag 는 반드시 여기 등록). 1.36.82 (검수 High#3): --require-referee 미등록으로 `gate --require-referee <id>` 의 값이 positional 경로로 흡수돼 root 가 <cwd>/<id> 가 됐다 — 툴이 안내하는 문법 그대로 쓰면 깨졌다. 1.36.80/81 신규 value-flag 도 소급 등록.
|
|
242
242
|
const a = process.argv.slice(2);
|
|
243
243
|
for (let i = 0; i < a.length; i++) {
|
|
244
244
|
const x = a[i];
|
|
@@ -619,6 +619,10 @@ leerness drift check . # 4 신호 + 4단계 레벨
|
|
|
619
619
|
\`\`\`
|
|
620
620
|
- 사용자 요청을 5W1H로 분해. 모호하면 명확화 질문 (autonomous 모드 제외).
|
|
621
621
|
- drift critical 시 \`leerness session close .\` 또는 \`drift check --auto-fix\` 우선 실행.
|
|
622
|
+
- **버그 수정 요청이면 코드를 고치기 전에 재현부터**: \`leerness bugfix start <T-ID> --repro "<재현 명령>" --expect-bad "<실패 신호>"\`
|
|
623
|
+
→ 지금 실패하는 probe 를 먼저 등록해야, 나중에 "고쳤다"가 **증상만 덮은 것인지** 구분된다. 수정 후엔
|
|
624
|
+
\`leerness bugfix receipt <T-ID> --root-cause "..." --siblings "같은 원인이 걸린 다른 지점들"\` 로 근본원인과 형제 범위를 남긴다.
|
|
625
|
+
(완료 게이트는 옵트인: \`leerness toggle set bugfix-receipt on\`)
|
|
622
626
|
|
|
623
627
|
## Step 2. 계획 수립
|
|
624
628
|
- 작업이 3 step 이상 → TodoWrite 또는 \`leerness plan add\` 사용.
|
|
@@ -631,10 +635,10 @@ leerness agents list # ready CLI 확인
|
|
|
631
635
|
leerness agents quota # 한도 확인
|
|
632
636
|
leerness agents dispatch "<task>" --to <id> # 작업 유형 추천 자동
|
|
633
637
|
\`\`\`
|
|
634
|
-
- 작업 유형별
|
|
635
|
-
- 텍스트/번역/분석 → claude
|
|
636
|
-
- 깊은 코드 추론 → codex
|
|
637
|
-
- 파일 직접 수정 → agy --yolo (
|
|
638
|
+
- 작업 유형별 기본 sub-agent 매핑 (leerness 의 **기본 관례**이며 측정된 성능·품질 순위가 아닙니다):
|
|
639
|
+
- 텍스트/번역/분석 → claude
|
|
640
|
+
- 깊은 코드 추론 → codex
|
|
641
|
+
- 파일 직접 수정 → agy --yolo (Antigravity CLI)
|
|
638
642
|
- 보안 리뷰 → \`leerness review --persona security\`
|
|
639
643
|
- **충돌 방지 규칙 (필수)**:
|
|
640
644
|
- 각 sub-agent에 *자신만 수정할 파일 경로* 명시
|
|
@@ -3413,10 +3417,23 @@ function _selfTestCases() {
|
|
|
3413
3417
|
fs.mkdirSync(path.join(tmp, '.harness'), { recursive: true });
|
|
3414
3418
|
// 토글 저장/로드 행위 + 기본 ON
|
|
3415
3419
|
const d0 = tg.loadToggles(tmp);
|
|
3416
|
-
|
|
3420
|
+
// 1.36.87: 원래 의도는 "**기존** 토글이 조용히 꺼지지 않는다" 이지 "모든 토글이 영원히 기본 ON" 이 아니다.
|
|
3421
|
+
// 기존 동작을 바꾸는 신규 토글은 반드시 defaultOff(옵트인)여야 업그레이드만으로 사용자 워크플로가 깨지지 않는다.
|
|
3422
|
+
// → 레지스트리 표시대로인지 양방향 단언(기본 ON 이어야 할 것 · 기본 OFF 여야 할 것 둘 다).
|
|
3423
|
+
const defOn = Object.entries(d0).every(([k, v]) => v === !(tg.TOGGLE_REGISTRY[k] || {}).defaultOff)
|
|
3424
|
+
&& ['gate', 'lens', 'auto-graph', 'delegation-brief'].every(k => d0[k] === true)
|
|
3425
|
+
&& Object.keys(tg.TOGGLE_REGISTRY).some(k => tg.TOGGLE_REGISTRY[k].defaultOff && d0[k] === false);
|
|
3417
3426
|
tg.saveToggles(tmp, Object.assign({}, d0, { gate: false }));
|
|
3418
3427
|
const offOk = tg.loadToggles(tmp).gate === false && tg.toggleOn(tmp, 'lens') === true;
|
|
3419
|
-
|
|
3428
|
+
// 1.36.87: 값 해석 — 인식 불가 값이 **차단 게이트를 켜면 안 된다**(종전 `v !== false` 는 null/0/"off" 를 ON 으로 읽었다, 실측).
|
|
3429
|
+
// 반대로 손으로 쓴 "off" 문자열이 보호 게이트를 켠 채로 두던 것도 함께 고친다.
|
|
3430
|
+
const tf = path.join(tmp, '.harness', 'toggles.json');
|
|
3431
|
+
const withVal = (v) => { fs.writeFileSync(tf, JSON.stringify({ 'bugfix-receipt': v, gate: v })); return tg.loadToggles(tmp); };
|
|
3432
|
+
const coerceOk = [null, 0, 'off', 'OFF', 'false', [], {}].every(v => withVal(v)['bugfix-receipt'] === false)
|
|
3433
|
+
&& withVal(true)['bugfix-receipt'] === true && withVal('on')['bugfix-receipt'] === true
|
|
3434
|
+
&& withVal('off').gate === false && withVal(undefined).gate === true && withVal(undefined)['bugfix-receipt'] === false;
|
|
3435
|
+
fs.writeFileSync(tf, JSON.stringify(Object.assign({}, d0, { gate: false })));
|
|
3436
|
+
behavOk = defOn && offOk && coerceOk;
|
|
3420
3437
|
// 그래프 HTML: 탭 3종 + roadmap/toggles 데이터 + 임베드 JS 문법
|
|
3421
3438
|
const m = require('../lib/graph');
|
|
3422
3439
|
const out = path.join(tmp, 'leerness.html');
|
|
@@ -3898,7 +3915,7 @@ function _selfTestCases() {
|
|
|
3898
3915
|
return compoundEnv && jsonKey && camelKey && slackApp && dictFP && plainStart && djangoKey && pkVariants && dbDefaultFP;
|
|
3899
3916
|
} },
|
|
3900
3917
|
{ name: 'honesty-check: AI 인식론적 정직성 3차원 + MCP/CLI/strict 통합 (사용자명시 1.9.305)', run: () => { const h = _epistemicHonestyCheck; const d1 = h('이 기능은 항상 정상 동작합니다').findings.some(f => f.dim === 'pretend-knowledge'); const d2 = h('아마 될 것 같습니다. 구현 완료했습니다').findings.some(f => f.dim === 'premature-judgment'); const d3 = h('이 API 의 rate limit 은 초당 5회입니다').findings.some(f => f.dim === 'no-info-gathering'); const clean = h('src/api.js 수정, 12/12 통과 (Exit: 0)').ok === true; const src = read(__filename); const wired = require('../lib/mcp-tools').some(t => t.name === 'leerness_honesty_check') && /if \(cmd === 'honesty-check'\)/.test(src) && /honestyFindings = _epistemicHonestyCheck/.test(src); return d1 && d2 && d3 && clean && wired; } },
|
|
3901
|
-
{ name: 'exit code 일관성: fail()→exitCode 1 행위 + unknown 명령 안내 (UR-0045 / CV-5 행위화 1.9.366)', run: () => { if (typeof fail !== 'function') return false; const saved = process.exitCode; const _w = process.stdout.write; let setOk = false; try { process.stdout.write = () => true; process.exitCode = 0; fail('selftest probe'); setOk = process.exitCode === 1; } finally { process.stdout.write = _w; process.exitCode = saved; } const src = read(__filename); const dispatchOk = /알 수 없는 명령: \$\{cmd\}/.test(src); return setOk && dispatchOk; } },
|
|
3918
|
+
{ name: 'exit code 일관성: fail()→exitCode 1 행위 + unknown 명령 안내 (UR-0045 / CV-5 행위화 1.9.366)', run: () => { if (typeof fail !== 'function') return false; const saved = process.exitCode; const _w = process.stdout.write, _e = process.stderr.write; let setOk = false; try { process.stdout.write = () => true; process.stderr.write = () => true; process.exitCode = 0; fail('selftest probe'); setOk = process.exitCode === 1; } finally { process.stdout.write = _w; process.stderr.write = _e; process.exitCode = saved; setQuiet(false); } const src = read(__filename); const dispatchOk = /알 수 없는 명령: \$\{cmd\}/.test(src); return setOk && dispatchOk; } },
|
|
3902
3919
|
{ name: 'brief: 프로젝트 청사진 set/show/export + README 개요 섹션 (UR-0055 사용자명시 1.9.307)', run: () => { const src = read(__filename); const fnOk = typeof briefCmd === 'function' && typeof _loadBrief === 'function' && typeof _briefBlueprint === 'function' && _BRIEF_FIELDS.length === 10; const b = { project: 'X', intro: 'i', purpose: 'p', problem: '', features: ['f1', 'f2'], stack: ['s'], architecture: '', users: [], success: [], nonGoals: [], currentState: '' }; const bp = _briefBlueprint(b, VERSION); const bpOk = /Blueprint/.test(bp) && /소개 \(Intro\)/.test(bp) && /f1/.test(bp) && /신규 프로젝트 시작 가이드/.test(bp); const rb = _briefReadmeBlock(b); const rbOk = rb.includes(BRIEF_START) && rb.includes(BRIEF_END) && /프로젝트 개요/.test(rb) && /\*\*목적\*\*/.test(rb); return fnOk && bpOk && rbOk && /if \(cmd === 'brief'\)/.test(src); } },
|
|
3903
3920
|
{ name: 'brief 2단계: update --direction 이력 + MCP leerness_brief + context 통합 (UR-0055 1.9.308)', run: () => { const src = read(__filename); const b = { project: 'X', intro: '', purpose: '', problem: '', features: [], stack: [], architecture: '', users: [], success: [], nonGoals: [], currentState: '', directionHistory: ['2026-06-04: 확대'] }; const bpOk = /개발 방향 이력/.test(_briefBlueprint(b, VERSION)) && /최근 개발 방향 변경/.test(_briefReadmeBlock(b)); const histWired = /sub === 'update'/.test(src) && /brief\.directionHistory \|\| \[\]\), `\$\{today\(\)\}/.test(src); const mcpOk = require('../lib/mcp-tools').some(t => t.name === 'leerness_brief'); const ctxOk = /brief: \{ intro:/.test(src); return bpOk && histWired && mcpOk && ctxOk; } },
|
|
3904
3921
|
{ name: 'verify-claim: done 주장 evidence 기본강제 + --lenient + MCP/json 도달 (UR-0048 설치리뷰 critical 1.9.309)', run: () => { const src = read(__filename); const def = /const mustHaveEvidence = !has\('--lenient'\) && \(isDoneClaim \|\| has\('--require-evidence'\)\)/.test(src); const threshold = /has\('--require-evidence'\) \? evq\.ok : \(evq\.hasFile \|\| evq\.hasTest \|\| evq\.hasLog\)/.test(src); const jsonWired = /evidenceComplete:/.test(src) && /if \(!evidenceQualityOk\) out\.reasons\.push\('evidence-incomplete'\)/.test(src) && /if \(!out\.ok\) return process\.exit\(1\)/.test(src); /* 1.35.7: reasons 기반 단일 exit 재배선(의도 동일 — evidence 게이트가 json exit 도달) */ const mcpLenient = !!require('../lib/mcp-tools').find(t => t.name === 'leerness_verify_claim').inputSchema.properties.lenient; return def && threshold && jsonWired && mcpLenient; } },
|
|
@@ -3996,7 +4013,7 @@ function _selfTestCases() {
|
|
|
3996
4013
|
{ name: 'UR-0025 심화: pulse 렌더 코어 분리 — _memorySurface + _renderPulseLine 행위 (1.9.379)', run: () => { const m = require('../lib/pure-utils'); if (typeof _memorySurface !== 'function' || typeof _renderPulseLine !== 'function' || m._memorySurface !== _memorySurface || m._renderPulseLine !== _renderPulseLine) return false; const ms = _memorySurface({ tasks: 1, decisions: 2, rules: 3, milestones: 4, lessons: 5 }) === 'T1/D2/R3/P4/L5' && _memorySurface({}) === 'T0/D0/R0/P0/L0'; const base = _renderPulseLine({ version: '1.0.0', roundCount: 7, mcpTools: 85, memorySurface: 'T0/D1/R0/P2/L0' }); const ln = base.includes('v1.0.0') && base.includes('R7') && base.includes('MCP 85') && base.includes('T0/D1/R0/P2/L0') && !base.includes('🎯') && !base.includes('abnormal'); const full = _renderPulseLine({ version: '1.0.0', roundCount: 7, mcpTools: 85, memorySurface: 'x', nextMilestone: 400, etaDays: 6, abnormalShutdown: 'high' }); const ln2 = full.includes('🎯 R400 (6d)') && full.includes('abnormal:high'); const wired = read(__filename).includes('const line = _renderPulseLine(data)') && read(__filename).includes('data.memorySurface = _memorySurface('); return ms && ln && ln2 && wired; } },
|
|
3997
4014
|
{ name: 'UR-0025: REQUIRED_WORKSPACE_FILES 단일출처 — verify/migrate audit·apply 3중 중복 제거 (1.9.380)', run: () => { const c = require('../lib/catalogs'); if (REQUIRED_WORKSPACE_FILES !== c.REQUIRED_WORKSPACE_FILES) return false; const listOk = Array.isArray(c.REQUIRED_WORKSPACE_FILES) && c.REQUIRED_WORKSPACE_FILES.length === 9 && c.REQUIRED_WORKSPACE_FILES.includes('AGENTS.md') && c.REQUIRED_WORKSPACE_FILES.includes('.harness/plan.md'); const harnessUses = (read(__filename).match(/const required = REQUIRED_WORKSPACE_FILES;/g) || []).length >= 1; const migUses = (read(path.join(path.dirname(__filename), '..', 'lib', 'migrate.js')).match(/const required = REQUIRED_WORKSPACE_FILES;/g) || []).length >= 2; return listOk && harnessUses && migUses; } },
|
|
3998
4015
|
{ name: 'UR-0025: KEYWORD_STOPWORDS 단일출처 — handoff/lessons 키워드 stopwords 2중 중복 제거 (1.9.381)', run: () => { const c = require('../lib/catalogs'); if (KEYWORD_STOPWORDS !== c.KEYWORD_STOPWORDS) return false; const setOk = c.KEYWORD_STOPWORDS instanceof Set && c.KEYWORD_STOPWORDS.has('작업') && c.KEYWORD_STOPWORDS.has('task') && !c.KEYWORD_STOPWORDS.has('고유단어') && c.KEYWORD_STOPWORDS.size >= 25; const usesConst = (read(__filename).match(/const stopwords = KEYWORD_STOPWORDS;/g) || []).length >= 2 && !/const stopwords = new Set\(\[/.test(read(__filename)); return setOk && usesConst; } },
|
|
3999
|
-
{ name: 'UR-0025 큰핸들러토대: lib/io.js 프리미티브(log/ok/warn/fail/today/now) 모듈 분리 + 동작 (1.9.382)', run: () => { const io = require('../lib/io'); const exportsOk = ['log', 'ok', 'warn', 'fail', 'today', 'now'].every(k => typeof io[k] === 'function') && io.log === log && io.fail === fail && io.now === now; const todayOk = /^\d{4}-\d{2}-\d{2}$/.test(io.today()) && /^\d{4}-\d{2}-\d{2}T/.test(io.now()); const src = read(__filename); const moved = src.includes("require('../lib/io')") && !/^function fail\(s\) \{ log/m.test(src) && !/^function now\(\) \{ return new Date/m.test(src); let exitOk = false; const saved = process.exitCode; const _w = process.stdout.write; try { process.stdout.write = () => true; process.exitCode = 0; io.fail('probe'); exitOk = process.exitCode === 1; } finally { process.stdout.write = _w; process.exitCode = saved; } return exportsOk && todayOk && moved && exitOk; } },
|
|
4016
|
+
{ name: 'UR-0025 큰핸들러토대: lib/io.js 프리미티브(log/ok/warn/fail/today/now) 모듈 분리 + 동작 (1.9.382)', run: () => { const io = require('../lib/io'); const exportsOk = ['log', 'ok', 'warn', 'fail', 'today', 'now'].every(k => typeof io[k] === 'function') && io.log === log && io.fail === fail && io.now === now; const todayOk = /^\d{4}-\d{2}-\d{2}$/.test(io.today()) && /^\d{4}-\d{2}-\d{2}T/.test(io.now()); const src = read(__filename); const moved = src.includes("require('../lib/io')") && !/^function fail\(s\) \{ log/m.test(src) && !/^function now\(\) \{ return new Date/m.test(src); let exitOk = false; const saved = process.exitCode; const _w = process.stdout.write, _e = process.stderr.write; try { process.stdout.write = () => true; process.stderr.write = () => true; process.exitCode = 0; io.fail('probe'); exitOk = process.exitCode === 1; } finally { process.stdout.write = _w; process.stderr.write = _e; process.exitCode = saved; io.setQuiet(false); } return exportsOk && todayOk && moved && exitOk; } },
|
|
4000
4017
|
{ name: 'UR-0025 큰핸들러토대: lib/io.js fs 프리미티브(read/writeUtf8/exists/mkdirp/append/rel/absRoot) 분리 + round-trip (1.9.383)', run: () => { const io = require('../lib/io'); const exp = ['absRoot', 'exists', 'read', 'readBuf', 'mkdirp', 'writeUtf8', 'append', 'rel'].every(k => typeof io[k] === 'function') && io.read === read && io.writeUtf8 === writeUtf8 && io.exists === exists; const src = read(__filename); const moved = !/^function writeUtf8\(p, s\) \{/m.test(src) && !/^function read\(p\) \{/m.test(src) && !/^function exists\(p\) \{/m.test(src); const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_io_')); let rt = false; try { const f = path.join(tmp, 'a', 'b.txt'); io.writeUtf8(f, '한글RT'); rt = io.exists(f) && io.read(f) === '한글RT' && io.rel(tmp, f) === 'a/b.txt'; } finally { try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} } return exp && moved && rt; } },
|
|
4001
4018
|
{ name: '5th외부평가/UR-0085: status --json 구조화 출력 + verify --json 와이어 (1.9.384)', run: () => { if (typeof status !== 'function' || typeof verify !== 'function') return false; const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_sj_')); const save = process.argv; const _w = process.stdout.write; let so = ''; try { fs.mkdirSync(path.join(tmp, '.harness'), { recursive: true }); process.argv = ['node', 'h', 'status', tmp, '--json']; process.stdout.write = s => { so += s; return true; }; status(tmp); } catch {} finally { process.stdout.write = _w; process.argv = save; try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} } let sj; try { sj = JSON.parse(so); } catch {} const statusOk = !!sj && typeof sj.total === 'number' && typeof sj.present === 'number' && 'healthy' in sj && Array.isArray(sj.missing); const src = read(__filename); const verifyWired = /function verify\(root\) \{[\s\S]*?has\('--json'\)[\s\S]*?JSON\.stringify\(\{ ok:/.test(src); return statusOk && verifyWired; } },
|
|
4002
4019
|
{ name: '5th외부평가/UR-0086: _parseContractSpec markdown bullet 함수 감지 + 순수 추출 (1.9.385)', run: () => { const m = require('../lib/pure-utils'); if (m._parseContractSpec !== _parseContractSpec) return false; const p = _parseContractSpec('# Spec\n- add(a,b)\n* subtract(a,b)\n1. multiply(a,b)\nfunction legacy(x)\n`mentioned(`\ntick.amount\n'); const declOk = ['add', 'subtract', 'multiply', 'legacy'].every(n => p.declared.includes(n)) && p.declared.length === 4; const menOk = p.mentioned.includes('mentioned') && !p.declared.includes('mentioned'); const fieldOk = p.fields.includes('amount'); const fpOk = _parseContractSpec('- 합계 (a+b)\n- result (total)\n- foo: bar(x)\n**bold**').declared.length === 0; const src = read(__filename); const moved = src.includes('_parseContractSpec(specText)') && !/specText\.matchAll\(\/function/.test(src); return declOk && menOk && fieldOk && fpOk && moved; } },
|
|
@@ -4022,7 +4039,7 @@ function _selfTestCases() {
|
|
|
4022
4039
|
{ name: '6번째 외부평가/codex P1-B: task drop 존재확인 가드 — 없는 ID 가짜 row 방지 (1.9.396)', run: () => { const src = read(__filename); const i = src.indexOf('function taskDrop(root, id)'); if (i < 0) return false; const body = src.slice(i, i + 700); return body.includes('not found in progress-tracker.md') && body.includes('rows.find(r => r.id === id)') && body.includes('_requireInit'); } },
|
|
4023
4040
|
{ 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; } },
|
|
4024
4041
|
{ 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; } },
|
|
4025
|
-
{ name: 'T-0077 graph --html: leerness.html 온톨로지 생성 + 노드/엣지/XSS 무결성 (1.34.3)', run: () => { const m = require('../lib/graph'); const expOk = typeof m.graphHtmlCmd === 'function' && typeof m.buildGraphData === 'function'; const src = read(__filename); const delegated = src.includes("require('../lib/" + "graph')") && src.includes('function graphHtmlCmd(root) { return ' + '_graph.graphHtmlCmd('); const gSrc = read(path.join(path.dirname(__filename), '..', 'lib', 'graph.js')); const movedToLib = gSrc.includes('buildGraphData') && gSrc.includes('String.raw') && gSrc.includes('/*__DATA__' + '*/null'); let behavOk = false; const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_graph_')); const _w = process.stdout.write; try { process.stdout.write = () => true; fs.mkdirSync(path.join(tmp, '.harness'), { recursive: true }); fs.writeFileSync(path.join(tmp, '.harness', 'progress-tracker.md'), '| ID | Status | Request | Evidence | Next | Updated |\n|---|---|---|---|---|---|\n| T-0001 | done | first task | - | - | 2026-06-26 |\n| T-0002 | in-progress | follow-up to T-0001 </scr' + 'ipt> | - | - | 2026-06-26 |\n'); const deps = { _roadmapData, _loadDecisions, _loadLessons }; const data = m.buildGraphData(tmp, deps); const dataOk = data.nodes.some(n => n.id === 'T-0001') && data.nodes.some(n => n.id === 'T-0002') && data.counts.task >= 2; const edgeOk = data.edges.some(e => e.source === 'T-0002' && e.target === 'T-0001'); const out = path.join(tmp, 'leerness.html'); const r = m.graphHtmlCmd(tmp, Object.assign({ has: () => false }, deps), out); const html = fs.readFileSync(out, 'utf8'); const placeholderGone = !html.includes('/*__DATA__' + '*/null'); const hasNode = html.includes('T-0002'); const xssSafe = (html.match(/<\/script>/g) || []).length === 1; behavOk = dataOk && edgeOk && placeholderGone && hasNode && xssSafe && !!r && r.ok === true && fs.existsSync(out); } catch (e) { behavOk = false; } finally { process.stdout.write = _w; try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} } return expOk && delegated && movedToLib && behavOk; } },
|
|
4042
|
+
{ name: 'T-0077 graph --html: leerness.html 온톨로지 생성 + 노드/엣지/XSS 무결성 (1.34.3)', run: () => { const m = require('../lib/graph'); const expOk = typeof m.graphHtmlCmd === 'function' && typeof m.buildGraphData === 'function'; const src = read(__filename); const delegated = src.includes("require('../lib/" + "graph')") && src.includes('function graphHtmlCmd(root) { return ' + '_graph.graphHtmlCmd('); const gSrc = read(path.join(path.dirname(__filename), '..', 'lib', 'graph.js')); const movedToLib = gSrc.includes('buildGraphData') && gSrc.includes('String.raw') && gSrc.includes('/*__DATA__' + '*/null'); let behavOk = false; const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_graph_')); const _w = process.stdout.write; try { process.stdout.write = () => true; fs.mkdirSync(path.join(tmp, '.harness'), { recursive: true }); fs.writeFileSync(path.join(tmp, '.harness', 'progress-tracker.md'), '| ID | Status | Request | Evidence | Next | Updated |\n|---|---|---|---|---|---|\n| T-0001 | done | first task | - | - | 2026-06-26 |\n| T-0002 | in-progress | follow-up to T-0001 </scr' + 'ipt> | - | - | 2026-06-26 |\n'); const deps = { _roadmapData, _loadDecisions, _loadLessons }; const data = m.buildGraphData(tmp, deps); const dataOk = data.nodes.some(n => n.id === 'T-0001') && data.nodes.some(n => n.id === 'T-0002') && data.counts.task >= 2; const edgeOk = data.edges.some(e => e.source === 'T-0002' && e.target === 'T-0001'); const out = path.join(tmp, 'leerness.html'); const r = m.graphHtmlCmd(tmp, Object.assign({ has: () => false }, deps, { quiet: true }), out); const html = fs.readFileSync(out, 'utf8'); const placeholderGone = !html.includes('/*__DATA__' + '*/null'); const hasNode = html.includes('T-0002'); const xssSafe = (html.match(/<\/script>/g) || []).length === 1; behavOk = dataOk && edgeOk && placeholderGone && hasNode && xssSafe && !!r && r.ok === true && fs.existsSync(out); } catch (e) { behavOk = false; } finally { process.stdout.write = _w; try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} } return expOk && delegated && movedToLib && behavOk; } },
|
|
4026
4043
|
{ name: 'T-0077 후속 graph auto-gen: handoff opt-in 배선 + quiet 무로그 (1.34.4)', run: () => { const m = require('../lib/graph'); const src = read(__filename); const wired = src.includes('_maybeAuto' + 'Graph(_hp)') && src.includes('LEERNESS_AUTO_' + 'GRAPH'); let quietOk = false; const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_autograph_')); const _w = process.stdout.write; let so = ''; try { process.stdout.write = s => { so += s; return true; }; fs.mkdirSync(path.join(tmp, '.harness'), { recursive: true }); fs.writeFileSync(path.join(tmp, '.harness', 'progress-tracker.md'), '| ID | Status | Request | Evidence | Next | Updated |\n|---|---|---|---|---|---|\n| T-0001 | done | x | - | - | 2026-06-26 |\n'); const out = path.join(tmp, 'leerness.html'); const r = m.graphHtmlCmd(tmp, { _roadmapData, _loadDecisions, _loadLessons, quiet: true }, out); quietOk = !!r && r.ok === true && fs.existsSync(out) && so === ''; } catch (e) { quietOk = false; } finally { process.stdout.write = _w; try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} } return wired && quietOk; } },
|
|
4027
4044
|
{ name: 'graph --html: 빈/미초기화 하네스 무크래시 + 유효 빈 HTML (1.35.0 방어가드)', run: () => { const m = require('../lib/graph'); let ok = false; const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_emptyg_')); const _w = process.stdout.write; let so = ''; try { process.stdout.write = s => { so += s; return true; }; fs.mkdirSync(path.join(tmp, '.harness'), { recursive: true }); const data = m.buildGraphData(tmp, {}); const out = path.join(tmp, 'leerness.html'); const r = m.graphHtmlCmd(tmp, { quiet: true }, out); const html = fs.readFileSync(out, 'utf8'); const closers = html.split('</' + 'script>').length - 1; const dm = html.match(/var DATA = (\{[\s\S]*?\});/); let parsed = null; try { parsed = JSON.parse(dm[1]); } catch (e) {} ok = !!data && Array.isArray(data.nodes) && data.nodes.length === 0 && !!r && r.ok === true && r.nodes === 0 && fs.existsSync(out) && closers === 1 && !!parsed && parsed.nodes.length === 0 && so === ''; } catch (e) { ok = false; } finally { process.stdout.write = _w; try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} } return ok; } },
|
|
4028
4045
|
{ name: 'graph --html: 임베드 script JS 신택스 유효성(U+2028/정규식 리터럴 회귀 영구 차단, 1.35.2)', run: () => { const m = require('../lib/graph'); let ok = false; const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_gjs_')); const _w = process.stdout.write; let so = ''; try { process.stdout.write = s => { so += s; return true; }; fs.mkdirSync(path.join(tmp, '.harness'), { recursive: true }); fs.writeFileSync(path.join(tmp, '.harness', 'progress-tracker.md'), '| ID | Status | Request | Evidence | Next | Updated |\n|---|---|---|---|---|---|\n| T-0001 | done | x </scr' + 'ipt> & <b> ' + String.fromCharCode(0x24) + '{y} | M-0002 | - | 2026-06-26 |\n'); const out = path.join(tmp, 'leerness.html'); m.graphHtmlCmd(tmp, { _roadmapData, _loadDecisions, _loadLessons, quiet: true }, out); const html = fs.readFileSync(out, 'utf8'); const o = '<scr' + 'ipt>', c = '</scr' + 'ipt>'; const js = html.slice(html.indexOf(o) + o.length, html.lastIndexOf(c)); let synOk = false; try { new Function(js); synOk = true; } catch (e) { synOk = false; } ok = js.length > 200 && synOk; } catch (e) { ok = false; } finally { process.stdout.write = _w; try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} } return ok; } },
|
|
@@ -5113,6 +5130,150 @@ function _selfTestCases() {
|
|
|
5113
5130
|
return lessonsOk && decisionsOk && humanOk;
|
|
5114
5131
|
} catch { return false; } finally { try { fs.rmSync(d, { recursive: true, force: true }); } catch {} }
|
|
5115
5132
|
} },
|
|
5133
|
+
{ name: 'bugfix 완료 영수증 (1.36.87, P-0005): 기본 무영향 · 탐지력 없는 probe 거부 · 미수정 done 차단 · 통과 시 선언 명시 (행위)', run: () => {
|
|
5134
|
+
// 설계 계약(검토 통과분): 개입 지점은 `--status done` 하나. 무영향 조건 3중 —
|
|
5135
|
+
// 토글 OFF(기본) · done 전이 아님 · 해당 task 에 probe 미등록. 셋 중 하나면 기존 동작 그대로여야 한다.
|
|
5136
|
+
// 기계 검증은 probe(수정 전 실패 → 수정 후 통과)뿐이고 근본원인/형제범위는 선언이다 — 출력이 그렇게 말해야 한다.
|
|
5137
|
+
// 비용 주의: 1차판은 CLI 를 10회 spawn 해 selftest 에 **+6초**를 더했고, 이미 여유 0.9초였던
|
|
5138
|
+
// e2e 의 doctor(내부에서 selftest 실행, 제한 20s)를 확정적으로 넘겼다 — 게이트가 잡았다.
|
|
5139
|
+
// 상태기계 전이는 **in-process** 로 검사하고, 배선(디스패치·토글·출력문구)만 CLI 1회로 확인한다.
|
|
5140
|
+
const _bf = require('../lib/bugfix');
|
|
5141
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_bfx_'));
|
|
5142
|
+
try {
|
|
5143
|
+
mkdirp(path.join(d, '.harness'));
|
|
5144
|
+
writeUtf8(path.join(d, '.harness', 'HARNESS_VERSION'), VERSION);
|
|
5145
|
+
const ID = 'T-0009';
|
|
5146
|
+
const _flag = path.join(d, 'bf_fixed.txt');
|
|
5147
|
+
const _probe = `node -e "const f=require('fs');if(!f.existsSync('bf_fixed.txt')){console.error('BUG_HERE');process.exit(3)}"`;
|
|
5148
|
+
// in-process 호출은 stdout 에 쓴다 — `selftest --json` 의 출력을 오염시켜 소비자의 JSON.parse 가 깨진다
|
|
5149
|
+
// (실측: 첫 `{` 가 이 가드의 출력이 됐다). --json 순수성은 leerness 가 반복해 고쳐온 계약이므로 억제한다.
|
|
5150
|
+
const _quiet = (fn) => { const w = process.stdout.write; process.stdout.write = () => true; try { return fn(); } finally { process.stdout.write = w; } };
|
|
5151
|
+
// ① probe 미등록 → 무영향(선언하지 않은 task 는 막지 않는다)
|
|
5152
|
+
if (_bf.checkDoneTransition(d, ID).blocked !== false) return false;
|
|
5153
|
+
// ② 재현되는 probe 등록(수정 전 상태에서 실패해야 등록됨)
|
|
5154
|
+
_quiet(() => _bf.bugfixCmd(d, 'start', [ID], { has: (f) => f === '--json', arg: (f) => (f === '--repro' ? _probe : f === '--expect-bad' ? 'BUG_HERE' : '') }));
|
|
5155
|
+
const _st = _bf._loadChecked(d);
|
|
5156
|
+
if (_st.invalid || !_st.list.some(x => x.id === ID && x.baseline && x.baseline.failed)) return false;
|
|
5157
|
+
// ③ 수정 전 done 차단
|
|
5158
|
+
if (_bf.checkDoneTransition(d, ID).code !== 'probe_still_failing') return false;
|
|
5159
|
+
// ④ 수정 후에도 영수증 없으면 차단
|
|
5160
|
+
writeUtf8(_flag, 'ok');
|
|
5161
|
+
if (_bf.checkDoneTransition(d, ID).code !== 'receipt_incomplete') return false;
|
|
5162
|
+
// ⑤ 영수증 후 통과 + 출력이 "선언"임을 명시
|
|
5163
|
+
_quiet(() => _bf.bugfixCmd(d, 'receipt', [ID], { has: (f) => f === '--json', arg: (f) => (f === '--root-cause' ? '원인 한 문장' : f === '--siblings-na' ? '단일 경로' : '') }));
|
|
5164
|
+
const _pass = _bf.checkDoneTransition(d, ID);
|
|
5165
|
+
if (_pass.blocked !== false) return false;
|
|
5166
|
+
const _txt = (_pass.lines || []).join('\n');
|
|
5167
|
+
if (!(/재현 probe 통과/.test(_txt) && /선언/.test(_txt) && /진위는 검증하지 않았/.test(_txt))) return false;
|
|
5168
|
+
// ⑥ **배선**만 CLI 로 1회 — 모듈 로드/디스패치/토글 연결이 실제로 되는지(in-process 로는 안 드러남)
|
|
5169
|
+
const _cli = cp.spawnSync(process.execPath, [__filename, 'bugfix', 'list', '--path', d, '--json'],
|
|
5170
|
+
{ encoding: 'utf8', timeout: 60000, maxBuffer: 8 * 1024 * 1024 });
|
|
5171
|
+
if (_cli.status !== 0) return false;
|
|
5172
|
+
const _j = JSON.parse(_cli.stdout.slice(_cli.stdout.indexOf('{')));
|
|
5173
|
+
if (!(_j.ok === true && (_j.receipts || []).some(r => r.id === ID && r.rootCause))) return false;
|
|
5174
|
+
// ⑦⑧⑨ 1.36.87 자체 헌트로 실측한 3결함 — 전부 손편집/구버전 스토어와 정상 앱 출력에서 나온다.
|
|
5175
|
+
const _put = (list) => writeUtf8(path.join(d, '.harness', 'bugfix-receipts.json'), JSON.stringify(list, null, 2));
|
|
5176
|
+
const _ok0 = `node -e "process.exit(0)"`;
|
|
5177
|
+
// ⑦ baseline 없는/재현된 적 없는 엔트리는 **형상 무효**로 보류한다 — 종전엔 유효로 통과해
|
|
5178
|
+
// (a) 완료 렌더에서 TypeError 로 `task update` 가 통째로 죽고 (b) 손으로 써넣은 probe 가 게이트를 통과했다.
|
|
5179
|
+
_put([{ id: 'T-0101', repro: _ok0, expectBad: 'X', rootCause: 'rc', siblingScope: { checked: ['a'] } }]);
|
|
5180
|
+
if (_bf.checkDoneTransition(d, 'T-0101').code !== 'store_invalid') return false; // 던지면 바깥 catch 로 떨어져 실패 처리됨
|
|
5181
|
+
_put([{ id: 'T-0101', repro: _ok0, expectBad: 'X', baseline: { failed: false, exit: 0 }, rootCause: 'rc', siblingScope: { checked: ['a'] } }]);
|
|
5182
|
+
if (_bf.checkDoneTransition(d, 'T-0101').code !== 'store_invalid') return false;
|
|
5183
|
+
// ⑧ 빈 형제 목록은 "기록됨"이 아니다 (`--siblings ","` 가 checked:[] 로 통과하던 공허 영수증)
|
|
5184
|
+
_put([{ id: 'T-0102', repro: _ok0, expectBad: 'X', baseline: { failed: true, exit: 3 }, rootCause: 'rc', siblingScope: { checked: [] } }]);
|
|
5185
|
+
if (_bf.checkDoneTransition(d, 'T-0102').code !== 'receipt_incomplete') return false;
|
|
5186
|
+
// ⑨ 앱이 정상적으로 뱉는 문자열(SyntaxError 등)을 런처 오류로 오분류해 **완료를 막던** false-BLOCK
|
|
5187
|
+
const _synProbe = `node -e "console.log('SyntaxError: Unexpected token < in JSON at position 0')"`;
|
|
5188
|
+
_put([{ id: 'T-0103', repro: _synProbe, expectBad: 'SyntaxError', baseline: { failed: true, exit: 1 }, rootCause: 'rc', siblingScope: { notApplicable: '단일' } }]);
|
|
5189
|
+
if (_bf.checkDoneTransition(d, 'T-0103').blocked !== false) return false;
|
|
5190
|
+
// ⑩ 1회성 probe 거부 — 상태를 남겨 등록 때만 실패하는 probe 는 제품 코드를 한 줄도 안 고치고 통과시킨다.
|
|
5191
|
+
// 등록은 **2회 연속** 재현을 요구해야 한다(codex 26차 #3).
|
|
5192
|
+
const _once = path.join(d, 'once.flag').replace(/\\/g, '/');
|
|
5193
|
+
const _oneShot = `node -e "const f=require('fs');if(!f.existsSync('${_once}')){f.writeFileSync('${_once}','1');console.error('BUG_HERE');process.exit(3)}"`;
|
|
5194
|
+
_put([]);
|
|
5195
|
+
// 등록 거부는 process.exitCode 를 1로 세운다 — in-process 로 부르면 selftest 자신의 종료코드가 오염된다. 복원.
|
|
5196
|
+
const _ec = process.exitCode;
|
|
5197
|
+
_quiet(() => { _bf.bugfixCmd(d, 'start', ['T-0105'], { has: (f) => f === '--json', arg: (f) => (f === '--repro' ? _oneShot : f === '--expect-bad' ? 'BUG_HERE' : '') }); });
|
|
5198
|
+
process.exitCode = _ec;
|
|
5199
|
+
if (_bf._loadChecked(d).list.some(x => x.id === 'T-0105')) return false;
|
|
5200
|
+
// ⑪ CLI 도 같은 규칙 — 빈 형제 목록은 거부(exit 1 + code) · 등록 취소 경로 존재
|
|
5201
|
+
_put([{ id: 'T-0104', repro: _ok0, expectBad: 'X', baseline: { failed: true, exit: 3 } }]);
|
|
5202
|
+
const _cli2 = cp.spawnSync(process.execPath, [__filename, 'bugfix', 'receipt', 'T-0104', '--siblings', ',', '--path', d, '--json'],
|
|
5203
|
+
{ encoding: 'utf8', timeout: 60000, maxBuffer: 8 * 1024 * 1024 });
|
|
5204
|
+
const _j2 = JSON.parse(_cli2.stdout.slice(_cli2.stdout.indexOf('{')));
|
|
5205
|
+
if (!(_cli2.status === 1 && _j2.ok === false && _j2.code === 'empty_siblings')) return false;
|
|
5206
|
+
_quiet(() => _bf.bugfixCmd(d, 'drop', ['T-0104'], { has: (f) => f === '--json', arg: () => '' }));
|
|
5207
|
+
return !_bf._loadChecked(d).list.some(x => x.id === 'T-0104') && _bf.checkDoneTransition(d, 'T-0104').blocked === false;
|
|
5208
|
+
} catch { return false; } finally { try { fs.rmSync(d, { recursive: true, force: true }); } catch {} }
|
|
5209
|
+
} },
|
|
5210
|
+
{ name: 'brainstorm 두 경로 등가 (1.36.86): 사람용/--json 이 표면별로 같은 결과 — 수집 단일화 회귀 가드 (행위)', run: () => {
|
|
5211
|
+
// 사고: 사람용이 자체 수집 코드를 갖고 있어 `--include-code` 에서 --json 은 code 1건,
|
|
5212
|
+
// 사람용은 "관련 자원 없음" 을 냈다. 수집을 _brainstormFor 로 단일화(중복 149줄 제거)하고
|
|
5213
|
+
// 총계도 _brainstormTotal 하나로 통일했다(이전엔 lessons/archive/code 를 서로 다르게 누락).
|
|
5214
|
+
// 표면별로 "JSON 이 hit 을 보고하면 사람용도 반드시 보여준다"를 강제한다.
|
|
5215
|
+
const d = _selftestFixture('ko');
|
|
5216
|
+
if (!d) return false;
|
|
5217
|
+
try {
|
|
5218
|
+
const KW = 'eqv' + 'probe';
|
|
5219
|
+
const _run = (a) => cp.spawnSync(process.execPath, [__filename, 'brainstorm', KW, '--path', d, ...a],
|
|
5220
|
+
{ encoding: 'utf8', timeout: 90000, maxBuffer: 16 * 1024 * 1024 });
|
|
5221
|
+
// 여러 표면에 동시에 심는다 — 한 표면만 보면 다른 표면의 불일치를 놓친다
|
|
5222
|
+
mkdirp(path.join(d, 'src'));
|
|
5223
|
+
writeUtf8(path.join(d, 'src', 'a.js'), `// ${KW} 구현\n`);
|
|
5224
|
+
writeUtf8(path.join(d, '.harness', 'lessons.md'), `# L\n\n### 2026-01-01\n- Lesson: ${KW} 교훈\n`);
|
|
5225
|
+
writeUtf8(path.join(d, '.harness', 'plan.md'), `# P\n\n### M-0001. ${KW} 마일스톤\nStatus: planned\nProgress: 0%\n`);
|
|
5226
|
+
writeUtf8(path.join(d, '.harness', 'decisions.md'), `# D\n\n### 2026-01-01 — ${KW} 결정\n- Decision: ${KW}\n`);
|
|
5227
|
+
const rj = _run(['--include-code', '--json']);
|
|
5228
|
+
if (rj.status !== 0) return false;
|
|
5229
|
+
const j = JSON.parse(rj.stdout.slice(rj.stdout.indexOf('{')));
|
|
5230
|
+
const h = j.hits || {};
|
|
5231
|
+
const n = (x) => (x && x.length) || 0;
|
|
5232
|
+
// JSON 이 실제로 여러 표면을 잡았는가(픽스처가 무력하면 이 가드도 공허해진다)
|
|
5233
|
+
if (!(n(h.code) && n(h.lessonsExplicit) && n(h.planMilestones) && n(h.decisions))) return false;
|
|
5234
|
+
// 총계는 **독립 계산**과 비교한다 — `_brainstormTotal(h)` 로 비교하면 그 함수를 망가뜨려도
|
|
5235
|
+
// 양변이 같이 변해 통과하는 동어반복이 된다(1차판이 실제로 그랬다: code 항을 빼도 초록).
|
|
5236
|
+
const _arc = h.archive ? Object.values(h.archive).reduce((s, v) => s + n(v), 0) : 0;
|
|
5237
|
+
const _expect = n(h.decisions) + n(h.skills) + n(h.tasks) + n(h.rules) + n(h.evidence) + n(h.lessons)
|
|
5238
|
+
+ n(h.code) + n(h.skillHistory) + n(h.taskLogFails) + n(h.lessonsExplicit) + n(h.planMilestones) + _arc;
|
|
5239
|
+
if (j.total !== _expect) return false;
|
|
5240
|
+
const ht = (_run(['--include-code']).stdout) || '';
|
|
5241
|
+
if (/관련 자원 없음/.test(ht)) return false; // hit 이 있는데 "없음" 은 자기모순
|
|
5242
|
+
// 표면별 건수가 사람용 헤딩에도 그대로 나타나야
|
|
5243
|
+
const shown = [
|
|
5244
|
+
[n(h.code), /관련 소스 코드 \((\d+)\)/],
|
|
5245
|
+
[n(h.lessonsExplicit), /관련 lessons \((\d+)\)/],
|
|
5246
|
+
[n(h.planMilestones), /관련 plan milestones \((\d+)\)/],
|
|
5247
|
+
[n(h.decisions), /관련 결정 \((\d+)\)/],
|
|
5248
|
+
];
|
|
5249
|
+
// 사람용 헤더의 **내역 합 = 총계** — 총계에만 반영되고 내역에서 빠진 표면이 있으면 사용자가 더했을 때 안 맞는다
|
|
5250
|
+
const _hdr = (ht.match(/총 (\d+)건 발견 \(([^)]*)\)/) || []);
|
|
5251
|
+
const _sum = (_hdr[2] || '').split('·').map(s => Number((s.trim().match(/(\d+)$/) || [])[1] || 0)).reduce((a, b) => a + b, 0);
|
|
5252
|
+
const headerOk = Number(_hdr[1]) === j.total && _sum === j.total;
|
|
5253
|
+
if (!(shown.every(([cnt, re]) => { const m = re.exec(ht); return !!m && Number(m[1]) === cnt; }) && headerOk)) return false;
|
|
5254
|
+
// 1.36.86 (검수 High#1): **workspace 경로**도 같은 계약을 지켜야 한다 —
|
|
5255
|
+
// 집계에는 넣고 렌더는 빠뜨려 "3건" 이라 적고 1건만 보여주던 상태였다(실측).
|
|
5256
|
+
// 프로젝트 헤더의 건수만큼 실제 항목 줄이 나와야 한다.
|
|
5257
|
+
const _wsBase = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_ws_'));
|
|
5258
|
+
try {
|
|
5259
|
+
const _app = path.join(_wsBase, 'app');
|
|
5260
|
+
mkdirp(path.join(_app, '.harness'));
|
|
5261
|
+
writeUtf8(path.join(_app, '.harness', 'HARNESS_VERSION'), VERSION);
|
|
5262
|
+
writeUtf8(path.join(_app, '.harness', 'lessons.md'), `# L\n\n### 2026-01-01\n- Lesson: ${KW} 교훈\n`);
|
|
5263
|
+
writeUtf8(path.join(_app, '.harness', 'plan.md'), `# P\n\n### M-0001. ${KW} 마일스톤\nStatus: planned\nProgress: 0%\n`);
|
|
5264
|
+
writeUtf8(path.join(_app, '.harness', 'decisions.md'), `# D\n\n### 2026-01-01 — ${KW} 결정\n- Decision: ${KW}\n`);
|
|
5265
|
+
const _opt = { cwd: _wsBase, encoding: 'utf8', timeout: 90000, maxBuffer: 16 * 1024 * 1024 };
|
|
5266
|
+
const _wj = cp.spawnSync(process.execPath, [__filename, 'brainstorm', KW, '--include', 'app', '--json'], _opt);
|
|
5267
|
+
const _wh = cp.spawnSync(process.execPath, [__filename, 'brainstorm', KW, '--include', 'app'], _opt);
|
|
5268
|
+
if (_wj.status !== 0 || _wh.status !== 0) return false;
|
|
5269
|
+
const _wjj = JSON.parse(_wj.stdout.slice(_wj.stdout.indexOf('{')));
|
|
5270
|
+
const _wt = (_wh.stdout || '');
|
|
5271
|
+
const _hdrN = Number((_wt.match(/^## app \((\d+)건\)/m) || [])[1] || -1);
|
|
5272
|
+
const _items = (_wt.match(/^ {4}- /gm) || []).length; // 실제 렌더된 항목 줄
|
|
5273
|
+
return _hdrN === _wjj.total && _items >= _wjj.total; // 헤더 건수 = 총계 = 실제 표시 항목
|
|
5274
|
+
} catch { return false; } finally { try { fs.rmSync(_wsBase, { recursive: true, force: true }); } catch {} }
|
|
5275
|
+
} catch { return false; } finally { try { fs.rmSync(d, { recursive: true, force: true }); } catch {} }
|
|
5276
|
+
} },
|
|
5116
5277
|
{ name: '자기소스 검사 동결 (1.36.84): selftest 가 자기 소스를 읽어 검증하는 케이스는 더 늘지 않는다 — 신규는 행위검사로 (메타가드)', run: () => {
|
|
5117
5278
|
// 왜 "탐지"가 아니라 "동결"인가:
|
|
5118
5279
|
// 1.36.82~83 에서 자기참조 공허 가드를 탐지하려 세 라운드 연속 탐지기를 고쳤는데,
|
|
@@ -7310,6 +7471,7 @@ function commandsCmd(root) {
|
|
|
7310
7471
|
{ cmd: 'integrity check [--repair] [--json]', desc: 'managed 정책-문서 12종 무결성(부재/H1 상실/절단) 점검 — --repair: archive 대피 후 템플릿 재생성 — 1.36.57 감사 F-04' },
|
|
7311
7472
|
{ cmd: 'referee add|verify|list|show|drop', desc: '검증기 캘리브레이션 — 신뢰 전에 탐지력 증명(known-good 통과 + 망가뜨린 known-bad 를 기대 사유로 거부). verify-claim --referee / gate 게이팅 — 1.36.80 P-0001' },
|
|
7312
7473
|
{ cmd: 'preview add|list|show|approve|revise|mockup', desc: '신규 기능 미리보기 승인 워크플로 — approve 전 코드 작성 금지 계약. mockup <P-ID> [--force]: 자립형 HTML 디자인 시안 스캐폴드 · add --mockup <파일>: 기존 시안 첨부 — 1.36.51 UR-0061 · 1.36.75 UR-0066' },
|
|
7474
|
+
{ cmd: 'bugfix start|receipt|drop|list', desc: 'bugfix 완료 영수증 — 수정 전 재현 probe 를 등록(2회 연속 실패해야 등록)하고, done 전이에서 통과 + 근본원인/형제범위 영수증을 요구. 토글 기본 OFF: leerness toggle set bugfix-receipt on — 1.36.87 P-0005' },
|
|
7313
7475
|
{ cmd: 'review <file> --persona <ids>', desc: '페르소나 리뷰 (1.9.29)' },
|
|
7314
7476
|
{ cmd: 'brainstorm "<topic>" [--include-code]', desc: '워크스페이스 회수 + 코드 grep' }
|
|
7315
7477
|
],
|
|
@@ -9851,7 +10013,21 @@ function writeProgressRows(root, header, rows, opts = {}) {
|
|
|
9851
10013
|
(rows.length ? '\n' : '') + _suffix;
|
|
9852
10014
|
writeUtf8(progressPath(root), composed);
|
|
9853
10015
|
}
|
|
10016
|
+
// 1.36.88 (출하전 헌트 #1/#2/#17/#18): bugfix 완료 게이트를 **여기 한 곳**에서 건다.
|
|
10017
|
+
// 종전엔 taskUpdate 에만 배선돼 있었고, 실측 결과 `task sync --from`(하네스가 세션 워크플로에서
|
|
10018
|
+
// 직접 지시하는 TodoWrite 왕복 경로)이 경고 한 줄 없이 exit 0 으로 done 을 기록했다.
|
|
10019
|
+
// progress 행을 쓰는 경로는 전부 이 함수를 지나므로, 새 호출부가 생겨도 자동으로 게이트를 탄다.
|
|
10020
|
+
// 막힐 때 **던지는** 이유: 반환값을 무시하면 무언 통과가 되고, 그게 이 결함 클래스의 재발 양식이다.
|
|
10021
|
+
let _lastBugfixLines = null; // 통과 시 렌더용 — 직후 호출자가 _consumeBugfixLines() 로 가져간다
|
|
10022
|
+
function _consumeBugfixLines() { const l = _lastBugfixLines; _lastBugfixLines = null; return l; }
|
|
9854
10023
|
function upsertProgress(root, row) {
|
|
10024
|
+
// 게이트는 **락 밖에서** — probe 는 최대 10분까지 도는 사용자 명령이라 progress 락을 물고 있으면 안 된다.
|
|
10025
|
+
if (row && row.status === 'done' && _tgl.toggleOn(root, 'bugfix-receipt')) {
|
|
10026
|
+
const _bfm = require('../lib/bugfix');
|
|
10027
|
+
const _r = _bfm.checkDoneTransition(root, row.id);
|
|
10028
|
+
if (_r.blocked) throw new _bfm.BugfixBlocked(_r, row.id);
|
|
10029
|
+
_lastBugfixLines = _r.lines || null;
|
|
10030
|
+
}
|
|
9855
10031
|
// 1.9.303 (UR-0043): read-modify-write 전체를 락으로 직렬화 — 동시 task 쓰기 lost-update 방지.
|
|
9856
10032
|
return _withLock(progressPath(root), () => {
|
|
9857
10033
|
const header = progressHeader(root);
|
|
@@ -10207,10 +10383,33 @@ function taskUpdate(root, id) {
|
|
|
10207
10383
|
if (arg('--evidence') !== null) patch.evidence = arg('--evidence');
|
|
10208
10384
|
if (arg('--next') !== null) patch.nextAction = arg('--next');
|
|
10209
10385
|
if (arg('--note')) patch.request = arg('--note');
|
|
10210
|
-
|
|
10386
|
+
// 1.36.87 (P-0005, codex+자체 이중검토 통과분): bugfix 완료 게이트 — **`done` 전이가 유일한 개입 지점**이다.
|
|
10387
|
+
// leerness 는 이미 debug 렌즈·review-request 로 올바른 절차를 안내하지만 그건 조언 텍스트일 뿐
|
|
10388
|
+
// 완료 시점에 소비되지 않는다(조언을 더 만드는 것이야말로 증상 처방).
|
|
10389
|
+
// 무영향 조건 3중: 토글 OFF(기본) · done 전이가 아님 · 해당 task 에 bugfix probe 미등록 → 기존 동작 그대로.
|
|
10390
|
+
// 1.36.88: 게이트 판정은 upsertProgress(단일 병목) 안에서 일어난다 — 여기서는 렌더만 한다.
|
|
10391
|
+
// 1.36.87 은 "이미 done 이면 스킵"을 넣었지만(codex 26차 #9), 그게 **재오픈 버그 시나리오를 정확히 무력화**했다:
|
|
10392
|
+
// 1라운드에서 done → 회귀 → probe 등록 → 다시 done 이 그대로 통과. 이 기능의 동기가 바로 그 시나리오다.
|
|
10393
|
+
// probe 가 낡아 갇히는 문제는 스킵이 아니라 `leerness bugfix drop <ID>` 로 푼다(문서화된 탈출구).
|
|
10394
|
+
let _bfLines = null;
|
|
10395
|
+
try {
|
|
10396
|
+
upsertProgress(root, patch);
|
|
10397
|
+
_bfLines = _consumeBugfixLines();
|
|
10398
|
+
} catch (e) {
|
|
10399
|
+
if (!e || e.name !== 'BugfixBlocked') throw e;
|
|
10400
|
+
if (has('--json')) { log(JSON.stringify({ ok: false, code: e.code, id, error: e.message }, null, 2)); }
|
|
10401
|
+
else {
|
|
10402
|
+
fail(`done 보류 — ${e.message}`);
|
|
10403
|
+
(e.lines || []).forEach(l => log(' ' + l));
|
|
10404
|
+
log(` ⓘ 탈출구: leerness bugfix drop ${id} (probe 등록 취소) · leerness toggle set bugfix-receipt off (프로젝트 정책)`);
|
|
10405
|
+
}
|
|
10406
|
+
process.exitCode = 1;
|
|
10407
|
+
return;
|
|
10408
|
+
}
|
|
10211
10409
|
// 1.36.0 (클린룸 리뷰 B, --json 무결성): task add(1.9.413) 는 --json 성공 출력이 있으나 task update 는 누락 — 성공 시 평문만 내보내 JSON 소비 스크립트가 성공 경로에서만 크래시(에러 경로는 이미 구조화). 동일 패턴으로 보강.
|
|
10212
|
-
if (has('--json')) { log(JSON.stringify({ ok: true, ...patch })); return; }
|
|
10410
|
+
if (has('--json')) { log(JSON.stringify({ ok: true, ...patch, ...(_bfLines ? { bugfixReceipt: _bfLines } : {}) })); return; }
|
|
10213
10411
|
ok(`task updated: ${id}`);
|
|
10412
|
+
if (_bfLines) _bfLines.forEach(l => log(' ' + l));
|
|
10214
10413
|
_autoRoadmap(absRoot(root), 'data-change');
|
|
10215
10414
|
_emitHandoffNudge(root); // 1.36.8: 넛지 확장 — 작업 중 가장 자주 실행하는 명령(update)에도 노출
|
|
10216
10415
|
}
|
|
@@ -15093,20 +15292,21 @@ function _recommendAgent(task) {
|
|
|
15093
15292
|
if (!task || typeof task !== 'string') return { target: null, reason: '' };
|
|
15094
15293
|
const t = task.toLowerCase();
|
|
15095
15294
|
const hasAny = (keywords) => keywords.some(k => t.includes(k));
|
|
15096
|
-
//
|
|
15295
|
+
// 1.36.88: 사유 문구에서 **검증되지 않은 성능·품질 주장**을 제거한다("1.7× 빠름" · "가장 상세" · "정확").
|
|
15296
|
+
// leerness 는 이 수치를 측정한 적이 없고, 벤더 모델은 몇 달마다 바뀌어 순위 주장은 곧 낡는다.
|
|
15297
|
+
// 이 매핑은 **기본 관례**이지 벤치마크 결과가 아니며, 사유 문구가 그렇게 말해야 한다.
|
|
15298
|
+
// (근거 표시는 추후 난이도 라우팅 P-0007 의 감사기록에서 확장된다.)
|
|
15097
15299
|
if (hasAny(['translate', 'summary', 'explain', 'describe', 'analyze', 'review',
|
|
15098
15300
|
'번역', '요약', '설명', '분석', '리뷰'])) {
|
|
15099
|
-
return { target: 'claude', reason: '텍스트
|
|
15301
|
+
return { target: 'claude', reason: '키워드 매칭: 텍스트 분석·요약·번역 → claude (기본 관례이며 측정된 성능 비교 아님)' };
|
|
15100
15302
|
}
|
|
15101
|
-
// 깊은 코드 추론
|
|
15102
15303
|
if (hasAny(['architecture', 'design pattern', 'refactor', 'trace', 'complex', 'critical path',
|
|
15103
15304
|
'아키텍처', '리팩터', '복잡'])) {
|
|
15104
|
-
return { target: 'codex', reason: '깊은 코드
|
|
15305
|
+
return { target: 'codex', reason: '키워드 매칭: 깊은 코드 추론 → codex (기본 관례이며 측정된 품질 비교 아님)' };
|
|
15105
15306
|
}
|
|
15106
|
-
// 파일 작성·수정·생성
|
|
15107
15307
|
if (hasAny(['create', 'write', 'generate', 'patch', 'fix', 'implement', 'edit',
|
|
15108
15308
|
'구현', '생성', '작성', '수정', '추가'])) {
|
|
15109
|
-
return { target: 'agy', reason: '워크스페이스 직접
|
|
15309
|
+
return { target: 'agy', reason: '키워드 매칭: 워크스페이스 직접 수정 → agy --yolo (기본 관례이며 측정된 정확도 비교 아님)' };
|
|
15110
15310
|
}
|
|
15111
15311
|
return { target: null, reason: '' };
|
|
15112
15312
|
}
|
|
@@ -16463,12 +16663,18 @@ function _brainstormFor(root, topic) {
|
|
|
16463
16663
|
// 1.9.25: --include-code 옵션 — 소스 본문 검색 추가 (모순 감지 핵심)
|
|
16464
16664
|
if (has('--include-code')) {
|
|
16465
16665
|
const codeDirs = ['src', 'tests', 'bin', 'lib'];
|
|
16666
|
+
// 1.36.86 (검수 M3): 상한 20 이 **재귀 프레임만** 종료해 형제 디렉토리와 다음 codeDir 은 계속 훑었다
|
|
16667
|
+
// (실측: 30개 하위 디렉토리 → 30건 수집). 진입·복귀·바깥 루프 모두에서 전역 상한을 본다.
|
|
16668
|
+
const CODE_CAP = 20;
|
|
16466
16669
|
for (const dir of codeDirs) {
|
|
16670
|
+
if (hits.code.length >= CODE_CAP) break;
|
|
16467
16671
|
const dp = path.join(root, dir);
|
|
16468
16672
|
if (!exists(dp)) continue;
|
|
16469
16673
|
function walkCode(d) {
|
|
16674
|
+
if (hits.code.length >= CODE_CAP) return;
|
|
16470
16675
|
let entries; try { entries = fs.readdirSync(d, { withFileTypes: true }); } catch { return; }
|
|
16471
16676
|
for (const e of entries) {
|
|
16677
|
+
if (hits.code.length >= CODE_CAP) return;
|
|
16472
16678
|
const p = path.join(d, e.name);
|
|
16473
16679
|
if (e.isDirectory()) { walkCode(p); continue; }
|
|
16474
16680
|
if (!/\.(js|ts|jsx|tsx|gd|cs|py|rb|go|rs)$/i.test(e.name)) continue;
|
|
@@ -16481,7 +16687,7 @@ function _brainstormFor(root, topic) {
|
|
|
16481
16687
|
line: firstHit >= 0 ? firstHit + 1 : 0,
|
|
16482
16688
|
preview: firstHit >= 0 ? lines[firstHit].trim().slice(0, 120) : ''
|
|
16483
16689
|
});
|
|
16484
|
-
if (hits.code.length >=
|
|
16690
|
+
if (hits.code.length >= CODE_CAP) return; // 전역 상한 — 위 진입/복귀 검사와 함께 동작
|
|
16485
16691
|
}
|
|
16486
16692
|
}
|
|
16487
16693
|
}
|
|
@@ -16491,7 +16697,17 @@ function _brainstormFor(root, topic) {
|
|
|
16491
16697
|
return hits;
|
|
16492
16698
|
}
|
|
16493
16699
|
|
|
16494
|
-
|
|
16700
|
+
// 1.36.86: 모든 표면을 센다 — 이전엔 `lessons` 와 `archive` 가 빠져 있어 **JSON 이 hit 을 보여주면서 total=0** 을
|
|
16701
|
+
// 보고했다(자기모순). 사람용도 자체 인라인 합계를 쓰다 `code` 까지 빠뜨렸다 → 두 경로가 이 한 함수를 공유한다.
|
|
16702
|
+
function _brainstormTotal(h) {
|
|
16703
|
+
if (!h) return 0;
|
|
16704
|
+
const n = (x) => (x && x.length) || 0;
|
|
16705
|
+
const arc = h.archive ? Object.values(h.archive).reduce((s, v) => s + n(v), 0) : 0;
|
|
16706
|
+
// `lessons` 는 **evidence 의 파생 뷰**다 — 같은 블록을 실패 키워드로 한 번 더 투영한 것이라
|
|
16707
|
+
// 더하면 같은 자원을 두 번 센다(검수 확인: evidence=1/lessons=1 인데 total 이 2). 고유 자원 수를 뜻하도록 제외한다.
|
|
16708
|
+
return n(h.decisions) + n(h.skills) + n(h.tasks) + n(h.rules) + n(h.evidence)
|
|
16709
|
+
+ n(h.code) + n(h.skillHistory) + n(h.taskLogFails) + n(h.lessonsExplicit) + n(h.planMilestones) + arc;
|
|
16710
|
+
}
|
|
16495
16711
|
|
|
16496
16712
|
// 1.9.16: 워크스페이스 통합 brainstorm
|
|
16497
16713
|
function _brainstormWorkspace(rootBase, topic) {
|
|
@@ -16535,8 +16751,35 @@ function _brainstormWorkspace(rootBase, topic) {
|
|
|
16535
16751
|
}
|
|
16536
16752
|
// 1.9.25: 소스 코드 본문 hits
|
|
16537
16753
|
if (h.code && h.code.length) {
|
|
16538
|
-
|
|
16539
|
-
|
|
16754
|
+
const _s = Math.min(5, h.code.length);
|
|
16755
|
+
log(` 💻 코드 (${h.code.length}${h.code.length > _s ? ` — 상위 ${_s}건` : ''})`);
|
|
16756
|
+
h.code.slice(0, _s).forEach(c => log(` - ${c.file}:${c.line} — ${c.preview}`));
|
|
16757
|
+
}
|
|
16758
|
+
// 1.36.86 (검수 High#1): 아래 5개 표면은 **집계에는 들어가는데 렌더되지 않아**
|
|
16759
|
+
// "3건" 이라 적고 1건만 보여주는 상태였다(실측). 단일 프로젝트 경로와 같은 표면을 모두 보여준다.
|
|
16760
|
+
if (h.skillHistory && h.skillHistory.length) {
|
|
16761
|
+
log(` 📒 skill match 이력 (${h.skillHistory.length})`);
|
|
16762
|
+
h.skillHistory.slice(0, 3).forEach(x => log(` - skill-suggestions.md:${x.line || '?'} — [${x.at}] "${x.query}"`));
|
|
16763
|
+
}
|
|
16764
|
+
if (h.taskLogFails && h.taskLogFails.length) {
|
|
16765
|
+
log(` 📜 task-log 실패 (${h.taskLogFails.length})`);
|
|
16766
|
+
h.taskLogFails.slice(0, 3).forEach(x => log(` - task-log.md:${x.line || '?'} — ${x.title}`));
|
|
16767
|
+
}
|
|
16768
|
+
if (h.lessonsExplicit && h.lessonsExplicit.length) {
|
|
16769
|
+
log(` 💡 lessons (${h.lessonsExplicit.length})`);
|
|
16770
|
+
h.lessonsExplicit.slice(0, 3).forEach(x => log(` - lessons.md:${x.line || '?'} — ${x.title}`));
|
|
16771
|
+
}
|
|
16772
|
+
if (h.planMilestones && h.planMilestones.length) {
|
|
16773
|
+
log(` 🗺 plan milestones (${h.planMilestones.length})`);
|
|
16774
|
+
h.planMilestones.slice(0, 3).forEach(x => log(` - plan.md:${x.line || '?'} — ${x.id} ${x.title}`));
|
|
16775
|
+
}
|
|
16776
|
+
if (h.archive) {
|
|
16777
|
+
const _arc = Object.entries(h.archive).filter(([, v]) => v && v.length);
|
|
16778
|
+
const _arcN = _arc.reduce((s, [, v]) => s + v.length, 0);
|
|
16779
|
+
if (_arcN) {
|
|
16780
|
+
log(` 🗑 archive 후보 (${_arcN})`);
|
|
16781
|
+
_arc.forEach(([k, v]) => v.slice(0, 2).forEach(x => log(` - ${k}-archive.md:${x.line || '?'} — ${x.originalHeader || x.target || ''}`)));
|
|
16782
|
+
}
|
|
16540
16783
|
}
|
|
16541
16784
|
}
|
|
16542
16785
|
log(`\n## 📊 워크스페이스 총합: ${grandTotal}건 매치 (${paths.length} 프로젝트)${has('--include-code') ? ' (소스 코드 포함)' : ''}`);
|
|
@@ -16559,157 +16802,24 @@ function brainstormCmd(root, topic) {
|
|
|
16559
16802
|
log(`# Brainstorm — "${topic}"`);
|
|
16560
16803
|
log(`\n누적된 leerness 데이터에서 주제 관련 자원을 회수합니다.`);
|
|
16561
16804
|
|
|
16562
|
-
// 1.
|
|
16563
|
-
|
|
16564
|
-
|
|
16565
|
-
const
|
|
16566
|
-
|
|
16567
|
-
|
|
16568
|
-
|
|
16569
|
-
|
|
16570
|
-
|
|
16571
|
-
const
|
|
16572
|
-
|
|
16573
|
-
|
|
16574
|
-
|
|
16575
|
-
|
|
16576
|
-
|
|
16577
|
-
|
|
16578
|
-
|
|
16579
|
-
|
|
16580
|
-
// skills
|
|
16581
|
-
const skillsDir = path.join(root, '.harness/skills');
|
|
16582
|
-
if (exists(skillsDir)) {
|
|
16583
|
-
for (const id of fs.readdirSync(skillsDir)) {
|
|
16584
|
-
const f = path.join(skillsDir, id, 'skill.json');
|
|
16585
|
-
if (!exists(f)) continue;
|
|
16586
|
-
try {
|
|
16587
|
-
const s = JSON.parse(read(f));
|
|
16588
|
-
const text = JSON.stringify(s);
|
|
16589
|
-
if (matches(text)) hits.skills.push({ id, displayNameKo: s.displayNameKo, capabilities: s.capabilities, usage: s.usage });
|
|
16590
|
-
} catch {}
|
|
16591
|
-
}
|
|
16592
|
-
}
|
|
16593
|
-
// tasks (1.9.14: token 매칭, 1.9.15: 매치 필드 + 라인 번호)
|
|
16594
|
-
const rows = readProgressRows(root);
|
|
16595
|
-
const progressText = exists(progressPath(root)) ? read(progressPath(root)) : '';
|
|
16596
|
-
for (const r of rows) {
|
|
16597
|
-
const fields = [];
|
|
16598
|
-
if (matches(r.request)) fields.push('request');
|
|
16599
|
-
if (matches(r.evidence)) fields.push('evidence');
|
|
16600
|
-
if (matches(r.nextAction)) fields.push('nextAction');
|
|
16601
|
-
if (fields.length) {
|
|
16602
|
-
const idx = progressText.indexOf(`| ${r.id} |`);
|
|
16603
|
-
const lineNo = idx >= 0 ? progressText.slice(0, idx).split('\n').length : 0;
|
|
16604
|
-
hits.tasks.push({ ...r, _fields: fields, line: lineNo });
|
|
16605
|
-
}
|
|
16606
|
-
}
|
|
16607
|
-
// rules (1.9.15: 라인 번호)
|
|
16608
|
-
if (exists(rulesPath(root))) {
|
|
16609
|
-
const rulesText = read(rulesPath(root));
|
|
16610
|
-
for (const r of readRules(root)) {
|
|
16611
|
-
if (matches(r.rule)) {
|
|
16612
|
-
const idx = rulesText.indexOf(`| ${r.id} |`);
|
|
16613
|
-
const lineNo = idx >= 0 ? rulesText.slice(0, idx).split('\n').length : 0;
|
|
16614
|
-
hits.rules.push({ ...r, line: lineNo });
|
|
16615
|
-
}
|
|
16616
|
-
}
|
|
16617
|
-
}
|
|
16618
|
-
// evidence — lessons 키워드 (fail/롤백/incomplete) 동반 (1.9.15: 라인 번호)
|
|
16619
|
-
const ev = exists(evidencePath(root)) ? read(evidencePath(root)) : '';
|
|
16620
|
-
for (const { block: block, line: _ln } of _blocksWithOffset(ev, /\n(?=## )/)) {
|
|
16621
|
-
if (!block.startsWith('## ')) continue;
|
|
16622
|
-
if (matches(block)) {
|
|
16623
|
-
const t = (block.match(/^## (.+)$/m) || [, ''])[1];
|
|
16624
|
-
const lineNo = _ln;
|
|
16625
|
-
hits.evidence.push({ title: t.trim(), preview: block.slice(0, 200).replace(/\n+/g, ' '), line: lineNo });
|
|
16626
|
-
if (/✗|fail|롤백|incomplete|버그/i.test(block)) hits.lessons.push({ title: t.trim(), line: lineNo });
|
|
16627
|
-
}
|
|
16628
|
-
}
|
|
16629
|
-
// 1.9.72: skill-suggestions.md rolling history hits (이전 매칭 결과 회수)
|
|
16630
|
-
const histPath = path.join(root, '.harness', 'skill-suggestions.md');
|
|
16631
|
-
if (exists(histPath)) {
|
|
16632
|
-
const histTxt = read(histPath);
|
|
16633
|
-
let pos = 0;
|
|
16634
|
-
for (const { block: block, line: _ln } of _blocksWithOffset(histTxt, /\n(?=## )/)) {
|
|
16635
|
-
if (!block.startsWith('## ')) { pos += block.length + 1; continue; }
|
|
16636
|
-
const h = block.match(/^## ([\d-]+ [\d:]+) — query "([^"]+)"/);
|
|
16637
|
-
if (h && matches(block)) {
|
|
16638
|
-
const lineNo = _ln;
|
|
16639
|
-
hits.skillHistory.push({ at: h[1], query: h[2], preview: block.slice(0, 220).replace(/\n+/g, ' '), line: lineNo });
|
|
16640
|
-
}
|
|
16641
|
-
}
|
|
16642
|
-
}
|
|
16643
|
-
// 1.9.72: task-log.md 실패 라인 hits
|
|
16644
|
-
const tlogPath = path.join(root, '.harness', 'task-log.md');
|
|
16645
|
-
if (exists(tlogPath)) {
|
|
16646
|
-
const tlog = read(tlogPath);
|
|
16647
|
-
const lines = tlog.split('\n');
|
|
16648
|
-
for (let i = 0; i < lines.length; i++) {
|
|
16649
|
-
const line = lines[i];
|
|
16650
|
-
if (line.length > 4 && /✗|\bfail|롤백|재발|incomplete|버그/i.test(line) && matches(line)) {
|
|
16651
|
-
hits.taskLogFails.push({ title: line.replace(/^[-*]\s*/, '').slice(0, 100), line: i + 1 });
|
|
16652
|
-
}
|
|
16653
|
-
}
|
|
16654
|
-
}
|
|
16655
|
-
// 1.9.131: 3 archive 파일 hits (brainstormCmd 변종) — DELETE 5종 archive 도 brainstorm 후보로
|
|
16656
|
-
if (!hits.archive) hits.archive = { decisions: [], lessons: [], plan: [] };
|
|
16657
|
-
const archiveSources_bsCmd = [
|
|
16658
|
-
{ key: 'decisions', file: 'decisions.archive.md' },
|
|
16659
|
-
{ key: 'lessons', file: 'lessons.archive.md' },
|
|
16660
|
-
{ key: 'plan', file: 'plan.archive.md' }
|
|
16661
|
-
];
|
|
16662
|
-
for (const src of archiveSources_bsCmd) {
|
|
16663
|
-
const fp = path.join(root, '.harness', src.file);
|
|
16664
|
-
if (!exists(fp)) continue;
|
|
16665
|
-
const txt = read(fp);
|
|
16666
|
-
const blocks = _blocksWithOffset(txt, /\n(?=## 제거 )/);
|
|
16667
|
-
for (const { block: b, line: _ln } of blocks) {
|
|
16668
|
-
const m = b.match(/^## 제거 (\d{4}-\d{2}-\d{2})\s*\(target:\s*"([^"]*)"\)/);
|
|
16669
|
-
if (!m) continue;
|
|
16670
|
-
if (matches(b)) {
|
|
16671
|
-
const headerMatch = b.match(/^### (.+)$/m);
|
|
16672
|
-
const lineNo = _ln;
|
|
16673
|
-
hits.archive[src.key].push({
|
|
16674
|
-
date: m[1],
|
|
16675
|
-
target: m[2],
|
|
16676
|
-
originalHeader: headerMatch ? headerMatch[1].trim() : null,
|
|
16677
|
-
preview: b.slice(0, 220).replace(/\n+/g, ' '),
|
|
16678
|
-
line: lineNo
|
|
16679
|
-
});
|
|
16680
|
-
}
|
|
16681
|
-
}
|
|
16682
|
-
}
|
|
16683
|
-
// 1.9.116: lessons.md + plan.md milestone hits (Memory Surface 5종 완전 통합)
|
|
16684
|
-
if (!hits.lessonsExplicit) hits.lessonsExplicit = [];
|
|
16685
|
-
if (!hits.planMilestones) hits.planMilestones = [];
|
|
16686
|
-
const lp_b2 = lessonsPath(root);
|
|
16687
|
-
if (exists(lp_b2)) {
|
|
16688
|
-
const lessonsText = read(lp_b2);
|
|
16689
|
-
for (const { block: block, line: _ln } of _blocksWithOffset(lessonsText, /\n(?=### )/)) {
|
|
16690
|
-
if (!block.startsWith('### ')) continue;
|
|
16691
|
-
const lessonMatch = block.match(/- Lesson:[ \t]*(.+)/);
|
|
16692
|
-
if (lessonMatch && matches(block)) {
|
|
16693
|
-
const lineNo = _ln;
|
|
16694
|
-
hits.lessonsExplicit.push({ title: lessonMatch[1].trim().slice(0, 120), preview: block.slice(0, 220).replace(/\n+/g, ' '), line: lineNo });
|
|
16695
|
-
}
|
|
16696
|
-
}
|
|
16697
|
-
}
|
|
16698
|
-
const planFile_b2 = planPath(root);
|
|
16699
|
-
if (exists(planFile_b2)) {
|
|
16700
|
-
const planText = read(planFile_b2);
|
|
16701
|
-
const milestoneBlocks = _blocksWithOffset(planText, /\n(?=### M-\d{4,}\.)/);
|
|
16702
|
-
for (const { block: b, line: _ln } of milestoneBlocks) {
|
|
16703
|
-
const m = b.match(/^### (M-\d{4,})\.[ \t]*(.+?)$/m);
|
|
16704
|
-
if (m && matches(b)) {
|
|
16705
|
-
const lineNo = _ln;
|
|
16706
|
-
hits.planMilestones.push({ id: m[1], title: m[2].trim().slice(0, 120), preview: b.slice(0, 220).replace(/\n+/g, ' '), line: lineNo });
|
|
16707
|
-
}
|
|
16708
|
-
}
|
|
16709
|
-
}
|
|
16710
|
-
|
|
16711
|
-
const total = hits.decisions.length + hits.skills.length + hits.tasks.length + hits.rules.length + hits.evidence.length + (hits.skillHistory ? hits.skillHistory.length : 0) + (hits.taskLogFails ? hits.taskLogFails.length : 0) + (hits.lessonsExplicit ? hits.lessonsExplicit.length : 0) + (hits.planMilestones ? hits.planMilestones.length : 0);
|
|
16712
|
-
log(`\n📦 총 ${total}건 발견 (decisions ${hits.decisions.length} · skills ${hits.skills.length} · tasks ${hits.tasks.length} · rules ${hits.rules.length} · evidence ${hits.evidence.length}${hits.skillHistory && hits.skillHistory.length ? ` · skill-history ${hits.skillHistory.length}` : ''}${hits.taskLogFails && hits.taskLogFails.length ? ` · task-log-fails ${hits.taskLogFails.length}` : ''}${hits.lessonsExplicit && hits.lessonsExplicit.length ? ` · lessons ${hits.lessonsExplicit.length}` : ''}${hits.planMilestones && hits.planMilestones.length ? ` · plan ${hits.planMilestones.length}` : ''})`);
|
|
16805
|
+
// 1.36.86: 사람용도 **같은 수집기**(_brainstormFor)를 쓴다 — 이전엔 사람용이 자체 수집 코드를 갖고 있어
|
|
16806
|
+
// `--include-code` 에서 --json 은 code 1건, 사람용은 "없음" 을 내는 등 두 경로가 실제로 갈렸다(실측).
|
|
16807
|
+
// 수집은 하나, 렌더만 둘 — 구조적으로 드리프트가 불가능해진다(중복 ~145줄 제거).
|
|
16808
|
+
const hits = _brainstormFor(root, topic);
|
|
16809
|
+
// 1.36.86: 사람용 인라인 합계는 code(그리고 lessons/archive)를 빠뜨려 --json 과 달랐다 → 공유 함수 사용.
|
|
16810
|
+
const total = _brainstormTotal(hits);
|
|
16811
|
+
// 1.36.86: 내역에 code/lessons(과거 실패)/archive 를 포함한다 — 총계에는 들어가는데 내역에 없어
|
|
16812
|
+
// 사용자가 내역을 더하면 총계와 맞지 않았다(총 7건인데 내역 합 6).
|
|
16813
|
+
const _arcN = hits.archive ? Object.values(hits.archive).reduce((s, v) => s + ((v && v.length) || 0), 0) : 0;
|
|
16814
|
+
const _part = (label, n) => (n ? ` · ${label} ${n}` : '');
|
|
16815
|
+
log(`\n📦 총 ${total}건 발견 (decisions ${hits.decisions.length} · skills ${hits.skills.length} · tasks ${hits.tasks.length} · rules ${hits.rules.length} · evidence ${hits.evidence.length}`
|
|
16816
|
+
+ _part('code', hits.code && hits.code.length)
|
|
16817
|
+
+ _part('past-fails', hits.lessons && hits.lessons.length)
|
|
16818
|
+
+ _part('skill-history', hits.skillHistory && hits.skillHistory.length)
|
|
16819
|
+
+ _part('task-log-fails', hits.taskLogFails && hits.taskLogFails.length)
|
|
16820
|
+
+ _part('lessons', hits.lessonsExplicit && hits.lessonsExplicit.length)
|
|
16821
|
+
+ _part('plan', hits.planMilestones && hits.planMilestones.length)
|
|
16822
|
+
+ _part('archive', _arcN) + ')');
|
|
16713
16823
|
|
|
16714
16824
|
// 1.9.15: 모든 출력에 출처 파일:라인 표시
|
|
16715
16825
|
if (hits.decisions.length) {
|
|
@@ -16736,6 +16846,14 @@ function brainstormCmd(root, topic) {
|
|
|
16736
16846
|
log(`\n## ⚠ 같은 주제 과거 실패/롤백 (${hits.lessons.length}) — 같은 실수 방지`);
|
|
16737
16847
|
hits.lessons.slice(0, 5).forEach(l => log(` - .harness/review-evidence.md:${l.line || '?'} — ${l.title}`));
|
|
16738
16848
|
}
|
|
16849
|
+
// 1.36.86: `--include-code` 결과를 사람용에도 렌더한다 — 이전엔 --json 만 code 를 보여주고
|
|
16850
|
+
// 사람용은 같은 입력에 "관련 자원 없음" 을 출력했다(실측 확인된 두 경로 불일치).
|
|
16851
|
+
if (hits.code && hits.code.length) {
|
|
16852
|
+
// 1.36.86 (검수): 헤딩은 전체 건수인데 5건만 보여주면서 안내가 없어 **조용한 절단**이었다 — 표시 수를 명시한다.
|
|
16853
|
+
const _shown = Math.min(5, hits.code.length);
|
|
16854
|
+
log(`\n## 💻 관련 소스 코드 (${hits.code.length}${hits.code.length > _shown ? ` — 상위 ${_shown}건 표시` : ''}) — --include-code 매칭`);
|
|
16855
|
+
hits.code.slice(0, _shown).forEach(c => log(` - ${c.file}${c.line ? ':' + c.line : ''}${c.preview ? ' — ' + String(c.preview).slice(0, 100) : ''}`));
|
|
16856
|
+
}
|
|
16739
16857
|
// 1.9.72: skill-suggestions.md rolling history hits
|
|
16740
16858
|
if (hits.skillHistory.length) {
|
|
16741
16859
|
log(`\n## 📒 같은 주제 이전 skill match 이력 (${hits.skillHistory.length}) — 1.9.68 누적`);
|
|
@@ -23027,21 +23145,41 @@ function taskSyncCmd(root) {
|
|
|
23027
23145
|
catch (e) { fail(`JSON 파싱 실패: ${e.message}`); return process.exit(1); }
|
|
23028
23146
|
if (!Array.isArray(todos)) { fail('JSON 최상위는 배열이어야 함'); return process.exit(1); }
|
|
23029
23147
|
let imported = 0, updated = 0;
|
|
23148
|
+
// 1.36.88 (출하전 헌트 #1/#17): 이 경로가 bugfix 완료 게이트를 통째로 우회했다 — TodoWrite 를 completed 로
|
|
23149
|
+
// 바꿔 되돌려 넣는 것이 이 명령의 정규 사용법이라, 게이트가 막으려는 주체가 명령 하나로 무력화했다.
|
|
23150
|
+
// 게이트는 upsertProgress 안에서 던진다. 여기서는 **한 건이 막혀도 나머지는 처리**하고, 막힌 건을 보고한다.
|
|
23151
|
+
const blocked = [];
|
|
23030
23152
|
for (const t of todos) {
|
|
23031
23153
|
if (!t || !t.content) continue;
|
|
23032
23154
|
const status = t.status === 'completed' ? 'done' : t.status === 'in_progress' ? 'in-progress' : 'planned';
|
|
23033
23155
|
// 이미 같은 request 있는지
|
|
23034
23156
|
const existing = readProgressRows(root).find(r => r.request === t.content);
|
|
23035
|
-
|
|
23036
|
-
if (existing
|
|
23037
|
-
|
|
23038
|
-
|
|
23157
|
+
try {
|
|
23158
|
+
if (existing) {
|
|
23159
|
+
if (existing.status !== status) {
|
|
23160
|
+
upsertProgress(root, { id: existing.id, status });
|
|
23161
|
+
updated++;
|
|
23162
|
+
}
|
|
23163
|
+
} else {
|
|
23164
|
+
const id = nextId(root, 'T');
|
|
23165
|
+
upsertProgress(root, { id, status, request: t.content, evidence: 'todowrite-sync', nextAction: t.activeForm || '다음 액션' });
|
|
23166
|
+
imported++;
|
|
23039
23167
|
}
|
|
23040
|
-
}
|
|
23041
|
-
|
|
23042
|
-
|
|
23043
|
-
|
|
23168
|
+
} catch (e) {
|
|
23169
|
+
if (!e || e.name !== 'BugfixBlocked') throw e;
|
|
23170
|
+
blocked.push({ id: e.taskId, content: String(t.content).slice(0, 60), code: e.code, message: e.message });
|
|
23171
|
+
}
|
|
23172
|
+
}
|
|
23173
|
+
if (blocked.length) {
|
|
23174
|
+
if (has('--json')) {
|
|
23175
|
+
log(JSON.stringify({ ok: false, code: 'bugfix_blocked', imported, updated, blocked }, null, 2));
|
|
23176
|
+
process.exitCode = 1;
|
|
23177
|
+
return;
|
|
23044
23178
|
}
|
|
23179
|
+
fail(`${blocked.length}건이 bugfix 완료 게이트에 걸려 done 으로 반영되지 않았습니다`);
|
|
23180
|
+
blocked.forEach(b => log(` - ${b.id} ${b.content} — ${b.message}`));
|
|
23181
|
+
log(` ⓘ 탈출구: leerness bugfix drop <ID> · leerness toggle set bugfix-receipt off (프로젝트 정책)`);
|
|
23182
|
+
process.exitCode = 1;
|
|
23045
23183
|
}
|
|
23046
23184
|
log(`# leerness task sync (1.9.38)`);
|
|
23047
23185
|
log(`from: ${full}`);
|
|
@@ -24053,6 +24191,25 @@ async function main() {
|
|
|
24053
24191
|
}
|
|
24054
24192
|
return refereeCmd(arg('--path', process.cwd()), _rvToks[0], _rvToks.slice(1));
|
|
24055
24193
|
}
|
|
24194
|
+
// 1.36.87 (P-0005): bugfix 완료 영수증 — 값 플래그가 positional 로 새지 않게 원시 argv 파싱(referee/preview 와 동일 패턴)
|
|
24195
|
+
if (cmd === 'bugfix') {
|
|
24196
|
+
const _bfRaw = process.argv.slice(2); const _bfI = _bfRaw.indexOf('bugfix'); const _bfToks = [];
|
|
24197
|
+
for (let i = _bfI + 1; i < _bfRaw.length && _bfI >= 0; i++) {
|
|
24198
|
+
if (_bfRaw[i].startsWith('--')) { if (['--repro', '--expect-bad', '--root-cause', '--siblings', '--siblings-na', '--previous-gap', '--path'].includes(_bfRaw[i]) && _bfRaw[i + 1]) i++; continue; }
|
|
24199
|
+
_bfToks.push(_bfRaw[i]);
|
|
24200
|
+
}
|
|
24201
|
+
// 1.36.88 (출하전 헌트 #7): `bugfix list <path>` 가 위치 경로를 조용히 무시하고 cwd 를 읽어,
|
|
24202
|
+
// 등록된 probe 를 "없음"으로 보고했다 — 다른 명령들과 같은 규율로 위치 경로를 받는다.
|
|
24203
|
+
// (start/receipt/drop 의 첫 토큰은 task id 이므로, 경로로 오인하지 않도록 list/무인자에서만 적용)
|
|
24204
|
+
const _bfSub = _bfToks[0];
|
|
24205
|
+
const _bfRest = _bfToks.slice(1);
|
|
24206
|
+
let _bfRoot = arg('--path', null);
|
|
24207
|
+
if (!_bfRoot && (!_bfSub || _bfSub === 'list')) _bfRoot = _taskPositionalPath(['bugfix', ...(_bfSub ? [_bfSub] : []), ..._bfRest], _bfSub ? 2 : 1);
|
|
24208
|
+
// _withLock: 동시 등록이 서로를 지우면 게이트가 조용히 꺼진다(실측 4→2)
|
|
24209
|
+
// _taskExists: 존재하지 않는 id 로 등록되면 오타 하나로 "보호받는 줄 아는" 무보호 상태가 된다(출하전 헌트 #3)
|
|
24210
|
+
return require('../lib/bugfix').bugfixCmd(_bfRoot || process.cwd(), _bfSub, _bfRest,
|
|
24211
|
+
{ has, arg, _withLock, _taskExists: (r, id) => readProgressRows(r).some(x => x.id === id) });
|
|
24212
|
+
}
|
|
24056
24213
|
if (cmd === 'lens') return lensCmd(args[1]); // 1.18.3 (UR-0003): 분야별 자기질문 품질 렌즈
|
|
24057
24214
|
// 1.9.233: leerness commands — 카테고리화된 전체 CLI 명령 목록
|
|
24058
24215
|
if (cmd === 'commands') return commandsCmd(arg('--path', null) || _taskPositionalPath(args, 1) || process.cwd());
|
|
@@ -24327,7 +24484,16 @@ async function main() {
|
|
|
24327
24484
|
if (require.main === module) {
|
|
24328
24485
|
main()
|
|
24329
24486
|
.then(() => { if (process.exitCode && process.exitCode !== 0) process.exit(process.exitCode); })
|
|
24330
|
-
.catch(err => {
|
|
24487
|
+
.catch(err => {
|
|
24488
|
+
fail(err && err.message ? err.message : String(err));
|
|
24489
|
+
// 1.36.88: 완료 게이트는 upsertProgress 에서 던진다 — taskUpdate/taskSync 외의 경로로 올라와도
|
|
24490
|
+
// 스택 트레이스 대신 탈출구를 보여준다(막힌 사용자가 빠져나갈 방법을 알아야 한다).
|
|
24491
|
+
if (err && err.name === 'BugfixBlocked') {
|
|
24492
|
+
(err.lines || []).forEach(l => log(' ' + l));
|
|
24493
|
+
log(` ⓘ 탈출구: leerness bugfix drop ${err.taskId} (probe 등록 취소) · leerness toggle set bugfix-receipt off (프로젝트 정책)`);
|
|
24494
|
+
}
|
|
24495
|
+
process.exit(1);
|
|
24496
|
+
});
|
|
24331
24497
|
}
|
|
24332
24498
|
|
|
24333
24499
|
// 1.9.255: 테스트/디버그용 내부 함수 export (require.main 가드 덕분에 require 시 CLI 미실행).
|