leerness 1.36.41 → 1.36.43
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 +21 -0
- package/README.md +4 -4
- package/bin/leerness.js +202 -18
- package/package.json +1 -1
- package/scripts/e2e.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.36.43 — 2026-07-16 — `leerness enforce` — 사용 강제를 문서에서 커밋 관문으로 (사용자 명시: codex goal 모드가 leerness 미참조)
|
|
4
|
+
|
|
5
|
+
**배경(사용자 실측 버그)**: codex 의 goal 모드처럼 AGENTS.md/CLAUDE.md 를 읽지 않는 에이전트 경로에선 하네스 문서 수준의 지시가 무력하다. 실효 강제는 **어떤 에이전트/모드든 통과해야 하는 보편 관문 = git pre-commit** 에서.
|
|
6
|
+
|
|
7
|
+
- **`leerness enforce install|check|status|remove [--window 24] [--strict]`**: 최근 window 내 handoff 흔적(`last-handoff.json`) 없으면 **커밋 차단** + 정확한 회복 명령(`leerness handoff .`) 안내. `--strict` 는 gate 전체 통과까지 요구. 긴급 우회는 명시적 env(`LEERNESS_ENFORCE_BYPASS=1`) — 무언 우회 없음.
|
|
8
|
+
- **훅은 자기완결 sh**: 설치된 leerness 버전/PATH/네트워크 무의존(`find -mmin` mtime 판정) — 검증 중 **구버전 전역 CLI(1.36.41)가 enforce 를 몰라 오차단하는 실측 함정**을 밟고 재설계한 결과. 기존 pre-commit 훅은 체인 보존(`pre-commit.pre-leerness` 로 이동 후 호출, remove 시 복원).
|
|
9
|
+
- **init/migrate 시 git 저장소면 기본 자동 설치** (사용자 의도: 설치된 프로젝트는 강제) — 옵트아웃 `--no-enforce` / `LEERNESS_NO_ENFORCE=1` / `--minimal`.
|
|
10
|
+
- **실증 사슬 5/5**: 무handoff 커밋 차단 → handoff 후 통과+기존 훅 체인 실행 → 25h 경과 차단 → 명시 우회 → remove 시 기존 훅 복원. init 자동/비-git 무설치/옵트아웃/migrate 획득 4종 추가 실증.
|
|
11
|
+
- **검증**: selftest 313/313, exit 전파형 게이트 e2e(verify-claim 픽스처만 주제 무관 옵트아웃 명시), 게시본 클린룸.
|
|
12
|
+
|
|
13
|
+
## 1.36.42 — 2026-07-16 — 판정 정직화 배치 C — codex 5차 헌트 10건 전건 처분 완결 (#4~#10)
|
|
14
|
+
|
|
15
|
+
- **#4 agent-mode**: 3개 자식 전부 실패해도 "자율 라운드 진행 준비" 선언 + exit 0 — 전부 실패=exit 1, 부분 실패=⚠ 명시, tick/stop 자식 실패 전파, 미지 하위명령 exit 1.
|
|
16
|
+
- **#5 orchestrate**: `--agents NaN/0/-1` 이 조용히 1로 화하고 전원 실패에도 exit 0 → 정수 1~256 검증(`invalid_agents`) + 성공 0=exit 1.
|
|
17
|
+
- **#6 release bump/note**: "banana" 를 0.0.1 로 덮어쓰기 → semver 검증 후 거부(`invalid_version`, 원본 보존) · prerelease 는 npm 의미론(1.2.3-beta.2→1.2.3 안정화) · 멀티라인 노트가 CHANGELOG 에 가짜 버전 헤더를 만들던 것 → 한 리스트 항목으로 정규화.
|
|
18
|
+
- **#7 consistency merge-design-guide**: `--dry-run` 실기록 + 재실행마다 중복 append → dry-run 무기록 + 소스별 멱등(이미 병합 skip 표시).
|
|
19
|
+
- **#8 whats-new `--to`**: value-flag 미등록으로 값이 positional 경로로 흡수돼 **엉뚱한(패키지 내장) CHANGELOG 를 읽던 것** → 등록. 실측: --to 지정 시에도 로컬 CHANGELOG.
|
|
20
|
+
- **#9 lens 커스텀**: 손상 quality-lenses.json 무언 무시 → 경고 노출(advisory 유지) · `constructor` 도메인명이 프로토타입 상속 키로 오인돼 크래시 → 자기 소유 키 검사.
|
|
21
|
+
- **#10 register-pending `--json`**: 사람용 3줄이 JSON 앞에 섞여 파서 실패 → 단일 JSON 문서. (review/persona/guide 의 JSON 계약은 별도 라운드 — 출력 구조 재설계 필요라 이연 명기.)
|
|
22
|
+
- **검증**: selftest 312/312, 13케이스 일괄 실증 스크립트 전부 ✓, exit 전파형 게이트 e2e, 클린룸. **codex 5차 10건 전건 처분(수정 10, guide-json 만 부분 이연).**
|
|
23
|
+
|
|
3
24
|
## 1.36.41 — 2026-07-16 — 데이터 보존 3종 2차 — .mcp.json 손상 중단 · register-pending 락 · glossary 마커 보존 (codex 5차 헌트 #1/#2/#3 High)
|
|
4
25
|
|
|
5
26
|
5차 헌트(adapter/ci/orchestrate/guide/doctor/glossary/consistency/release/lens 표면)의 High 3건 — 전부 기존 결함 클래스의 새 발화점:
|
package/README.md
CHANGED
|
@@ -121,7 +121,7 @@ MIT
|
|
|
121
121
|
<!-- leerness:project-readme:start -->
|
|
122
122
|
## Leerness Project Harness
|
|
123
123
|
|
|
124
|
-
이 프로젝트는 Leerness v1.36.
|
|
124
|
+
이 프로젝트는 Leerness v1.36.43 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
|
|
125
125
|
|
|
126
126
|
### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
|
|
127
127
|
|
|
@@ -175,7 +175,7 @@ leerness memory restore decision <date|title>
|
|
|
175
175
|
|
|
176
176
|
### MCP server (외부 AI 통합)
|
|
177
177
|
|
|
178
|
-
Leerness v1.36.
|
|
178
|
+
Leerness v1.36.43는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **86개 도구**를 노출:
|
|
179
179
|
|
|
180
180
|
```jsonc
|
|
181
181
|
// 카테고리별
|
|
@@ -196,7 +196,7 @@ Leerness v1.36.41는 stdio JSON-RPC MCP server를 내장합니다 — Claude Cod
|
|
|
196
196
|
`<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
|
|
197
197
|
1) 다음 라운드 후보 선정 → 2) 코드 변경 → 3) 회귀 테스트 갱신 → 4) 전체 e2e 스위트 통과 → 5) npm publish + git tag → 6) main push → 7) session close → 8) 다음 라운드 예약.
|
|
198
198
|
|
|
199
|
-
현재 누적: **v1.9.x → 1.36.
|
|
199
|
+
현재 누적: **v1.9.x → 1.36.43 릴리스 태그 이력** (수백 라운드) · _reports/는 비공개 보존.
|
|
200
200
|
|
|
201
201
|
### 성능 가이드
|
|
202
202
|
|
|
@@ -234,6 +234,6 @@ leerness release pack --close --auto-main-push
|
|
|
234
234
|
- `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
|
|
235
235
|
- `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
|
|
236
236
|
|
|
237
|
-
Last synced by Leerness v1.36.
|
|
237
|
+
Last synced by Leerness v1.36.43: 2026-07-16
|
|
238
238
|
<!-- leerness:project-readme:end -->
|
|
239
239
|
|
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.43';
|
|
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') 시 호스트 프로세스 오염.
|
|
@@ -218,7 +218,7 @@ function _resolveRoot(positional) {
|
|
|
218
218
|
}
|
|
219
219
|
function nonFlagArgs() {
|
|
220
220
|
const out = [];
|
|
221
|
-
const withValue = new Set(['--language','--skills','--path','--status','--progress','--goal','--reason','--next','--target','--token-env','--package','--out','--from','--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']); // 1.14.2 (UR-0032): --done-when 값이 positional 로 누출돼 milestone 제목에 흡수되던 것 차단. 1.17.2 (UR-0045): --test-cmd 동일 원칙(신규 value-flag 는 반드시 여기 등록)
|
|
221
|
+
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']); // 1.14.2 (UR-0032): --done-when 값이 positional 로 누출돼 milestone 제목에 흡수되던 것 차단. 1.17.2 (UR-0045): --test-cmd 동일 원칙(신규 value-flag 는 반드시 여기 등록)
|
|
222
222
|
const a = process.argv.slice(2);
|
|
223
223
|
for (let i = 0; i < a.length; i++) {
|
|
224
224
|
const x = a[i];
|
|
@@ -1177,6 +1177,14 @@ async function install(root, opts = {}) {
|
|
|
1177
1177
|
if (!has('--no-auto-roadmap') && !opts.minimal) {
|
|
1178
1178
|
try { _autoRoadmap(root, 'install'); } catch (e) { warn('auto-roadmap 실패: ' + (e && e.message)); }
|
|
1179
1179
|
}
|
|
1180
|
+
// 1.36.43 (사용자 명시): git 저장소면 사용 강제(pre-commit) 기본 설치 — 문서를 읽지 않는 에이전트 경로(codex goal 모드 등)도
|
|
1181
|
+
// 커밋 관문에서 강제. 옵트아웃: --no-enforce / LEERNESS_NO_ENFORCE=1 / --minimal. 이미 설치돼 있으면 멱등(마커 확인).
|
|
1182
|
+
if (!has('--no-enforce') && process.env.LEERNESS_NO_ENFORCE !== '1' && !opts.minimal && exists(path.join(root, '.git'))) {
|
|
1183
|
+
try {
|
|
1184
|
+
const _hp2 = _gitHookPath(root);
|
|
1185
|
+
if (!exists(_hp2) || !read(_hp2).includes(_ENFORCE_MARK)) enforceCmd(root, 'install');
|
|
1186
|
+
} catch (e) { warn('enforce install skipped: ' + (e && e.message)); }
|
|
1187
|
+
}
|
|
1180
1188
|
// 1.9.148: 1.9.32 중복 prompt 제거 (사용자 명시 — CLI 에이전트 prompt 중복).
|
|
1181
1189
|
// resolveInstallOptions (1.9.146) 가 이미 모든 prompt 모은 위치에 통합된 4지선다 prompt 있음.
|
|
1182
1190
|
// 별도 setupAgents 명령은 사용자가 명시적으로 `leerness setup-agents` 호출 시에만.
|
|
@@ -2396,7 +2404,11 @@ function agentModeCmd(root, sub) {
|
|
|
2396
2404
|
const r3 = spawnChild(['handoff', root]);
|
|
2397
2405
|
if (r3.status === 0) log(gr(` ✓ handoff 출력 완료 (위 참조)`)); else log(yl(` ⚠ handoff 종료 ${r3.status}`));
|
|
2398
2406
|
log('');
|
|
2399
|
-
|
|
2407
|
+
// 1.36.42 (codex 5차 #4): 세 자식 전부 실패(status null 포함)여도 "준비" 를 선언하던 허위 성공 — 실패 수 기준 정직 판정.
|
|
2408
|
+
const _fails = [r1, r2, r3].filter(r => r.status !== 0).length;
|
|
2409
|
+
if (_fails === 3) { fail(`agent-mode start 실패 — 3개 단계 전부 실패 (경로/설치 확인: ${root})`); process.exitCode = 1; return; }
|
|
2410
|
+
if (_fails > 0) { log(yl(`⚠ agent-mode start 부분 완료 — ${_fails}개 단계 실패 (위 ⚠ 참조)`)); }
|
|
2411
|
+
else log(gr(`✓ agent-mode start 완료 — 자율 라운드 진행 준비`));
|
|
2400
2412
|
return;
|
|
2401
2413
|
}
|
|
2402
2414
|
if (sub === 'tick') {
|
|
@@ -2404,6 +2416,7 @@ function agentModeCmd(root, sub) {
|
|
|
2404
2416
|
log(cy(`# leerness agent-mode tick (1.9.239) — 가벼운 상태 확인`));
|
|
2405
2417
|
const r = spawnChild(['pulse', '--path', root]);
|
|
2406
2418
|
process.stdout.write(r.stdout || '');
|
|
2419
|
+
if (r.status !== 0) process.exitCode = 1; // 1.36.42 (#4): 자식 실패 전파
|
|
2407
2420
|
return;
|
|
2408
2421
|
}
|
|
2409
2422
|
if (sub === 'stop') {
|
|
@@ -2411,10 +2424,12 @@ function agentModeCmd(root, sub) {
|
|
|
2411
2424
|
log(dm(` → session close --auto-apply-delivered --auto-cleanup-branches --auto-fix-encoding`));
|
|
2412
2425
|
const r = spawnChild(['session', 'close', root, '--auto-apply-delivered', '--auto-cleanup-branches', '--auto-fix-encoding']);
|
|
2413
2426
|
process.stdout.write(r.stdout || '');
|
|
2427
|
+
if (r.status !== 0) process.exitCode = 1; // 1.36.42 (#4): 자식 실패 전파
|
|
2414
2428
|
return;
|
|
2415
2429
|
}
|
|
2416
|
-
|
|
2430
|
+
fail(`unknown subcommand: ${sub}`); // 1.36.42 (#4): 미지 하위명령 exit 1 (종전 exit 0)
|
|
2417
2431
|
log(dm(` → leerness agent-mode help`));
|
|
2432
|
+
process.exitCode = 1;
|
|
2418
2433
|
}
|
|
2419
2434
|
|
|
2420
2435
|
// 1.9.234: recentChanges — 최근 N 라운드의 핵심 변경 요약 (git tag commit subject)
|
|
@@ -3291,6 +3306,45 @@ function _selfTestCases() {
|
|
|
3291
3306
|
} catch {} finally { try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} }
|
|
3292
3307
|
return mcpGuard && rpLock && gOk;
|
|
3293
3308
|
} },
|
|
3309
|
+
{ name: '판정 정직화 배치C (1.36.42, codex 5차 #4~#10): agent-mode/orchestrate exit + release semver/note + consistency dry-run·멱등 + whats-new --to + lens 커스텀 + JSON 순수 (소스가드+행위)', run: () => {
|
|
3310
|
+
const s = read(__filename);
|
|
3311
|
+
const amOk = s.includes('if (_fails === 3) { fail(`agent-mode start 실패') && s.includes('미지 하위명령 exit 1');
|
|
3312
|
+
const orOk = s.includes("'invalid_agents'") && s.includes('if (!ok.length) process.exitCode = 1;');
|
|
3313
|
+
const relOk = s.includes("'invalid_version'") && s.includes('멀티라인은 한 리스트 항목의 연속 들여쓰기로 정규화');
|
|
3314
|
+
const toOk = s.includes("'--from','--to','--repo'");
|
|
3315
|
+
const conOk = s.includes('이미 병합됨(skip)') && /mergeDesign[\s\S]{0,600}has\('--dry-run'\)/.test(s);
|
|
3316
|
+
const lensOk = s.includes('function _loadProjectLensesEx') && s.includes('Object.prototype.hasOwnProperty.call(out, k)');
|
|
3317
|
+
const rpJsonOk = /registerPendingCmd[\s\S]{0,1600}if \(has\('--json'\)\) \{ log\(JSON\.stringify\(\{ ok: true, id, \.\.\.row \}, null, 2\)\); return; \}/.test(s);
|
|
3318
|
+
// 행위: prerelease 안정화 + 프로토타입 키 무크래시
|
|
3319
|
+
const p = require('../lib/pure-utils');
|
|
3320
|
+
const preOk = p.compareVer('1.2.3', '1.2.3-beta.2') === 1; // bump 결과가 상위인지 간접 확인
|
|
3321
|
+
let protoOk = false;
|
|
3322
|
+
try { const m = _mergeLensCatalog(LENS_CATALOG, { constructor: { title: 'C', persona: 'r', questions: ['q1'] } }); protoOk = !!(m.constructor && Array.isArray(m.constructor.questions) && m.constructor.questions[0] === 'q1'); } catch {}
|
|
3323
|
+
return amOk && orOk && relOk && toOk && conOk && lensOk && rpJsonOk && preOk && protoOk;
|
|
3324
|
+
} },
|
|
3325
|
+
{ name: 'enforce 사용 강제 (1.36.43, 사용자 명시 — codex goal 모드 미참조 버그): 자기완결 pre-commit 훅 + 체인 보존 + init 자동설치 — 행위검사', run: () => {
|
|
3326
|
+
const s = read(__filename);
|
|
3327
|
+
const wired = typeof enforceCmd === 'function' && s.includes("cmd === 'enforce'");
|
|
3328
|
+
// 훅 자기완결(구버전 전역 CLI 무의존 — 실측 함정): leerness 호출 없이 find -mmin 으로 판정
|
|
3329
|
+
const selfContained = s.includes('find "$LH" -mmin -"$WIN_MIN"') && s.includes('LEERNESS_ENFORCE_BYPASS');
|
|
3330
|
+
const chainOk = s.includes('pre-commit.pre-leerness');
|
|
3331
|
+
const initAuto = s.includes("!has('--no-enforce') && process.env.LEERNESS_NO_ENFORCE !== '1'");
|
|
3332
|
+
// 행위: 임시 git repo — install→(handoff 無)차단 메시지, mtime 신선→통과 (git 없으면 skip-통과)
|
|
3333
|
+
let behavOk = true;
|
|
3334
|
+
try {
|
|
3335
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_enf_'));
|
|
3336
|
+
const g = cp.spawnSync('git', ['-C', tmp, 'init'], { encoding: 'utf8', timeout: 10000 });
|
|
3337
|
+
if (g.status === 0) {
|
|
3338
|
+
fs.mkdirSync(path.join(tmp, '.harness'), { recursive: true });
|
|
3339
|
+
const save = process.argv; const _w = process.stdout.write;
|
|
3340
|
+
try { process.argv = ['node', 'h', 'enforce', 'install']; process.stdout.write = () => true; enforceCmd(tmp, 'install'); } finally { process.stdout.write = _w; process.argv = save; }
|
|
3341
|
+
const hk = fs.readFileSync(path.join(tmp, '.git', 'hooks', 'pre-commit'), 'utf8');
|
|
3342
|
+
behavOk = hk.includes(_ENFORCE_MARK) && hk.includes('find "$LH"');
|
|
3343
|
+
}
|
|
3344
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
3345
|
+
} catch { behavOk = false; }
|
|
3346
|
+
return wired && selfContained && chainOk && initAuto && behavOk;
|
|
3347
|
+
} },
|
|
3294
3348
|
{ name: 'debug 렌즈 (1.36.27, obra/superpowers systematic-debugging): 자기질문 6문항 ko/en 락스텝 + affects 유효 + route bugfix 힌트 + 파일매핑 미확장 — 행위검사', run: () => {
|
|
3295
3349
|
const d = LENS_CATALOG.debug;
|
|
3296
3350
|
if (!d) return false;
|
|
@@ -4966,6 +5020,102 @@ function shellGuardCmd(root, cmd, opts = {}) {
|
|
|
4966
5020
|
if (analysis.issues.some(i => i.severity === 'error')) process.exitCode = 1;
|
|
4967
5021
|
}
|
|
4968
5022
|
|
|
5023
|
+
// 1.36.43 (사용자 명시): leerness 사용 강제 — 문서(AGENTS.md) 지시는 codex goal 모드처럼 문서를 읽지 않는
|
|
5024
|
+
// 에이전트 경로에서 무력하다(사용자 실측 버그). 어떤 에이전트든 통과해야 하는 보편 관문 = git pre-commit 훅에서 강제:
|
|
5025
|
+
// 최근 handoff 흔적(기본 24h)이 없으면 커밋 차단 + 정확한 회복 명령 안내. --strict 는 gate 전체까지 요구.
|
|
5026
|
+
// 긴급 우회: LEERNESS_ENFORCE_BYPASS=1 (문서화된 명시 우회 — 무언 우회 아님).
|
|
5027
|
+
const _ENFORCE_MARK = '# leerness-enforce v1';
|
|
5028
|
+
function _enforceCfgPath(root) { return path.join(root, '.harness', 'enforce.json'); }
|
|
5029
|
+
function _enforceCfg(root) {
|
|
5030
|
+
const def = { windowHours: 24, strict: false };
|
|
5031
|
+
try { if (exists(_enforceCfgPath(root))) return Object.assign(def, JSON.parse(read(_enforceCfgPath(root)))); } catch {}
|
|
5032
|
+
return def;
|
|
5033
|
+
}
|
|
5034
|
+
function _gitHookPath(root) { return path.join(root, '.git', 'hooks', 'pre-commit'); }
|
|
5035
|
+
function enforceCmd(root, sub) {
|
|
5036
|
+
root = absRoot(root);
|
|
5037
|
+
const json = has('--json');
|
|
5038
|
+
sub = sub || 'status';
|
|
5039
|
+
const hookP = _gitHookPath(root);
|
|
5040
|
+
const chainedP = hookP + '.pre-leerness';
|
|
5041
|
+
if (sub === 'install') {
|
|
5042
|
+
if (!exists(path.join(root, '.git'))) { failJson(json, 'not_a_git_repo', `git 저장소가 아님: ${root} — pre-commit 강제는 git 저장소에서만 가능`); return; }
|
|
5043
|
+
if (!exists(path.join(root, '.harness'))) { failJson(json, 'harness_missing', `leerness 미설치: ${root} — 먼저 leerness init`); return; }
|
|
5044
|
+
const windowHours = Math.max(1, parseInt(arg('--window', '24'), 10) || 24);
|
|
5045
|
+
const strict = has('--strict');
|
|
5046
|
+
mkdirp(path.dirname(_enforceCfgPath(root)));
|
|
5047
|
+
writeUtf8(_enforceCfgPath(root), JSON.stringify({ windowHours, strict, installedAt: now() }, null, 2) + '\n');
|
|
5048
|
+
mkdirp(path.dirname(hookP));
|
|
5049
|
+
// 기존 훅 보존: 우리 것이 아니면 체인 파일로 이동 후 호출
|
|
5050
|
+
if (exists(hookP) && !read(hookP).includes(_ENFORCE_MARK)) {
|
|
5051
|
+
fs.copyFileSync(hookP, chainedP);
|
|
5052
|
+
try { fs.chmodSync(chainedP, 0o755); } catch {}
|
|
5053
|
+
}
|
|
5054
|
+
// 훅은 자기완결 sh — 설치된 leerness 버전/네트워크/PATH 에 무의존(구버전 전역 CLI 가 enforce 를 몰라 오차단하던 실측 함정 회피).
|
|
5055
|
+
// 핵심 체크 = last-handoff.json 의 mtime 이 window 안인지 (find -mmin, git bash 표준).
|
|
5056
|
+
const hook = [
|
|
5057
|
+
'#!/bin/sh',
|
|
5058
|
+
_ENFORCE_MARK + ' — leerness 사용 강제 (제거: leerness enforce remove)',
|
|
5059
|
+
'if [ "$LEERNESS_ENFORCE_BYPASS" = "1" ]; then exit 0; fi',
|
|
5060
|
+
`LH=".harness/last-handoff.json"`,
|
|
5061
|
+
`WIN_MIN=${windowHours * 60}`,
|
|
5062
|
+
'if [ ! -f "$LH" ] || [ -z "$(find "$LH" -mmin -"$WIN_MIN" 2>/dev/null)" ]; then',
|
|
5063
|
+
` echo "🔒 leerness 강제: 최근 ${windowHours}h 내 handoff 흔적 없음 — 커밋 전 실행: leerness handoff ." >&2`,
|
|
5064
|
+
' echo " 긴급 우회(명시): LEERNESS_ENFORCE_BYPASS=1 git commit ..." >&2',
|
|
5065
|
+
' exit 1',
|
|
5066
|
+
'fi',
|
|
5067
|
+
...(strict ? [
|
|
5068
|
+
'if command -v leerness >/dev/null 2>&1; then LEERNESS_INTERNAL=1 leerness gate . || exit 1;',
|
|
5069
|
+
'elif command -v npx >/dev/null 2>&1; then LEERNESS_INTERNAL=1 npx -y leerness gate . || exit 1; fi',
|
|
5070
|
+
] : []),
|
|
5071
|
+
'if [ -f "$(dirname "$0")/pre-commit.pre-leerness" ]; then sh "$(dirname "$0")/pre-commit.pre-leerness" "$@" || exit 1; fi',
|
|
5072
|
+
'',
|
|
5073
|
+
].join('\n');
|
|
5074
|
+
writeUtf8(hookP, hook);
|
|
5075
|
+
try { fs.chmodSync(hookP, 0o755); } catch {}
|
|
5076
|
+
if (json) { log(JSON.stringify({ ok: true, installed: true, windowHours, strict, hook: hookP }, null, 2)); return; }
|
|
5077
|
+
ok(`enforce 설치 — 커밋 전 leerness 사용(최근 ${windowHours}h 내 handoff${strict ? ' + gate 통과' : ''})을 git pre-commit 에서 강제`);
|
|
5078
|
+
log(` 대상: 어떤 에이전트/모드든(codex goal 모드 포함) — 커밋이 보편 관문이라 문서를 안 읽는 경로도 강제됨`);
|
|
5079
|
+
log(` 긴급 우회: LEERNESS_ENFORCE_BYPASS=1 git commit … · 해제: leerness enforce remove`);
|
|
5080
|
+
return;
|
|
5081
|
+
}
|
|
5082
|
+
if (sub === 'check') {
|
|
5083
|
+
if (!exists(path.join(root, '.harness'))) return; // leerness 미설치 저장소 — 강제 대상 아님(무음 통과)
|
|
5084
|
+
const cfg = _enforceCfg(root);
|
|
5085
|
+
const lhf = _lastHandoffPath(root);
|
|
5086
|
+
let lastAt = null;
|
|
5087
|
+
try { if (exists(lhf)) { const j = JSON.parse(read(lhf)); lastAt = j.last || (Array.isArray(j.history) && j.history.length ? j.history[j.history.length - 1] : null); } } catch {}
|
|
5088
|
+
const ageH = lastAt ? (Date.now() - new Date(lastAt).getTime()) / 3600000 : Infinity;
|
|
5089
|
+
if (ageH > cfg.windowHours) {
|
|
5090
|
+
fail(`🔒 leerness 강제: 최근 ${cfg.windowHours}h 내 handoff 흔적 없음${lastAt ? ` (마지막: ${lastAt})` : ''}`);
|
|
5091
|
+
log(` → 커밋 전 실행: leerness handoff . (컨텍스트 적재 + 사용 흔적)`);
|
|
5092
|
+
log(` → 긴급 우회(명시): LEERNESS_ENFORCE_BYPASS=1`);
|
|
5093
|
+
process.exitCode = 1; return;
|
|
5094
|
+
}
|
|
5095
|
+
if (cfg.strict) {
|
|
5096
|
+
const r = cp.spawnSync(process.execPath, [__filename, 'gate', root], { encoding: 'utf8', env: { ...process.env, LEERNESS_INTERNAL: '1' } });
|
|
5097
|
+
if (r.status !== 0) { fail(`🔒 leerness 강제(strict): gate 실패 — 커밋 차단`); process.stdout.write((r.stdout || '').slice(-1500)); process.exitCode = 1; return; }
|
|
5098
|
+
}
|
|
5099
|
+
if (!json) ok(`enforce check 통과 (handoff ${lastAt ? Math.round(ageH * 10) / 10 + 'h 전' : '-'}${cfg.strict ? ' + gate' : ''})`);
|
|
5100
|
+
return;
|
|
5101
|
+
}
|
|
5102
|
+
if (sub === 'remove') {
|
|
5103
|
+
if (exists(hookP) && read(hookP).includes(_ENFORCE_MARK)) {
|
|
5104
|
+
if (exists(chainedP)) { fs.copyFileSync(chainedP, hookP); fs.unlinkSync(chainedP); try { fs.chmodSync(hookP, 0o755); } catch {} }
|
|
5105
|
+
else fs.unlinkSync(hookP);
|
|
5106
|
+
ok('enforce 제거 — pre-commit 훅 해제' + (exists(hookP) ? ' (기존 훅 복원)' : ''));
|
|
5107
|
+
} else log('enforce 훅 없음 — 변경 없음');
|
|
5108
|
+
return;
|
|
5109
|
+
}
|
|
5110
|
+
// status
|
|
5111
|
+
const installed = exists(hookP) && read(hookP).includes(_ENFORCE_MARK);
|
|
5112
|
+
const cfg = _enforceCfg(root);
|
|
5113
|
+
if (json) { log(JSON.stringify({ installed, ...cfg }, null, 2)); return; }
|
|
5114
|
+
log(`# leerness enforce — 사용 강제 (git pre-commit)`);
|
|
5115
|
+
log(` 상태: ${installed ? `🟢 설치됨 (window ${cfg.windowHours}h${cfg.strict ? ' · strict=gate' : ''})` : '⚪ 미설치'}`);
|
|
5116
|
+
log(` ${installed ? '해제: leerness enforce remove' : '설치: leerness enforce install [--window 24] [--strict]'}`);
|
|
5117
|
+
}
|
|
5118
|
+
|
|
4969
5119
|
// 1.36.36 (도그푸딩 실측 후속): `leerness anchors [status|draft [--apply]]` — 정체성앵커(brief Purpose / plan Goal) 미작성 전환 지원.
|
|
4970
5120
|
// 실측: 1.36.19 감지 출하 후에도 실프로젝트 brief 4/7 · Goal 6/7 미작성 — 노출만으론 전환 안 됨. 초안 합성으로 마찰 제거.
|
|
4971
5121
|
// 안전: --apply 는 해당 섹션이 "미작성일 때만" 교체(작성된 내용 절대 불클로버), 초안 표식 주석 명시.
|
|
@@ -5579,12 +5729,18 @@ const LENS_CATALOG = {
|
|
|
5579
5729
|
};
|
|
5580
5730
|
// 1.19.3 (UR-0003 렌즈 완전판 v3): 프로젝트별 커스텀 렌즈 — .harness/quality-lenses.json 읽기-병합(쓰기 명령 없음, AI/사용자가 편집).
|
|
5581
5731
|
// 포맷: { "domains": { "code": { "questions": ["추가 질문"] }, "a11y": { "title":"접근성", "persona":"스크린리더 사용자", "questions":[...], "affects":["design"] } } }
|
|
5582
|
-
|
|
5732
|
+
// 1.36.42 (codex 5차 #9): 손상 파일을 무언 무시하면 사용자는 커스텀 렌즈가 적용된 줄 안다 — 에러를 함께 반환(Ex), 기존 시그니처는 래퍼.
|
|
5733
|
+
function _loadProjectLensesEx(root) {
|
|
5734
|
+
const p = path.join(absRoot(root || process.cwd()), '.harness', 'quality-lenses.json');
|
|
5735
|
+
if (!exists(p)) return { domains: {}, error: null };
|
|
5583
5736
|
try {
|
|
5584
|
-
const p = path.join(absRoot(root || process.cwd()), '.harness', 'quality-lenses.json');
|
|
5585
|
-
if (!exists(p)) return {};
|
|
5586
5737
|
const j = JSON.parse(read(p));
|
|
5587
|
-
return (j && typeof j === 'object' && j.domains && typeof j.domains === 'object') ? j.domains : {};
|
|
5738
|
+
return { domains: (j && typeof j === 'object' && j.domains && typeof j.domains === 'object') ? j.domains : {}, error: null };
|
|
5739
|
+
} catch (e) { return { domains: {}, error: `quality-lenses.json 파싱 실패(커스텀 렌즈 미적용): ${e.message}` }; }
|
|
5740
|
+
}
|
|
5741
|
+
function _loadProjectLenses(root) {
|
|
5742
|
+
try {
|
|
5743
|
+
return _loadProjectLensesEx(root).domains;
|
|
5588
5744
|
} catch { return {}; }
|
|
5589
5745
|
}
|
|
5590
5746
|
// 내장 + 프로젝트 커스텀 병합. 기존 도메인 → 질문 추가(dedup, 최대 16 — 내장 최대치(database 12) + 커스텀 여유; 낮으면 내장이 잘림); 신규 도메인 → 기본값으로 추가. _custom 플래그로 표시.
|
|
@@ -5594,7 +5750,9 @@ function _mergeLensCatalog(builtin, custom) {
|
|
|
5594
5750
|
for (const [k, c] of Object.entries(custom || {})) {
|
|
5595
5751
|
if (!c || typeof c !== 'object') continue;
|
|
5596
5752
|
const cq = Array.isArray(c.questions) ? c.questions.filter(q => typeof q === 'string' && q.trim()) : [];
|
|
5597
|
-
|
|
5753
|
+
// 1.36.42 (codex 5차 #9b): truthy 검사는 'constructor' 같은 프로토타입 키에서 상속 함수를 기존 도메인으로
|
|
5754
|
+
// 오인해 .questions.push 크래시 — 자기 소유 키만 병합 대상으로.
|
|
5755
|
+
if (Object.prototype.hasOwnProperty.call(out, k)) {
|
|
5598
5756
|
const seen = new Set(out[k].questions);
|
|
5599
5757
|
for (const q of cq) if (!seen.has(q)) { out[k].questions.push(q); seen.add(q); }
|
|
5600
5758
|
const before = out[k].questions.length;
|
|
@@ -5678,6 +5836,8 @@ function lensCmd(domain, opts = {}) {
|
|
|
5678
5836
|
if (domain != null) domain = String(domain).trim().toLowerCase();
|
|
5679
5837
|
// 1.19.3: 내장 + 프로젝트 커스텀(.harness/quality-lenses.json) 병합 catalog.
|
|
5680
5838
|
const root = opts.root || arg('--path', process.cwd());
|
|
5839
|
+
// 1.36.42 (#9): 손상 커스텀 렌즈 파일은 무언 무시 대신 경고 (advisory — lens 자체는 계속 동작)
|
|
5840
|
+
try { const _lx = _loadProjectLensesEx(root); if (_lx.error && !jsonMode) warn(_lx.error); } catch {}
|
|
5681
5841
|
// 1.36.30: 토글 준수 (⚙ 탭 / leerness toggle set lens off)
|
|
5682
5842
|
if (!_tgl.toggleOn(root, 'lens')) {
|
|
5683
5843
|
if (jsonMode) { log(JSON.stringify({ ok: true, skipped: true, reason: 'toggle_off', hint: 'leerness toggle set lens on' })); return; }
|
|
@@ -12393,7 +12553,11 @@ async function orchestrateCmd(root, goalParts) {
|
|
|
12393
12553
|
return process.exit(1);
|
|
12394
12554
|
}
|
|
12395
12555
|
|
|
12396
|
-
|
|
12556
|
+
// 1.36.42 (codex 5차 #5): NaN/0/-1 이 조용히 1(또는 NaN 루프)로 화하던 것 — 정수 1..256 검증.
|
|
12557
|
+
const _agRaw = arg('--agents', '4');
|
|
12558
|
+
const _agN = Number(_agRaw);
|
|
12559
|
+
if (!Number.isInteger(_agN) || _agN < 1 || _agN > 256) { failJson(has('--json'), 'invalid_agents', `--agents 는 1~256 정수여야 합니다 (받음: ${_agRaw})`); return; }
|
|
12560
|
+
const agentCount = _agN;
|
|
12397
12561
|
const model = arg('--model', process.env.LEERNESS_OLLAMA_MODEL || 'qwen2.5:7b-instruct');
|
|
12398
12562
|
const timeoutMs = parseInt(arg('--timeout', '300000'), 10);
|
|
12399
12563
|
const retryOnFail = parseInt(arg('--retry-on-fail', '0'), 10); // 1.9.22 후보 2 통합
|
|
@@ -12428,6 +12592,7 @@ async function orchestrateCmd(root, goalParts) {
|
|
|
12428
12592
|
if (failures.length) {
|
|
12429
12593
|
for (const f of failures.slice(0, 3)) log(` · agent ${f.agent}: ${f.error}`);
|
|
12430
12594
|
}
|
|
12595
|
+
if (!ok.length) process.exitCode = 1; // 1.36.42 (#5): 전원 실패인데 exit 0 이던 허위 성공 — 성공 0 = exit 1
|
|
12431
12596
|
|
|
12432
12597
|
if (ok.length) {
|
|
12433
12598
|
const totalPromptTokens = ok.reduce((a, b) => a + b.promptTokens, 0);
|
|
@@ -14014,10 +14179,11 @@ function registerPendingCmd(root, requestParts) {
|
|
|
14014
14179
|
return rec;
|
|
14015
14180
|
});
|
|
14016
14181
|
const id = row.id;
|
|
14182
|
+
// 1.36.42 (codex 5차 #10): --json 인데 사람용 3줄이 앞에 섞여 파서가 깨짐 — JSON 모드는 단일 JSON 문서만.
|
|
14183
|
+
if (has('--json')) { log(JSON.stringify({ ok: true, id, ...row }, null, 2)); return; }
|
|
14017
14184
|
log(`✓ ${id} 등록됨 (in-progress) by ${agent}`);
|
|
14018
14185
|
log(` request: ${row.request}`);
|
|
14019
14186
|
log(` 💡 작업 완료 후: leerness task update ${id} --status done --evidence "..."`);
|
|
14020
|
-
if (has('--json')) log(JSON.stringify({ ok: true, id, ...row }, null, 2));
|
|
14021
14187
|
}
|
|
14022
14188
|
|
|
14023
14189
|
// 1.9.22 후보 4: llm-bench record + retro 통합
|
|
@@ -14060,10 +14226,20 @@ function mergeDesign(root) {
|
|
|
14060
14226
|
root = absRoot(root);
|
|
14061
14227
|
const canonical = path.join(root,'.harness/design-system.md');
|
|
14062
14228
|
const cands = ['designguide.md','design-guide.md','.harness/designguide.md','docs/designguide.md','docs/design-guide.md'];
|
|
14229
|
+
// 1.36.42 (codex 5차 #7): (a) --dry-run 인데 실기록, (b) 재실행마다 같은 내용 중복 append(비멱등) — 둘 다 교정.
|
|
14230
|
+
const dry = has('--dry-run');
|
|
14231
|
+
const already = exists(canonical) ? read(canonical) : '';
|
|
14063
14232
|
let merged = '';
|
|
14064
|
-
|
|
14233
|
+
const skipped = [];
|
|
14234
|
+
for (const f of cands) {
|
|
14235
|
+
const p = path.join(root,f);
|
|
14236
|
+
if (!exists(p)) continue;
|
|
14237
|
+
if (already.includes(`## Merged from ${f}`)) { skipped.push(f); continue; } // 멱등: 같은 소스 재병합 방지
|
|
14238
|
+
merged += `\n\n## Merged from ${f}\n\n` + read(p);
|
|
14239
|
+
}
|
|
14240
|
+
if (dry) { ok(`[dry-run] ${merged ? '병합 예정' : '병합 대상 없음'}${skipped.length ? ` · 이미 병합됨(skip): ${skipped.join(', ')}` : ''} — 실제 변경 0`); return; }
|
|
14065
14241
|
if (merged) append(canonical, merged);
|
|
14066
|
-
ok(merged ?
|
|
14242
|
+
ok(merged ? `design guides merged into .harness/design-system.md${skipped.length ? ` (이미 병합 skip: ${skipped.join(', ')})` : ''}` : (skipped.length ? `nothing to merge (이미 전부 병합됨: ${skipped.join(', ')})` : 'nothing to merge'));
|
|
14067
14243
|
}
|
|
14068
14244
|
|
|
14069
14245
|
// 1.9.2: self check를 update --check의 thin wrapper로 통합 (단일 출처).
|
|
@@ -15526,12 +15702,16 @@ function releaseBump(root) {
|
|
|
15526
15702
|
if (!exists(pkgFile)) return fail('package.json 없음');
|
|
15527
15703
|
let pkg; try { pkg = JSON.parse(read(pkgFile)); } catch (e) { return fail('package.json 파싱 실패: ' + e.message); }
|
|
15528
15704
|
const cur = String(pkg.version || '0.0.0');
|
|
15529
|
-
|
|
15530
|
-
|
|
15705
|
+
// 1.36.42 (codex 5차 #6): (a) 비 semver("banana")를 0.0.0 으로 오인해 0.0.1 로 덮어쓰던 것 → 거부.
|
|
15706
|
+
// (b) prerelease patch bump 는 npm 의미론(1.2.3-beta.2 → 1.2.3: pre 태그 제거=안정화)을 따름.
|
|
15707
|
+
const _vm = cur.match(/^(\d+)\.(\d+)\.(\d+)(-[0-9A-Za-z.-]+)?$/);
|
|
15708
|
+
if (!_vm) { failJson(has('--json'), 'invalid_version', `package.json version 이 semver 가 아님: "${cur}" — bump 거부(수동 교정 필요)`); return; }
|
|
15709
|
+
const [maj, min, pat] = [parseInt(_vm[1], 10), parseInt(_vm[2], 10), parseInt(_vm[3], 10)];
|
|
15710
|
+
const isPre = !!_vm[4];
|
|
15531
15711
|
let next;
|
|
15532
|
-
if (kind === 'major') next = `${maj + 1}.0.0`;
|
|
15533
|
-
else if (kind === 'minor') next = `${maj}.${min + 1}.0`;
|
|
15534
|
-
else next = `${maj}.${min}.${pat + 1}`;
|
|
15712
|
+
if (kind === 'major') next = isPre && min === 0 && pat === 0 ? `${maj}.0.0` : `${maj + 1}.0.0`;
|
|
15713
|
+
else if (kind === 'minor') next = isPre && pat === 0 ? `${maj}.${min}.0` : `${maj}.${min + 1}.0`;
|
|
15714
|
+
else next = isPre ? `${maj}.${min}.${pat}` : `${maj}.${min}.${pat + 1}`;
|
|
15535
15715
|
pkg.version = next;
|
|
15536
15716
|
writeUtf8(pkgFile, JSON.stringify(pkg, null, 2) + '\n');
|
|
15537
15717
|
const hv = path.join(root, '.harness/HARNESS_VERSION');
|
|
@@ -15542,6 +15722,9 @@ function releaseBump(root) {
|
|
|
15542
15722
|
function releaseNote(root, text) {
|
|
15543
15723
|
root = absRoot(root);
|
|
15544
15724
|
if (!text) return fail('note text required (e.g., release note "내용")');
|
|
15725
|
+
// 1.36.42 (codex 5차 #6b): 개행 포함 노트가 CHANGELOG 에 가짜 버전 헤더("## 9.9.9 …")를 만들 수 있었음 —
|
|
15726
|
+
// 멀티라인은 한 리스트 항목의 연속 들여쓰기로 정규화(헤더 문자로 시작하는 줄도 텍스트로 유지).
|
|
15727
|
+
text = String(text).replace(/\r\n?/g, '\n').split('\n').map((l, i) => i === 0 ? l : ' ' + l.replace(/^#+\s*/, '')).join('\n');
|
|
15545
15728
|
const pkgFile = path.join(root, 'package.json');
|
|
15546
15729
|
let version = 'unknown';
|
|
15547
15730
|
if (exists(pkgFile)) { try { version = JSON.parse(read(pkgFile)).version || 'unknown'; } catch {} }
|
|
@@ -21993,6 +22176,7 @@ async function main() {
|
|
|
21993
22176
|
if (cmd === 'milestones') return milestonesCmd(_resolveRoot(args[1]));
|
|
21994
22177
|
// 1.9.231: leerness pulse — 한 줄 종합 요약 (10 핵심 지표)
|
|
21995
22178
|
if (cmd === 'pulse') return pulseCmd(_resolveRoot(args[1]));
|
|
22179
|
+
if (cmd === 'enforce') return enforceCmd(arg('--path', null) || _taskPositionalPath(args, 2) || process.cwd(), args[1]); // 1.36.43: 사용 강제 (git pre-commit)
|
|
21996
22180
|
if (cmd === 'anchors') return anchorsCmd(arg('--path', null) || _taskPositionalPath(args, 1) || process.cwd(), args[1] && !args[1].startsWith('-') ? args[1] : null); // 1.36.36: 정체성앵커 초안
|
|
21997
22181
|
if (cmd === 'toggle') return _tgl.toggleCmd(arg('--path', process.cwd()), args[1], args[2], args[3], { has, VERSION }); // 1.36.30: 기능 토글 (그래프 ⚙ 탭 연동)
|
|
21998
22182
|
if (cmd === 'lens') return lensCmd(args[1]); // 1.18.3 (UR-0003): 분야별 자기질문 품질 렌즈
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leerness",
|
|
3
|
-
"version": "1.36.
|
|
3
|
+
"version": "1.36.43",
|
|
4
4
|
"description": "The AI-coding operations layer that makes \"done\" require evidence — persistent memory, evidence-gated completion checks, and clean handoffs for any AI agent (Claude Code, Codex, Cursor). State lives as plain files in your repo. CLI + MCP, 0 runtime dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"leerness",
|
package/scripts/e2e.js
CHANGED
|
@@ -3386,7 +3386,7 @@ total++;
|
|
|
3386
3386
|
const gi = git('init');
|
|
3387
3387
|
if (gi.status !== 0) throw new Error('git 없음'); // git 미설치 환경 → skip(아래 catch 로 ok=false 방지 위해 통과 처리)
|
|
3388
3388
|
git('config', 'user.email', 't@t'); git('config', 'user.name', 't');
|
|
3389
|
-
cp.spawnSync(process.execPath, [CLI, 'init', vDir, '--yes', '--language', 'ko', '--skills', 'recommended'], { encoding: 'utf8', timeout: 30000 });
|
|
3389
|
+
cp.spawnSync(process.execPath, [CLI, 'init', vDir, '--yes', '--language', 'ko', '--skills', 'recommended', '--no-enforce'], { encoding: 'utf8', timeout: 30000 }); // 1.36.43: 이 케이스 주제는 verify-claim — enforce 훅(handoff 없인 커밋 차단)은 옵트아웃
|
|
3390
3390
|
fs.mkdirSync(path.join(vDir, 'src'), { recursive: true });
|
|
3391
3391
|
fs.writeFileSync(path.join(vDir, 'src', 'api.js'), 'v1'); fs.writeFileSync(path.join(vDir, 'old.js'), 'old');
|
|
3392
3392
|
git('add', '-A'); git('commit', '-m', 'init');
|