leerness 1.36.77 → 1.36.79
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 +28 -0
- package/README.md +4 -4
- package/bin/leerness.js +194 -50
- package/lib/agents.js +14 -1
- package/lib/audit.js +19 -0
- package/lib/team.js +31 -11
- package/lib/tech-profile.js +12 -3
- package/package.json +1 -1
- package/scripts/e2e.js +163 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.36.79 — 2026-07-28 — 실 프로젝트 60개 도그푸딩: P1 5건 (합성 헌트 10회가 못 본 결함)
|
|
4
|
+
|
|
5
|
+
사용자의 **실제 프로젝트 60개**(v0.1.40 ~ 1.36.77)를 5개 렌즈로 병렬 감사(읽기 전용 + temp 복사본 실험). 합성 픽스처 기반 codex 헌트 10라운드가 한 번도 못 본 P1 5건이 나왔다 — 실데이터에서만 발화하는 조건들이었다.
|
|
6
|
+
|
|
7
|
+
- **P1 progress-tracker 행 무단 삭제 (실 데이터 손실)**: ID 접두를 `T|M|D` 로만 인정해 `R-`/`UR-` 등 다른 접두 행이 파싱에서 탈락 → 재작성 시 **사용자 이력이 조용히 삭제**됐다(실측: auto-Trading 9행 → 1행, 942B, exit 0 무경고). migrate 만이 아니라 `task add` 등 모든 쓰기 경로 공통. 접두 일반화 + **클래스 가드**(디스크 행 수 > 쓰려는 행 수면 쓰기 중단) — 규칙 하나가 아니라 불변식으로 차단.
|
|
8
|
+
- **P1 `handoff --json` 이 사람용 drift 배너로 오염**: stale 프로젝트에서만 발화해(실 프로젝트 27/37 해당) JSON.parse 가 실패, MCP/AI 소비자가 전부 깨졌다 → `--json` 이면 배너 억제(텍스트 모드는 유지).
|
|
9
|
+
- **P1 `parent detect <path>` 가 positional 경로 무시**: cwd 를 분석해 자신만만한 오답을 exit 0 으로 반환(positional path 잔존 클래스의 미소진 지점).
|
|
10
|
+
- **P1 tech 언어 오판**: devDependency 1개짜리 package.json 이 **205개 .py 파일을 이겨** Python 모노레포를 "javascript" 로 라벨링 → 실제 소스 파일 수를 1순위 근거로 재정렬 + 스캔 깊이 2→3(3단 레이아웃 누락 해소). 진짜 JS 프로젝트는 무회귀.
|
|
11
|
+
- **P1 audit 이 파괴된 설치를 healthy 로 보고**: 56개 중 44개가 사라진 설치에 `healthy:true·failures:0`(같은 디렉토리에서 integrity 는 `ok:false` — 두 판정 모순) → integrity 의 관리문서 부재/손상 신호를 audit 이 공유(단일 진실). 3건 이상 부재는 차단 등급.
|
|
12
|
+
- (문서) `doctor` 는 설계상 CLI 설치 진단이라 경로 인자가 없다 — usage 2곳의 잘못된 `doctor [path]` 광고 정정.
|
|
13
|
+
- **(R-0001 검수 21회전이 제 수정에서 P1급 5건을 더 차단)**: ① 클래스 가드의 기준선이 틀렸다 — "변이 후 행 수" 비교는 upsert 가 행을 추가하면 손실을 상쇄해 무력화(실측) → **"디스크에 파서가 못 읽는 행-모양 라인이 있는가"**로 재설계. ② `allowShrink` boolean 우회가 가드를 통째로 꺼 dedup 중 손상 행까지 삭제 → **expectedShrink 수량제**로. ③ 접두 전면 개방(`[A-Z]{1,3}`)이 무관한 참조표(`| API-123 | owner |`)를 task 로 흡수 → **등록 네임스페이스(T/M/D/R/UR/F/P)** 한정. ④ 표 **뒤 사용자 콘텐츠**(참조표·메모·푸터)가 재작성 때 삭제되던 것 → 후행 보존. ⑤ audit 이 integrity 임계값을 재구현해 판정이 갈림 → **단일 판정기 공유**. ⑥ tech 디렉토리당 400개 컷이 집계를 편향(1501 JS < 500 PY) → 상한 5000 + 전체 예산 20000. ⑦ parent 가 맨 상대경로를 무시하고 없는 경로도 성공 → 위치인자 파싱 + 존재 검증.
|
|
14
|
+
- 검증: P1 5건 + 검수 7건 재현→수정→재확인 실측(무회귀 포함), selftest 334, e2e +1(10 단언).
|
|
15
|
+
|
|
16
|
+
## 1.36.78 — 2026-07-28 — codex 10차 홀리스틱 헌트: 오케스트레이션/분석 명령군 7건 수정 (병렬 적대검증 8건 중 7 CONFIRMED·1 REFUTED)
|
|
17
|
+
|
|
18
|
+
표면 회전으로 team/agents·constraints/intent·contract·milestones 를 헌트. 8건 보고를 8개 독립 에이전트로 병렬 재현(워크플로) → 7 CONFIRMED 수정, 1 REFUTED(pulse).
|
|
19
|
+
|
|
20
|
+
- **F1 team 스토어 형상 무효 → 데이터 손실**: 객체 루트(`{teams:[...]}`)/문자열 members 를 조용히 `[]`로 coerce → list 는 0건(무경고), 이어지는 add 가 원본을 통째로 덮었다. previews/state 규율 적용 — 로더가 invalid 반환, 변경 거부(원본 보존), 유효 항목 members/personas 배열 정규화(string→[] 크래시 제거).
|
|
21
|
+
- **F2 constraints 무효/중복 저장**: `--constraint` 가 `kind:detail` 형식 위반(콜론 없음/빈 kind/빈 detail)이면 조용히 버려지고 exit 0 + 빈 플랫폼을 쓰던 거짓 성공 → 저장 없이 거부. 동일 {kind,detail} 중복 누적 방지(멱등).
|
|
22
|
+
- **F3 손상 constraints 읽기가 기본값을 권위처럼 노출**: list/check 가 손상 store 를 조용히 defaults 로 폴백 → `corruptStore` 표기 + 경고(커스텀 제약 미반영 명시). add 는 이미 fail-closed.
|
|
23
|
+
- **F4 team --json 계약/exit 불일치**: add/remove/show/preview/deploy 의 --json 이 사람용 텍스트를 뱉고, deploy 는 사람모드 exit 1 vs --json exit 0 이던 것 → 전 서브명령 구조화 JSON + exit 일치.
|
|
24
|
+
- **F5 광고된 `agents recommend` 미구현**: review-request 가 안내하는 명령이 핸들러 부재로 usage(exit 1)만 뱉던 것 → 내부 `_recommendAgent` 를 정식 서브명령으로 노출(구조화 JSON).
|
|
25
|
+
- **F6 빈 contract 공허 통과**: 선언 0건 spec 이 항상 `ok:true`(CI 에서 빈/오탈자 spec 통과 위험) → 검증 불가로 거부, 의도적이면 `--allow-empty`.
|
|
26
|
+
- **F8 1태그 ETA 조작**: 태그 1개면 경과 구간이 없어 속도 관측 불가인데 `days=max(1,0)` 로 avg=1 을 지어내 구체 ETA 를 보였다 → 유효 태그 2+ & 양의 구간일 때만 속도/ETA(아니면 avg=0·eta=null). milestones + round-history 양쪽.
|
|
27
|
+
- **F7 REFUTED**: pulse 는 `.json` 만 손상시켜도 MD 프로젝션 폴백이 정확한 카운트를 복구 — 4파일 모두 지워야 0(진짜 빈 상태). 설계가 이미 옳아 무변경.
|
|
28
|
+
- **(R-0001 검수 20회전이 제 수정의 불완전성 3건 차단)**: ① team add 락 획득 후 재로드가 여전히 무효→[] coerce 라 대기 중 스토어가 무효로 바뀌면 덮어썼다 → 락내 재로드도 checked. ② deploy execute --json 이 gate JSON 후 사람 로그+inherited child stdout 을 뱉어 exact-once JSON 위반 → 자식 출력 캡처 후 단일 JSON. ③ `typeof []==='object'` 라 배열형 platforms(`{"platforms":[]}`)가 손상으로 안 잡혀 add 가 덮어썼다 → 배열 명시 배제 + add 도 checked 로더 fail-closed.
|
|
29
|
+
- 검증: 7건 재현→수정→재확인 실측(F8 은 실 git repo) + 검수 3건 재확인, selftest 334, e2e +1(15 단언).
|
|
30
|
+
|
|
3
31
|
## 1.36.77 — 2026-07-25 — UR-0061/0066 MCP 완결: leerness_clarify + leerness_preview (86→88 도구)
|
|
4
32
|
|
|
5
33
|
clarify/preview 워크플로가 CLI 전용이라 MCP 전용 에이전트(Claude Desktop 등)는 질문·시안 승인 흐름을 쓸 수 없던 갭.
|
package/README.md
CHANGED
|
@@ -122,7 +122,7 @@ MIT
|
|
|
122
122
|
<!-- leerness:project-readme:start -->
|
|
123
123
|
## Leerness Project Harness
|
|
124
124
|
|
|
125
|
-
이 프로젝트는 Leerness v1.36.
|
|
125
|
+
이 프로젝트는 Leerness v1.36.79 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
|
|
126
126
|
|
|
127
127
|
### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
|
|
128
128
|
|
|
@@ -176,7 +176,7 @@ leerness memory restore decision <date|title>
|
|
|
176
176
|
|
|
177
177
|
### MCP server (외부 AI 통합)
|
|
178
178
|
|
|
179
|
-
Leerness v1.36.
|
|
179
|
+
Leerness v1.36.79는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **88개 도구**를 노출:
|
|
180
180
|
|
|
181
181
|
```jsonc
|
|
182
182
|
// 카테고리별
|
|
@@ -197,7 +197,7 @@ Leerness v1.36.77는 stdio JSON-RPC MCP server를 내장합니다 — Claude Cod
|
|
|
197
197
|
`<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
|
|
198
198
|
1) 다음 라운드 후보 선정 → 2) 코드 변경 → 3) 회귀 테스트 갱신 → 4) 전체 e2e 스위트 통과 → 5) npm publish + git tag → 6) main push → 7) session close → 8) 다음 라운드 예약.
|
|
199
199
|
|
|
200
|
-
현재 누적: **v1.9.x → 1.36.
|
|
200
|
+
현재 누적: **v1.9.x → 1.36.79 릴리스 태그 이력** (수백 라운드) · _reports/는 비공개 보존.
|
|
201
201
|
|
|
202
202
|
### 성능 가이드
|
|
203
203
|
|
|
@@ -235,5 +235,5 @@ leerness release pack --close --auto-main-push
|
|
|
235
235
|
- `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
|
|
236
236
|
- `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
|
|
237
237
|
|
|
238
|
-
Last synced by Leerness v1.36.
|
|
238
|
+
Last synced by Leerness v1.36.79: 2026-07-28
|
|
239
239
|
<!-- leerness:project-readme:end -->
|
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.79';
|
|
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') 시 호스트 프로세스 오염.
|
|
@@ -2400,12 +2400,17 @@ function _computeRoundHistory(root) {
|
|
|
2400
2400
|
result.nextMilestone = next;
|
|
2401
2401
|
result.roundsToNextMilestone = next - result.roundCount;
|
|
2402
2402
|
}
|
|
2403
|
-
// 일일 평균
|
|
2403
|
+
// 일일 평균 — 1.36.78 (F8): 태그 2+ & 양의 구간일 때만(1개면 속도 관측 불가 → 0 유지)
|
|
2404
2404
|
const first = new Date(result.firstTagAt).getTime();
|
|
2405
2405
|
const last = new Date(result.latestTagAt).getTime();
|
|
2406
|
-
const
|
|
2407
|
-
|
|
2408
|
-
|
|
2406
|
+
const spanDays = (last - first) / 86400000;
|
|
2407
|
+
if (tags.length >= 2 && spanDays > 0) {
|
|
2408
|
+
result.daysActive = Math.max(1, Math.floor(spanDays));
|
|
2409
|
+
result.avgRoundsPerDay = Math.round((result.roundCount / spanDays) * 100) / 100;
|
|
2410
|
+
} else {
|
|
2411
|
+
result.daysActive = 0;
|
|
2412
|
+
result.avgRoundsPerDay = 0;
|
|
2413
|
+
}
|
|
2409
2414
|
} catch {}
|
|
2410
2415
|
return result;
|
|
2411
2416
|
}
|
|
@@ -2451,16 +2456,23 @@ function _computeMilestones(root) {
|
|
|
2451
2456
|
}
|
|
2452
2457
|
}
|
|
2453
2458
|
// 다음 마일스톤 + 평균 속도로 ETA 계산
|
|
2459
|
+
// 1.36.78 (10차 헌트 F8): 태그 1개면 경과 구간이 없어 속도를 관측할 수 없다 — 종전엔 days=max(1,0)=1 로
|
|
2460
|
+
// avgPerDay=1 을 지어내 구체적 ETA 를 보였다. 유효 태그 2+ 이고 양의 구간일 때만 속도/ETA 산출(아니면 null).
|
|
2454
2461
|
const lastTime = new Date(tags[tags.length - 1].date).getTime();
|
|
2455
|
-
const
|
|
2456
|
-
const avgPerDay = (tags.length / days);
|
|
2457
|
-
result.avgRoundsPerDay = Math.round(avgPerDay * 100) / 100;
|
|
2462
|
+
const spanDays = (lastTime - baselineTime) / 86400000;
|
|
2458
2463
|
const nextM = milestones.find(m => m > tags.length);
|
|
2459
|
-
if (
|
|
2460
|
-
const
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
+
if (tags.length >= 2 && spanDays > 0) {
|
|
2465
|
+
const avgPerDay = tags.length / spanDays;
|
|
2466
|
+
result.avgRoundsPerDay = Math.round(avgPerDay * 100) / 100;
|
|
2467
|
+
if (nextM != null) {
|
|
2468
|
+
const roundsRemaining = nextM - tags.length;
|
|
2469
|
+
const etaDays = Math.ceil(roundsRemaining / avgPerDay);
|
|
2470
|
+
const etaDate = new Date(Date.now() + etaDays * 86400000).toISOString().split('T')[0];
|
|
2471
|
+
result.next = { milestone: nextM, roundsRemaining, etaDays, etaDate };
|
|
2472
|
+
}
|
|
2473
|
+
} else if (nextM != null) {
|
|
2474
|
+
// 속도 관측 불가 — 다음 마일스톤은 표시하되 ETA 는 null(추정 없음)
|
|
2475
|
+
result.next = { milestone: nextM, roundsRemaining: nextM - tags.length, etaDays: null, etaDate: null };
|
|
2464
2476
|
}
|
|
2465
2477
|
} catch {}
|
|
2466
2478
|
return result;
|
|
@@ -3375,10 +3387,10 @@ function _selfTestCases() {
|
|
|
3375
3387
|
const stateLock = s.includes("const _mut = ['start', 'record', 'verify', 'handoff'].includes(sub || '')")
|
|
3376
3388
|
&& s.includes("_withLock(path.join(_leernessStateDir(root), 'state.json'), () => stateCmd(root, sub, ...args))")
|
|
3377
3389
|
&& s.includes("!_heldLocks.has(path.join(_leernessStateDir(root), 'state.json') + '.lock')");
|
|
3378
|
-
// #2: team add 가 락 안 재로드→중복검사→저장 + EPERM
|
|
3390
|
+
// #2: team add 가 락 안 재로드→중복검사→저장 + EPERM 재시도. 1.36.78: 락내 재로드가 checked 로더로(형상무효 레이스 거부).
|
|
3379
3391
|
const teamSrc = read(path.join(path.dirname(__filename), '..', 'lib', 'team.js'));
|
|
3380
|
-
const teamLock = teamSrc.includes('
|
|
3381
|
-
const teamWired = s.includes('_detectShellCtx, arg, has, _withLock }');
|
|
3392
|
+
const teamLock = teamSrc.includes('_loadTeamsChecked(root)') && teamSrc.includes("deps._withLock(require('path').join(root, '.harness', 'teams.json'), _doAdd)") && teamSrc.includes("e.code === 'EPERM'") && teamSrc.includes('_rechk.invalid');
|
|
3393
|
+
const teamWired = s.includes('_loadTeamsChecked, _saveTeams, _detectShellCtx, arg, has, _withLock, failJson }');
|
|
3382
3394
|
// #7: pre-wake 매처가 테이블형+YAML형 모두 (행위)
|
|
3383
3395
|
const both = ('| T-0001 | in-progress | x |\nstatus: in-progress\n'.match(/^\|\s*T-\d{4,}\s*\|\s*in-progress\s*\|/gm) || []).length === 1;
|
|
3384
3396
|
const pwWired = /_runPreWakeAudit[\s\S]{0,2200}\^\\\|\\s\*T-\\d\{4,\}\\s\*\\\|\\s\*in-progress/.test(s);
|
|
@@ -5868,6 +5880,41 @@ const _INTEGRITY_DOCS = [
|
|
|
5868
5880
|
'.harness/consistency-policy.md', '.harness/testing-strategy.md', '.harness/review-checklist.md',
|
|
5869
5881
|
'.harness/release-checklist.md', '.harness/session-close-policy.md', '.harness/plan-progress-boundary.md',
|
|
5870
5882
|
];
|
|
5883
|
+
// 1.36.79 (도그푸딩 P1-E): integrity 의 "관리 문서 부재/손상" 판정을 audit 도 공유하도록 경량 추출.
|
|
5884
|
+
// audit 이 존재-여부만 보고 healthy 를 선언해 integrity 와 모순되던 것 해소(두 명령 단일 진실).
|
|
5885
|
+
// (검수 #5) 임계값·H1 판정을 재구현하면 두 명령이 갈린다(실측: 10KB→520B 문서를 integrity 는 truncated, audit 은 무탐).
|
|
5886
|
+
// integrityCmd 와 **동일한 단일 판정기**를 쓴다 — 여기가 유일한 구현이고 integrityCmd 도 이 함수를 호출한다.
|
|
5887
|
+
function _integrityFindings(root) {
|
|
5888
|
+
root = absRoot(root);
|
|
5889
|
+
if (!exists(path.join(root, '.harness'))) return [];
|
|
5890
|
+
let minimal = false;
|
|
5891
|
+
try { minimal = !!JSON.parse(read(path.join(root, '.harness', 'manifest.json'))).minimal; } catch {}
|
|
5892
|
+
const lang = detectLanguageValue(root, 'auto');
|
|
5893
|
+
const templates = coreFiles(root, lang, [], { minimal });
|
|
5894
|
+
const out = [];
|
|
5895
|
+
for (const key of _INTEGRITY_DOCS.filter(k => typeof templates[k] === 'string')) {
|
|
5896
|
+
const tmpl = templates[key];
|
|
5897
|
+
const fp = path.join(root, key);
|
|
5898
|
+
let kind = null;
|
|
5899
|
+
if (!exists(fp)) kind = 'missing';
|
|
5900
|
+
else {
|
|
5901
|
+
let cur = ''; try { cur = read(fp); } catch { kind = 'unreadable'; }
|
|
5902
|
+
if (!kind) {
|
|
5903
|
+
// 코드펜스 안의 `# 주석`을 H1 로 오인하지 않도록 펜스 밖 라인만 검사 (integrityCmd 와 동일 규칙)
|
|
5904
|
+
let _fence = false;
|
|
5905
|
+
const _hasH1 = cur.split('\n').some(l => {
|
|
5906
|
+
const t = l.trim();
|
|
5907
|
+
if (t.startsWith('```')) { _fence = !_fence; return false; }
|
|
5908
|
+
return !_fence && /^# \S/.test(t);
|
|
5909
|
+
});
|
|
5910
|
+
if (!_hasH1) kind = 'h1_lost';
|
|
5911
|
+
else if (cur.length < tmpl.length * 0.4) kind = 'truncated';
|
|
5912
|
+
}
|
|
5913
|
+
}
|
|
5914
|
+
if (kind) out.push({ file: key, kind, templateBytes: Buffer.byteLength(tmpl, 'utf8') });
|
|
5915
|
+
}
|
|
5916
|
+
return out;
|
|
5917
|
+
}
|
|
5871
5918
|
function integrityCmd(root) {
|
|
5872
5919
|
root = absRoot(root);
|
|
5873
5920
|
const json = has('--json');
|
|
@@ -6735,7 +6782,7 @@ function commandsCmd(root) {
|
|
|
6735
6782
|
{ cmd: 'session close [path] [--json] [--auto-apply-delivered]', desc: '세션 마감 + 9 카테고리 + 자동 통합' },
|
|
6736
6783
|
{ cmd: 'resume [path]', desc: 'auto-resume-plan 적용 (1.9.203)' },
|
|
6737
6784
|
{ cmd: 'route <task-type>', desc: '작업 유형 분류 (11종)' },
|
|
6738
|
-
{ cmd: 'agents list|check|quota|dispatch|multi', desc: '외부 AI CLI 오케스트레이션 (dispatch --role 모델 라우팅 1.9.270)' },
|
|
6785
|
+
{ cmd: 'agents list|check|quota|dispatch|multi|recommend', desc: '외부 AI CLI 오케스트레이션 (dispatch --role 모델 라우팅 1.9.270)' },
|
|
6739
6786
|
{ cmd: 'roles list|set|unset|catalog|suggest|verify', desc: '모델별 역할 부여 (코딩/검수/지휘/디자인/디버그/설계/분배) — 1.9.270' },
|
|
6740
6787
|
{ cmd: 'capabilities [--json]', desc: '권한·보안 표면 공개 (무엇을 하는지 + opt-out + 주의 명령) — 1.9.272' },
|
|
6741
6788
|
{ cmd: 'state show|start|record|verify|handoff', desc: '.leerness/ JSON 상태 substrate (에이전트 간 인수인계 표준) — 1.9.278' },
|
|
@@ -6870,19 +6917,22 @@ function roundHistoryCmd(root) {
|
|
|
6870
6917
|
// 기본 catalog 6종: Stripe / OpenAI / Anthropic / GitHub / Discord / Twitter
|
|
6871
6918
|
function _platformConstraintsPath(root) { return path.join(root, '.harness', 'platform-constraints.json'); }
|
|
6872
6919
|
// 1.9.333 (UR-0025 심층): _DEFAULT_PLATFORM_CONSTRAINTS → lib/catalogs.js 로 이동 (순수 데이터, require 사용).
|
|
6920
|
+
// 1.36.78 (10차 헌트 F3): 손상 store 를 조용히 defaults 로 폴백해 "권위 있는 정상 목록"처럼 보이던 것 —
|
|
6921
|
+
// 사용자 커스텀 제약이 사라졌는데도 알 수 없었다. 손상 여부를 함께 반환해 list/check 가 표면화(read 는 폴백 유지 + 경고).
|
|
6922
|
+
function _loadPlatformConstraintsChecked(root) {
|
|
6923
|
+
const fp = _platformConstraintsPath(root);
|
|
6924
|
+
if (!exists(fp)) return { catalog: _DEFAULT_PLATFORM_CONSTRAINTS, corrupt: false };
|
|
6925
|
+
let j;
|
|
6926
|
+
try { j = JSON.parse(read(fp)); } catch { return { catalog: _DEFAULT_PLATFORM_CONSTRAINTS, corrupt: true }; }
|
|
6927
|
+
// 1.36.78 (검수 #3): typeof []==='object' 라 배열 루트/배열 platforms 가 통과하던 것 — 명시 배제(비객체 취급).
|
|
6928
|
+
if (!j || typeof j !== 'object' || Array.isArray(j) || (j.platforms != null && (typeof j.platforms !== 'object' || Array.isArray(j.platforms)))) return { catalog: _DEFAULT_PLATFORM_CONSTRAINTS, corrupt: true };
|
|
6929
|
+
const merged = { ...JSON.parse(JSON.stringify(_DEFAULT_PLATFORM_CONSTRAINTS)) };
|
|
6930
|
+
if (j.platforms) for (const k of Object.keys(j.platforms)) merged.platforms[k] = j.platforms[k];
|
|
6931
|
+
return { catalog: merged, corrupt: false };
|
|
6932
|
+
}
|
|
6873
6933
|
function _loadPlatformConstraints(root) {
|
|
6874
6934
|
try {
|
|
6875
|
-
|
|
6876
|
-
if (!exists(fp)) return _DEFAULT_PLATFORM_CONSTRAINTS;
|
|
6877
|
-
const j = JSON.parse(read(fp));
|
|
6878
|
-
// user catalog 와 default merge — user override 우선
|
|
6879
|
-
const merged = { ...JSON.parse(JSON.stringify(_DEFAULT_PLATFORM_CONSTRAINTS)) };
|
|
6880
|
-
if (j.platforms) {
|
|
6881
|
-
for (const k of Object.keys(j.platforms)) {
|
|
6882
|
-
merged.platforms[k] = j.platforms[k];
|
|
6883
|
-
}
|
|
6884
|
-
}
|
|
6885
|
-
return merged;
|
|
6935
|
+
return _loadPlatformConstraintsChecked(root).catalog;
|
|
6886
6936
|
} catch { return _DEFAULT_PLATFORM_CONSTRAINTS; }
|
|
6887
6937
|
}
|
|
6888
6938
|
function _writePlatformConstraints(root, catalog) {
|
|
@@ -7882,8 +7932,10 @@ function constraintsCmd(root, sub, ...rest) {
|
|
|
7882
7932
|
}
|
|
7883
7933
|
|
|
7884
7934
|
if (sub === 'list') {
|
|
7885
|
-
const
|
|
7886
|
-
|
|
7935
|
+
const _chk = _loadPlatformConstraintsChecked(root); // 1.36.78 (F3): 손상 표면화
|
|
7936
|
+
const catalog = _chk.catalog;
|
|
7937
|
+
if (has('--json')) { log(JSON.stringify({ ...catalog, ...(_chk.corrupt ? { corruptStore: true, note: 'platform-constraints.json 손상 — 기본값만 표시(커스텀 제약 미반영)' } : {}) }, null, 2)); return; }
|
|
7938
|
+
if (_chk.corrupt) log(yel(_t(' ⚠ platform-constraints.json 손상 — 기본값만 표시(커스텀 제약 미반영). 복구 후 재시도', ' ⚠ platform-constraints.json corrupt — showing defaults only (custom constraints omitted). Repair and retry')));
|
|
7887
7939
|
log(cyan(`# leerness constraints list (1.9.208)`));
|
|
7888
7940
|
log(` total platforms: ${Object.keys(catalog.platforms).length}`);
|
|
7889
7941
|
log('');
|
|
@@ -7904,8 +7956,10 @@ function constraintsCmd(root, sub, ...rest) {
|
|
|
7904
7956
|
if (sub === 'check') {
|
|
7905
7957
|
const text = rest.filter(x => !x.startsWith('-')).join(' ');
|
|
7906
7958
|
if (!text) return failJson(has('--json'), 'missing_query', 'Usage: leerness constraints check "<request text>"'); // 1.32.1 (15th리뷰 C2): --json 구조화
|
|
7959
|
+
const _corrupt = _loadPlatformConstraintsChecked(root).corrupt; // 1.36.78 (F3): 손상 표면화
|
|
7907
7960
|
const result = _checkRequestConstraints(root, text, _L);
|
|
7908
|
-
if (has('--json')) { log(JSON.stringify({ query: text, ...result }, null, 2)); return; }
|
|
7961
|
+
if (has('--json')) { log(JSON.stringify({ query: text, ...result, ...(_corrupt ? { corruptStore: true } : {}) }, null, 2)); return; }
|
|
7962
|
+
if (_corrupt) log(yel(_t(' ⚠ platform-constraints.json 손상 — 커스텀 제약 미반영(기본값만 대조)', ' ⚠ platform-constraints.json corrupt — custom constraints omitted (defaults only)')));
|
|
7909
7963
|
log(cyan(`# leerness constraints check (1.9.208)`));
|
|
7910
7964
|
log(` query: ${text.slice(0, 80)}${text.length > 80 ? '…' : ''}`);
|
|
7911
7965
|
log('');
|
|
@@ -7936,15 +7990,24 @@ function constraintsCmd(root, sub, ...rest) {
|
|
|
7936
7990
|
if (sub === 'add') {
|
|
7937
7991
|
const id = rest[0];
|
|
7938
7992
|
if (!id) return failJson(has('--json'), 'missing_platform_id', 'Usage: leerness constraints add <platform-id> --constraint "kind:detail"'); // 1.32.1 (15th리뷰 C2)
|
|
7939
|
-
|
|
7993
|
+
// 1.36.78 (검수 #3): add 도 checked 로더로 — 손상/배열형 store 위 변경 거부(기존 커스텀·metadata 보존).
|
|
7994
|
+
const _cchk = _loadPlatformConstraintsChecked(root);
|
|
7995
|
+
if (_cchk.corrupt) return failJson(has('--json'), 'store_corrupt', `platform-constraints.json 손상/형상 무효 — 덮어쓰기 거부: ${_platformConstraintsPath(root)} (복구 후 재시도)`);
|
|
7996
|
+
const catalog = _cchk.catalog;
|
|
7940
7997
|
const aliases = (arg('--alias', '') || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
7941
7998
|
const detailRaw = arg('--constraint', '');
|
|
7942
|
-
if (!detailRaw) return failJson(has('--json'), 'missing_constraint', '--constraint "kind:detail" required'); // 1.32.1 (15th리뷰 C2)
|
|
7943
|
-
const [kind, ...rest2] = detailRaw.split(':');
|
|
7999
|
+
if (!detailRaw || detailRaw === true) return failJson(has('--json'), 'missing_constraint', '--constraint "kind:detail" required'); // 1.32.1 (15th리뷰 C2)
|
|
8000
|
+
const [kind, ...rest2] = String(detailRaw).split(':');
|
|
8001
|
+
const kindTrim = (kind || '').trim();
|
|
7944
8002
|
const detail = rest2.join(':').trim();
|
|
8003
|
+
// 1.36.78 (10차 헌트 F2): 무효 kind:detail(콜론 없음/빈 kind/빈 detail)은 조용히 버려지고 exit 0 + 빈-constraints 플랫폼을
|
|
8004
|
+
// 쓰던 것(거짓 성공). 형식 위반은 저장 없이 거부. 플랫폼 생성도 검증 통과 후로 이동.
|
|
8005
|
+
if (!kindTrim || !detail) return failJson(has('--json'), 'invalid_constraint', '--constraint 는 "kind:detail" 형식(빈 kind/detail 불가) — 예: "auth:token required"');
|
|
7945
8006
|
if (!catalog.platforms[id]) catalog.platforms[id] = { aliases: [], constraints: [] };
|
|
7946
8007
|
if (aliases.length) catalog.platforms[id].aliases = [...new Set([...(catalog.platforms[id].aliases || []), ...aliases])];
|
|
7947
|
-
|
|
8008
|
+
// 1.36.78 (F2): 동일 {kind,detail} 중복 누적 방지(멱등)
|
|
8009
|
+
const _cons = catalog.platforms[id].constraints;
|
|
8010
|
+
if (!_cons.some(c => c.kind === kindTrim && c.detail === detail)) _cons.push({ kind: kindTrim, detail });
|
|
7948
8011
|
_writePlatformConstraints(root, catalog);
|
|
7949
8012
|
if (has('--json')) { log(JSON.stringify(catalog.platforms[id], null, 2)); return; }
|
|
7950
8013
|
log(grn(_t(`✓ platform "${id}" 갱신 — constraints: ${catalog.platforms[id].constraints.length}`, `✓ platform "${id}" updated — constraints: ${catalog.platforms[id].constraints.length}`)));
|
|
@@ -8513,7 +8576,7 @@ function _autoFixIdempotency(root) {
|
|
|
8513
8576
|
kept.push(r);
|
|
8514
8577
|
}
|
|
8515
8578
|
if (removedExact > 0 || droppedSameText > 0) {
|
|
8516
|
-
writeProgressRows(root, header, kept);
|
|
8579
|
+
writeProgressRows(root, header, kept, { expectedShrink: removedExact }); // 1.36.79 (검수 #1): 제거한 만큼만 축소 허용 — 손상 행은 여전히 차단
|
|
8517
8580
|
fixed.push({ kind: 'task-duplicate-request', action: 'deduped', removedExact, droppedSameText });
|
|
8518
8581
|
}
|
|
8519
8582
|
}
|
|
@@ -9126,11 +9189,20 @@ function _saveDecisions(root, decisions) {
|
|
|
9126
9189
|
// 1.9.371 (UR-0073 Phase A): agent team 정의 레지스트리 — canonical JSON(teams.json) 주 + teams.md projection. opt-in · 정의 전용(자동 실행 X).
|
|
9127
9190
|
const teamsJsonPath = root => path.join(root, '.harness/teams.json');
|
|
9128
9191
|
const teamsPath = root => path.join(root, '.harness/teams.md');
|
|
9129
|
-
|
|
9192
|
+
// 1.36.78 (10차 헌트 F1): teams.json 이 "파싱은 되지만 형상 무효"(객체 루트 {teams:[...]}/항목 비객체/id 없음)일 때
|
|
9193
|
+
// 종전엔 조용히 []로 coerce → list 는 count:0(무경고), 이어지는 add 가 배열을 써서 원본을 통째로 덮었다(데이터 손실).
|
|
9194
|
+
// previews/state 와 동일 규율: 로더가 invalid 를 함께 반환하고 변경 진입점은 거부. 유효 항목은 members/personas 를 배열로 정규화(string→[]).
|
|
9195
|
+
function _loadTeamsChecked(root) {
|
|
9130
9196
|
const jp = teamsJsonPath(root);
|
|
9131
|
-
if (exists(jp))
|
|
9132
|
-
|
|
9133
|
-
}
|
|
9197
|
+
if (!exists(jp)) return { teams: [], invalid: false };
|
|
9198
|
+
let parsed;
|
|
9199
|
+
try { parsed = JSON.parse(read(jp)); } catch { return { teams: [], invalid: true }; }
|
|
9200
|
+
if (!Array.isArray(parsed) || parsed.some(e => !e || typeof e !== 'object' || typeof e.id !== 'string')) return { teams: [], invalid: true };
|
|
9201
|
+
const _arr = a => Array.isArray(a) ? a : (a == null ? [] : [String(a)]); // 무효 members(string) → 크래시 대신 배열 정규화
|
|
9202
|
+
const teams = parsed.map(t => Object.assign({}, t, { members: _arr(t.members), personas: _arr(t.personas) }));
|
|
9203
|
+
return { teams, invalid: false };
|
|
9204
|
+
}
|
|
9205
|
+
function _loadTeams(root) { return _loadTeamsChecked(root).teams; }
|
|
9134
9206
|
function _saveTeams(root, teams) {
|
|
9135
9207
|
const arr = Array.isArray(teams) ? teams : [];
|
|
9136
9208
|
_assertStoreParsable(teamsJsonPath(root), 'team'); // 1.36.28 (#3): 손상 teams.json 을 덮어써 기존 팀 유실 방지
|
|
@@ -9144,7 +9216,7 @@ const _team = require('../lib/team');
|
|
|
9144
9216
|
const _tgl = require('../lib/toggles'); // 1.36.30: 기능 토글 (그래프 ⚙ 탭 연동 — gate/lens/auto-graph/delegation-brief)
|
|
9145
9217
|
const _clar = require('../lib/clarify'); // 1.36.51 (UR-0061): 모호성 질문 + 미리보기 승인 워크플로
|
|
9146
9218
|
const _tech = require('../lib/tech-profile'); // 1.36.53 (UR-0062): 기술 프로필 (언어·서비스 감지 + 변경 이력)
|
|
9147
|
-
function teamCmd(root, sub, id, opts = {}) { return _team.teamCmd(root, sub, id, opts, { VERSION, _loadTeams, _saveTeams, _detectShellCtx, arg, has, _withLock }); } // 1.36.31: add 경합 락
|
|
9219
|
+
function teamCmd(root, sub, id, opts = {}) { return _team.teamCmd(root, sub, id, opts, { VERSION, _loadTeams, _loadTeamsChecked, _saveTeams, _detectShellCtx, arg, has, _withLock, failJson }); } // 1.36.31: add 경합 락 · 1.36.78: 형상무효 가드 + failJson
|
|
9148
9220
|
|
|
9149
9221
|
// 1.9.112: 전용 lessons.md (Memory Write Surface 5번째)
|
|
9150
9222
|
const lessonsPath = root => path.join(root, '.harness/lessons.md');
|
|
@@ -9173,11 +9245,19 @@ function nextId(root, prefix) {
|
|
|
9173
9245
|
return `${prefix}-${String(max + 1).padStart(4, '0')}`;
|
|
9174
9246
|
}
|
|
9175
9247
|
|
|
9248
|
+
// 1.36.79 (도그푸딩 P1-A, 실 프로젝트 데이터 손실): ID 접두를 T/M/D 로만 인정해 R-/UR- 등 다른 접두 행이
|
|
9249
|
+
// 파싱에서 탈락 → writeProgressRows 가 파싱된 행만으로 파일을 재작성하며 **사용자 이력을 조용히 삭제**했다
|
|
9250
|
+
// (실측: auto-Trading 9행 → 1행, 942B 소실, exit 0 무경고). migrate 만이 아니라 task add 등 모든 쓰기 경로 공통.
|
|
9251
|
+
// 접두를 일반화(대문자 1~3자)해 왕복 보존.
|
|
9252
|
+
// (검수 #2) 다만 `[A-Z]{1,3}` 전면 개방은 과했다 — 사용자가 트래커에 붙여 둔 무관한 표(`| API-123 | owner | ... |`)까지
|
|
9253
|
+
// task 로 흡수해 task list 에 노출·정규화됐다(실측). leerness 가 실제 발급하는 **등록 네임스페이스만** 인정한다.
|
|
9254
|
+
const _PROGRESS_ID_NS = ['T', 'M', 'D', 'R', 'UR', 'F', 'P'];
|
|
9255
|
+
const _PROGRESS_ROW_RE = new RegExp('^\\|\\s*(?:' + _PROGRESS_ID_NS.join('|') + ')-\\d{3,}\\s*\\|');
|
|
9176
9256
|
function readProgressRows(root) {
|
|
9177
9257
|
const text = exists(progressPath(root)) ? read(progressPath(root)) : '';
|
|
9178
9258
|
const rows = [];
|
|
9179
9259
|
for (const line of text.split('\n')) {
|
|
9180
|
-
if (
|
|
9260
|
+
if (!_PROGRESS_ROW_RE.test(line)) continue;
|
|
9181
9261
|
// 1.9.399 (7번째 버그헌트 P1-A, UR-0104): 비이스케이프 파이프에서만 분리 + 셀 복원 — 사용자 텍스트의 '|'(이스케이프됨)이 컬럼을 깨지 않음.
|
|
9182
9262
|
const cells = line.split(/(?<!\\)\|/).slice(1, -1).map(s => _cellUnescape(s).trim());
|
|
9183
9263
|
if (cells.length < 6) continue;
|
|
@@ -9208,11 +9288,54 @@ function progressHeader(root) {
|
|
|
9208
9288
|
}
|
|
9209
9289
|
return text.slice(0, text.indexOf('\n', idx)).trimEnd();
|
|
9210
9290
|
}
|
|
9211
|
-
function writeProgressRows(root, header, rows) {
|
|
9291
|
+
function writeProgressRows(root, header, rows, opts = {}) {
|
|
9292
|
+
// 1.36.79 (도그푸딩 P1-A 클래스 가드): 파서가 놓친 행이 재작성으로 사라지는 사고를 규칙 하나가 아니라 **불변식**으로 차단 —
|
|
9293
|
+
// 디스크의 행-모양 라인 수가 쓰려는 행 수보다 많으면 쓰기를 중단한다(향후 파서 강화/CRLF/셀부족 등 같은 클래스 전부 포착).
|
|
9294
|
+
// 단 **의도적 행 감소**(audit --fix 의 완전중복 dedup)는 opts.allowShrink 로 명시 옵트인 — 오탐으로 정당한 정리를 막지 않는다.
|
|
9295
|
+
// 판정 기준은 "변이 후 행 수"가 아니라 **디스크에 파서가 못 읽는 행-모양 라인이 있는가** — upsert 가 행을 추가하면
|
|
9296
|
+
// 개수 비교는 손실을 상쇄해 무력화된다(실측: 디스크 2 vs 쓰기 2 로 통과, 셀부족 행 소실).
|
|
9297
|
+
// (검수 #1) 의도적 축소는 boolean 우회가 아니라 **예상 감소량**으로 — allowShrink:true 는 가드를 통째로 꺼서
|
|
9298
|
+
// dedup 중 손상 행까지 조용히 삭제했다. expectedShrink 만큼만 허용하고 손상 행 존재는 여전히 차단한다.
|
|
9299
|
+
try {
|
|
9300
|
+
const _cur = exists(progressPath(root)) ? read(progressPath(root)) : '';
|
|
9301
|
+
if (_cur) {
|
|
9302
|
+
let _shaped = 0, _parsable = 0;
|
|
9303
|
+
for (const line of _cur.split('\n')) {
|
|
9304
|
+
if (!_PROGRESS_ROW_RE.test(line)) continue;
|
|
9305
|
+
_shaped++;
|
|
9306
|
+
if (line.split(/(?<!\\)\|/).slice(1, -1).length >= 6) _parsable++;
|
|
9307
|
+
}
|
|
9308
|
+
if (_parsable < _shaped) {
|
|
9309
|
+
const _msg = `progress-tracker.md 행 손실 위험 — 행-모양 ${_shaped}건 중 ${_parsable}건만 파싱 가능(셀 부족/형식 이상). ` +
|
|
9310
|
+
`쓰면 나머지 ${_shaped - _parsable}행이 사라지므로 중단합니다(데이터 보호): ${progressPath(root)} 의 해당 행을 6칸으로 고치거나 .harness/archive 백업 복원 후 재시도`;
|
|
9311
|
+
throw Object.assign(new Error(_msg), { code: 'E_PROGRESS_ROW_LOSS' });
|
|
9312
|
+
}
|
|
9313
|
+
const _expect = Number.isInteger(opts.expectedShrink) ? Math.max(0, opts.expectedShrink) : 0;
|
|
9314
|
+
if (_parsable - _expect > rows.length) {
|
|
9315
|
+
const _msg = `progress-tracker.md 행 손실 위험 — 파싱 ${_parsable}행 − 예상감소 ${_expect} > 쓰려는 ${rows.length}행. 쓰기 중단(데이터 보호): ${progressPath(root)}`;
|
|
9316
|
+
throw Object.assign(new Error(_msg), { code: 'E_PROGRESS_ROW_LOSS' });
|
|
9317
|
+
}
|
|
9318
|
+
}
|
|
9319
|
+
} catch (e) { if (e && e.code === 'E_PROGRESS_ROW_LOSS') throw e; }
|
|
9212
9320
|
// 1.9.399 (7번째 버그헌트 P1-A, UR-0104): 셀 콘텐츠 안전화 — 개행→공백, '|'→'\|'. 사용자 텍스트의 파이프/개행이 표 행을 손상/주입 못 함.
|
|
9321
|
+
// 1.36.79 (검수 #2 후반): 표 **뒤에 사용자가 쓴 내용**(참조표·메모·`Status values:` 푸터)이 재작성 때 통째로 사라지던 것 —
|
|
9322
|
+
// 마지막 task 행 이후의 비-행 콘텐츠를 그대로 이어붙여 보존한다.
|
|
9323
|
+
let _suffix = '';
|
|
9324
|
+
try {
|
|
9325
|
+
const _cur0 = exists(progressPath(root)) ? read(progressPath(root)) : '';
|
|
9326
|
+
if (_cur0) {
|
|
9327
|
+
const _lines = _cur0.split('\n');
|
|
9328
|
+
let _lastRow = -1;
|
|
9329
|
+
for (let i = 0; i < _lines.length; i++) if (_PROGRESS_ROW_RE.test(_lines[i])) _lastRow = i;
|
|
9330
|
+
if (_lastRow >= 0) {
|
|
9331
|
+
const _tail = _lines.slice(_lastRow + 1).join('\n').replace(/^\n+/, '');
|
|
9332
|
+
if (_tail.trim()) _suffix = '\n' + _tail.replace(/\s+$/, '') + '\n';
|
|
9333
|
+
}
|
|
9334
|
+
}
|
|
9335
|
+
} catch {}
|
|
9213
9336
|
const composed = header + '\n' +
|
|
9214
9337
|
rows.map(r => `| ${_cellSafe(r.id)} | ${_cellSafe(r.status)} | ${_cellSafe(r.request)} | ${_cellSafe(r.evidence)} | ${_cellSafe(r.nextAction)} | ${_cellSafe(r.updated)} |`).join('\n') +
|
|
9215
|
-
(rows.length ? '\n' : '');
|
|
9338
|
+
(rows.length ? '\n' : '') + _suffix;
|
|
9216
9339
|
writeUtf8(progressPath(root), composed);
|
|
9217
9340
|
}
|
|
9218
9341
|
function upsertProgress(root, row) {
|
|
@@ -10258,7 +10381,7 @@ function debug(root) {
|
|
|
10258
10381
|
|
|
10259
10382
|
const _audit = require('../lib/audit');
|
|
10260
10383
|
// 1.9.421 (UR-0025/UR-0125 큰 핸들러 모듈화 6번째): audit → lib/audit.js (DI 위임, thin wrapper)
|
|
10261
|
-
function audit(root, opts = {}) { return _audit.audit(root, opts, { VERSION, arg, has, planPath, readProgressRows, currentStatePath, handoffPath, envDiff, _readFeatureGraph, _matchAPISkills, _listAPISkills, _collectSecretFindings }); }
|
|
10384
|
+
function audit(root, opts = {}) { return _audit.audit(root, opts, { VERSION, arg, has, planPath, readProgressRows, currentStatePath, handoffPath, envDiff, _readFeatureGraph, _matchAPISkills, _listAPISkills, _collectSecretFindings, _integrityFindings }); } // 1.36.79: integrity 신호 공유
|
|
10262
10385
|
|
|
10263
10386
|
// 1.9.312 (UR-0050, 설치리뷰 3중수렴): secret 스캐너 현대 키 패턴 보강.
|
|
10264
10387
|
// 배경: 기존 OpenAI 패턴 `sk-[A-Za-z0-9]{32,}` 은 하이픈에서 끊겨 sk-proj-/sk-svcacct- (modern 프로젝트/서비스 키)를 놓침.
|
|
@@ -12403,7 +12526,10 @@ function handoffCmd(root) {
|
|
|
12403
12526
|
// 1.9.38 (A): drift 임계 시 .harness/agent-reminders.md 자동 생성 — 메인 에이전트 프롬프트에 표시되도록.
|
|
12404
12527
|
// 1.9.38 (D): skip 횟수 학습 — --no-drift-check 빈도 ≥5 시 임계 완화 (1d → 2d).
|
|
12405
12528
|
const absR0 = absRoot(root || process.cwd());
|
|
12406
|
-
|
|
12529
|
+
// 1.36.79 (도그푸딩 P1-B, 실 프로젝트 27/37 해당): 이 사람용 drift 배너가 --json 모드에서도 stdout 에 찍혀
|
|
12530
|
+
// handoff --json 등의 응답을 통째로 파싱 불가로 만들었다(실측: JSON.parse 실패). stale 프로젝트에서만 발화해
|
|
12531
|
+
// 합성 픽스처 헌트가 못 봤다. --json 이면 배너 억제(구조화 필드/텍스트 모드로만 노출).
|
|
12532
|
+
if (exists(path.join(absR0, '.harness')) && process.env.LEERNESS_NO_DRIFT_CHECK !== '1' && !has('--json')) {
|
|
12407
12533
|
// skip 카운트
|
|
12408
12534
|
if (has('--no-drift-check')) {
|
|
12409
12535
|
try {
|
|
@@ -22363,6 +22489,12 @@ function contractVerifyCmd(specPath, implPath) {
|
|
|
22363
22489
|
const _fe = f.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
22364
22490
|
if (!new RegExp(`(?<![\\w$])${_fe}(?![\\w$])`).test(implSrc)) fieldMissing.push(f);
|
|
22365
22491
|
}
|
|
22492
|
+
// 1.36.78 (10차 헌트 F6): 선언(강함수+필드)이 0건인 spec 은 검사할 게 없어 항상 ok:true 로 "공허 통과"했다
|
|
22493
|
+
// (CI 에서 빈/오탈자 spec 이 계약을 통과시키는 위험). 검증 불가로 취급 — 명시적 --allow-empty 로만 허용.
|
|
22494
|
+
if (declaredSpec.size === 0 && fieldSpec.size === 0 && !has('--allow-empty')) {
|
|
22495
|
+
failJson(_j, 'empty_contract', 'spec 에 검증할 함수/필드 선언이 없습니다 — 공허 통과 방지(의도된 빈 계약이면 --allow-empty). "- name(args)" 또는 "## Fields\\n- fieldName" 으로 선언하세요');
|
|
22496
|
+
return process.exit(process.exitCode || 1);
|
|
22497
|
+
}
|
|
22366
22498
|
// 출력
|
|
22367
22499
|
if (has('--json')) {
|
|
22368
22500
|
const okJson = missing.length === 0 && fieldMissing.length === 0;
|
|
@@ -22963,7 +23095,7 @@ SETUP & UPDATE
|
|
|
22963
23095
|
|
|
22964
23096
|
STATUS & DIAGNOSTICS
|
|
22965
23097
|
status [path] Install status (files present)
|
|
22966
|
-
health [path] | doctor [
|
|
23098
|
+
health [path] | doctor [--json] health: project state · doctor: CLI install/environment (no path arg)
|
|
22967
23099
|
verify [path] Required-file verification
|
|
22968
23100
|
which [--json] Resolve current binary/version (npm cache conflicts)
|
|
22969
23101
|
selftest [--json] Core-function integrity self-check (CI-friendly)
|
|
@@ -22973,7 +23105,7 @@ STATUS & DIAGNOSTICS
|
|
|
22973
23105
|
VERIFICATION (evidence-gated "done")
|
|
22974
23106
|
verify-claim <T-ID> [--run-tests] [--test-cmd "..."] [--json]
|
|
22975
23107
|
Check evidence vs reality (stub / fake-test / inflated-count detection)
|
|
22976
|
-
contract verify <spec.md> <impl.js> [--json]
|
|
23108
|
+
contract verify <spec.md> <impl.js> [--json] [--allow-empty] Spec <-> implementation match (empty spec rejected unless --allow-empty)
|
|
22977
23109
|
verify-code [path] [--build] [--bench] Run tests/lint/typecheck, record evidence
|
|
22978
23110
|
gate [path] One-call CI gate: verify + audit + scan + encoding + lazy
|
|
22979
23111
|
lens [code|design|docs|test|security|database|debug] [--json] Quality self-question lenses
|
|
@@ -23050,7 +23182,7 @@ function help() {
|
|
|
23050
23182
|
leerness rule add "<설명>" --trigger every-session|every-update|every-commit|session-start|session-close|pre-publish # 사용자 룰 등록 (1.9.8)
|
|
23051
23183
|
leerness rule list|verify|pause <id>|resume <id>|remove <id>|stop|resume-all
|
|
23052
23184
|
leerness context [path] [--json] # 에이전트 온보딩 컨텍스트 (1.9.292, UR-0065)
|
|
23053
|
-
leerness health [path] [--json] · leerness doctor [
|
|
23185
|
+
leerness health [path] [--json] · leerness doctor [--json] # health: 프로젝트 상태 · doctor: CLI 설치/환경(경로 인자 없음)
|
|
23054
23186
|
leerness pulse [path] · leerness milestones [path] · leerness round-history [path] # 종합 지표
|
|
23055
23187
|
leerness decision add "<title>" [--reason ...] | decision list|drop <id> # 결정 메모리 (.harness, canonical JSON)
|
|
23056
23188
|
leerness lesson save "<text>" [--tag t] | lesson list|drop <date|text> # 교훈 메모리
|
|
@@ -23378,7 +23510,19 @@ async function main() {
|
|
|
23378
23510
|
if (cmd === 'wakeup-interval') return wakeupIntervalCmd(arg('--path', process.cwd()), args[1], args[2]);
|
|
23379
23511
|
// 1.9.211: leerness workspace-dir <get|guide> — 현재 워크스페이스 디렉토리 / AI 참조 가이드 (사용자 명시)
|
|
23380
23512
|
if (cmd === 'workspace-dir') return workspaceDirCmd(arg('--path', process.cwd()), args[1]);
|
|
23381
|
-
|
|
23513
|
+
// 1.36.79 (도그푸딩 P1-C): positional 경로(`parent detect <path>`)를 무시하고 cwd 를 분석해 자신만만한 오답(exit 0)을 내던 것
|
|
23514
|
+
// — positional path 잔존 클래스(1.36.33)의 미소진 지점. --path 우선, 없으면 sub 다음 positional.
|
|
23515
|
+
// (검수 #3) _taskPositionalPath 는 './' 접두 등 "경로처럼 보이는" 값만 인정해 `parent detect parent\child` 같은 맨 상대경로를
|
|
23516
|
+
// 무시하고 cwd 를 분석했다. parent 는 하위명령 뒤 첫 non-flag 를 경로로 받고, 존재하는 디렉토리인지 먼저 검증한다.
|
|
23517
|
+
if (cmd === 'parent') {
|
|
23518
|
+
const _pPos = args.slice(2).find(a => a && !a.startsWith('-'));
|
|
23519
|
+
const _pRoot = arg('--path', null) || _pPos || process.cwd();
|
|
23520
|
+
const _pAbs = path.resolve(_pRoot);
|
|
23521
|
+
if (_pPos || arg('--path', null)) {
|
|
23522
|
+
if (!exists(_pAbs) || !fs.statSync(_pAbs).isDirectory()) { failJson(has('--json'), 'path_not_found', `경로 없음 또는 디렉토리 아님: ${_pAbs}`); return; }
|
|
23523
|
+
}
|
|
23524
|
+
return parentCmd(_pAbs, args[1]);
|
|
23525
|
+
}
|
|
23382
23526
|
// 1.9.211: leerness migrate-workspace-dir — .harness → .leerness 마이그레이션 (사용자 명시)
|
|
23383
23527
|
if (cmd === 'migrate-workspace-dir') return migrateWorkspaceDirCmd(arg('--path', null) || _taskPositionalPath(args, 1) || process.cwd()); // 1.36.33: positional path 무시 → cwd 오염 (1.36.21/24 계열 잔존 — 실제로 이 버그로 소스 저장소가 오염될 뻔)
|
|
23384
23528
|
// 1.9.212: leerness idempotency audit — 멱등성 위반 탐지 (사용자 명시)
|
package/lib/agents.js
CHANGED
|
@@ -59,6 +59,19 @@ function agentsCmd(root, sub, args = [], deps = {}) {
|
|
|
59
59
|
return;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
// 1.36.78 (10차 헌트 F5): review-request 가 광고하는 `agents recommend <task>` 가 핸들러 부재로 usage(exit 1)만 뱉던 것.
|
|
63
|
+
// 내부 _recommendAgent 로직을 정식 서브명령으로 노출 — 작업 텍스트/유형 → 적합 CLI + 이유.
|
|
64
|
+
if (sub === 'recommend') {
|
|
65
|
+
const task = _taskArg();
|
|
66
|
+
if (!task) { failJson(has('--json'), 'missing_task', 'Usage: leerness agents recommend "<작업 설명 또는 유형>"'); return; }
|
|
67
|
+
const rec = _recommendAgent(task) || { target: null, reason: '' };
|
|
68
|
+
if (has('--json')) { log(JSON.stringify({ ok: true, task, recommended: rec.target, reason: rec.reason }, null, 2)); return; }
|
|
69
|
+
log(`# leerness agents recommend (1.36.78)`);
|
|
70
|
+
log(` 작업: ${task.slice(0, 80)}${task.length > 80 ? '…' : ''}`);
|
|
71
|
+
if (rec.target) { log(` 💡 추천: ${rec.target}${rec.reason ? ` — ${rec.reason}` : ''}`); }
|
|
72
|
+
else { log(` (특정 추천 없음 — 기본 provider 사용. leerness agents list 로 활성 확인)`); }
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
62
75
|
if (sub === 'check') {
|
|
63
76
|
// list의 alias, 단 명시적 재확인 (JSON 출력 기본)
|
|
64
77
|
// 1.9.157: Provider Registry 통합
|
|
@@ -467,7 +480,7 @@ function agentsCmd(root, sub, args = [], deps = {}) {
|
|
|
467
480
|
return;
|
|
468
481
|
}
|
|
469
482
|
|
|
470
|
-
fail('사용법: leerness agents list|check|quota|dispatch|bench [--write] "<task>" [--to <id>]');
|
|
483
|
+
fail('사용법: leerness agents list|check|quota|dispatch|multi|bench|recommend [--write] "<task>" [--to <id>]');
|
|
471
484
|
return process.exit(1);
|
|
472
485
|
}
|
|
473
486
|
|
package/lib/audit.js
CHANGED
|
@@ -34,6 +34,25 @@ function audit(root, opts = {}, deps = {}) {
|
|
|
34
34
|
process.exitCode = 1;
|
|
35
35
|
return;
|
|
36
36
|
}
|
|
37
|
+
// 1.36.79 (도그푸딩 P1-E, 실 프로젝트): .harness/AGENTS.md 존재만 보고 통과시켜 **56개 중 44개가 사라진 설치**를
|
|
38
|
+
// healthy:true·failures:0 으로 보고했다(같은 디렉토리에서 integrity check 는 ok:false — 두 판정이 모순).
|
|
39
|
+
// integrity 가 이미 계산하는 관리 문서 부재/손상 신호를 audit 에도 반영해 "가장 먼저 부르는 명령"이 진실을 말하게 한다.
|
|
40
|
+
try {
|
|
41
|
+
if (typeof deps._integrityFindings === 'function') {
|
|
42
|
+
const _integ = deps._integrityFindings(root) || [];
|
|
43
|
+
// (검수 #5) integrity 와 동일 kind 집합 — truncated 도 포함해 두 명령의 판정이 갈리지 않게(단, 차단 등급은 missing 기준)
|
|
44
|
+
const _missing = _integ.filter(f => f && ['missing', 'h1_lost', 'truncated', 'unreadable'].includes(f.kind));
|
|
45
|
+
if (_missing.length) {
|
|
46
|
+
// 코어 문서 다수 부재 = 설치 손상(차단). 소수는 경고.
|
|
47
|
+
const _sev = _missing.filter(f => f.kind === 'missing').length >= 3 ? 'fail' : 'warn';
|
|
48
|
+
if (_sev === 'fail') { failures++; fail(`관리 문서 ${_missing.length}건 부재/손상 — 설치 불완전 (leerness init 또는 integrity check --repair)`); }
|
|
49
|
+
else { warnings++; warn(`관리 문서 ${_missing.length}건 부재/손상 (leerness integrity check --repair)`); }
|
|
50
|
+
_finding('managed_docs_incomplete', _sev, 'managed policy docs missing/damaged (install incomplete)', { count: _missing.length, files: _missing.slice(0, 12).map(f => f.file || f.name), kinds: [...new Set(_missing.map(f => f.kind))] });
|
|
51
|
+
} else {
|
|
52
|
+
ok('관리 문서 무결성 OK (integrity)');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
} catch {}
|
|
37
56
|
// 1.36.1 (클린룸 리뷰 FN): .harness/*.json 상태파일 JSON 무결성 — 깨진 JSON 을 그레이스풀 폴백(빈 상태)이 "healthy" 로 감추던 false-negative 차단.
|
|
38
57
|
// 손상 파일을 warning + corrupted_state_json finding 으로 표면화(--strict 시 failure 로 승격). 비-크래시(헬퍼가 파서 예외를 흡수).
|
|
39
58
|
try {
|
package/lib/team.js
CHANGED
|
@@ -10,11 +10,16 @@ const { _composeTeamPlan, _teamDeployGate } = require('./pure-utils');
|
|
|
10
10
|
const { _shellGuardAnalyze } = require('./analyzers');
|
|
11
11
|
|
|
12
12
|
function teamCmd(root, sub, id, opts = {}, deps = {}) {
|
|
13
|
-
const { VERSION, _loadTeams, _saveTeams, _detectShellCtx, arg, has } = deps;
|
|
13
|
+
const { VERSION, _loadTeams, _loadTeamsChecked, _saveTeams, _detectShellCtx, arg, has, failJson } = deps;
|
|
14
14
|
root = absRoot(root);
|
|
15
15
|
const json = opts.json || has('--json');
|
|
16
|
-
const teams = _loadTeams(root);
|
|
17
16
|
sub = sub || 'list';
|
|
17
|
+
// 1.36.78 (10차 헌트 F1): 형상 무효 스토어 위 변경 거부 — 읽기는 관용(빈 목록). failJson 이 없으면(구 DI) 기존 경로 유지.
|
|
18
|
+
const _chk = (typeof _loadTeamsChecked === 'function') ? _loadTeamsChecked(root) : { teams: _loadTeams(root), invalid: false };
|
|
19
|
+
const teams = _chk.teams;
|
|
20
|
+
const _fj = failJson || ((j, code, msg) => { if (j) log(JSON.stringify({ ok: false, code, error: msg }, null, 2)); else fail(msg); });
|
|
21
|
+
const _mutating = ['add', 'remove', 'deploy'].includes(sub);
|
|
22
|
+
if (_mutating && _chk.invalid) { _fj(json, 'store_invalid', `teams.json 형상 무효(비배열 루트 또는 null/ID 없는 항목) — 덮어쓰기 거부: ${require('path').join(root, '.harness', 'teams.json')} (수동 복구 후 재시도)`); return; }
|
|
18
23
|
if (sub === 'list') {
|
|
19
24
|
if (json) { log(JSON.stringify({ version: VERSION, root, count: teams.length, teams }, null, 2)); return; }
|
|
20
25
|
log(`# leerness team (1.9.371, UR-0073 Phase A) — 에이전트 팀 정의 (opt-in · 정의 전용)`);
|
|
@@ -25,8 +30,8 @@ function teamCmd(root, sub, id, opts = {}, deps = {}) {
|
|
|
25
30
|
}
|
|
26
31
|
if (sub === 'add') {
|
|
27
32
|
const teamId = String(id || '').toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^[.\-]+|[.\-]+$/g, '');
|
|
28
|
-
if (!teamId || teamId.includes('..')) {
|
|
29
|
-
if (teams.some(t => t.id === teamId)) {
|
|
33
|
+
if (!teamId || teamId.includes('..')) { _fj(json, 'invalid_id', `무효한 team id: "${id}" (영숫자/._- 만)`); return; } // 1.36.78 (F4): --json 구조화
|
|
34
|
+
if (teams.some(t => t.id === teamId)) { _fj(json, 'already_exists', `이미 존재: ${teamId} (제거 후 재정의: leerness team remove ${teamId})`); return; }
|
|
30
35
|
const splitCsv = v => (v && v !== true) ? String(v).split(',').map(s => s.trim()).filter(Boolean) : [];
|
|
31
36
|
const sched = arg('--schedule', 'manual');
|
|
32
37
|
const validSched = new Set(['manual', 'every-session', 'daily', 'weekly']);
|
|
@@ -44,8 +49,11 @@ function teamCmd(root, sub, id, opts = {}, deps = {}) {
|
|
|
44
49
|
};
|
|
45
50
|
// 1.36.31 (codex 미검토표면 #2): 병렬 team add 가 서로의 등록을 덮어쓰던 lost update — 락 안에서 재로드→중복검사→추가→저장.
|
|
46
51
|
const _doAdd = () => {
|
|
47
|
-
|
|
48
|
-
|
|
52
|
+
// 1.36.78 (검수 #1): 락 획득 후 재로드도 checked 로 — 대기 중 스토어가 형상 무효로 바뀌면 덮어쓰기 거부(레이스 데이터 손실 차단).
|
|
53
|
+
const _rechk = (typeof _loadTeamsChecked === 'function') ? _loadTeamsChecked(root) : { teams: _loadTeams(root), invalid: false };
|
|
54
|
+
if (_rechk.invalid) { _fj(json, 'store_invalid', `teams.json 형상 무효 — 덮어쓰기 거부(락 재확인)`); return false; }
|
|
55
|
+
const cur = _rechk.teams;
|
|
56
|
+
if (cur.some(t => t.id === teamId)) { _fj(json, 'already_exists', `이미 존재: ${teamId} (제거 후 재정의: leerness team remove ${teamId})`); return false; }
|
|
49
57
|
cur.push(team);
|
|
50
58
|
// Windows: 락 밖의 병렬 _loadTeams 읽기가 파일을 잠깐 열어둔 사이 원자 rename 이 EPERM(일시적) — 짧은 재시도.
|
|
51
59
|
let _lastErr = null;
|
|
@@ -60,13 +68,14 @@ function teamCmd(root, sub, id, opts = {}, deps = {}) {
|
|
|
60
68
|
? deps._withLock(require('path').join(root, '.harness', 'teams.json'), _doAdd)
|
|
61
69
|
: _doAdd();
|
|
62
70
|
if (!_added) return;
|
|
71
|
+
if (json) { log(JSON.stringify({ ok: true, id: teamId, personas: team.personas.length, members: team.members.length, schedule: team.schedule, review: team.review }, null, 2)); return; } // 1.36.78 (F4)
|
|
63
72
|
ok(`team 정의: ${teamId} (personas:${team.personas.length} members:${team.members.length} schedule:${team.schedule} review:${team.review ? 'on' : 'off'})`);
|
|
64
73
|
log(` ⓘ 정의 전용 — 자동 실행 없음. 목록: leerness team list`);
|
|
65
74
|
return;
|
|
66
75
|
}
|
|
67
76
|
if (sub === 'show') {
|
|
68
77
|
const t = teams.find(x => x.id === id);
|
|
69
|
-
if (!t) {
|
|
78
|
+
if (!t) { _fj(json, 'not_found', `team 없음: ${id}`); return; } // 1.36.78 (F4): --json 구조화
|
|
70
79
|
if (json) { log(JSON.stringify(t, null, 2)); return; }
|
|
71
80
|
log(`# team ${t.id}`);
|
|
72
81
|
log(` name: ${t.name || ''}`);
|
|
@@ -81,15 +90,16 @@ function teamCmd(root, sub, id, opts = {}, deps = {}) {
|
|
|
81
90
|
if (sub === 'remove') {
|
|
82
91
|
const before = teams.length;
|
|
83
92
|
const next = teams.filter(x => x.id !== id);
|
|
84
|
-
if (next.length === before) {
|
|
93
|
+
if (next.length === before) { _fj(json, 'not_found', `team 없음: ${id}`); return; } // 1.36.78 (F4): --json 구조화 + exit 1
|
|
85
94
|
_saveTeams(root, next);
|
|
95
|
+
if (json) { log(JSON.stringify({ ok: true, removed: id, count: next.length }, null, 2)); return; }
|
|
86
96
|
ok(`team 제거: ${id}`);
|
|
87
97
|
return;
|
|
88
98
|
}
|
|
89
99
|
// 1.9.372 (UR-0073 Phase B): team preview — dry-run 실행 계획 미리보기 (실제 dispatch/spawn/배포 없음).
|
|
90
100
|
if (sub === 'preview') {
|
|
91
101
|
const t = teams.find(x => x.id === id);
|
|
92
|
-
if (!t) {
|
|
102
|
+
if (!t) { _fj(json, 'not_found', `team 없음: ${id}`); return; } // 1.36.78 (F4): --json 구조화
|
|
93
103
|
const plan = _composeTeamPlan(t, arg('--task', null));
|
|
94
104
|
if (json) { log(JSON.stringify({ version: VERSION, dryRun: true, ...plan }, null, 2)); return; }
|
|
95
105
|
log(`# team preview ${t.id} (1.9.372, UR-0073 Phase B) — dry-run (실제 실행 없음)`);
|
|
@@ -113,9 +123,19 @@ function teamCmd(root, sub, id, opts = {}, deps = {}) {
|
|
|
113
123
|
// 1.9.376 (UR-0073 Phase D): team deploy — 사용자 설정 deployCommand 실행. 안전: dry-run 기본 + --yes + LEERNESS_TEAM_DEPLOY=1 이중 게이트 + shell-guard.
|
|
114
124
|
if (sub === 'deploy') {
|
|
115
125
|
const t = teams.find(x => x.id === id);
|
|
116
|
-
if (!t) {
|
|
126
|
+
if (!t) { _fj(json, 'not_found', `team 없음: ${id}`); return; }
|
|
117
127
|
const gate = _teamDeployGate(t, { yes: has('--yes'), envOn: process.env.LEERNESS_TEAM_DEPLOY === '1' });
|
|
118
|
-
|
|
128
|
+
// 1.36.78 (F4): --json 모드도 사람 모드와 exit 일치 — no-command/gated 는 실패(exit 1), dry-run 은 정상(exit 0).
|
|
129
|
+
if (gate.mode === 'no-command' || gate.mode === 'gated') process.exitCode = 1;
|
|
130
|
+
// 1.36.78 (검수 #2): execute 는 exact-once JSON 위반(gate JSON 후 사람 로그 + inherited child stdout) → --json execute 전용 분기.
|
|
131
|
+
// 자식 출력을 캡처하고 실행 후 단일 JSON 하나만 방출한다.
|
|
132
|
+
if (json && gate.mode === 'execute') {
|
|
133
|
+
const r = cp.spawnSync(gate.command, { cwd: root, shell: true, encoding: 'utf8', timeout: 600000 });
|
|
134
|
+
if (r.status !== 0) process.exitCode = 1;
|
|
135
|
+
log(JSON.stringify({ version: VERSION, teamId: t.id, ...gate, executed: true, exitStatus: r.status, stdout: (r.stdout || '').slice(0, 4000), stderr: (r.stderr || '').slice(0, 4000) }, null, 2));
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (json) { log(JSON.stringify({ version: VERSION, teamId: t.id, ...gate }, null, 2)); return; } // non-execute --json: 단일 JSON
|
|
119
139
|
if (gate.mode === 'no-command') { fail(`team '${t.id}' deployCommand 미설정 — leerness team add ${t.id} --deploy "<배포 명령>"`); return; }
|
|
120
140
|
if (gate.mode === 'dry-run') {
|
|
121
141
|
log(`# team deploy ${t.id} (1.9.376, UR-0073 Phase D) — dry-run (실행 없음)`);
|
package/lib/tech-profile.js
CHANGED
|
@@ -58,18 +58,22 @@ function _envKeys(root) {
|
|
|
58
58
|
|
|
59
59
|
function _shallowExts(root) {
|
|
60
60
|
const counts = {};
|
|
61
|
+
let _extBudget = 20000; // 1.36.79 (검수 #4): 전체 예산 — 거대 저장소에서도 공정 집계 + 상한 보장
|
|
61
62
|
const scan = (dir, depth) => {
|
|
62
63
|
let entries = [];
|
|
63
64
|
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
64
|
-
|
|
65
|
+
// 1.36.79 (검수 #4): 디렉토리당 400개 컷이 큰 플랫 디렉토리에서 **집계를 편향**시켰다(1501 JS vs 500 PY 인데 Python 1위).
|
|
66
|
+
// 순위가 실제 파일 수에 기반하려면 세는 범위가 공정해야 한다 — 상한을 크게(디렉토리당 5000, 전체 예산 20000) 두고 공정 집계.
|
|
67
|
+
for (const e of entries.slice(0, 5000)) {
|
|
68
|
+
if (_extBudget <= 0) return;
|
|
65
69
|
if (e.name.startsWith('.') || e.name === 'node_modules' || e.name === 'dist' || e.name === 'build') continue;
|
|
66
70
|
const p = path.join(dir, e.name);
|
|
67
71
|
if (e.isDirectory()) { if (depth > 0) scan(p, depth - 1); continue; }
|
|
68
72
|
const ext = path.extname(e.name).toLowerCase();
|
|
69
|
-
if (EXT_LANG[ext]) counts[EXT_LANG[ext]] = (counts[EXT_LANG[ext]] || 0) + 1;
|
|
73
|
+
if (EXT_LANG[ext]) { counts[EXT_LANG[ext]] = (counts[EXT_LANG[ext]] || 0) + 1; _extBudget--; }
|
|
70
74
|
}
|
|
71
75
|
};
|
|
72
|
-
scan(absRoot(root),
|
|
76
|
+
scan(absRoot(root), 3); // 1.36.79 (도그푸딩 P1-D): depth 2→3 — src/pkg/mod 3단 레이아웃의 소스가 통째로 안 세어지던 것
|
|
73
77
|
return counts;
|
|
74
78
|
}
|
|
75
79
|
|
|
@@ -84,6 +88,11 @@ function detectTechProfile(root) {
|
|
|
84
88
|
}
|
|
85
89
|
const extCounts = _shallowExts(root);
|
|
86
90
|
for (const [lang, n] of Object.entries(extCounts)) if (n >= 3) addL(lang, `${n} source files`);
|
|
91
|
+
// 1.36.79 (도그푸딩 P1-D, 실 프로젝트): 마커 파일 존재만으로 순서가 정해져 **devDependency 1개짜리 package.json 이
|
|
92
|
+
// 205개 .py 파일을 이겨** Python 모노레포가 "javascript" 로 라벨링됐다(실측). 실제 소스 파일 수를 1순위 근거로
|
|
93
|
+
// 재정렬 — 파일이 많은 언어가 앞, 동수면 기존(마커) 순서 유지. 마커-only(0 파일)는 뒤로.
|
|
94
|
+
languages.sort((a, b) => (extCounts[b.id] || 0) - (extCounts[a.id] || 0));
|
|
95
|
+
for (const l of languages) { const n = extCounts[l.id] || 0; if (n >= 3 && !/source files/.test(l.evidence)) l.evidence = `${l.evidence} · ${n} source files`; }
|
|
87
96
|
// deps 수집 — 1.36.54 (#5): 모노레포 대응, 루트 + 얕은 하위(2단계) 매니페스트 병합 (vendor 류 제외)
|
|
88
97
|
let deps = {};
|
|
89
98
|
let reqTxt = '';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leerness",
|
|
3
|
-
"version": "1.36.
|
|
3
|
+
"version": "1.36.79",
|
|
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
|
@@ -6103,11 +6103,15 @@ total++;
|
|
|
6103
6103
|
// (4) codex #9: 코드펜스 예제 함수는 계약 아님 → 통과(exit 0) + specFunctions 에 helper 없음
|
|
6104
6104
|
fs.writeFileSync(path.join(d, 'fen.md'), '# S\n```js\nfunction helper(){}\n```\n');
|
|
6105
6105
|
fs.writeFileSync(path.join(d, 'fen.js'), 'module.exports={};\n');
|
|
6106
|
-
|
|
6106
|
+
// 1.36.78 (10차 F6): 펜스-only spec 은 선언 0건이라 이제 empty_contract 로 거부됨 — 이 테스트 의도는 "helper 가 specFunctions 에 없음"(펜스 제외)이므로 --allow-empty 로 유지.
|
|
6107
|
+
const cfen = cp.spawnSync(process.execPath, [CLI, 'contract', 'verify', path.join(d, 'fen.md'), path.join(d, 'fen.js'), '--json', '--allow-empty'], { encoding: 'utf8', timeout: 15000 });
|
|
6107
6108
|
let fenceOk = false; try { const j = JSON.parse(cfen.stdout); fenceOk = cfen.status === 0 && j.ok === true && !(j.specFunctions || []).includes('helper'); } catch {}
|
|
6109
|
+
// 1.36.78 (10차 F6): 펜스-only(--allow-empty 없음)는 empty_contract 거부(exit 1)
|
|
6110
|
+
const cfenReject = cp.spawnSync(process.execPath, [CLI, 'contract', 'verify', path.join(d, 'fen.md'), path.join(d, 'fen.js'), '--json'], { encoding: 'utf8', timeout: 15000 });
|
|
6111
|
+
let fenceRejectOk = false; try { fenceRejectOk = cfenReject.status === 1 && JSON.parse(cfenReject.stdout).code === 'empty_contract'; } catch {}
|
|
6108
6112
|
fs.rmSync(d, { recursive: true, force: true });
|
|
6109
|
-
ok = fpFixed && fnGuard && bracketOk && fenceOk;
|
|
6110
|
-
if (!ok) console.log(` [cvdollar 디버그] fpFixed=${fpFixed} fnGuard=${fnGuard} bracket=${bracketOk} fence=${fenceOk}`);
|
|
6113
|
+
ok = fpFixed && fnGuard && bracketOk && fenceOk && fenceRejectOk;
|
|
6114
|
+
if (!ok) console.log(` [cvdollar 디버그] fpFixed=${fpFixed} fnGuard=${fnGuard} bracket=${bracketOk} fence=${fenceOk} fenceReject=${fenceRejectOk}`);
|
|
6111
6115
|
} catch {}
|
|
6112
6116
|
console.log(ok ? '✓ B(1.35.11) 자체 contract 헌트 + codex 교차: $필드 정규식 안전화 + bracket export(#8) + 코드펜스 예제 제외(#9) FP 3종 + no-FN' : '✗ contract 헌트 FP 3종 수정 실패');
|
|
6113
6117
|
if (!ok) failed++;
|
|
@@ -7546,5 +7550,161 @@ total++;
|
|
|
7546
7550
|
if (!ok) failed++;
|
|
7547
7551
|
}
|
|
7548
7552
|
|
|
7553
|
+
// 1.36.78 (10차 헌트 확정 7건): team 형상/계약 · constraints 검증/손상 · agents recommend · 빈 contract
|
|
7554
|
+
total++;
|
|
7555
|
+
{
|
|
7556
|
+
let ok = false;
|
|
7557
|
+
const _d = [];
|
|
7558
|
+
try {
|
|
7559
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-h10-')); _d.push(d);
|
|
7560
|
+
cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes', '--no-env', '--no-stale-check'], { encoding: 'utf8', timeout: 40000 });
|
|
7561
|
+
const T = (a) => cp.spawnSync(process.execPath, [CLI, ...a, '--path', d], { encoding: 'utf8', timeout: 20000 });
|
|
7562
|
+
const J = (r) => { try { return JSON.parse(r.stdout); } catch { return null; } };
|
|
7563
|
+
// F1: 객체 루트 teams.json → 변경 거부 + 원본 보존
|
|
7564
|
+
const tj = path.join(d, '.harness', 'teams.json');
|
|
7565
|
+
fs.writeFileSync(tj, '{"teams":[{"id":"preserved","members":["codex"]}],"metadata":{"revision":7}}');
|
|
7566
|
+
const beforeT = fs.readFileSync(tj, 'utf8');
|
|
7567
|
+
const f1a = T(['team', 'add', 'newteam', '--members', 'claude', '--json']);
|
|
7568
|
+
const f1Ok = f1a.status === 1 && fs.readFileSync(tj, 'utf8') === beforeT;
|
|
7569
|
+
// F1 variant2: string members → 크래시 대신 정규화(list 정상)
|
|
7570
|
+
fs.writeFileSync(tj, '[{"id":"bad","members":"codex","personas":[],"schedule":"manual","status":"active"}]');
|
|
7571
|
+
let f1bOk = false; try { f1bOk = Array.isArray(J(T(['team', 'list', '--json'])).teams[0].members); } catch {}
|
|
7572
|
+
// F4: team --json 순수 + exit 일치
|
|
7573
|
+
fs.writeFileSync(tj, '[]');
|
|
7574
|
+
const addJson = T(['team', 'add', 'alpha', '--members', 'claude,codex', '--json']);
|
|
7575
|
+
const f4addOk = J(addJson) && J(addJson).ok === true;
|
|
7576
|
+
const dupJson = T(['team', 'add', 'alpha', '--members', 'x', '--json']);
|
|
7577
|
+
const f4dupOk = J(dupJson) && J(dupJson).ok === false && dupJson.status === 1;
|
|
7578
|
+
const rmMiss = T(['team', 'remove', 'nope', '--json']);
|
|
7579
|
+
const f4rmOk = J(rmMiss) && rmMiss.status === 1;
|
|
7580
|
+
const depHuman = T(['team', 'deploy', 'alpha']);
|
|
7581
|
+
const depJson = T(['team', 'deploy', 'alpha', '--json']);
|
|
7582
|
+
const f4depOk = depHuman.status === depJson.status && depHuman.status === 1 && J(depJson) != null;
|
|
7583
|
+
// F2: 무효 constraint 거부 + dedup
|
|
7584
|
+
const _pcf = path.join(d, '.harness', 'platform-constraints.json'); // 무효 add 는 파일 자체를 안 만들 수 있음(부재 = 미저장)
|
|
7585
|
+
const f2malOk = T(['constraints', 'add', 'x1', '--alias', 'x1', '--constraint', 'rate-limit', '--json']).status === 1
|
|
7586
|
+
&& T(['constraints', 'add', 'x2', '--alias', 'x2', '--constraint', 'auth:', '--json']).status === 1
|
|
7587
|
+
&& !(fs.existsSync(_pcf) && fs.readFileSync(_pcf, 'utf8').includes('"x1"'));
|
|
7588
|
+
T(['constraints', 'add', 'dp', '--alias', 'dp', '--constraint', 'auth:token required', '--json']);
|
|
7589
|
+
const dup2 = J(T(['constraints', 'add', 'dp', '--alias', 'dp', '--constraint', 'auth:token required', '--json']));
|
|
7590
|
+
const f2dupOk = dup2 && dup2.constraints.length === 1;
|
|
7591
|
+
// F3: 손상 constraints store 표면화
|
|
7592
|
+
fs.writeFileSync(path.join(d, '.harness', 'platform-constraints.json'), '{"platforms":');
|
|
7593
|
+
const f3Ok = J(T(['constraints', 'list', '--json'])).corruptStore === true
|
|
7594
|
+
&& J(T(['constraints', 'check', 'stripe API', '--json'])).corruptStore === true;
|
|
7595
|
+
// F5: agents recommend 존재
|
|
7596
|
+
const rec = J(cp.spawnSync(process.execPath, [CLI, 'agents', 'recommend', 'refactor payment module', '--json'], { encoding: 'utf8', timeout: 20000 }));
|
|
7597
|
+
const f5Ok = rec && rec.ok === true && typeof rec.recommended === 'string';
|
|
7598
|
+
// F6: 빈 contract 거부 + --allow-empty
|
|
7599
|
+
fs.writeFileSync(path.join(d, 'SPEC.md'), '# Spec\nprose'); fs.writeFileSync(path.join(d, 'impl.js'), '// empty');
|
|
7600
|
+
const f6Ok = cp.spawnSync(process.execPath, [CLI, 'contract', 'verify', path.join(d, 'SPEC.md'), path.join(d, 'impl.js'), '--json'], { encoding: 'utf8', timeout: 20000 }).status === 1
|
|
7601
|
+
&& cp.spawnSync(process.execPath, [CLI, 'contract', 'verify', path.join(d, 'SPEC.md'), path.join(d, 'impl.js'), '--json', '--allow-empty'], { encoding: 'utf8', timeout: 20000 }).status === 0;
|
|
7602
|
+
// (검수 #3) 배열형 platforms 도 손상 표면화 + add 거부(metadata 보존)
|
|
7603
|
+
const pc = path.join(d, '.harness', 'platform-constraints.json');
|
|
7604
|
+
fs.writeFileSync(pc, '{"platforms":[],"metadata":{"revision":7}}');
|
|
7605
|
+
const beforePc = fs.readFileSync(pc, 'utf8');
|
|
7606
|
+
const arrOk = J(T(['constraints', 'list', '--json'])).corruptStore === true
|
|
7607
|
+
&& T(['constraints', 'add', 'p', '--alias', 'p', '--constraint', 'auth:x', '--json']).status === 1
|
|
7608
|
+
&& fs.readFileSync(pc, 'utf8') === beforePc;
|
|
7609
|
+
// (검수 #2) deploy execute --json exact-once JSON
|
|
7610
|
+
const d2 = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-h10dep-')); _d.push(d2);
|
|
7611
|
+
cp.spawnSync(process.execPath, [CLI, 'init', d2, '--yes', '--no-env', '--no-stale-check'], { encoding: 'utf8', timeout: 40000 });
|
|
7612
|
+
cp.spawnSync(process.execPath, [CLI, 'team', 'add', 'dep', '--members', 'claude', '--deploy', 'node -e "process.exit(0)"', '--path', d2], { encoding: 'utf8', timeout: 20000 });
|
|
7613
|
+
const depExec = cp.spawnSync(process.execPath, [CLI, 'team', 'deploy', 'dep', '--yes', '--json', '--path', d2], { encoding: 'utf8', timeout: 30000, env: { ...process.env, LEERNESS_TEAM_DEPLOY: '1' } });
|
|
7614
|
+
let depExecOk = false; try { const j = JSON.parse(depExec.stdout); depExecOk = j.executed === true && j.exitStatus === 0 && depExec.status === 0; } catch {}
|
|
7615
|
+
ok = f1Ok && f1bOk && f4addOk && f4dupOk && f4rmOk && f4depOk && f2malOk && f2dupOk && f3Ok && f5Ok && f6Ok && arrOk && depExecOk;
|
|
7616
|
+
if (!ok) console.log(` [h10 디버그] f1=${f1Ok} f1b=${f1bOk} f4add=${f4addOk} f4dup=${f4dupOk} f4rm=${f4rmOk} f4dep=${f4depOk} f2mal=${f2malOk} f2dup=${f2dupOk} f3=${f3Ok} f5=${f5Ok} f6=${f6Ok} arr=${arrOk} depExec=${depExecOk}`);
|
|
7617
|
+
} catch (e) {} finally { _d.forEach(x => { try { fs.rmSync(x, { recursive: true, force: true }); } catch {} }); }
|
|
7618
|
+
console.log(ok ? '✓ B(1.36.78) 10차헌트: team 형상거부/정규화·JSON계약 · constraints 검증/dedup/손상표면 · agents recommend · 빈 contract 거부' : '✗ 10차 헌트 수정 실패');
|
|
7619
|
+
if (!ok) failed++;
|
|
7620
|
+
}
|
|
7621
|
+
|
|
7622
|
+
// 1.36.79 (실 프로젝트 60개 도그푸딩 P1 5건): progress 행 보존 · handoff --json 순수 · parent path · tech 언어 순위 · audit=integrity 정합
|
|
7623
|
+
total++;
|
|
7624
|
+
{
|
|
7625
|
+
let ok = false;
|
|
7626
|
+
const _d = [];
|
|
7627
|
+
try {
|
|
7628
|
+
// P1-A: 비-T/M/D 행(R-/UR-)이 쓰기 경로에서 소실되면 안 된다 + 클래스 가드
|
|
7629
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-dog-a-')); _d.push(d);
|
|
7630
|
+
cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes', '--minimal', '--no-env', '--no-stale-check'], { encoding: 'utf8', timeout: 40000 });
|
|
7631
|
+
const pt = path.join(d, '.harness', 'progress-tracker.md');
|
|
7632
|
+
fs.writeFileSync(pt, fs.readFileSync(pt, 'utf8').trimEnd() + '\n| R-0001 | done | 사용자 이력 | 증거 | - | 2026-05-07 |\n| UR-0002 | done | 다른 요청 | 증거2 | - | 2026-05-07 |\n');
|
|
7633
|
+
const rowsOf = () => (fs.readFileSync(pt, 'utf8').match(/^\|\s*[A-Z]{1,3}-\d{3,}\s*\|/gm) || []).length;
|
|
7634
|
+
const beforeRows = rowsOf();
|
|
7635
|
+
cp.spawnSync(process.execPath, [CLI, 'task', 'add', 'brand new', '--path', d], { encoding: 'utf8', timeout: 20000 });
|
|
7636
|
+
const rowOk = rowsOf() === beforeRows + 1
|
|
7637
|
+
&& fs.readFileSync(pt, 'utf8').includes('R-0001') && fs.readFileSync(pt, 'utf8').includes('UR-0002');
|
|
7638
|
+
// 클래스 가드: 파서가 못 읽는 행-모양 라인(셀 부족)이 있으면 쓰기 중단 + 원본 보존 (자체 재설계 후 실동작)
|
|
7639
|
+
const dG = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-dog-a2-')); _d.push(dG);
|
|
7640
|
+
cp.spawnSync(process.execPath, [CLI, 'init', dG, '--yes', '--minimal', '--no-env', '--no-stale-check'], { encoding: 'utf8', timeout: 40000 });
|
|
7641
|
+
const ptG = path.join(dG, '.harness', 'progress-tracker.md');
|
|
7642
|
+
fs.appendFileSync(ptG, '| R-0007 | done | 셀부족행 |\n');
|
|
7643
|
+
const beforeG = fs.readFileSync(ptG, 'utf8');
|
|
7644
|
+
const guardR = cp.spawnSync(process.execPath, [CLI, 'task', 'add', 'x', '--path', dG], { encoding: 'utf8', timeout: 20000 });
|
|
7645
|
+
const guardOk = guardR.status === 1 && fs.readFileSync(ptG, 'utf8') === beforeG;
|
|
7646
|
+
// (검수 #2) 무관한 참조표는 task 로 흡수되지 않고, 표 뒤 사용자 콘텐츠는 재작성에도 보존
|
|
7647
|
+
const dS = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-dog-a3-')); _d.push(dS);
|
|
7648
|
+
cp.spawnSync(process.execPath, [CLI, 'init', dS, '--yes', '--minimal', '--no-env', '--no-stale-check'], { encoding: 'utf8', timeout: 40000 });
|
|
7649
|
+
const ptS = path.join(dS, '.harness', 'progress-tracker.md');
|
|
7650
|
+
fs.appendFileSync(ptS, '\n## 참고표\n\n| Code | Owner | Note |\n|---|---|---|\n| API-123 | owner | 무관 참조 |\n');
|
|
7651
|
+
let absorbed = true;
|
|
7652
|
+
try { absorbed = (JSON.parse(cp.spawnSync(process.execPath, [CLI, 'task', 'list', '--path', dS, '--json'], { encoding: 'utf8', timeout: 20000 }).stdout).tasks || []).some(t => t.id === 'API-123'); } catch {}
|
|
7653
|
+
cp.spawnSync(process.execPath, [CLI, 'task', 'add', 'x', '--path', dS], { encoding: 'utf8', timeout: 20000 });
|
|
7654
|
+
const suffixOk = !absorbed && fs.readFileSync(ptS, 'utf8').includes('| API-123 | owner |') && fs.readFileSync(ptS, 'utf8').includes('## 참고표');
|
|
7655
|
+
// P1-B: stale 프로젝트에서도 handoff --json 은 순수 JSON (사람용 drift 배너 미오염)
|
|
7656
|
+
const d2 = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-dog-b-')); _d.push(d2);
|
|
7657
|
+
cp.spawnSync(process.execPath, [CLI, 'init', d2, '--yes', '--no-env', '--no-stale-check'], { encoding: 'utf8', timeout: 40000 });
|
|
7658
|
+
const hp = path.join(d2, '.harness', 'session-handoff.md');
|
|
7659
|
+
if (fs.existsSync(hp)) fs.writeFileSync(hp, fs.readFileSync(hp, 'utf8').replace(/Last generated:.*/, 'Last generated: 2026-01-01T00:00:00.000Z'));
|
|
7660
|
+
const pt2 = path.join(d2, '.harness', 'progress-tracker.md');
|
|
7661
|
+
if (fs.existsSync(pt2)) fs.writeFileSync(pt2, fs.readFileSync(pt2, 'utf8').replace(/2026-\d\d-\d\d/g, '2026-01-01'));
|
|
7662
|
+
const hj = cp.spawnSync(process.execPath, [CLI, 'handoff', d2, '--json'], { encoding: 'utf8', timeout: 40000, maxBuffer: 32 * 1024 * 1024 });
|
|
7663
|
+
let jsonPure = false; try { JSON.parse(hj.stdout); jsonPure = true; } catch {}
|
|
7664
|
+
// 텍스트 모드에서는 배너 유지(무회귀)
|
|
7665
|
+
const ht = cp.spawnSync(process.execPath, [CLI, 'handoff', d2], { encoding: 'utf8', timeout: 40000, maxBuffer: 32 * 1024 * 1024 });
|
|
7666
|
+
const bannerKept = /drift 감지|drift detected/.test(ht.stdout);
|
|
7667
|
+
// P1-C: parent detect 가 positional path 를 존중
|
|
7668
|
+
const d3 = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-dog-c-')); _d.push(d3);
|
|
7669
|
+
cp.spawnSync(process.execPath, [CLI, 'init', d3, '--yes', '--minimal', '--no-env', '--no-stale-check'], { encoding: 'utf8', timeout: 40000 });
|
|
7670
|
+
let parentOk = false;
|
|
7671
|
+
try { const j = JSON.parse(cp.spawnSync(process.execPath, [CLI, 'parent', 'detect', d3, '--json'], { encoding: 'utf8', timeout: 20000, cwd: path.dirname(path.dirname(CLI)) }).stdout); parentOk = String(j.root || j.child || '').includes(path.basename(d3)); } catch {}
|
|
7672
|
+
// P1-D: 소스 파일 수가 마커 파일보다 우선 (Python 모노레포 오라벨 방지) + 진짜 JS 무회귀
|
|
7673
|
+
const d4 = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-dog-d-')); _d.push(d4);
|
|
7674
|
+
cp.spawnSync(process.execPath, [CLI, 'init', d4, '--yes', '--minimal', '--no-env', '--no-stale-check'], { encoding: 'utf8', timeout: 40000 });
|
|
7675
|
+
fs.mkdirSync(path.join(d4, 'src'), { recursive: true });
|
|
7676
|
+
for (let i = 0; i < 30; i++) fs.writeFileSync(path.join(d4, 'src', `m${i}.py`), 'def f():\n return 1\n');
|
|
7677
|
+
fs.writeFileSync(path.join(d4, 'requirements.txt'), 'requests\n');
|
|
7678
|
+
fs.writeFileSync(path.join(d4, 'package.json'), JSON.stringify({ name: 'x', devDependencies: { tailwindcss: '^3' } }));
|
|
7679
|
+
let techOk = false;
|
|
7680
|
+
try { techOk = JSON.parse(cp.spawnSync(process.execPath, [CLI, 'tech', '--json', '--path', d4], { encoding: 'utf8', timeout: 25000 }).stdout).current.languages[0].id === 'python'; } catch {}
|
|
7681
|
+
const d5 = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-dog-d2-')); _d.push(d5);
|
|
7682
|
+
cp.spawnSync(process.execPath, [CLI, 'init', d5, '--yes', '--minimal', '--no-env', '--no-stale-check'], { encoding: 'utf8', timeout: 40000 });
|
|
7683
|
+
fs.mkdirSync(path.join(d5, 'src'), { recursive: true });
|
|
7684
|
+
for (let i = 0; i < 20; i++) fs.writeFileSync(path.join(d5, 'src', `m${i}.js`), 'export const a = 1;\n');
|
|
7685
|
+
fs.writeFileSync(path.join(d5, 'package.json'), JSON.stringify({ name: 'js', dependencies: { react: '^18' } }));
|
|
7686
|
+
let jsOk = false;
|
|
7687
|
+
try { jsOk = JSON.parse(cp.spawnSync(process.execPath, [CLI, 'tech', '--json', '--path', d5], { encoding: 'utf8', timeout: 25000 }).stdout).current.languages[0].id === 'javascript'; } catch {}
|
|
7688
|
+
// P1-E: 코어 문서가 대량 사라진 설치를 audit 이 healthy 로 보고하면 안 된다(integrity 와 정합)
|
|
7689
|
+
const d6 = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-dog-e-')); _d.push(d6);
|
|
7690
|
+
cp.spawnSync(process.execPath, [CLI, 'init', d6, '--yes', '--no-env', '--no-stale-check'], { encoding: 'utf8', timeout: 40000 });
|
|
7691
|
+
const hd = path.join(d6, '.harness');
|
|
7692
|
+
const keep = new Set(['progress-tracker.md', 'HARNESS_VERSION', 'plan.md']);
|
|
7693
|
+
for (const f of fs.readdirSync(hd)) { if (!keep.has(f)) { try { fs.rmSync(path.join(hd, f), { recursive: true, force: true }); } catch {} } }
|
|
7694
|
+
let auditOk = false;
|
|
7695
|
+
try {
|
|
7696
|
+
const au = JSON.parse(cp.spawnSync(process.execPath, [CLI, 'audit', d6, '--json'], { encoding: 'utf8', timeout: 30000 }).stdout);
|
|
7697
|
+
const ic = JSON.parse(cp.spawnSync(process.execPath, [CLI, 'integrity', 'check', d6, '--json'], { encoding: 'utf8', timeout: 30000 }).stdout);
|
|
7698
|
+
auditOk = au.healthy === false && au.failures >= 1 && ic.ok === false; // 두 명령 판정 일치
|
|
7699
|
+
} catch {}
|
|
7700
|
+
// (검수 #3) parent 없는 경로 거부
|
|
7701
|
+
const parentBadOk = cp.spawnSync(process.execPath, [CLI, 'parent', 'detect', 'C:/no/such/dir', '--json'], { encoding: 'utf8', timeout: 20000 }).status === 1;
|
|
7702
|
+
ok = rowOk && guardOk && suffixOk && jsonPure && bannerKept && parentOk && parentBadOk && techOk && jsOk && auditOk;
|
|
7703
|
+
if (!ok) console.log(` [dogfood 디버그] row=${rowOk} guard=${guardOk} suffix=${suffixOk} json=${jsonPure} banner=${bannerKept} parent=${parentOk} parentBad=${parentBadOk} tech=${techOk} js=${jsOk} audit=${auditOk}`);
|
|
7704
|
+
} catch (e) {} finally { _d.forEach(x => { try { fs.rmSync(x, { recursive: true, force: true }); } catch {} }); }
|
|
7705
|
+
console.log(ok ? '✓ B(1.36.79) 도그푸딩 P1×5: progress 행 보존 · handoff --json 순수(텍스트 배너 유지) · parent path · tech 파일수 우선 · audit=integrity 정합' : '✗ 도그푸딩 P1 수정 실패');
|
|
7706
|
+
if (!ok) failed++;
|
|
7707
|
+
}
|
|
7708
|
+
|
|
7549
7709
|
console.log(`\nE2E result: ${total - failed}/${total} passed · ${((Date.now() - _e2eStart) / 1000).toFixed(0)}s`);
|
|
7550
7710
|
if (failed > 0) process.exit(1);
|