leerness 1.36.49 → 1.36.51
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 +20 -0
- package/README.md +4 -4
- package/bin/leerness.js +127 -16
- package/lib/audit.js +7 -3
- package/lib/clarify.js +148 -0
- package/lib/drift.js +6 -3
- package/lib/io.js +74 -5
- package/package.json +1 -1
- package/scripts/e2e.js +34 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.36.51 — 2026-07-21 — 모호성 질문 + 미리보기 승인 워크플로 (사용자 요청 UR-0061)
|
|
4
|
+
|
|
5
|
+
사용자 요청: "자연어에 판단 모호한 부분이 있으면 사용자에게 질문하고, 신규 기능은 코드 작성 전 디자인/기능 미리보기를 제시해 승인/수정을 받도록".
|
|
6
|
+
|
|
7
|
+
- **`leerness clarify "<요청>"`** (lib/clarify.js 신설): 모호성 신호 6종(모호 수식어/지시대명사/복수 선택지/불명 범위/불명 수량/불명 시점) 감지 → **AI 가 사용자에게 그대로 물을 질문 목록** 생성. 신호 없으면 무질문(false-PASS 편향 — "잘"은 요청동사 동반 시만, 과질문으로 흐름 차단 금지). --json 지원.
|
|
8
|
+
- **`leerness preview add|list|show|approve|revise`**: 신규 기능 미리보기 스토어(.harness/previews.json, P-XXXX). add(--design/--features) → proposed → 사용자 제시 → approve(구현 허가) / revise --note(수정요구, 이력 누적). **approve 전 코드 작성 금지가 계약**. 손상 스토어 위 변경 fail-closed. 플래그 값의 제목 흡수 차단(원시 argv 파싱).
|
|
9
|
+
- **지시 레이어 강제**: 신규 init 의 AGENTS.md Required behavior 에 "모호성 질문 의무"+"미리보기 승인 의무(①등록 ②제시·질문 ③approve 후 구현)" 주입 — 자연어만 쓰는 에이전트도 절차를 따르게 됨.
|
|
10
|
+
- **handoff 헤드라인**: 미승인 미리보기 N건 노출("approve 전 코드 금지") — 세션이 바뀌어도 대기 상태가 이월.
|
|
11
|
+
- 검증: selftest 325(+2 행위: 신호→질문/무신호, add→revise→approve 이력+손상거부), e2e +1 라운드트립(제목 미흡수·헤드라인·not_found JSON·AGENTS 주입).
|
|
12
|
+
|
|
13
|
+
## 1.36.50 — 2026-07-21 — 결함 클래스 3종 전수 스윕: 발견이 아니라 클래스를 고친다 (시스템적 진입점 3곳)
|
|
14
|
+
|
|
15
|
+
6차 헌트의 개별 결함 3건을 클래스로 확장해 코드베이스 전수 스윕(교훈: fix-class call-site sweep). 개별 지점 수선 대신 시스템적 진입점 수정으로 현재+미래 지점까지 커버.
|
|
16
|
+
|
|
17
|
+
- **클래스 1 — NaN 날짜 비교 fail-open** (enforce #2 의 클래스): `new Date(손상).getTime()` → NaN 비교가 조용히 통과하던 7개소. drift 4곳(무효 날짜 → mtime 폴백/제외 — advisory 는 false-BLOCK 회피 원칙), audit 1곳(무효 `Updated:` → stale 취급, --fix 가 유효 날짜로 자가 복구), handoff-gap(정보 부재 취급), session-resume(신호 산출 불가 스킵). 실측: `Updated: 2026-99-99` 종전 무언 통과 → 경고+자가복구.
|
|
18
|
+
- **클래스 A — 손상 JSON 스토어 클로버** (toggle #5 의 클래스, codex 실측 21개 스토어): 개별 가드 대신 단일 진입점 `writeUtf8` 에서 "손상 JSON 위에 유효 JSON 저장"(클로버 시그니처) 감지 시 원본을 `<file>.corrupt-<ts>` 로 대피 후 저장 — 바이트 유실 0 · 수동 흐름(handoff 등) 비차단 · 미래 스토어 자동 커버. 알림은 stderr(stdout JSON 계약 불오염). 명시 fail-closed 스토어(state/teams/toggles)는 상류 거부 유지.
|
|
19
|
+
- **클래스 B — --json 오류 계약** (review #7 의 클래스, codex 전수 매트릭스 40+ 실측): `fail()` 진입점 자체가 --json 시 첫 오류를 단일 `{ok:false,error,code:'error'}` 로 방출하고 이후 사람용 출력을 묵음(래치는 argv 정체성 스코프 — REPL/인프로세스에서 자동 해제). failJson 구체 code 경로는 그대로 우선. stderr-only 빈 stdout 6개소(requests complete/drop·wakeup-interval set/record·intent classify/expand)는 fail() 로 전환. enforce remove --json 성공 JSON 화.
|
|
20
|
+
- 이연(정직): --json 성공경로 사람용 텍스트 6개소(resume·pre-wake-audit --last·release cleanup·lessons --auto·bare-help 4종·legacy migrate preview)는 명령별 데이터 형상이 필요해 다음 라운드 — 오류경로와 달리 exit 0 이라 소비자 오판 위험이 낮다.
|
|
21
|
+
- 검증: 클로버 대피/fail 래치/NaN 3종 행위검사 포함 selftest 323(+3), 대표 오류경로 8종 + 기존 PASS 회귀 실측, e2e.
|
|
22
|
+
|
|
3
23
|
## 1.36.49 — 2026-07-21 — codex 6차 헌트: 신작 표면(1.36.28~48) 10건 전수 수정 (High 3 · Medium 5 · Low 2)
|
|
4
24
|
|
|
5
25
|
이번 창에서 신설된 기능들(enforce/toggles/anchors/release note/JSON 계약)을 외부 적대 검증(codex, 30만 토큰) — 10건 발견, 10건 전수 재현 확정 후 전부 수정 (반박 0, DEFER 0).
|
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.51 하네스를 사용합니다. 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.51는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **86개 도구**를 노출:
|
|
180
180
|
|
|
181
181
|
```jsonc
|
|
182
182
|
// 카테고리별
|
|
@@ -197,7 +197,7 @@ Leerness v1.36.49는 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.51 릴리스 태그 이력** (수백 라운드) · _reports/는 비공개 보존.
|
|
201
201
|
|
|
202
202
|
### 성능 가이드
|
|
203
203
|
|
|
@@ -235,6 +235,6 @@ 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.51: 2026-07-21
|
|
239
239
|
<!-- leerness:project-readme:end -->
|
|
240
240
|
|
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.51';
|
|
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') 시 호스트 프로세스 오염.
|
|
@@ -429,7 +429,7 @@ function coreFiles(root, lang = 'ko', selectedSkills = [], opts = {}) {
|
|
|
429
429
|
const project = detectProjectName(root);
|
|
430
430
|
const skillRows = Object.entries(skillCatalog).map(([k, v]) => `| ${k} | ${v.displayNameKo} | ${v.capabilities.join(', ')} | ${v.lastUpdated} | ${v.verification} |`).join('\n');
|
|
431
431
|
const _files = {
|
|
432
|
-
'AGENTS.md': `${MARK}\n# Leerness Agent Instructions\n\n## ⭐ 매 세션 첫 행동\n**반드시 \`.harness/session-workflow.md\`를 먼저 읽고 6단계 워크플로를 따른다**: 요청분석→계획→분배→sub-agent작업→종합검증→마감. 라운드 길이/복잡도 무관, drift 방지를 위해 모든 작업에 동일 흐름 유지.\n\n## 정적 vs 동적 — leerness 역할 경계\n**AGENTS.md = 정적 프로젝트 지침** (코딩 규칙·테스트 명령·금지 사항·배포 절차 — 자주 안 변함).\n**leerness = 동적 작업 상태·기억·검증·인수인계** (현재 목표·수정 파일·실패 시도·검증 결과·다음 에이전트 인계 — 매 작업 변함).\n- 규칙/명령/금지는 여기 AGENTS.md 에 적는다.\n- 동적 상태(결정/교훈/계획/진행/검증/인수인계)는 leerness 가 **기본 워크스페이스 \`.harness/\`** 에 기록한다 (decisions.md / lessons.md / plan.md / progress-tracker.md / session-handoff.md). \`leerness handoff\` · \`decision add\` · \`lesson save\` 등이 여기에 쓴다.\n- (선택) \`leerness state show|start|record|verify|handoff\` (또는 MCP \`leerness_state_*\`) 의 JSON 상태 substrate 는 \`.leerness/\` (에이전트 간 인수인계 표준 — state 명령 사용 시 생성). 메인 워크스페이스(.harness)와 별개.\n- leerness 는 AGENTS.md 를 **대체하지 않고 보완**한다. 정적 지침은 여기, 동적 상태는 leerness.\n\n## Mandatory read order (session start)\n1. **.harness/session-workflow.md** (6단계 워크플로 — 최우선)\n2. .harness/context-routing.md\n3. .harness/session-handoff.md\n4. .harness/current-state.md\n5. .harness/plan.md\n6. .harness/progress-tracker.md\n7. .harness/guideline.md\n8. .harness/protected-files.md\n9. .harness/writeback-policy.md\n10. .harness/anti-lazy-work-policy.md\n11. **.harness/rules.md** (사용자 정의 영구 룰 — 매 세션 반드시 따름)\n\n## Required behavior\n- 작업 시작 시 \`leerness handoff .\`를 실행해 컨텍스트를 적재합니다 (handoff가 active rules를 자동 출력).\n- 작업 분류는 \`leerness route <task-type>\`로 확인합니다 (planning, feature, bugfix, refactor, research, consistency, release, migration, session-start, session-close, harness-maintenance).\n- 보호 파일/관리 섹션을 삭제하지 않습니다. 머지·아카이브·deprecated 표시를 사용합니다.\n- 의미 있는 변경 후 progress-tracker, current-state, task-log, session-handoff를 갱신합니다.\n- 완료 선언 전 \`leerness check .\` 또는 \`leerness lazy detect .\`로 자기검증하고, \`leerness lens\`의 분야별 자기질문에 답합니다 (코드: "선임 개발자가 복잡하다고 느끼지 않을까?" / 디자인: "선임 디자이너와 일반 사용자가 이쁘고 직관적이라 느낄까?").\n- 변경 전 secret/encoding 가드: \`leerness scan secrets .\`, \`leerness encoding check .\`.\n- 같은 기능 중복 생성 전 design-system.md, consistency-policy.md, reuse-map.md를 확인합니다.\n- 매 세션 종료 시 \`leerness session close .\`로 9개 카테고리(완료/진행중/미완료/예정/대기/보류/차단/드랍/검증) + **활성 룰 검증 결과**를 보고합니다.\n- 업데이트는 \`leerness update --check\` (감지) → \`leerness update --yes\` (자동 마이그레이션).\n\n## 자연어 회고/통찰/브레인스토밍\n사용자가 자연어로 회고/통찰/브레인스토밍을 요청하면 즉시 leerness 명령으로 호출합니다.\n\n| 사용자 발화 (자연어) | 즉시 실행할 명령 |\n|---|---|\n| "회고해줘 / 돌아보자 / 정리해줘" | \`leerness retro\` |\n| "최근 N일 회고" | \`leerness retro --days N\` |\n| "통계 / 누적 지표 / insights" | \`leerness insights\` |\n| "X에 대해 브레인스토밍 / X 관련 자료 / X 시작 전 검토" | \`leerness brainstorm "X"\` |\n\nsession close가 매번 자동으로 한 줄 요약을 출력하고, 5세션마다 자동 깊은 회고를 실행합니다. 사용자가 명시 요청 시 즉시 호출.\n\n## 자연어 룰 처리\n사용자가 자연어로 영구 룰을 요청하면 즉시 leerness rule 명령으로 등록합니다.\n\n| 사용자 발화 (자연어) | 즉시 실행할 명령 |\n|---|---|\n| "매 업데이트마다 버전 bump해줘" | \`leerness rule add "버전을 patch로 bump" --trigger every-update\` |\n| "매 커밋마다 패치노트 추가해줘" | \`leerness rule add "패치노트 추가" --trigger every-commit\` |\n| "세션 종료마다 배포해줘" | \`leerness rule add "배포 (release publish)" --trigger session-close\` |\n| "X 룰 중지/그만/끄기" | \`leerness rule pause <ID>\` (해당 룰 ID는 list로 확인) |\n| "X 룰 제거/삭제" | \`leerness rule remove <ID>\` |\n| "모든 룰 중지" | \`leerness rule stop\` |\n| "룰 다시 켜줘" | \`leerness rule resume-all\` 또는 \`leerness rule resume <ID>\` |\n\n룰을 등록한 후 사용자에게 등록 결과(ID + trigger + 설명)를 보고하고, 그 이후 매 세션마다 자동 적용합니다. 사용자가 "중지" 또는 "제거"를 명시적으로 말하기 전까지는 룰을 비활성화하지 않습니다.\n\n## 룰 자동 적용\nleerness가 자동 검증 가능한 trigger:\n- **every-update / version bump 키워드 룰**: package.json의 version이 갱신됐는지 검사 (handoff/session close가 baseline 캐시와 비교).\n- **CHANGELOG / 패치노트 키워드 룰**: CHANGELOG.md의 mtime이 갱신됐는지 검사.\n- **test / 테스트 / verify 키워드 룰**: review-evidence.md에 오늘 verify-code 흔적이 있는지 검사.\n- **배포 / publish / push 키워드 룰**: 자동 검증 불가 → 사용자에게 release publish 명령을 안내.\n\n자동 검증 가능한 룰의 실행은 \`leerness release bump\`, \`leerness release note "..."\`, \`leerness release publish\`를 사용해 자동화합니다.\n`,
|
|
432
|
+
'AGENTS.md': `${MARK}\n# Leerness Agent Instructions\n\n## ⭐ 매 세션 첫 행동\n**반드시 \`.harness/session-workflow.md\`를 먼저 읽고 6단계 워크플로를 따른다**: 요청분석→계획→분배→sub-agent작업→종합검증→마감. 라운드 길이/복잡도 무관, drift 방지를 위해 모든 작업에 동일 흐름 유지.\n\n## 정적 vs 동적 — leerness 역할 경계\n**AGENTS.md = 정적 프로젝트 지침** (코딩 규칙·테스트 명령·금지 사항·배포 절차 — 자주 안 변함).\n**leerness = 동적 작업 상태·기억·검증·인수인계** (현재 목표·수정 파일·실패 시도·검증 결과·다음 에이전트 인계 — 매 작업 변함).\n- 규칙/명령/금지는 여기 AGENTS.md 에 적는다.\n- 동적 상태(결정/교훈/계획/진행/검증/인수인계)는 leerness 가 **기본 워크스페이스 \`.harness/\`** 에 기록한다 (decisions.md / lessons.md / plan.md / progress-tracker.md / session-handoff.md). \`leerness handoff\` · \`decision add\` · \`lesson save\` 등이 여기에 쓴다.\n- (선택) \`leerness state show|start|record|verify|handoff\` (또는 MCP \`leerness_state_*\`) 의 JSON 상태 substrate 는 \`.leerness/\` (에이전트 간 인수인계 표준 — state 명령 사용 시 생성). 메인 워크스페이스(.harness)와 별개.\n- leerness 는 AGENTS.md 를 **대체하지 않고 보완**한다. 정적 지침은 여기, 동적 상태는 leerness.\n\n## Mandatory read order (session start)\n1. **.harness/session-workflow.md** (6단계 워크플로 — 최우선)\n2. .harness/context-routing.md\n3. .harness/session-handoff.md\n4. .harness/current-state.md\n5. .harness/plan.md\n6. .harness/progress-tracker.md\n7. .harness/guideline.md\n8. .harness/protected-files.md\n9. .harness/writeback-policy.md\n10. .harness/anti-lazy-work-policy.md\n11. **.harness/rules.md** (사용자 정의 영구 룰 — 매 세션 반드시 따름)\n\n## Required behavior\n- 작업 시작 시 \`leerness handoff .\`를 실행해 컨텍스트를 적재합니다 (handoff가 active rules를 자동 출력).\n- **모호성 질문 의무**: 사용자 요청에 판단이 갈리는 부분(모호한 수식어/지시대명사/복수 선택지/불명확한 범위)이 있으면 **추측으로 구현하지 말고 먼저 사용자에게 질문**합니다. \`leerness clarify "<요청>"\` 이 감지한 질문 목록을 그대로 사용자에게 물어보세요. 신호가 없어도 스스로 판단이 갈리면 질문이 우선입니다.\n- **미리보기 승인 의무 (신규 기능)**: 사용자가 신규 기능 추가/구현을 요청하면 **코드를 먼저 작성하지 않습니다**. ① \`leerness preview add "<기능>" --design "<디자인/UX 설명>" --features "<기능 목록>"\` 으로 미리보기를 등록하고 ② 그 내용을 사용자에게 제시해 승인 또는 수정사항을 질문으로 받습니다. ③ 사용자가 승인하면 \`leerness preview approve <P-ID>\`, 수정 요구면 \`leerness preview revise <P-ID> --note "..."\` 후 미리보기를 고쳐 다시 제시합니다. **approve 전에는 해당 기능의 코드를 작성하지 않습니다.**\n- 작업 분류는 \`leerness route <task-type>\`로 확인합니다 (planning, feature, bugfix, refactor, research, consistency, release, migration, session-start, session-close, harness-maintenance).\n- 보호 파일/관리 섹션을 삭제하지 않습니다. 머지·아카이브·deprecated 표시를 사용합니다.\n- 의미 있는 변경 후 progress-tracker, current-state, task-log, session-handoff를 갱신합니다.\n- 완료 선언 전 \`leerness check .\` 또는 \`leerness lazy detect .\`로 자기검증하고, \`leerness lens\`의 분야별 자기질문에 답합니다 (코드: "선임 개발자가 복잡하다고 느끼지 않을까?" / 디자인: "선임 디자이너와 일반 사용자가 이쁘고 직관적이라 느낄까?").\n- 변경 전 secret/encoding 가드: \`leerness scan secrets .\`, \`leerness encoding check .\`.\n- 같은 기능 중복 생성 전 design-system.md, consistency-policy.md, reuse-map.md를 확인합니다.\n- 매 세션 종료 시 \`leerness session close .\`로 9개 카테고리(완료/진행중/미완료/예정/대기/보류/차단/드랍/검증) + **활성 룰 검증 결과**를 보고합니다.\n- 업데이트는 \`leerness update --check\` (감지) → \`leerness update --yes\` (자동 마이그레이션).\n\n## 자연어 회고/통찰/브레인스토밍\n사용자가 자연어로 회고/통찰/브레인스토밍을 요청하면 즉시 leerness 명령으로 호출합니다.\n\n| 사용자 발화 (자연어) | 즉시 실행할 명령 |\n|---|---|\n| "회고해줘 / 돌아보자 / 정리해줘" | \`leerness retro\` |\n| "최근 N일 회고" | \`leerness retro --days N\` |\n| "통계 / 누적 지표 / insights" | \`leerness insights\` |\n| "X에 대해 브레인스토밍 / X 관련 자료 / X 시작 전 검토" | \`leerness brainstorm "X"\` |\n\nsession close가 매번 자동으로 한 줄 요약을 출력하고, 5세션마다 자동 깊은 회고를 실행합니다. 사용자가 명시 요청 시 즉시 호출.\n\n## 자연어 룰 처리\n사용자가 자연어로 영구 룰을 요청하면 즉시 leerness rule 명령으로 등록합니다.\n\n| 사용자 발화 (자연어) | 즉시 실행할 명령 |\n|---|---|\n| "매 업데이트마다 버전 bump해줘" | \`leerness rule add "버전을 patch로 bump" --trigger every-update\` |\n| "매 커밋마다 패치노트 추가해줘" | \`leerness rule add "패치노트 추가" --trigger every-commit\` |\n| "세션 종료마다 배포해줘" | \`leerness rule add "배포 (release publish)" --trigger session-close\` |\n| "X 룰 중지/그만/끄기" | \`leerness rule pause <ID>\` (해당 룰 ID는 list로 확인) |\n| "X 룰 제거/삭제" | \`leerness rule remove <ID>\` |\n| "모든 룰 중지" | \`leerness rule stop\` |\n| "룰 다시 켜줘" | \`leerness rule resume-all\` 또는 \`leerness rule resume <ID>\` |\n\n룰을 등록한 후 사용자에게 등록 결과(ID + trigger + 설명)를 보고하고, 그 이후 매 세션마다 자동 적용합니다. 사용자가 "중지" 또는 "제거"를 명시적으로 말하기 전까지는 룰을 비활성화하지 않습니다.\n\n## 룰 자동 적용\nleerness가 자동 검증 가능한 trigger:\n- **every-update / version bump 키워드 룰**: package.json의 version이 갱신됐는지 검사 (handoff/session close가 baseline 캐시와 비교).\n- **CHANGELOG / 패치노트 키워드 룰**: CHANGELOG.md의 mtime이 갱신됐는지 검사.\n- **test / 테스트 / verify 키워드 룰**: review-evidence.md에 오늘 verify-code 흔적이 있는지 검사.\n- **배포 / publish / push 키워드 룰**: 자동 검증 불가 → 사용자에게 release publish 명령을 안내.\n\n자동 검증 가능한 룰의 실행은 \`leerness release bump\`, \`leerness release note "..."\`, \`leerness release publish\`를 사용해 자동화합니다.\n`,
|
|
433
433
|
'CLAUDE.md': `${MARK}\n# Claude Code Instructions\n\nFollow AGENTS.md. Always run \`leerness handoff .\` at the start and \`leerness session close .\` before ending a session.\n\n**⭐ 매 세션 첫 행동**: \`.harness/session-workflow.md\`의 6단계 워크플로(요청분석→계획→분배→sub-agent→종합검증→마감)를 따라야 함. drift critical 시 \`leerness drift check --auto-fix\`로 자동 회복.\n\nProtected files must not be deleted. Read .harness/anti-lazy-work-policy.md before claiming completion.\n\n## 자연어 영구 룰\n사용자가 "매 X마다 Y를 해줘" 같은 자연어 룰을 말하면 즉시 \`leerness rule add "Y" --trigger every-X\`로 등록하세요. 등록된 룰은 매 세션 \`handoff\`가 자동 출력하고, \`session close\`가 자동 검증해 보고합니다. 사용자가 "중지" / "그만" / "끄기"를 명시할 때만 \`rule pause/remove\`를 호출합니다.\n\n자세한 매핑은 AGENTS.md의 "자연어 룰 처리" 표를 참고하세요.\n`,
|
|
434
434
|
'.cursor/rules/leerness.mdc': `${MARK}\n---\nalwaysApply: true\n---\nFollow AGENTS.md and .harness/context-routing.md.\nRun: \`leerness handoff .\` at session start.\nRun: \`leerness session close .\` at session end.\nPreserve Leerness protected files.\n`,
|
|
435
435
|
'.github/copilot-instructions.md': `${MARK}\n# Copilot Instructions\n\nUse AGENTS.md and .harness/ as project memory.\nDo not remove protected Leerness files.\nBefore completion, ensure plan.md, progress-tracker.md, current-state.md, session-handoff.md are updated.\n`,
|
|
@@ -4113,10 +4113,12 @@ function _selfTestCases() {
|
|
|
4113
4113
|
const io = require('../lib/io');
|
|
4114
4114
|
if (typeof io.setQuiet !== 'function') return false;
|
|
4115
4115
|
// quiet 면 log/ok/warn 묵음, fail/failJson 은 노출
|
|
4116
|
-
|
|
4116
|
+
// 1.36.50: fail 은 json-argv 모드에서 stderr 강등(계약 v2) — "오류는 항상 노출" 의도는 양 스트림으로 검증
|
|
4117
|
+
let out = ''; const _w = process.stdout.write, _e = process.stderr.write;
|
|
4118
|
+
process.stdout.write = s => { out += s; return true; }; process.stderr.write = s => { out += s; return true; };
|
|
4117
4119
|
let quietLogSuppressed, failShown;
|
|
4118
4120
|
try { io.setQuiet(true); io.log('HUMAN'); io.ok('OK'); quietLogSuppressed = !/HUMAN|OK/.test(out); out = ''; io.fail('ERR'); failShown = /ERR/.test(out); }
|
|
4119
|
-
finally { io.setQuiet(false); process.stdout.write = _w; process.exitCode = 0; }
|
|
4121
|
+
finally { io.setQuiet(false); process.stdout.write = _w; process.stderr.write = _e; process.exitCode = 0; }
|
|
4120
4122
|
const src = read(__filename);
|
|
4121
4123
|
const wired = src.includes("setQuiet(true);") && src.includes("action: 'init', version: VERSION, path: _initRoot, harnessFiles");
|
|
4122
4124
|
return quietLogSuppressed && failShown && wired;
|
|
@@ -4933,6 +4935,90 @@ function _selfTestCases() {
|
|
|
4933
4935
|
const dg = read(path.join(__dirname, '..', 'lib', 'diagnostics.js'));
|
|
4934
4936
|
const wnOk = /compareVer\(fromV, toV\) > 0/.test(dg) && dg.includes("code: 'invalid_range'");
|
|
4935
4937
|
return tOk && noteOk && revOk && wnOk;
|
|
4938
|
+
} },
|
|
4939
|
+
{ name: '스토어 클로버 대피 (1.36.50, 클래스 스윕 A): 손상 JSON 위 유효 JSON 저장 시 원본 .corrupt-* 대피 — 행위검사', run: () => {
|
|
4940
|
+
const io = require('../lib/io');
|
|
4941
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_cw_'));
|
|
4942
|
+
const _ce = console.error; console.error = () => {}; // 대피 알림이 doctor(en) 등 상위 출력에 새지 않게 격리
|
|
4943
|
+
try {
|
|
4944
|
+
const f = path.join(tmp, 's.json');
|
|
4945
|
+
fs.writeFileSync(f, '{"keep":[1,'); // 손상
|
|
4946
|
+
io.writeUtf8(f, '{"fresh":true}');
|
|
4947
|
+
const baks = fs.readdirSync(tmp).filter(n => n.startsWith('s.json.corrupt-'));
|
|
4948
|
+
const rescued = baks.length === 1 && fs.readFileSync(path.join(tmp, baks[0]), 'utf8') === '{"keep":[1,' && read(f) === '{"fresh":true}';
|
|
4949
|
+
// 정상 파일 덮어쓰기·비-JSON 새 내용은 대피 없음(백업 증식 방지)
|
|
4950
|
+
io.writeUtf8(f, '{"fresh":2}');
|
|
4951
|
+
const f2 = path.join(tmp, 'n.json'); fs.writeFileSync(f2, 'not json'); io.writeUtf8(f2, 'still not json');
|
|
4952
|
+
const noExtra = fs.readdirSync(tmp).filter(n => n.includes('.corrupt-')).length === 1;
|
|
4953
|
+
return rescued && noExtra;
|
|
4954
|
+
} finally { console.error = _ce; try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} }
|
|
4955
|
+
} },
|
|
4956
|
+
{ name: 'NaN 날짜 fail-open 클래스 스윕 (1.36.50): drift mtime 폴백·무효날짜 제외 + audit stale 취급 + handoff-gap 정보부재 — 소스가드', run: () => {
|
|
4957
|
+
const dr = read(path.join(__dirname, '..', 'lib', 'drift.js'));
|
|
4958
|
+
const drOk = (dr.match(/Number\.isFinite/g) || []).length >= 4;
|
|
4959
|
+
const au = read(path.join(__dirname, '..', 'lib', 'audit.js'));
|
|
4960
|
+
const auOk = /if \(!Number\.isFinite\(dDays\)\) dDays = Infinity/.test(au);
|
|
4961
|
+
const s = read(__filename);
|
|
4962
|
+
const gapOk = /if \(!Number\.isFinite\(lastMs\)\) return \{ hasLast: false \}/.test(s);
|
|
4963
|
+
return drOk && auOk && gapOk;
|
|
4964
|
+
} },
|
|
4965
|
+
{ name: '--json 오류 계약 진입점 v2 (1.36.50, 클래스 스윕 B): fail 래치 — 사람용 stderr 강등 + JSON 페이로드 통과 + 명시 failJson(false) human 보장 — 행위검사', run: () => {
|
|
4966
|
+
const io = require('../lib/io');
|
|
4967
|
+
const saveArgv = process.argv, saveExit = process.exitCode; const outs = [], errs = [];
|
|
4968
|
+
const _log = console.log, _err = console.error;
|
|
4969
|
+
try {
|
|
4970
|
+
process.argv = ['node', 'x', 'cmd', '--json'];
|
|
4971
|
+
console.log = (s) => outs.push(String(s)); console.error = (s) => errs.push(String(s));
|
|
4972
|
+
io.setQuiet(false); // 래치 초기화
|
|
4973
|
+
io.fail('첫 오류'); io.fail('둘째'); io.log('사람용 잔여줄'); io.log('{"agg":true}');
|
|
4974
|
+
// stdout: 집계 JSON 만 통과 · ✗/잔여줄은 stderr
|
|
4975
|
+
const passThru = outs.length === 1 && outs[0] === '{"agg":true}';
|
|
4976
|
+
const demoted = errs.some(e => e.includes('첫 오류')) && errs.some(e => e === '사람용 잔여줄');
|
|
4977
|
+
// 명시 failJson(false) → argv 에 --json 있어도 human ✗ (호출자 결정 우선, UR-0099 계약)
|
|
4978
|
+
io.setQuiet(false); outs.length = 0;
|
|
4979
|
+
io.failJson(false, 'c', '사람지정');
|
|
4980
|
+
const humanExplicit = outs.length === 1 && outs[0] === '✗ 사람지정';
|
|
4981
|
+
// 비-json argv 사람용 ✗ 유지
|
|
4982
|
+
io.setQuiet(false); process.argv = ['node', 'x', 'cmd']; outs.length = 0;
|
|
4983
|
+
io.fail('사람용');
|
|
4984
|
+
const human = outs.length === 1 && outs[0] === '✗ 사람용';
|
|
4985
|
+
return passThru && demoted && humanExplicit && human;
|
|
4986
|
+
} finally { console.log = _log; console.error = _err; process.argv = saveArgv; io.setQuiet(false); process.exitCode = saveExit; }
|
|
4987
|
+
} },
|
|
4988
|
+
{ name: 'clarify 모호성 감지 (1.36.51, UR-0061): 신호어→질문 생성 + 무신호 false-PASS 편향 — 순수 행위검사', run: () => {
|
|
4989
|
+
const c = require('../lib/clarify');
|
|
4990
|
+
const r1 = c._clarifySignals('로그인 화면 적당히 이쁘게 만들어줘, 그리고 그거도 고쳐줘');
|
|
4991
|
+
const hit = r1.ambiguous && r1.signals.some(s => s.kind === 'vague-quality') && r1.signals.some(s => s.kind === 'pronoun') && r1.questions.length >= 2 && r1.questions.every(q => q.includes('?') || q.includes('요'));
|
|
4992
|
+
const r2 = c._clarifySignals('src/login.js 의 validateEmail 함수가 빈 문자열에서 true 를 반환하는 버그를 수정');
|
|
4993
|
+
const clean = !r2.ambiguous && r2.questions.length === 0; // 구체 요청은 무질문 (false-BLOCK 방지)
|
|
4994
|
+
const wired = read(__filename).includes("cmd === 'clarify'") && read(__filename).includes('모호성 질문 의무');
|
|
4995
|
+
return hit && clean && wired;
|
|
4996
|
+
} },
|
|
4997
|
+
{ name: 'preview 승인 워크플로 (1.36.51, UR-0061): add→proposed→pending 노출→approve/revise 이력 + 손상 스토어 거부 — 행위검사', run: () => {
|
|
4998
|
+
const c = require('../lib/clarify');
|
|
4999
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_pv_'));
|
|
5000
|
+
const _w = process.stdout.write; const _ce = console.error;
|
|
5001
|
+
try {
|
|
5002
|
+
process.stdout.write = () => true; console.error = () => {};
|
|
5003
|
+
fs.mkdirSync(path.join(tmp, '.harness'), { recursive: true });
|
|
5004
|
+
const deps = { has: () => false, arg: (k, d) => (k === '--design' ? '카드형 목록 UI' : k === '--features' ? '검색,정렬' : d) };
|
|
5005
|
+
c.previewCmd(tmp, 'add', ['주문', '목록'], deps);
|
|
5006
|
+
let list = c._loadPreviews(tmp);
|
|
5007
|
+
const added = list.length === 1 && list[0].id === 'P-0001' && list[0].status === 'proposed' && list[0].design === '카드형 목록 UI' && list[0].features.length === 2;
|
|
5008
|
+
const pending1 = c.pendingPreviews(tmp).length === 1;
|
|
5009
|
+
c.previewCmd(tmp, 'revise', ['P-0001'], { has: () => false, arg: (k, d) => (k === '--note' ? '색상 변경 요청' : d) });
|
|
5010
|
+
list = c._loadPreviews(tmp);
|
|
5011
|
+
const revised = list[0].status === 'revision-requested' && list[0].history.some(h => h.note === '색상 변경 요청') && c.pendingPreviews(tmp).length === 1;
|
|
5012
|
+
c.previewCmd(tmp, 'approve', ['P-0001'], deps);
|
|
5013
|
+
const approved = c._loadPreviews(tmp)[0].status === 'approved' && c.pendingPreviews(tmp).length === 0;
|
|
5014
|
+
// 손상 스토어 위 변경 거부 (fail-closed) — 원본 보존
|
|
5015
|
+
fs.writeFileSync(c._previewsPath(tmp), '[{"id":"P-0001",');
|
|
5016
|
+
const saveExit = process.exitCode;
|
|
5017
|
+
c.previewCmd(tmp, 'add', ['새기능'], deps);
|
|
5018
|
+
process.exitCode = saveExit;
|
|
5019
|
+
const preserved = fs.readFileSync(c._previewsPath(tmp), 'utf8') === '[{"id":"P-0001",';
|
|
5020
|
+
return added && pending1 && revised && approved && preserved;
|
|
5021
|
+
} finally { process.stdout.write = _w; console.error = _ce; try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} }
|
|
4936
5022
|
} }
|
|
4937
5023
|
];
|
|
4938
5024
|
}
|
|
@@ -5227,11 +5313,16 @@ function enforceCmd(root, sub) {
|
|
|
5227
5313
|
return;
|
|
5228
5314
|
}
|
|
5229
5315
|
if (sub === 'remove') {
|
|
5316
|
+
let removed = false, restored = false;
|
|
5230
5317
|
if (exists(hookP) && read(hookP).includes(_ENFORCE_MARK)) {
|
|
5231
|
-
if (exists(chainedP)) { fs.copyFileSync(chainedP, hookP); fs.unlinkSync(chainedP); try { fs.chmodSync(hookP, 0o755); } catch {} }
|
|
5318
|
+
if (exists(chainedP)) { fs.copyFileSync(chainedP, hookP); fs.unlinkSync(chainedP); try { fs.chmodSync(hookP, 0o755); } catch {} restored = true; }
|
|
5232
5319
|
else fs.unlinkSync(hookP);
|
|
5233
|
-
|
|
5234
|
-
}
|
|
5320
|
+
removed = true;
|
|
5321
|
+
}
|
|
5322
|
+
// 1.36.50 (스윕 B): --json 성공도 단일 JSON — 종전엔 사람용 텍스트/무출력이었다
|
|
5323
|
+
if (json) { log(JSON.stringify({ ok: true, removed, restoredChain: restored }, null, 2)); return; }
|
|
5324
|
+
if (removed) ok('enforce 제거 — pre-commit 훅 해제' + (restored ? ' (기존 훅 복원)' : ''));
|
|
5325
|
+
else log('enforce 훅 없음 — 변경 없음');
|
|
5235
5326
|
return;
|
|
5236
5327
|
}
|
|
5237
5328
|
// status
|
|
@@ -6066,6 +6157,8 @@ function commandsCmd(root) {
|
|
|
6066
6157
|
{ cmd: 'release channel [--json]', desc: '릴리스 채널 정책 (latest 안정 / next 실험 / 버전 고정) — 1.9.275' },
|
|
6067
6158
|
{ cmd: 'slash-commands [agent] [--json --record --detect --refresh]', desc: 'CLI 에이전트 슬래시 명령 레지스트리 + --help probe 자동 갱신 (1.9.265~267, UR-0021)' },
|
|
6068
6159
|
{ cmd: 'review-request "<request>"', desc: '사용자 요청 사전 검토 (1.9.176)' },
|
|
6160
|
+
{ cmd: 'clarify "<사용자 요청>" [--json]', desc: '요청 모호성 신호 감지 → 사용자에게 물을 질문 생성 (추측 구현 방지) — 1.36.51 UR-0061' },
|
|
6161
|
+
{ cmd: 'preview add|list|show|approve|revise', desc: '신규 기능 미리보기 승인 워크플로 — approve 전 코드 작성 금지 계약 — 1.36.51 UR-0061' },
|
|
6069
6162
|
{ cmd: 'review <file> --persona <ids>', desc: '페르소나 리뷰 (1.9.29)' },
|
|
6070
6163
|
{ cmd: 'brainstorm "<topic>" [--include-code]', desc: '워크스페이스 회수 + 코드 grep' }
|
|
6071
6164
|
],
|
|
@@ -6380,7 +6473,7 @@ function _detectAbnormalShutdown(root) {
|
|
|
6380
6473
|
if (exists(lhFp)) {
|
|
6381
6474
|
const j = JSON.parse(read(lhFp));
|
|
6382
6475
|
const last = j.lastAt || (j.history && j.history.length > 0 ? j.history[j.history.length - 1] : null);
|
|
6383
|
-
if (last) {
|
|
6476
|
+
if (last && Number.isFinite(new Date(last).getTime())) { // 1.36.50 (NaN 날짜 클래스): 손상 타임스탬프는 신호 산출 불가 — 스킵
|
|
6384
6477
|
const lastMs = new Date(last).getTime();
|
|
6385
6478
|
const gapMin = Math.floor((now - lastMs) / 60000);
|
|
6386
6479
|
if (gapMin > 60) {
|
|
@@ -7016,9 +7109,9 @@ function requestsCmd(root, sub, ...rest) {
|
|
|
7016
7109
|
|
|
7017
7110
|
if (sub === 'complete') {
|
|
7018
7111
|
const id = rest[0];
|
|
7019
|
-
if (!id) {
|
|
7112
|
+
if (!id) { fail('Usage: leerness requests complete <id>'); process.exit(1); } // 1.36.50 (스윕 B): stderr-only → stdout 계약(fail 이 --json 인지)
|
|
7020
7113
|
const updated = _updateUserRequest(root, id, { status: 'completed' });
|
|
7021
|
-
if (!updated) {
|
|
7114
|
+
if (!updated) { fail(`request not found: ${id}`); process.exit(1); }
|
|
7022
7115
|
if (has('--json')) { log(JSON.stringify(updated, null, 2)); return; }
|
|
7023
7116
|
log(grn(`✓ completed ${updated.id}`));
|
|
7024
7117
|
return;
|
|
@@ -7026,9 +7119,9 @@ function requestsCmd(root, sub, ...rest) {
|
|
|
7026
7119
|
|
|
7027
7120
|
if (sub === 'drop') {
|
|
7028
7121
|
const id = rest[0];
|
|
7029
|
-
if (!id) {
|
|
7122
|
+
if (!id) { fail('Usage: leerness requests drop <id>'); process.exit(1); } // 1.36.50 (스윕 B)
|
|
7030
7123
|
const updated = _updateUserRequest(root, id, { status: 'dropped' });
|
|
7031
|
-
if (!updated) {
|
|
7124
|
+
if (!updated) { fail(`request not found: ${id}`); process.exit(1); }
|
|
7032
7125
|
if (has('--json')) { log(JSON.stringify(updated, null, 2)); return; }
|
|
7033
7126
|
log(yel(`✓ dropped ${updated.id}`)); // 1.9.354 (UR-0072 외부리뷰): 성공 동작에 실패 아이콘(✗) 대신 ✓
|
|
7034
7127
|
return;
|
|
@@ -7405,7 +7498,7 @@ function wakeupIntervalCmd(root, sub, val) {
|
|
|
7405
7498
|
|
|
7406
7499
|
if (sub === 'set') {
|
|
7407
7500
|
const n = Number(val);
|
|
7408
|
-
if (!val || isNaN(n) || n < 60) {
|
|
7501
|
+
if (!val || isNaN(n) || n < 60) { fail('Usage: leerness wakeup-interval set <secs ≥ 60>'); process.exit(1); } // 1.36.50 (스윕 B)
|
|
7409
7502
|
const state = _loadWakeupHistory(root);
|
|
7410
7503
|
state.override = n;
|
|
7411
7504
|
_writeWakeupHistory(root, state);
|
|
@@ -7444,7 +7537,7 @@ function wakeupIntervalCmd(root, sub, val) {
|
|
|
7444
7537
|
if (sub === 'record') {
|
|
7445
7538
|
const kind = val || 'auto';
|
|
7446
7539
|
const validKinds = ['auto', 'user-trigger', 'wakeup-miss'];
|
|
7447
|
-
if (!validKinds.includes(kind)) {
|
|
7540
|
+
if (!validKinds.includes(kind)) { fail(`kind must be: ${validKinds.join('/')}`); process.exit(1); } // 1.36.50 (스윕 B)
|
|
7448
7541
|
_recordWakeupFire(root, kind);
|
|
7449
7542
|
if (has('--json')) { log(JSON.stringify({ recorded: kind, at: new Date().toISOString() }, null, 2)); return; }
|
|
7450
7543
|
log(grn(`✓ wakeup fire 기록: ${kind}`));
|
|
@@ -7950,7 +8043,7 @@ function intentCmd(root, sub, ...rest) {
|
|
|
7950
8043
|
|
|
7951
8044
|
if (sub === 'classify') {
|
|
7952
8045
|
const text = rest.filter(x => !x.startsWith('-')).join(' ');
|
|
7953
|
-
if (!text) {
|
|
8046
|
+
if (!text) { fail('Usage: leerness intent classify "<request text>"'); process.exit(1); } // 1.36.50 (스윕 B)
|
|
7954
8047
|
const result = _classifyIntent(text);
|
|
7955
8048
|
if (has('--json')) { log(JSON.stringify({ text, ...result }, null, 2)); return; }
|
|
7956
8049
|
log(cyan(`# leerness intent classify (1.9.213)`));
|
|
@@ -7968,7 +8061,7 @@ function intentCmd(root, sub, ...rest) {
|
|
|
7968
8061
|
|
|
7969
8062
|
if (sub === 'expand') {
|
|
7970
8063
|
const text = rest.filter(x => !x.startsWith('-')).join(' ');
|
|
7971
|
-
if (!text) {
|
|
8064
|
+
if (!text) { fail('Usage: leerness intent expand "<request text>"'); process.exit(1); } // 1.36.50 (스윕 B)
|
|
7972
8065
|
const result = _inferScopeExpansion(text, root);
|
|
7973
8066
|
if (has('--json')) { log(JSON.stringify(result, null, 2)); return; }
|
|
7974
8067
|
log(cyan(`# leerness intent expand (1.9.213) [DRY-RUN]`));
|
|
@@ -8047,6 +8140,7 @@ function _getLastHandoffGap(root) {
|
|
|
8047
8140
|
const j = JSON.parse(read(fp));
|
|
8048
8141
|
if (!j.last) return { hasLast: false };
|
|
8049
8142
|
const lastMs = new Date(j.last).getTime();
|
|
8143
|
+
if (!Number.isFinite(lastMs)) return { hasLast: false }; // 1.36.50 (NaN 날짜 클래스): 손상 타임스탬프 = 정보 부재로 취급
|
|
8050
8144
|
const gapMs = Date.now() - lastMs;
|
|
8051
8145
|
const gapMin = Math.floor(gapMs / 60000);
|
|
8052
8146
|
// R-0001 영구 룰: 25분 (1500s). gap > 35분 (140% buffer) → wakeup miss 의심
|
|
@@ -8454,6 +8548,7 @@ function _saveTeams(root, teams) {
|
|
|
8454
8548
|
// harness 는 deps(VERSION · 공유 저장 _loadTeams/_saveTeams · _detectShellCtx · argv 파서 arg/has)를 구성해 위임(thin wrapper). 호출부/동작 무변경.
|
|
8455
8549
|
const _team = require('../lib/team');
|
|
8456
8550
|
const _tgl = require('../lib/toggles'); // 1.36.30: 기능 토글 (그래프 ⚙ 탭 연동 — gate/lens/auto-graph/delegation-brief)
|
|
8551
|
+
const _clar = require('../lib/clarify'); // 1.36.51 (UR-0061): 모호성 질문 + 미리보기 승인 워크플로
|
|
8457
8552
|
function teamCmd(root, sub, id, opts = {}) { return _team.teamCmd(root, sub, id, opts, { VERSION, _loadTeams, _saveTeams, _detectShellCtx, arg, has, _withLock }); } // 1.36.31: add 경합 락
|
|
8458
8553
|
|
|
8459
8554
|
// 1.9.112: 전용 lessons.md (Memory Write Surface 5번째)
|
|
@@ -10428,6 +10523,11 @@ function handoff(root) {
|
|
|
10428
10523
|
parts.push(t(`📥 요청 ${audit.open} (tracked)`, `📥 ${audit.open} request(s) (tracked)`));
|
|
10429
10524
|
}
|
|
10430
10525
|
} catch {}
|
|
10526
|
+
// 1.36.51 (UR-0061): 미승인 미리보기 — 사용자 답을 기다리는 기능이 있으면 헤드라인 노출 (코드 작성 보류 계약)
|
|
10527
|
+
try {
|
|
10528
|
+
const _pp = _clar.pendingPreviews(root);
|
|
10529
|
+
if (_pp.length) parts.push(t(`🎨 미승인 미리보기 ${_pp.length}건 (approve 전 코드 금지)`, `🎨 ${_pp.length} preview(s) awaiting approval (no code before approve)`));
|
|
10530
|
+
} catch {}
|
|
10431
10531
|
// 14) 1.9.209: pre-wake-audit 최근 보고서 (사용자 명시) — 깨어남 직후 자동 노출
|
|
10432
10532
|
try {
|
|
10433
10533
|
const pwState = _loadPreWakeReport(root);
|
|
@@ -22380,6 +22480,17 @@ async function main() {
|
|
|
22380
22480
|
if (cmd === 'enforce') return enforceCmd(arg('--path', null) || _taskPositionalPath(args, 2) || process.cwd(), args[1]); // 1.36.43: 사용 강제 (git pre-commit)
|
|
22381
22481
|
if (cmd === 'anchors') return anchorsCmd(arg('--path', null) || _taskPositionalPath(args, 1) || process.cwd(), args[1] && !args[1].startsWith('-') ? args[1] : null); // 1.36.36: 정체성앵커 초안
|
|
22382
22482
|
if (cmd === 'toggle') return _tgl.toggleCmd(arg('--path', process.cwd()), args[1], args[2], args[3], { has, VERSION }); // 1.36.30: 기능 토글 (그래프 ⚙ 탭 연동)
|
|
22483
|
+
// 1.36.51 (사용자 요청 UR-0061): 모호성 질문 + 미리보기 승인 워크플로
|
|
22484
|
+
if (cmd === 'clarify') return _clar.clarifyCmd(arg('--path', process.cwd()), args.slice(1).filter(x => !x.startsWith('-')).join(' '), { has });
|
|
22485
|
+
if (cmd === 'preview') {
|
|
22486
|
+
// 미지 플래그(--design/--features/--note)의 값이 positional 로 새는 것 차단 — 원시 argv 에서 플래그+값 스킵 (1.36.49 release note 패턴)
|
|
22487
|
+
const _pvRaw = process.argv.slice(2); const _pvI = _pvRaw.indexOf('preview'); const _pvToks = [];
|
|
22488
|
+
for (let i = _pvI + 1; i < _pvRaw.length && _pvI >= 0; i++) {
|
|
22489
|
+
if (_pvRaw[i].startsWith('--')) { if (['--design', '--features', '--note', '--path'].includes(_pvRaw[i]) && _pvRaw[i + 1]) i++; continue; }
|
|
22490
|
+
_pvToks.push(_pvRaw[i]);
|
|
22491
|
+
}
|
|
22492
|
+
return _clar.previewCmd(arg('--path', process.cwd()), _pvToks[0], _pvToks.slice(1), { has, arg });
|
|
22493
|
+
}
|
|
22383
22494
|
if (cmd === 'lens') return lensCmd(args[1]); // 1.18.3 (UR-0003): 분야별 자기질문 품질 렌즈
|
|
22384
22495
|
// 1.9.233: leerness commands — 카테고리화된 전체 CLI 명령 목록
|
|
22385
22496
|
if (cmd === 'commands') return commandsCmd(arg('--path', null) || _taskPositionalPath(args, 1) || process.cwd());
|
package/lib/audit.js
CHANGED
|
@@ -102,10 +102,14 @@ function audit(root, opts = {}, deps = {}) {
|
|
|
102
102
|
const cur = exists(currentStatePath(root)) ? read(currentStatePath(root)) : '';
|
|
103
103
|
const updMatch = cur.match(/Updated: (\d{4}-\d{2}-\d{2})/);
|
|
104
104
|
if (updMatch) {
|
|
105
|
-
|
|
105
|
+
// 1.36.50 (NaN 날짜 클래스 스윕): "Updated: 2026-99-99" 같은 무효 날짜는 NaN 비교로 무언 통과했다 —
|
|
106
|
+
// 신선함 증명 불가 = stale 취급(여긴 --fix 가 유효 날짜로 재스탬프하므로 자가 복구 경로).
|
|
107
|
+
let dDays = (Date.now() - new Date(updMatch[1]).getTime()) / 86400000;
|
|
108
|
+
if (!Number.isFinite(dDays)) dDays = Infinity;
|
|
106
109
|
if (dDays > 7) {
|
|
107
|
-
|
|
108
|
-
|
|
110
|
+
const dShow = Number.isFinite(dDays) ? Math.round(dDays) : '무효 날짜';
|
|
111
|
+
warnings++; warn(`current-state.md stale (${dShow} days)`);
|
|
112
|
+
_finding('current_state_stale', 'warn', 'current-state.md stale', { days: Number.isFinite(dDays) ? Math.round(dDays) : -1 });
|
|
109
113
|
// 1.9.35 #5: --fix → current-state.md Updated 라인 갱신
|
|
110
114
|
if (fix) {
|
|
111
115
|
const stamped = cur.replace(/Updated: \d{4}-\d{2}-\d{2}/, `Updated: ${today()}`);
|
package/lib/clarify.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// lib/clarify.js — 모호성 질문 + 미리보기 승인 워크플로 (1.36.51, 사용자 요청 UR-0061).
|
|
2
|
+
// ① clarify: 사용자 요청 텍스트에서 판단-모호 신호를 감지해 "AI 가 사용자에게 그대로 물을 질문"을 생성.
|
|
3
|
+
// 원칙: 휴리스틱은 false-PASS 편향(신호어 없으면 명확 판정) — 과질문(false-BLOCK)으로 흐름을 막지 않는다.
|
|
4
|
+
// ② preview: 신규 기능은 코드 작성 전 디자인/기능 미리보기를 제시하고 사용자 승인(approve)/수정요구(revise)를
|
|
5
|
+
// 기록하는 스토어(.harness/previews.json). 승인 전 코드 작성 금지가 지시 레이어 계약.
|
|
6
|
+
'use strict';
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { absRoot, exists, read, writeUtf8, log, ok, warn, fail, failJson, now, today } = require('./io');
|
|
9
|
+
|
|
10
|
+
// ── ① 모호성 신호 카탈로그 (kind → { re, q(match) }) ──────────────────────────
|
|
11
|
+
const CLARIFY_SIGNALS = [
|
|
12
|
+
// '잘'은 "잘 통과하는지" 같은 정상 문장에 흔해 뒤따르는 요청동사가 있을 때만 (false-PASS 편향)
|
|
13
|
+
{ kind: 'vague-quality', re: /(적당히|알아서|이쁘게|예쁘게|깔끔하게|멋지게|잘\s?(?:좀|만들|해줘|해 줘|부탁)|as appropriate|make it nice|make it pretty)/,
|
|
14
|
+
q: (m) => `「${m.trim()}」의 기대 수준이 불명확합니다 — 참고할 예시(스크린샷/사이트/기존 화면)나 구체 기준이 있나요?` },
|
|
15
|
+
{ kind: 'pronoun', re: /(그거|이거|저거|아까\s?그|그 부분|위에서 말한|the one before)/,
|
|
16
|
+
q: (m) => `「${m.trim()}」가 무엇을 가리키는지 확인이 필요합니다 — 대상(파일/화면/기능 이름)을 지정해 주세요.` },
|
|
17
|
+
{ kind: 'alternative', re: /(\S+)\s?(?:이나|나|또는|혹은)\s(\S+)|(\b\w+\b) or (\b\w+\b)/,
|
|
18
|
+
q: (m) => `복수 선택지(${m.trim()})가 언급됐습니다 — 어느 쪽을 원하시나요, 아니면 둘 다인가요?` },
|
|
19
|
+
{ kind: 'vague-scope', re: /(전부 다|전부|모두 다|모든 걸|싹 다|everything|all of (?:it|them))/,
|
|
20
|
+
q: (m) => `「${m.trim()}」의 범위가 넓습니다 — 포함/제외할 대상을 구체적으로 확인해도 될까요?` },
|
|
21
|
+
{ kind: 'vague-amount', re: /(약간|조금|살짝|몇 ?개|여러 ?개|어느 ?정도|a few|some of)/,
|
|
22
|
+
q: (m) => `「${m.trim()}」의 수량/정도가 불명확합니다 — 대략적인 숫자나 기준을 주실 수 있나요?` },
|
|
23
|
+
{ kind: 'undefined-later', re: /(나중에|이따가?|추후|필요하면|여차하면|later|eventually)/,
|
|
24
|
+
q: (m) => `「${m.trim()}」 시점 조건이 모호합니다 — 지금 범위에 포함할지, 이번엔 제외할지 확인이 필요합니다.` },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
// 순수 감지 코어 — 텍스트에서 신호와 질문 목록 생성 (신호 없으면 ambiguous:false)
|
|
28
|
+
function _clarifySignals(text) {
|
|
29
|
+
const t = String(text || '');
|
|
30
|
+
const signals = [];
|
|
31
|
+
for (const s of CLARIFY_SIGNALS) {
|
|
32
|
+
const m = t.match(s.re);
|
|
33
|
+
if (m) signals.push({ kind: s.kind, match: m[0], question: s.q(m[0]) });
|
|
34
|
+
}
|
|
35
|
+
return { ambiguous: signals.length > 0, signals, questions: signals.map(s => s.question) };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// `leerness clarify "<사용자 요청>" [--json]`
|
|
39
|
+
function clarifyCmd(root, text, deps = {}) {
|
|
40
|
+
const { has } = deps;
|
|
41
|
+
const json = !!(has && has('--json'));
|
|
42
|
+
if (!text || !String(text).trim()) { failJson(json, 'text_required', 'clarify "<사용자 요청 텍스트>" 필요 — 모호성 신호를 감지해 사용자에게 물을 질문을 생성'); return; }
|
|
43
|
+
const r = _clarifySignals(text);
|
|
44
|
+
if (json) { log(JSON.stringify({ ok: true, ambiguous: r.ambiguous, signals: r.signals, questions: r.questions }, null, 2)); return; }
|
|
45
|
+
log(`# leerness clarify — 요청 모호성 점검`);
|
|
46
|
+
if (!r.ambiguous) { ok(' 모호 신호 없음 — 그대로 진행 가능 (판단이 갈리면 그래도 물어보는 것이 안전)'); return; }
|
|
47
|
+
warn(` 모호 신호 ${r.signals.length}건 — 작업 시작 전 사용자에게 아래 질문을 하세요:`);
|
|
48
|
+
r.questions.forEach((q, i) => log(` ${i + 1}. ${q}`));
|
|
49
|
+
log(`\n ⓘ 계약: 답을 받기 전에는 추측으로 구현하지 않는다 (AGENTS.md 모호성 규칙)`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ── ② 미리보기 승인 스토어 ─────────────────────────────────────────────────
|
|
53
|
+
function _previewsPath(root) { return path.join(absRoot(root), '.harness', 'previews.json'); }
|
|
54
|
+
|
|
55
|
+
function _loadPreviews(root) {
|
|
56
|
+
const f = _previewsPath(root);
|
|
57
|
+
if (!exists(f)) return [];
|
|
58
|
+
try { const j = JSON.parse(read(f)); return Array.isArray(j) ? j : []; } catch { return []; }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function _savePreviews(root, list, json) {
|
|
62
|
+
const f = _previewsPath(root);
|
|
63
|
+
// 변경 진입점 fail-closed (1.36.28 스토어 손상 클래스) — 손상 파일 위 저장 거부
|
|
64
|
+
if (exists(f)) { try { JSON.parse(read(f)); } catch { failJson(json, 'store_corrupt', `previews.json 손상 — 덮어쓰기 거부: ${f} (복구/삭제 후 재시도)`); return false; } }
|
|
65
|
+
writeUtf8(f, JSON.stringify(list, null, 2) + '\n');
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function _nextPreviewId(list) {
|
|
70
|
+
let max = 0;
|
|
71
|
+
for (const p of list) { const m = String(p.id || '').match(/^P-(\d{4,})$/); if (m) max = Math.max(max, Number(m[1])); }
|
|
72
|
+
return `P-${String(max + 1).padStart(4, '0')}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function pendingPreviews(root) { return _loadPreviews(root).filter(p => p.status === 'proposed' || p.status === 'revision-requested'); }
|
|
76
|
+
|
|
77
|
+
// `leerness preview add "<제목>" [--design "..."] [--features "a,b"] | list | show <id> | approve <id> | revise <id> --note "..."`
|
|
78
|
+
function previewCmd(root, sub, rest, deps = {}) {
|
|
79
|
+
const { has, arg } = deps;
|
|
80
|
+
root = absRoot(root);
|
|
81
|
+
const json = !!(has && has('--json'));
|
|
82
|
+
if (!exists(path.join(root, '.harness'))) { failJson(json, 'harness_missing', `leerness 미설치: ${root} — 먼저 leerness init`); return; }
|
|
83
|
+
const list = _loadPreviews(root);
|
|
84
|
+
sub = sub || 'list';
|
|
85
|
+
|
|
86
|
+
if (sub === 'add') {
|
|
87
|
+
const title = (rest || []).join(' ').trim();
|
|
88
|
+
if (!title) { failJson(json, 'title_required', 'preview add "<기능 제목>" 필요 (+ --design "설명" --features "a,b")'); return; }
|
|
89
|
+
const design = arg ? (arg('--design', '') || '') : '';
|
|
90
|
+
const features = (arg ? (arg('--features', '') || '') : '').split(',').map(s => s.trim()).filter(Boolean);
|
|
91
|
+
const p = { id: _nextPreviewId(list), title, design, features, status: 'proposed', createdAt: now(), history: [{ at: now(), event: 'proposed' }] };
|
|
92
|
+
list.push(p);
|
|
93
|
+
if (!_savePreviews(root, list, json)) return;
|
|
94
|
+
if (json) { log(JSON.stringify({ ok: true, ...p }, null, 2)); return; }
|
|
95
|
+
ok(`preview 등록: ${p.id} 「${title}」 (status: proposed)`);
|
|
96
|
+
log(` → 사용자에게 이 미리보기(디자인/기능)를 제시하고 승인/수정 답을 받으세요.`);
|
|
97
|
+
log(` → 승인: leerness preview approve ${p.id} · 수정요구: leerness preview revise ${p.id} --note "..."`);
|
|
98
|
+
log(` ⓘ 계약: approve 전에는 이 기능의 코드를 작성하지 않는다.`);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (sub === 'list') {
|
|
103
|
+
if (json) { log(JSON.stringify({ ok: true, total: list.length, pending: pendingPreviews(root).length, previews: list }, null, 2)); return; }
|
|
104
|
+
log(`# leerness preview — 기능 미리보기 승인 상태 (${list.length}건)`);
|
|
105
|
+
if (!list.length) { log(' (없음) — 신규 기능 착수 전: leerness preview add "<제목>" --design "..." --features "a,b"'); return; }
|
|
106
|
+
for (const p of list) {
|
|
107
|
+
const icon = p.status === 'approved' ? '✅' : (p.status === 'revision-requested' ? '📝' : '⏳');
|
|
108
|
+
log(` ${icon} ${p.id} ${p.title} [${p.status}]`);
|
|
109
|
+
}
|
|
110
|
+
const pend = pendingPreviews(root).length;
|
|
111
|
+
if (pend) warn(` 미승인 ${pend}건 — 사용자 답을 받기 전 해당 기능 코드 작성 금지`);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const id = (rest || [])[0];
|
|
116
|
+
const p = list.find(x => x.id === id);
|
|
117
|
+
if (sub === 'show' || sub === 'approve' || sub === 'revise') {
|
|
118
|
+
if (!id) { failJson(json, 'id_required', `preview ${sub} <P-ID> 필요 (leerness preview list 로 확인)`); return; }
|
|
119
|
+
if (!p) { failJson(json, 'not_found', `preview 없음: ${id}`); return; }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (sub === 'show') {
|
|
123
|
+
if (json) { log(JSON.stringify({ ok: true, ...p }, null, 2)); return; }
|
|
124
|
+
log(`# ${p.id} 「${p.title}」 [${p.status}]`);
|
|
125
|
+
if (p.design) log(` 디자인: ${p.design}`);
|
|
126
|
+
if (p.features && p.features.length) { log(' 기능:'); p.features.forEach(f => log(` - ${f}`)); }
|
|
127
|
+
(p.history || []).forEach(h => log(` · ${h.at} ${h.event}${h.note ? ` — ${h.note}` : ''}`));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (sub === 'approve') {
|
|
131
|
+
p.status = 'approved'; (p.history = p.history || []).push({ at: now(), event: 'approved' });
|
|
132
|
+
if (!_savePreviews(root, list, json)) return;
|
|
133
|
+
if (json) { log(JSON.stringify({ ok: true, id: p.id, status: p.status }, null, 2)); return; }
|
|
134
|
+
ok(`${p.id} 승인 — 이제 구현을 시작해도 됩니다.`);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (sub === 'revise') {
|
|
138
|
+
const note = arg ? (arg('--note', '') || '') : '';
|
|
139
|
+
p.status = 'revision-requested'; (p.history = p.history || []).push({ at: now(), event: 'revision-requested', note });
|
|
140
|
+
if (!_savePreviews(root, list, json)) return;
|
|
141
|
+
if (json) { log(JSON.stringify({ ok: true, id: p.id, status: p.status, note }, null, 2)); return; }
|
|
142
|
+
ok(`${p.id} 수정요구 기록${note ? ` — ${note}` : ''} — 미리보기를 수정해 다시 제시하세요 (코드 작성은 계속 보류).`);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
failJson(json, 'unknown_subcommand', `알 수 없는 preview 하위명령: ${sub} (가능: add, list, show, approve, revise)`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
module.exports = { CLARIFY_SIGNALS, _clarifySignals, clarifyCmd, previewCmd, pendingPreviews, _previewsPath, _loadPreviews };
|
package/lib/drift.js
CHANGED
|
@@ -29,7 +29,9 @@ function driftCheckCmd(root, opts = {}, deps = {}) {
|
|
|
29
29
|
const m = allGen.length ? allGen[allGen.length - 1] : null;
|
|
30
30
|
let ageDays;
|
|
31
31
|
if (m) {
|
|
32
|
+
// 1.36.50 (NaN 날짜 클래스 스윕): 손상 타임스탬프(NaN)는 신선함을 증명 못 함 — mtime 폴백(무언 fresh 오판 방지)
|
|
32
33
|
ageDays = (now - new Date(m[1]).getTime()) / 86400000;
|
|
34
|
+
if (!Number.isFinite(ageDays)) ageDays = _ageDays(shPath);
|
|
33
35
|
} else {
|
|
34
36
|
ageDays = _ageDays(shPath);
|
|
35
37
|
}
|
|
@@ -39,13 +41,14 @@ function driftCheckCmd(root, opts = {}, deps = {}) {
|
|
|
39
41
|
const csPath = currentStatePath(root);
|
|
40
42
|
if (exists(csPath)) {
|
|
41
43
|
const m = read(csPath).match(/Updated:\s*(\d{4}-\d{2}-\d{2})/);
|
|
42
|
-
|
|
44
|
+
let ageDays = m ? (now - new Date(m[1]).getTime()) / 86400000 : _ageDays(csPath);
|
|
45
|
+
if (!Number.isFinite(ageDays)) ageDays = _ageDays(csPath); // 1.36.50: NaN 날짜 → mtime 폴백
|
|
43
46
|
signals.push({ file: 'current-state.md', ageDays, threshold: 2, weight: 20, label: t('current-state 갱신 없음', 'current-state not updated') });
|
|
44
47
|
}
|
|
45
48
|
// 3. progress-tracker.md 마지막 row의 updated 컬럼
|
|
46
49
|
const rows = readProgressRows(root);
|
|
47
50
|
if (rows.length) {
|
|
48
|
-
const dates = rows.map(r => (r.updated || '').match(/\d{4}-\d{2}-\d{2}/)).filter(Boolean).map(m => m[0]);
|
|
51
|
+
const dates = rows.map(r => (r.updated || '').match(/\d{4}-\d{2}-\d{2}/)).filter(Boolean).map(m => m[0]).filter(d => Number.isFinite(new Date(d).getTime())); // 1.36.50: 무효 날짜("2026-99-99") 제외
|
|
49
52
|
if (dates.length) {
|
|
50
53
|
dates.sort();
|
|
51
54
|
const latest = dates[dates.length - 1];
|
|
@@ -58,7 +61,7 @@ function driftCheckCmd(root, opts = {}, deps = {}) {
|
|
|
58
61
|
// 4. task-log.md 마지막 entry "## YYYY-MM-DD"
|
|
59
62
|
const tlPath = taskLogPath(root);
|
|
60
63
|
if (exists(tlPath)) {
|
|
61
|
-
const dates = Array.from(read(tlPath).matchAll(/^## (\d{4}-\d{2}-\d{2})/gm)).map(m => m[1]);
|
|
64
|
+
const dates = Array.from(read(tlPath).matchAll(/^## (\d{4}-\d{2}-\d{2})/gm)).map(m => m[1]).filter(d => Number.isFinite(new Date(d).getTime())); // 1.36.50: 무효 날짜 제외
|
|
62
65
|
if (dates.length) {
|
|
63
66
|
dates.sort();
|
|
64
67
|
const latest = dates[dates.length - 1];
|
package/lib/io.js
CHANGED
|
@@ -9,17 +9,71 @@ const path = require('path');
|
|
|
9
9
|
// 1.10.2 (UR-0146): quiet 모드 — 사람용 출력(log/ok/warn) 억제. init --json 등에서 큰 핸들러의 다수 log 를 비침투적으로 묵음 → 순수 JSON 1개만 출력.
|
|
10
10
|
// fail/failJson(오류)은 묵음 대상 아님(에러는 항상 노출). setQuiet 로 토글, 호출부 finally 에서 반드시 복구.
|
|
11
11
|
let _quiet = false;
|
|
12
|
-
function setQuiet(v) { _quiet = !!v; }
|
|
13
|
-
function log(s = '') {
|
|
12
|
+
function setQuiet(v) { _quiet = !!v; if (!v) { _jsonErrEmitted = false; _failLatched = false; _firstFail = null; } } // 1.36.50: 장수 프로세스(REPL)가 명령 경계에서 복구 시 JSON-오류 래치도 해제
|
|
13
|
+
function log(s = '') {
|
|
14
|
+
_unlatchIfNewCmd();
|
|
15
|
+
if (_quiet) return;
|
|
16
|
+
if (_failLatched || _jsonErrEmitted) {
|
|
17
|
+
// json-오류 래치 중: 명령 자신의 구조화 출력(JSON 페이로드)만 통과, 사람용 잔여 줄은 stderr 강등
|
|
18
|
+
const t = typeof s === 'string' ? s.trim() : '';
|
|
19
|
+
if (t && (t[0] === '{' || t[0] === '[')) { try { JSON.parse(t); console.log(s); _stdoutWrote = true; return; } catch {} }
|
|
20
|
+
console.error(s); return;
|
|
21
|
+
}
|
|
22
|
+
console.log(s); _stdoutWrote = true;
|
|
23
|
+
}
|
|
14
24
|
function ok(s) { log('✓ ' + s); }
|
|
15
25
|
function warn(s) { log('⚠ ' + s); }
|
|
16
26
|
// fail() 은 오류 신호 → exit code 1 설정 (CI/MCP/에이전트가 실패를 성공으로 오판 방지, UR-0045).
|
|
17
|
-
|
|
27
|
+
// 1.36.50 (codex 스윕 B, 40+ 실측): --json 요청 시 fail() 자체가 단일 JSON 계약을 이행 —
|
|
28
|
+
// 첫 오류를 {ok:false,error,code:'error'} 로 stdout 에 내고 이후 사람용 출력(log/ok/warn·추가 fail)은
|
|
29
|
+
// 묵음/stderr 로 강등해 "✗ 텍스트·혼합·빈 stdout" 클래스를 진입점 한 곳에서 종결한다.
|
|
30
|
+
// 개별 명령이 failJson 으로 구체 code 를 주는 기존 경로는 그대로 우선.
|
|
31
|
+
// 설계 v2 (게이트 실측 후 재설계): 즉시 JSON 방출+전면 묵음은 "내부 fail 후 자기 집계 JSON 을 내는 명령"(gate 등)을 질식시켰다.
|
|
32
|
+
// → fail() 은 json 모드에서 stdout 에 아무것도 안 쓰고(✗ 는 stderr), 사람용 후속 log 만 억제하되
|
|
33
|
+
// JSON 페이로드 log 는 통과시킨다. 프로세스 exit 때 stdout 이 여전히 비어 있으면 첫 오류를 폴백 JSON 으로 방출 —
|
|
34
|
+
// 결과적으로 stdout 은 "명령 자신의 JSON" 또는 "폴백 오류 JSON" 중 정확히 하나.
|
|
35
|
+
let _jsonErrEmitted = false; // failJson 즉시 방출 후 후속 사람용 출력 억제 표식
|
|
36
|
+
let _failLatched = false; // fail() json 모드 발화 표식 (사람용 log 억제 + exit 폴백 대기)
|
|
37
|
+
let _firstFail = null;
|
|
38
|
+
let _stdoutWrote = false;
|
|
39
|
+
let _exitHooked = false;
|
|
40
|
+
let _latchArgv = null; // 래치는 argv 정체성 스코프 — 새 명령(REPL/인프로세스 테스트가 argv 교체) 시 자동 해제
|
|
41
|
+
// stdout 기록 추적은 스트림 레벨 — 일부 명령(audit 등)이 process.stdout.write 를 직접 캡처/사용해 log() 를 우회한다.
|
|
42
|
+
// 모듈 로드 시 1회 투명 래핑: 이후 어떤 경로(직접 write 포함)로든 stdout 에 쓰면 exit 폴백이 이중 JSON 을 내지 않는다.
|
|
43
|
+
{
|
|
44
|
+
const _rawWrite = process.stdout.write.bind(process.stdout);
|
|
45
|
+
process.stdout.write = function (...a) { _stdoutWrote = true; return _rawWrite(...a); };
|
|
46
|
+
}
|
|
47
|
+
function _argvJsonMode() { try { return process.argv.includes('--json'); } catch { return false; } }
|
|
48
|
+
function _unlatchIfNewCmd() {
|
|
49
|
+
if ((_jsonErrEmitted || _failLatched) && _latchArgv !== process.argv) { _jsonErrEmitted = false; _failLatched = false; _firstFail = null; _quiet = false; _latchArgv = null; }
|
|
50
|
+
}
|
|
51
|
+
function fail(s) {
|
|
52
|
+
_unlatchIfNewCmd();
|
|
53
|
+
if (_argvJsonMode()) {
|
|
54
|
+
if (_firstFail == null) _firstFail = s;
|
|
55
|
+
_failLatched = true; _latchArgv = process.argv;
|
|
56
|
+
console.error('✗ ' + s);
|
|
57
|
+
if (!_exitHooked) {
|
|
58
|
+
_exitHooked = true;
|
|
59
|
+
process.on('exit', () => { if (_failLatched && !_stdoutWrote && _firstFail != null) console.log(JSON.stringify({ ok: false, error: _firstFail, code: 'error' }, null, 2)); });
|
|
60
|
+
}
|
|
61
|
+
process.exitCode = 1; return;
|
|
62
|
+
}
|
|
63
|
+
console.log('✗ ' + s); process.exitCode = 1; // quiet 무시(오류는 항상 노출)
|
|
64
|
+
}
|
|
18
65
|
// 1.9.398 (6번째 외부평가/codex P1-C, UR-0099): --json 모드 에러는 구조화 출력 — AI 에이전트가 에러 경로에서 JSON.parse 실패하지 않도록.
|
|
19
66
|
// jsonMode 면 {ok:false,error,code} + exit1, 아니면 사람용 fail(). 양쪽 exit code 1 일관.
|
|
20
67
|
function failJson(jsonMode, code, msg) {
|
|
21
|
-
|
|
22
|
-
|
|
68
|
+
_unlatchIfNewCmd();
|
|
69
|
+
if (jsonMode) {
|
|
70
|
+
if (!_jsonErrEmitted) { console.log(JSON.stringify({ ok: false, error: msg, code }, null, 2)); _jsonErrEmitted = true; _stdoutWrote = true; _failLatched = true; _latchArgv = process.argv; }
|
|
71
|
+
else console.error('✗ ' + msg);
|
|
72
|
+
process.exitCode = 1;
|
|
73
|
+
} else {
|
|
74
|
+
// 명시적 jsonMode=false 는 호출자의 결정 — argv 스니핑(fail)보다 우선해 human 출력 보장
|
|
75
|
+
console.log('✗ ' + msg); process.exitCode = 1;
|
|
76
|
+
}
|
|
23
77
|
}
|
|
24
78
|
function today() { return new Date().toISOString().slice(0, 10); }
|
|
25
79
|
function now() { return new Date().toISOString(); }
|
|
@@ -38,8 +92,23 @@ function mkdirp(p) { fs.mkdirSync(p, { recursive: true }); }
|
|
|
38
92
|
// 1.9.298 (UR-0038, 외부리뷰 3중수렴): 원자적 쓰기 — temp 파일에 기록 후 rename(원자 교체)으로 부분쓰기 손상 방지.
|
|
39
93
|
// temp 이름은 .tmp-PID-SEQ 라 *.md glob 에 안 걸림. 실패 시 temp 정리.
|
|
40
94
|
let _writeSeq = 0;
|
|
95
|
+
// 1.36.50 (스토어 손상 클래스 전수 스윕): "손상 JSON 위에 유효 JSON 저장" = 클로버 시그니처.
|
|
96
|
+
// 개별 스토어(21곳+)를 각자 가드하는 대신 단일 진입점에서: 기존 파일이 JSON 파싱 불가이고 새 내용은 유효 JSON 이면
|
|
97
|
+
// 원본을 <file>.corrupt-<ts> 로 대피(rename) 후 저장 — 유실 0 · 흐름(handoff 등 수동 기록) 비차단.
|
|
98
|
+
// 명시 fail-closed 스토어(state/teams/toggles 등)는 상류에서 이미 거부하므로 여기 도달하지 않는다.
|
|
99
|
+
function _rescueCorruptJson(p, s) {
|
|
100
|
+
if (!/\.json$/i.test(p) || !fs.existsSync(p)) return;
|
|
101
|
+
try { JSON.parse(s); } catch { return; } // 새 내용이 JSON 아니면 대상 아님
|
|
102
|
+
let old;
|
|
103
|
+
try { old = read(p); } catch { return; }
|
|
104
|
+
try { JSON.parse(old); return; } catch {} // 기존이 정상이면 통상 덮어쓰기
|
|
105
|
+
const bak = `${p}.corrupt-${Date.now()}`;
|
|
106
|
+
// 알림은 stderr — --json 명령의 stdout 단일-JSON 계약을 오염하지 않는다 (codex 스윕 B #1 실측)
|
|
107
|
+
try { fs.renameSync(p, bak); console.error(`⚠ 손상 JSON 감지 — 원본 대피 후 저장: ${path.basename(bak)} (수동 복구용)`); } catch {}
|
|
108
|
+
}
|
|
41
109
|
function writeUtf8(p, s) {
|
|
42
110
|
mkdirp(path.dirname(p));
|
|
111
|
+
_rescueCorruptJson(p, s);
|
|
43
112
|
const tmp = `${p}.tmp-${process.pid}-${++_writeSeq}`;
|
|
44
113
|
try {
|
|
45
114
|
fs.writeFileSync(tmp, s, { encoding: 'utf8' });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leerness",
|
|
3
|
-
"version": "1.36.
|
|
3
|
+
"version": "1.36.51",
|
|
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
|
@@ -6994,5 +6994,39 @@ total++;
|
|
|
6994
6994
|
if (!ok) failed++;
|
|
6995
6995
|
}
|
|
6996
6996
|
|
|
6997
|
+
// 1.36.51 (사용자 요청 UR-0061): clarify 모호성 질문 + preview 승인 워크플로 — CLI 라운드트립
|
|
6998
|
+
total++;
|
|
6999
|
+
{
|
|
7000
|
+
let ok = false;
|
|
7001
|
+
try {
|
|
7002
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-clarify-'));
|
|
7003
|
+
cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes'], { encoding: 'utf8', timeout: 30000 });
|
|
7004
|
+
const R = (a) => cp.spawnSync(process.execPath, [CLI, ...a, '--path', d], { encoding: 'utf8', timeout: 15000 });
|
|
7005
|
+
// clarify: 모호 요청 → ambiguous + 질문 / 구체 요청 → 무신호
|
|
7006
|
+
const c1 = JSON.parse(R(['clarify', '메인 화면 적당히 이쁘게 바꿔줘, 그거도 같이', '--json']).stdout);
|
|
7007
|
+
const c2 = JSON.parse(R(['clarify', 'src/a.js 의 sum 함수 오버플로 버그 수정', '--json']).stdout);
|
|
7008
|
+
const clarifyOk = c1.ambiguous === true && c1.questions.length >= 2 && c2.ambiguous === false;
|
|
7009
|
+
// preview: add(플래그 값 제목 미흡수) → pending 헤드라인 → approve → pending 해소
|
|
7010
|
+
const pAdd = JSON.parse(R(['preview', 'add', '주문 목록 화면', '--design', '카드형 그리드', '--features', '검색,정렬', '--json']).stdout);
|
|
7011
|
+
const addOk = pAdd.id === 'P-0001' && pAdd.title === '주문 목록 화면' && pAdd.design === '카드형 그리드' && pAdd.features.length === 2 && pAdd.status === 'proposed';
|
|
7012
|
+
const ho1 = cp.spawnSync(process.execPath, [CLI, 'handoff', d], { encoding: 'utf8', timeout: 25000 });
|
|
7013
|
+
const headlineOk = /미승인 미리보기 1건|1 preview\(s\) awaiting/.test((ho1.stdout || '') + (ho1.stderr || ''));
|
|
7014
|
+
const ap = JSON.parse(R(['preview', 'approve', 'P-0001', '--json']).stdout);
|
|
7015
|
+
const lst = JSON.parse(R(['preview', 'list', '--json']).stdout);
|
|
7016
|
+
const approveOk = ap.status === 'approved' && lst.pending === 0 && lst.total === 1;
|
|
7017
|
+
// 오류경로 JSON 계약: 미존재 id
|
|
7018
|
+
const nf = R(['preview', 'show', 'P-9999', '--json']);
|
|
7019
|
+
let nfOk = false; try { const j = JSON.parse(nf.stdout); nfOk = j.ok === false && j.code === 'not_found' && nf.status === 1; } catch {}
|
|
7020
|
+
// 신규 init 산출물에 의무 절차 지시 존재
|
|
7021
|
+
const agents = fs.readFileSync(path.join(d, 'AGENTS.md'), 'utf8');
|
|
7022
|
+
const docOk = agents.includes('모호성 질문 의무') && agents.includes('미리보기 승인 의무') && agents.includes('approve 전에는 해당 기능의 코드를 작성하지 않습니다');
|
|
7023
|
+
fs.rmSync(d, { recursive: true, force: true });
|
|
7024
|
+
ok = clarifyOk && addOk && headlineOk && approveOk && nfOk && docOk;
|
|
7025
|
+
if (!ok) console.log(` [clarify 디버그] c=${clarifyOk} a=${addOk} h=${headlineOk} ap=${approveOk} nf=${nfOk} doc=${docOk}`);
|
|
7026
|
+
} catch (e) {}
|
|
7027
|
+
console.log(ok ? '✓ B(1.36.51) UR-0061: clarify 모호성 질문 생성(+구체요청 무신호) + preview add/approve 워크플로(제목 미흡수·헤드라인·not_found JSON) + AGENTS 의무 절차 주입' : '✗ clarify/preview 워크플로 실패');
|
|
7028
|
+
if (!ok) failed++;
|
|
7029
|
+
}
|
|
7030
|
+
|
|
6997
7031
|
console.log(`\nE2E result: ${total - failed}/${total} passed · ${((Date.now() - _e2eStart) / 1000).toFixed(0)}s`);
|
|
6998
7032
|
if (failed > 0) process.exit(1);
|