leerness 1.36.88 → 1.36.89
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 +35 -0
- package/README.md +4 -4
- package/bin/leerness.js +14 -1
- package/lib/routing.js +279 -0
- package/lib/toggles.js +2 -0
- package/package.json +1 -1
- package/scripts/e2e.js +110 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.36.89 — 2026-07-29 — 작업 난이도별 모델 라우팅 (P-0007, 옵트인 · 제안 후 확인 실행)
|
|
4
|
+
|
|
5
|
+
사용자 요청: "codex/claude 등 CLI 설치·사용 가능 여부에 따라 작업 난이도별 AI 모델을 라우팅, 켜고 끌 수 있게." 함께 받은 제3자 제안(GPT-5.6=설계·판정 / Claude=지휘 / Codex-Spark=고속 작업자 고정 배치)은 codex 와 함께 검토해 **오래 가는 정책과 곧 낡을 정보를 갈라서** 전자만 반영했다.
|
|
6
|
+
|
|
7
|
+
- **`leerness agents route "<작업>"`** — 난이도(tiny/normal/high-risk)를 결정적 규칙으로 판정하고, 등급별 추상 역할 구성을 제안한다. 최상위 `route` 는 이미 컨텍스트 라우팅 명령이라(실행해보고 알았다) `agents recommend|dispatch` 와 같은 집에 두었다.
|
|
8
|
+
- **모델 이름은 한 줄도 하드코딩하지 않는다.** 정책은 추상 역할(`architect`/`commander`/`coder`/`reviewer`)로만 쓰고, 역할→provider+model 매핑은 기존 `leerness roles set` 사용자 설정이 소유한다. 벤더 모델명·성능 순위·컨텍스트 크기는 몇 달이면 낡아 제품 정책이 될 수 없다.
|
|
9
|
+
- **4중 fail-closed** — 토글 OFF · 역할 미설정 · 고위험인데 사람 승인(`--approved-by`) 없음 · 검수자와 구현자가 같은 provider. 마지막 항목은 같은 벤더가 구현과 검수를 겸하면 같은 맹점을 공유하므로 "검수됨"이라고 부를 수 없기 때문이다.
|
|
10
|
+
- **토글 `difficulty-routing` 기본 OFF.** 켜도 자동 실행은 없다 — `--dispatch` 를 **허용**할 뿐이고, 제안·열람은 토글과 무관하게 동작한다. 토글을 켜는 것이 무인 과금을 승인하는 뜻이 되면 안 된다.
|
|
11
|
+
- **감사 기록** `.harness/routing-log.json` (200건 cap, 원자 쓰기 + 락) — 입력·매칭된 신호·후보·선택·승인자·거부 사유·실행 여부. 거부된 시도도 남는다. 남기지 않으면 사후에 라우팅을 검증할 수 없다.
|
|
12
|
+
|
|
13
|
+
**정직성**: 판정하는 것은 **텍스트에 대한 규칙 매칭**뿐이고 난이도를 측정하지 않는다. 출력이 그대로 말한다 — "난이도는 텍스트 규칙 매칭 결과이며 측정값이 아닙니다" · "설치·활성 여부만 확인했습니다 — 인증·모델 사용권한·쿼터·과금 권한은 검증하지 않았습니다." `--version` 이 도는 것은 그중 무엇도 증명하지 않는다.
|
|
14
|
+
|
|
15
|
+
**규칙이 못 가르는 경우를 숨기지 않는다.** 검토에서 나온 함정 케이스 — "인증 문서 오타 수정"(고위험 키워드 + 초소형 키워드 동시) — 은 규칙만으로 가릴 수 없다. 보수적으로 high-risk 를 유지하되 `conflict: true` · `confidence: low` 를 노출하고 "사소한 작업이면 `--tier tiny` 로 낮추세요"를 안내한다. 근거가 없을 때도 tiny 로 내리지 않는다: 신호 0 + 짧은 설명은 `normal`(confidence low)이다 — **과소평가는 사고, 과대평가는 비용**이라 비용 쪽을 택했다.
|
|
16
|
+
|
|
17
|
+
적대 검증 실측: 혼합 입력 5종("CSS 색상 조정하면서 결제 버튼 동작도 바꿈" 등) 전부 high-risk 로 상승 · 37만 자 입력 분류 1ms(ReDoS 없음) · 소스 주석의 "한글에는 `\b` 가 동작하지 않는다"는 주장도 실행으로 확인했다.
|
|
18
|
+
|
|
19
|
+
**출하 전 codex 검수(28차)가 BLOCK 2건 포함 10건을 냈고, 8건이 재현돼 전부 고쳤다** (1건은 반증 — `--approved-by --json` 은 실제로 `human_approval_required` 로 정확히 거부된다):
|
|
20
|
+
|
|
21
|
+
- **`dispatched: true` 라고 보고하면서 아무것도 실행하지 않았다.** 이 제품이 존재하는 이유가 "하지 않은 일을 했다고 말하지 않게 하는 것"인데 정확히 그 거짓말을 했다. 실제로 하는 일은 계획 확정이므로 `--confirm` / `confirmed` 로 이름을 바로잡고 `executed: false` 를 명시하며, "leerness 는 여기서 외부 CLI 를 실행하지 않습니다"를 사람용·JSON·`--log` 세 경로 모두에 넣었다.
|
|
22
|
+
- **영문 어간이 정규식을 통째로 빠져나갔다.** `\bauth\b` 는 `authentication` 에 매치되지 않아 **"fix typo in authentication middleware" 가 tiny 로 떨어졌다**(실측). `authorization`·`permissions`·`concurrency`·`idempotency` 도 같은 이유로 전부 누락. 어간 + `\w*` 로 바꾸고, `author`(글쓴이)·`tokenizer`(파서 용어)가 걸리지 않도록 오탐 대조군을 테스트에 함께 넣었다. **내 적대 테스트가 한글 입력만 써서 이 구멍을 통째로 놓쳤다.**
|
|
23
|
+
- **초소형 키워드 하나로 거대 작업이 tiny 가 됐다** — "Rewrite the entire compiler … and also fix one typo" 가 tiny 였다. tiny 는 "범위가 좁다"는 주장이므로 광범위 신호(entire/전체/갈아엎…)나 긴 설명이면 그 주장을 인정하지 않는다.
|
|
24
|
+
- **`--tier` 만 주면 빈 작업이 통과했다** — 빈 작업 검사가 declared 분기 뒤에 있었다.
|
|
25
|
+
- **빈 provider 가 "해석됨"이었다** — `resolved: !!r` 이라 공백 provider 로 확정이 통과했다.
|
|
26
|
+
- **손상된 감사 기록을 조용히 덮어썼다** — 감사 기록을 파괴하는 코드가 감사 기록 기능 안에 있었다. 손상 시 쓰지 않고 그 사실을 알린다(스토어 손상 = 덮어쓰기 거부, 기존 규율). 파싱 전에 파일 크기(4MB)도 본다.
|
|
27
|
+
- **하지 않은 확인을 했다고 말했다** — 고지 문구가 "설치·활성 여부만 확인했습니다"였는데 이 모듈은 설치 확인을 **한 번도 하지 않는다**. 실제로 하는 일(역할 매핑 읽기)만 말하도록 고치고, 고지가 없던 `--log` 경로에도 넣었다.
|
|
28
|
+
|
|
29
|
+
**신설 e2e 가드는 변이 14종으로 탐지력을 증명했다**(토글 무시 · 역할 미설정 무시 · 사람 승인 생략 · 독립성 검사 제거 · 충돌시 tiny 로 내림 · 근거부족을 tiny 로 · `--tier` 검증 제거 · 감사기록 미기록 · 고지를 옛 거짓 문구로 회귀 · 실행안함 고지 삭제 · 손상로그 덮어쓰기 복귀 · 빈 provider 를 resolved 로 · 빈 작업 검사 제거 · tiny 범위 가드 제거 → **14/14 검출**).
|
|
30
|
+
|
|
31
|
+
세 번 고쳐서 14/14 가 됐고, 살아남은 변이의 원인이 매번 달랐다:
|
|
32
|
+
- `--tier` 검증을 없애도 exit 1 이 나왔다 — **검증이 아니라 크래시** 때문이었다(사유 `invalid_tier` 까지 단언 + 알 수 없는 등급은 크래시 대신 가장 엄격한 구성으로 fail-closed).
|
|
33
|
+
- probe 검사와 영수증 검사처럼 **두 거부 조건이 서로를 가렸다**(판별 케이스 추가).
|
|
34
|
+
- 마지막 하나는 코드가 아니라 **내 픽스처가 잘못된 스키마(`{roles:{…}}` 대신 최상위)로 저장돼 셋업이 조용히 실패한 것**이었다 — 빈 provider 를 검증한 게 아니라 "역할 없음"을 통과로 읽고 있었다. 셋업 성공 여부까지 함께 단언하도록 고쳤다.
|
|
35
|
+
|
|
36
|
+
게이트: exit 0 · E2E 409/409 · selftest 341/341 · ✗ 0건
|
|
37
|
+
|
|
3
38
|
## 1.36.88 — 2026-07-29 — `selftest --json` 잡음 3종 제거 · "우연히 지켜지던" 단일 JSON 계약을 원인 단계에서 고정
|
|
4
39
|
|
|
5
40
|
`selftest --json` 이 JSON 앞에 잡음 5줄(`✗ selftest probe` · `✗ probe` · graph 사람용 3줄)을 낸다는 제보로 시작했다. **먼저 계약이 실제로 깨졌는지 측정했고, 제보의 전제 두 가지는 사실이 아니었다.**
|
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.89 하네스를 사용합니다. 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.89는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **89개 도구**를 노출:
|
|
180
180
|
|
|
181
181
|
```jsonc
|
|
182
182
|
// 카테고리별
|
|
@@ -197,7 +197,7 @@ Leerness v1.36.88는 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.89 릴리스 태그 이력** (수백 라운드) · _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.89: 2026-07-29
|
|
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.89';
|
|
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') 시 호스트 프로세스 오염.
|
|
@@ -7471,6 +7471,7 @@ function commandsCmd(root) {
|
|
|
7471
7471
|
{ cmd: 'integrity check [--repair] [--json]', desc: 'managed 정책-문서 12종 무결성(부재/H1 상실/절단) 점검 — --repair: archive 대피 후 템플릿 재생성 — 1.36.57 감사 F-04' },
|
|
7472
7472
|
{ cmd: 'referee add|verify|list|show|drop', desc: '검증기 캘리브레이션 — 신뢰 전에 탐지력 증명(known-good 통과 + 망가뜨린 known-bad 를 기대 사유로 거부). verify-claim --referee / gate 게이팅 — 1.36.80 P-0001' },
|
|
7473
7473
|
{ cmd: 'preview add|list|show|approve|revise|mockup', desc: '신규 기능 미리보기 승인 워크플로 — approve 전 코드 작성 금지 계약. mockup <P-ID> [--force]: 자립형 HTML 디자인 시안 스캐폴드 · add --mockup <파일>: 기존 시안 첨부 — 1.36.51 UR-0061 · 1.36.75 UR-0066' },
|
|
7474
|
+
{ cmd: 'agents route "<작업>" [--tier tiny|normal|high-risk] [--confirm --approved-by "<승인자>"] [--log]', desc: '작업 난이도 판정(결정적 규칙) → 추상 역할(architect/commander/coder/reviewer) 배치 제안. 모델명 하드코딩 없이 `roles set` 매핑을 쓰고, 실행은 명시 확인 필요. 토글 기본 OFF: leerness toggle set difficulty-routing on — 1.36.89 P-0007' },
|
|
7474
7475
|
{ cmd: 'bugfix start|receipt|drop|list', desc: 'bugfix 완료 영수증 — 수정 전 재현 probe 를 등록(2회 연속 실패해야 등록)하고, done 전이에서 통과 + 근본원인/형제범위 영수증을 요구. 토글 기본 OFF: leerness toggle set bugfix-receipt on — 1.36.87 P-0005' },
|
|
7475
7476
|
{ cmd: 'review <file> --persona <ids>', desc: '페르소나 리뷰 (1.9.29)' },
|
|
7476
7477
|
{ cmd: 'brainstorm "<topic>" [--include-code]', desc: '워크스페이스 회수 + 코드 grep' }
|
|
@@ -24046,6 +24047,18 @@ async function main() {
|
|
|
24046
24047
|
if (cmd === 'honesty-check') return honestyCheckCmd(arg('--path', process.cwd()), args[1]);
|
|
24047
24048
|
if (cmd === 'persona') return personaCmd(arg('--path', process.cwd()), args[1], args[2]);
|
|
24048
24049
|
if (cmd === 'review') return reviewCmd(arg('--path', process.cwd()), args[1]);
|
|
24050
|
+
// 1.36.89 (P-0007): agents route — 난이도 판정 + 추상 역할 배치 제안. `agents recommend|dispatch` 와 같은 집에 둔다.
|
|
24051
|
+
// (최상위 `route` 는 이미 컨텍스트 라우팅 명령이라 가리면 안 된다 — 실측으로 확인하고 이름을 옮겼다.)
|
|
24052
|
+
// 값 플래그가 positional 로 새지 않게 원시 argv 파싱(referee/preview/bugfix 와 동일 패턴).
|
|
24053
|
+
if (cmd === 'agents' && args[1] === 'route') {
|
|
24054
|
+
const _rtRaw = process.argv.slice(2); const _rtI = _rtRaw.indexOf('route'); const _rtToks = [];
|
|
24055
|
+
for (let i = _rtI + 1; i < _rtRaw.length && _rtI >= 0; i++) {
|
|
24056
|
+
if (_rtRaw[i].startsWith('--')) { if (['--tier', '--approved-by', '--path'].includes(_rtRaw[i]) && _rtRaw[i + 1] && !_rtRaw[i + 1].startsWith('--')) i++; continue; }
|
|
24057
|
+
_rtToks.push(_rtRaw[i]);
|
|
24058
|
+
}
|
|
24059
|
+
return require('../lib/routing').routeCmd(arg('--path', process.cwd()), _rtToks.join(' '),
|
|
24060
|
+
{ has, arg, _withLock, _resolveRole, _toggleOn: (r, id) => _tgl.toggleOn(r, id) });
|
|
24061
|
+
}
|
|
24049
24062
|
if (cmd === 'agents') return agentsCmd(arg('--path', process.cwd()), args[1], ...args.slice(2));
|
|
24050
24063
|
// 1.9.157: Provider Registry — 사용자 정의 provider 동적 추가
|
|
24051
24064
|
if (cmd === 'provider') return providerCmd(arg('--path', process.cwd()), args[1], ...args.slice(2));
|
package/lib/routing.js
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
// lib/routing.js — 작업 난이도별 모델 라우팅 (1.36.89, P-0007 · 사용자 승인 범위 = **제안 + 확인 후 실행**).
|
|
2
|
+
//
|
|
3
|
+
// 사용자 요청: "codex/claude 등의 CLI 설치·사용 가능 여부에 따라 작업 난이도별 AI 모델을 라우팅, 켜고 끌 수 있게".
|
|
4
|
+
//
|
|
5
|
+
// 검토(codex 27차 + 자체)에서 **기각/제한한 것**:
|
|
6
|
+
// · 자유 텍스트로부터의 **조용한 자동 라우팅** — 잘못된 등급이 조용히 선택되면 사용자는 알 수 없고,
|
|
7
|
+
// 비싼 등급이 선택되면 과금이 발생한다. 그래서 기본은 제안이고, 실행은 명시 확인(--dispatch)이 필요하다.
|
|
8
|
+
// · **모델 이름 하드코딩** — 벤더 모델은 몇 달마다 바뀐다. 정책은 추상 역할(architect/commander/coder/reviewer)로만
|
|
9
|
+
// 쓰고, 역할→provider+model 매핑은 사용자 설정(`leerness roles set`)에 맡긴다.
|
|
10
|
+
// · **"설치됨 = 사용 가능"** — `--version` 이 도는 것은 인증·모델 사용권한·쿼터·비대화형 동작·과금 권한을
|
|
11
|
+
// 아무것도 증명하지 않는다. 그래서 상태를 그대로 라벨링하고, 검증하지 않은 것은 검증하지 않았다고 적는다.
|
|
12
|
+
// · **변경 파일 수 기준 라우팅** — 일이 끝나야 알 수 있는 값이라 초기 라우팅 근거가 될 수 없다.
|
|
13
|
+
//
|
|
14
|
+
// 정직성 계약: 여기서 판정하는 것은 **텍스트에 대한 결정적 규칙 매칭**뿐이다. 난이도를 "측정"하지 않는다.
|
|
15
|
+
// 규칙이 겹치거나(오타 수정인데 인증 도메인) 신호가 약하면 그 사실을 그대로 노출하고 보수적으로 normal 이상을 준다.
|
|
16
|
+
'use strict';
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const { absRoot, exists, read, writeUtf8, mkdirp, log, ok, warn, failJson, now } = require('./io');
|
|
19
|
+
|
|
20
|
+
const TIERS = ['tiny', 'normal', 'high-risk'];
|
|
21
|
+
|
|
22
|
+
// 고위험 도메인 — 결정적 규칙. 여기 걸리면 tiny 로 내려가지 않는다(과대평가는 비용, 과소평가는 사고).
|
|
23
|
+
// 한글은 단어경계(\b)가 동작하지 않으므로 부분일치를 쓰고, 영문만 경계를 건다.
|
|
24
|
+
// 1.36.89 (codex 28차 #5): 종전엔 `\b(auth|permission|concurren|idempoten)\b` 처럼 **뒤쪽 \b** 를 붙여
|
|
25
|
+
// `authentication` · `authorization` · `permissions` · `concurrency` · `idempotency` 가 전부 매치되지 않았다.
|
|
26
|
+
// 그 결과 "fix typo in authentication middleware" 가 **tiny** 로 떨어졌다(실측). 어간 + `\w*` 로 바꾼다.
|
|
27
|
+
// 오탐 가드: `author`(글쓴이)는 매치되면 안 되므로 authentic/authoriz 어간을 따로 적고,
|
|
28
|
+
// `tokenizer`(파서 용어)를 피하려 token 뒤 `iz` 를 제외한다.
|
|
29
|
+
const HIGH_RISK = [
|
|
30
|
+
{ id: 'auth', ko: '인증·인가', re: /\b(auth|authn|authz|authentic\w*|authoriz\w*|authoris\w*|login|log in|logout|sign in|session\w*|token(?!iz)\w*|jwt|oauth|sso|permission\w*|privilege\w*|rbac|acl)\b|인증|인가|로그인|세션|토큰|권한/i },
|
|
31
|
+
{ id: 'payment', ko: '결제·정산', re: /\b(payment\w*|billing|checkout|invoice\w*|refund\w*|subscription\w*|stripe|paypal)\b|결제|정산|환불|청구|구독/i },
|
|
32
|
+
{ id: 'data-migration', ko: '데이터 마이그레이션·스키마', re: /\b(migrat\w*|schema\w*|alter table|drop table|backfill\w*)\b|마이그레이션|스키마|백필/i },
|
|
33
|
+
{ id: 'pii', ko: '개인정보', re: /\b(pii|gdpr|personal data|ssn|passport)\b|개인정보|주민등록|여권/i },
|
|
34
|
+
{ id: 'secrets', ko: '시크릿·키', re: /\b(secret\w*|credential\w*|api key|private key|password\w*)\b|시크릿|비밀번호|자격증명|비밀키/i },
|
|
35
|
+
{ id: 'public-api', ko: '공개 API 계약', re: /\b(public api|breaking change\w*|api contract|versioning)\b|공개 api|하위호환|호환성 깨/i },
|
|
36
|
+
{ id: 'concurrency', ko: '동시성·트랜잭션', re: /\b(concurren\w*|transaction\w*|race condition\w*|deadlock\w*|idempoten\w*|mutex|semaphore)\b|동시성|트랜잭션|경쟁 상태|교착|멱등/i },
|
|
37
|
+
{ id: 'infra', ko: '인프라·배포', re: /\b(deploy\w*|infra\w*|terraform|kubernetes|k8s|dockerfile|ci\/cd|pipeline\w*)\b|배포|인프라|파이프라인/i },
|
|
38
|
+
{ id: 'destructive', ko: '파괴적 작업', re: /\b(delete all|truncate\w*|rm -rf|purge\w*|wipe\w*)\b|전체 삭제|일괄 삭제|초기화/i },
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
// 초소형 — **범위가 명시적으로 좁을 때만**. 고위험 신호가 하나라도 있으면 적용하지 않는다.
|
|
42
|
+
const TINY = [
|
|
43
|
+
{ id: 'typo', ko: '오타·문구', re: /\b(typo\w*|wording|copy fix)\b|오타|문구 수정|띄어쓰기/i },
|
|
44
|
+
{ id: 'rename', ko: '이름 변경', re: /\b(rename|변수명|함수명)\b|이름 변경|네이밍/i },
|
|
45
|
+
{ id: 'style', ko: '포맷·린트·CSS', re: /\b(lint\w*|format\w*|prettier|eslint|css|padding|margin|색상)\b|포맷|린트|여백|스타일/i },
|
|
46
|
+
{ id: 'comment', ko: '주석·문서 문구', re: /\b(comment\w*|docstring\w*)\b|주석/i },
|
|
47
|
+
{ id: 'single-test', ko: '테스트 1건', re: /\b(add a test|one test|테스트 한|테스트 1)\b/i },
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
// 1.36.89 (codex 28차 #4): 초소형 키워드가 **하나라도** 있으면 tiny 로 떨어지던 것 —
|
|
51
|
+
// "Rewrite the entire compiler, replace the storage engine, and also fix one typo" 가 tiny 였다(실측).
|
|
52
|
+
// tiny 는 "범위가 좁다"는 주장이므로, 광범위 신호가 있거나 설명이 길면 그 주장을 인정하지 않는다.
|
|
53
|
+
const BROAD = /\b(entire|whole|all |everything|rewrite|overhaul|refactor all|across the|end.to.end)\b|전체|전면|모든|갈아엎|재작성|통째/i;
|
|
54
|
+
const TINY_MAX_LEN = 80;
|
|
55
|
+
|
|
56
|
+
function _norm(s) { return String(s == null ? '' : s).trim(); }
|
|
57
|
+
|
|
58
|
+
// 사용자에게 반복 노출되는 고지 — **이 모듈이 실제로 한 일만** 말한다.
|
|
59
|
+
// 1.36.89 (codex 28차 #7): 종전 문구는 "설치·활성 여부만 확인했습니다"였는데, 이 모듈은 역할 매핑만 읽고
|
|
60
|
+
// 설치 확인(`--version`)을 **한 번도 하지 않는다**. 하지 않은 확인을 했다고 말하는 것이 바로 이 제품이 잡는 결함이다.
|
|
61
|
+
const _AVAIL_NOTE = '이 명령은 역할 매핑(leerness roles)만 읽습니다 — 해당 CLI 의 설치·인증·모델 사용권한·쿼터·과금 권한은 확인하지 않았습니다(확인: leerness agents check).';
|
|
62
|
+
const _CLASS_NOTE = '난이도는 텍스트 규칙 매칭 결과이며 측정값이 아닙니다.';
|
|
63
|
+
const _EXEC_NOTE = 'leerness 는 여기서 외부 CLI 를 실행하지 않습니다 — 계획을 확정하고 기록할 뿐입니다.';
|
|
64
|
+
|
|
65
|
+
// 난이도 판정 — 순서가 곧 정책이다. (1) 사용자 명시 (2) 고위험 도메인 (3) 초소형 (4) 기본 normal
|
|
66
|
+
function classify(text, opts = {}) {
|
|
67
|
+
const t = _norm(text);
|
|
68
|
+
const declared = _norm(opts.tier).toLowerCase();
|
|
69
|
+
const high = HIGH_RISK.filter(r => r.re.test(t));
|
|
70
|
+
const tiny = TINY.filter(r => r.re.test(t));
|
|
71
|
+
const signals = [
|
|
72
|
+
...high.map(r => ({ kind: 'high-risk', id: r.id, ko: r.ko })),
|
|
73
|
+
...tiny.map(r => ({ kind: 'tiny', id: r.id, ko: r.ko })),
|
|
74
|
+
];
|
|
75
|
+
const reasons = [];
|
|
76
|
+
|
|
77
|
+
// 1.36.89 (codex 28차 #3): 빈 작업 검사가 declared 분기 **뒤에** 있어, `--tier tiny` 만 주면
|
|
78
|
+
// 빈 작업이 통과했다(실측). 어떤 경로로도 빈 작업은 받지 않는다.
|
|
79
|
+
if (!t) return { ok: false, code: 'empty_task', error: '작업 설명이 비어 있습니다 — leerness agents route "<작업 설명>"' };
|
|
80
|
+
|
|
81
|
+
if (declared) {
|
|
82
|
+
if (!TIERS.includes(declared)) return { ok: false, code: 'invalid_tier', error: `--tier 는 ${TIERS.join('|')} (받음: ${declared})` };
|
|
83
|
+
reasons.push(`사용자가 --tier ${declared} 로 명시 — 규칙 판정보다 우선합니다`);
|
|
84
|
+
if (declared !== 'high-risk' && high.length) {
|
|
85
|
+
reasons.push(`⚠ 고위험 신호(${high.map(r => r.ko).join(', ')})가 있는데 ${declared} 로 지정됐습니다 — 의도한 것인지 확인하세요`);
|
|
86
|
+
}
|
|
87
|
+
return { ok: true, tier: declared, confidence: 'declared', signals, reasons, conflict: declared !== 'high-risk' && high.length > 0 };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (high.length) {
|
|
91
|
+
// 고위험 + 초소형 신호 동시 매칭(예: "인증 문서 오타 수정")은 규칙만으로 못 가른다 — 사실을 노출하고 보수적으로 간다.
|
|
92
|
+
const conflict = tiny.length > 0;
|
|
93
|
+
reasons.push(`고위험 도메인 규칙 매칭: ${high.map(r => r.ko).join(', ')}`);
|
|
94
|
+
if (conflict) reasons.push(`동시에 초소형 신호(${tiny.map(r => r.ko).join(', ')})도 있습니다 — 규칙으로는 가릴 수 없어 보수적으로 high-risk 로 둡니다. 사소한 작업이면 --tier tiny 로 낮추세요`);
|
|
95
|
+
return { ok: true, tier: 'high-risk', confidence: conflict ? 'low' : 'high', signals, reasons, conflict };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 신호가 없고 설명이 짧으면 판정 근거가 없는 것이다 — tiny 로 내리지 않고 normal 로 둔다(과소평가 금지).
|
|
99
|
+
if (!tiny.length) {
|
|
100
|
+
const weak = t.length < 12;
|
|
101
|
+
reasons.push(weak
|
|
102
|
+
? '매칭된 규칙이 없고 설명이 짧습니다 — 판정 근거가 부족해 기본값 normal 로 둡니다'
|
|
103
|
+
: '매칭된 고위험/초소형 규칙 없음 — 기본값 normal');
|
|
104
|
+
return { ok: true, tier: 'normal', confidence: weak ? 'low' : 'medium', signals, reasons, conflict: false };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// tiny 는 "범위가 좁다"는 주장이다 — 광범위 신호가 있거나 설명이 길면 그 주장을 인정하지 않는다.
|
|
108
|
+
const broad = BROAD.test(t);
|
|
109
|
+
const longText = t.length > TINY_MAX_LEN;
|
|
110
|
+
if (broad || longText) {
|
|
111
|
+
reasons.push(`초소형 신호(${tiny.map(r => r.ko).join(', ')})가 있지만 ${broad ? '광범위 신호가 함께 있어' : `설명이 ${t.length}자로 길어(기준 ${TINY_MAX_LEN}자)`} 좁은 범위로 볼 수 없습니다 — normal 로 둡니다`);
|
|
112
|
+
return { ok: true, tier: 'normal', confidence: 'low', signals, reasons, conflict: true };
|
|
113
|
+
}
|
|
114
|
+
reasons.push(`초소형 신호 매칭: ${tiny.map(r => r.ko).join(', ')} · 고위험/광범위 신호 없음`);
|
|
115
|
+
return { ok: true, tier: 'tiny', confidence: 'medium', signals, reasons, conflict: false };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 등급별 역할 구성 — **추상 역할 id 만** 쓴다(모델 이름 없음). 매핑은 `leerness roles set` 이 소유한다.
|
|
119
|
+
const TIER_ROLES = {
|
|
120
|
+
tiny: { roles: ['coder'], humanApproval: false, ko: '작업자 1명 + 기존 테스트' },
|
|
121
|
+
normal: { roles: ['commander', 'coder', 'reviewer'], humanApproval: false, ko: '지휘·구현·검수' },
|
|
122
|
+
'high-risk': { roles: ['architect', 'commander', 'coder', 'reviewer'], humanApproval: true, ko: '설계 선행 · 독립 검수 · 사람 승인' },
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// 라우팅 계획 — 역할을 사용자 설정으로 해석하고, 해석 못 한 역할과 독립성 결함을 그대로 보고한다.
|
|
126
|
+
function plan(root, cls, deps = {}) {
|
|
127
|
+
// 알 수 없는 등급은 여기서 크래시할 게 아니라 가장 보수적인 구성으로 떨어뜨린다 —
|
|
128
|
+
// 상류 검증이 사라져도 "조용히 느슨해지는" 대신 가장 엄격한 쪽으로 실패하게(fail-closed).
|
|
129
|
+
const spec = TIER_ROLES[cls.tier] || TIER_ROLES['high-risk'];
|
|
130
|
+
const resolve = typeof deps._resolveRole === 'function' ? deps._resolveRole : () => null;
|
|
131
|
+
// 1.36.89 (codex 28차 #6): 종전엔 `resolved: !!r` 라 provider 가 빈 문자열이어도 "해석됨"이었다 —
|
|
132
|
+
// 빈 provider 로 dispatch 가 통과하고 `coder:` 로 표시됐다(실측). 실제로 쓸 수 있는 값인지까지 본다.
|
|
133
|
+
const assignments = spec.roles.map(role => {
|
|
134
|
+
const r = resolve(root, role);
|
|
135
|
+
const provider = r && typeof r.provider === 'string' ? r.provider.trim() : '';
|
|
136
|
+
return { role, provider: provider || null, model: (r && r.model) || null, resolved: !!provider };
|
|
137
|
+
});
|
|
138
|
+
const unresolved = assignments.filter(a => !a.resolved).map(a => a.role);
|
|
139
|
+
const coder = assignments.find(a => a.role === 'coder');
|
|
140
|
+
const reviewer = assignments.find(a => a.role === 'reviewer');
|
|
141
|
+
// 독립 검수: 같은 provider 가 구현과 검수를 겸하면 "검수됨"이라고 말할 수 없다(같은 맹점을 공유한다).
|
|
142
|
+
const reviewerIndependent = !reviewer || !reviewer.resolved || !coder || !coder.resolved
|
|
143
|
+
? null
|
|
144
|
+
: reviewer.provider !== coder.provider;
|
|
145
|
+
const blockers = [];
|
|
146
|
+
if (unresolved.length) blockers.push({ code: 'role_unresolved', message: `역할 미설정: ${unresolved.join(', ')} — leerness roles set <역할> --provider <id>` });
|
|
147
|
+
if (spec.humanApproval && reviewerIndependent === false) blockers.push({ code: 'reviewer_not_independent', message: `검수자와 구현자가 같은 provider(${coder.provider})입니다 — 고위험 작업에서는 독립 검수라고 볼 수 없습니다` });
|
|
148
|
+
return { tier: cls.tier, ko: spec.ko, humanApproval: spec.humanApproval, assignments, unresolved, reviewerIndependent, blockers };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function _logPath(root) { return path.join(absRoot(root), '.harness', 'routing-log.json'); }
|
|
152
|
+
const _LOG_CAP = 200;
|
|
153
|
+
|
|
154
|
+
const _LOG_MAX_BYTES = 4 * 1024 * 1024; // codex 28차 #9: 파싱 전에 크기를 본다 — 거대 파일을 동기로 읽어 CLI 가 멎으면 안 된다
|
|
155
|
+
|
|
156
|
+
function _loadLog(root) {
|
|
157
|
+
const f = _logPath(root);
|
|
158
|
+
if (!exists(f)) return { list: [], invalid: false };
|
|
159
|
+
try {
|
|
160
|
+
const sz = require('fs').statSync(f).size;
|
|
161
|
+
if (sz > _LOG_MAX_BYTES) return { list: [], invalid: true, reason: `파일이 너무 큽니다(${Math.round(sz / 1048576)}MB > 4MB)` };
|
|
162
|
+
const j = JSON.parse(read(f));
|
|
163
|
+
if (!Array.isArray(j)) return { list: [], invalid: true, reason: '최상위가 배열이 아닙니다' };
|
|
164
|
+
return { list: j, invalid: false };
|
|
165
|
+
} catch (e) { return { list: [], invalid: true, reason: 'JSON 파싱 실패' }; }
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// 감사 기록 — 무엇을 보고 무엇을 골랐는지 남지 않으면 사후에 라우팅을 검증할 수 없다.
|
|
169
|
+
// 1.36.89 (codex 28차 #8): 손상 파일을 `[rec]` 으로 **조용히 덮어썼다**(실측) — 감사 기록을 파괴하는
|
|
170
|
+
// 동작이 감사 기록 기능 안에 있었다. 손상 시에는 쓰지 않고 그 사실을 반환한다(스토어 손상 = 덮어쓰기 거부, 기존 규율).
|
|
171
|
+
function _appendLog(root, rec, deps = {}) {
|
|
172
|
+
const f = _logPath(root);
|
|
173
|
+
const write = () => {
|
|
174
|
+
const cur = _loadLog(root);
|
|
175
|
+
if (cur.invalid) return { written: false, invalid: true, reason: cur.reason || '형상 무효' };
|
|
176
|
+
const list = cur.list.concat([rec]).slice(-_LOG_CAP);
|
|
177
|
+
mkdirp(path.dirname(f));
|
|
178
|
+
const tmp = f + '.tmp' + process.pid;
|
|
179
|
+
writeUtf8(tmp, JSON.stringify(list, null, 2) + '\n');
|
|
180
|
+
require('fs').renameSync(tmp, f);
|
|
181
|
+
return { written: true, invalid: false, total: list.length };
|
|
182
|
+
};
|
|
183
|
+
return (typeof deps._withLock === 'function') ? deps._withLock(f, write) : write();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// `leerness agents route "<작업>" [--tier x] [--dispatch] [--approved-by "이름"] [--json]`
|
|
187
|
+
function routeCmd(root, taskText, deps = {}) {
|
|
188
|
+
const { has, arg } = deps;
|
|
189
|
+
root = absRoot(root);
|
|
190
|
+
const json = !!(has && has('--json'));
|
|
191
|
+
if (!exists(path.join(root, '.harness'))) { failJson(json, 'harness_missing', `leerness 미설치: ${root} — 먼저 leerness init`); return; }
|
|
192
|
+
|
|
193
|
+
if (has && has('--log')) return _showLog(root, json);
|
|
194
|
+
|
|
195
|
+
const cls = classify(taskText, { tier: (arg ? arg('--tier', '') : '') || '' });
|
|
196
|
+
if (!cls.ok) { failJson(json, cls.code, cls.error); return; }
|
|
197
|
+
const p = plan(root, cls, deps);
|
|
198
|
+
|
|
199
|
+
const wantDispatch = !!(has && (has('--confirm') || has('--dispatch')));
|
|
200
|
+
const approvedBy = _norm(arg ? arg('--approved-by', '') : '');
|
|
201
|
+
const gateOn = typeof deps._toggleOn === 'function' ? deps._toggleOn(root, 'difficulty-routing') : false;
|
|
202
|
+
|
|
203
|
+
// 실행 거부 사유 — 하나라도 있으면 dispatch 하지 않는다(고위험은 fail-closed).
|
|
204
|
+
const refusals = [];
|
|
205
|
+
if (wantDispatch && !gateOn) refusals.push({ code: 'routing_disabled', message: '난이도 라우팅 토글이 꺼져 있습니다 — leerness toggle set difficulty-routing on' });
|
|
206
|
+
if (wantDispatch && p.blockers.length) p.blockers.forEach(b => refusals.push(b));
|
|
207
|
+
if (wantDispatch && p.humanApproval && !approvedBy) refusals.push({ code: 'human_approval_required', message: '고위험 작업은 사람 승인이 필요합니다 — --approved-by "<승인자>"' });
|
|
208
|
+
|
|
209
|
+
// 1.36.89 (codex 28차 #1): 종전 필드명은 `dispatched` 였는데 이 모듈은 **외부 CLI 를 실행하지 않는다** —
|
|
210
|
+
// 실행했다고 보고하면서 아무것도 실행하지 않는, 이 제품이 가장 경계하는 형태의 거짓말이었다(실측).
|
|
211
|
+
// 실제로 하는 일은 "계획을 확정하고 실행 명령을 내주는 것"이므로 그대로 `confirmed` 라고 부른다.
|
|
212
|
+
const confirmed = wantDispatch && !refusals.length;
|
|
213
|
+
const rec = {
|
|
214
|
+
at: now(),
|
|
215
|
+
task: _norm(taskText).slice(0, 300),
|
|
216
|
+
tier: cls.tier, confidence: cls.confidence, conflict: !!cls.conflict,
|
|
217
|
+
signals: cls.signals.map(s => `${s.kind}:${s.id}`),
|
|
218
|
+
assignments: p.assignments.map(a => ({ role: a.role, provider: a.provider, model: a.model })),
|
|
219
|
+
reviewerIndependent: p.reviewerIndependent,
|
|
220
|
+
requested: wantDispatch ? 'confirm' : 'suggest',
|
|
221
|
+
confirmed, executed: false, approvedBy: approvedBy || null,
|
|
222
|
+
refusals: refusals.map(r => r.code),
|
|
223
|
+
};
|
|
224
|
+
const logged = _appendLog(root, rec, deps) || {};
|
|
225
|
+
|
|
226
|
+
if (json) {
|
|
227
|
+
log(JSON.stringify({
|
|
228
|
+
ok: !wantDispatch || confirmed, tier: cls.tier, confidence: cls.confidence, conflict: !!cls.conflict,
|
|
229
|
+
signals: cls.signals, reasons: cls.reasons, plan: p, gateEnabled: gateOn,
|
|
230
|
+
confirmed, executed: false, refusals,
|
|
231
|
+
auditWritten: logged.written !== false, auditWarning: logged.invalid ? `감사 기록에 남기지 못했습니다(${logged.reason}) — ${_logPath(root)}` : undefined,
|
|
232
|
+
availabilityNote: _AVAIL_NOTE,
|
|
233
|
+
classificationNote: _CLASS_NOTE,
|
|
234
|
+
executionNote: _EXEC_NOTE,
|
|
235
|
+
}, null, 2));
|
|
236
|
+
if (wantDispatch && !confirmed) process.exitCode = 1;
|
|
237
|
+
return { cls, plan: p, confirmed };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
log(`# leerness agents route — 난이도 ${cls.tier} (신뢰도 ${cls.confidence}${cls.conflict ? ' · 규칙 충돌' : ''})`);
|
|
241
|
+
cls.reasons.forEach(r => log(` · ${r}`));
|
|
242
|
+
log(`\n 구성(${p.ko}):`);
|
|
243
|
+
p.assignments.forEach(a => log(` ${a.role.padEnd(10)} ${a.resolved ? `${a.provider}${a.model ? ' / ' + a.model : ''}` : '⚠ 미설정 — leerness roles set ' + a.role + ' --provider <id>'}`));
|
|
244
|
+
if (p.reviewerIndependent === false) log(` ⚠ 검수자와 구현자가 같은 provider — 독립 검수가 아닙니다`);
|
|
245
|
+
log(`\n ⓘ ${_AVAIL_NOTE}`);
|
|
246
|
+
log(` ⓘ ${_CLASS_NOTE} 틀렸다면 --tier ${TIERS.join('|')} 로 지정하세요.`);
|
|
247
|
+
if (logged.invalid) warn(` 감사 기록에 남기지 못했습니다(${logged.reason}) — ${_logPath(root)} 를 고치거나 삭제하세요`);
|
|
248
|
+
if (!wantDispatch) {
|
|
249
|
+
log(`\n → 이 구성을 확정: leerness agents route "<작업>" --confirm${p.humanApproval ? ' --approved-by "<승인자>"' : ''}${gateOn ? '' : ' (먼저 leerness toggle set difficulty-routing on)'}`);
|
|
250
|
+
} else if (!confirmed) {
|
|
251
|
+
warn(`확정하지 않았습니다:`);
|
|
252
|
+
refusals.forEach(r => log(` - ${r.message}`));
|
|
253
|
+
process.exitCode = 1;
|
|
254
|
+
} else {
|
|
255
|
+
ok(`라우팅 확정 — ${p.assignments.filter(a => a.resolved).map(a => `${a.role}:${a.provider}`).join(' · ')}`);
|
|
256
|
+
log(` ⓘ ${_EXEC_NOTE}`);
|
|
257
|
+
log(` 아래 명령을 직접 실행하세요:`);
|
|
258
|
+
p.assignments.filter(a => a.resolved).forEach(a => log(` leerness agents dispatch "<${a.role} 작업 계약>" --role ${a.role}`));
|
|
259
|
+
log(` ⓘ 기록: ${_logPath(root)} (leerness agents route --log 로 열람)`);
|
|
260
|
+
}
|
|
261
|
+
return { cls, plan: p, confirmed };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function _showLog(root, json) {
|
|
265
|
+
const cur = _loadLog(root);
|
|
266
|
+
// codex 28차 #7: 고지가 --log 경로에는 없어서, 기록만 보는 사용자는 "provider/model 이 검증된 값"으로 읽었다.
|
|
267
|
+
if (json) { log(JSON.stringify({ ok: !cur.invalid, invalid: cur.invalid, reason: cur.reason, total: cur.list.length, entries: cur.list.slice(-20), availabilityNote: _AVAIL_NOTE, executionNote: _EXEC_NOTE }, null, 2)); if (cur.invalid) process.exitCode = 1; return; }
|
|
268
|
+
if (cur.invalid) { warn(`routing-log.json 형상 무효(${cur.reason}): ${_logPath(root)}`); process.exitCode = 1; return; }
|
|
269
|
+
if (!cur.list.length) { log('라우팅 기록 없음 — leerness agents route "<작업 설명>"'); return; }
|
|
270
|
+
log(`# leerness agents route --log (최근 ${Math.min(20, cur.list.length)} / 총 ${cur.list.length})`);
|
|
271
|
+
cur.list.slice(-20).forEach(e => {
|
|
272
|
+
const state = e.executed ? '실행' : e.confirmed ? '확정' : (e.requested === 'confirm' || e.requested === 'dispatch') ? '거부' : '제안';
|
|
273
|
+
log(` ${String(e.at).slice(0, 16)} ${String(e.tier).padEnd(9)} ${String(e.confidence).padEnd(8)} ${state} ${String(e.task).slice(0, 46)}`);
|
|
274
|
+
});
|
|
275
|
+
log(`\n ⓘ ${_AVAIL_NOTE}`);
|
|
276
|
+
log(` ⓘ ${_EXEC_NOTE}`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
module.exports = { routeCmd, classify, plan, TIERS, TIER_ROLES, HIGH_RISK, TINY, _logPath, _loadLog };
|
package/lib/toggles.js
CHANGED
|
@@ -16,6 +16,8 @@ const TOGGLE_REGISTRY = {
|
|
|
16
16
|
// 1.36.88 (출하전 헌트 #11): 켜기만 하면 뭔가 강제되는 것처럼 읽혔다 — 실제로는 `bugfix start` 로
|
|
17
17
|
// probe 를 등록한 task 에만 적용된다. 광고 문구가 적용 범위를 말해야 한다.
|
|
18
18
|
'bugfix-receipt': { desc: 'bugfix 완료 게이트 — `leerness bugfix start` 로 probe 를 등록한 task 에 한해, done 시 probe 통과 + 근본원인/형제범위 영수증 요구 (기본 OFF)', affects: 'task update --status done · task sync --from', defaultOff: true },
|
|
19
|
+
// 1.36.89 (P-0007): 켜도 **자동 실행은 없다** — `route --dispatch` 를 허용할 뿐이고, 고위험은 사람 승인이 별도로 필요하다.
|
|
20
|
+
'difficulty-routing': { desc: '작업 난이도별 모델 라우팅 — `leerness agents route` 의 --confirm 허용(제안/열람은 토글과 무관). 고위험은 --approved-by 필수 (기본 OFF)', affects: 'leerness agents route --confirm', defaultOff: true },
|
|
19
21
|
};
|
|
20
22
|
|
|
21
23
|
function _togglesPath(root) { return path.join(absRoot(root), '.harness', 'toggles.json'); }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leerness",
|
|
3
|
-
"version": "1.36.
|
|
3
|
+
"version": "1.36.89",
|
|
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
|
@@ -7707,6 +7707,116 @@ total++;
|
|
|
7707
7707
|
if (!ok) failed++;
|
|
7708
7708
|
}
|
|
7709
7709
|
|
|
7710
|
+
// 1.36.89 (P-0007, 사용자 승인 범위=제안+확인후실행): agents route 난이도 라우팅.
|
|
7711
|
+
// 판별 케이스를 함께 넣는다 — 거부 사유(code)까지 단언하지 않으면 여러 거부 조건이 서로를 가려
|
|
7712
|
+
// 하나를 죽여도 다른 하나가 막는다(1.36.88 에서 변이 6/8 로 실측한 함정).
|
|
7713
|
+
total++;
|
|
7714
|
+
{
|
|
7715
|
+
let ok = false;
|
|
7716
|
+
const _d = [];
|
|
7717
|
+
const R = (args, cwd) => { const r = cp.spawnSync(process.execPath, [CLI, ...args], { encoding: 'utf8', timeout: 120000, cwd }); return { s: r.status, o: ((r.stdout || '') + (r.stderr || '')).trim() }; };
|
|
7718
|
+
const J = (x) => { try { return JSON.parse(x.o.slice(x.o.indexOf('{'))); } catch { return null; } };
|
|
7719
|
+
let C1 = false, C2 = false, C3 = false, C4 = false, D1 = false, D2 = false, D3 = false, D4 = false, D5 = false, H1 = false, H2 = false;
|
|
7720
|
+
try {
|
|
7721
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-route-')); _d.push(d);
|
|
7722
|
+
R(['init', d, '--yes', '--language', 'ko', '--no-stale-check']);
|
|
7723
|
+
const cls = (t, extra = []) => J(R(['agents', 'route', t, '--path', d, '--json', ...extra]));
|
|
7724
|
+
// 분류: 고위험 도메인 · 고위험+초소형 충돌은 보수적으로 high-risk · 초소형 · 근거부족은 normal(과소평가 금지)
|
|
7725
|
+
C1 = (cls('결제 환불 로직 수정') || {}).tier === 'high-risk'
|
|
7726
|
+
// codex 28차 #5: 영문 어간(authentication/authorization/permissions/concurrency/idempotency)이
|
|
7727
|
+
// 뒤쪽 \b 때문에 전부 빠져나가 **tiny 로 떨어졌다**(실측). 한글 케이스만 보면 놓친다.
|
|
7728
|
+
&& ['fix typo in authentication middleware', 'fix typo in authorization permissions',
|
|
7729
|
+
'fix typo in concurrency and idempotency control'].every(t => (cls(t) || {}).tier === 'high-risk')
|
|
7730
|
+
// 오탐 대조군: author 는 auth 가 아니고, tokenizer 는 token 이 아니다(과대escalation 방지)
|
|
7731
|
+
&& (cls('fix typo in author bio css') || {}).tier === 'tiny';
|
|
7732
|
+
const conf = cls('인증 문서 오타 수정') || {};
|
|
7733
|
+
C2 = conf.tier === 'high-risk' && conf.conflict === true && conf.confidence === 'low';
|
|
7734
|
+
C3 = (cls('버튼 여백 4px 조정') || {}).tier === 'tiny'
|
|
7735
|
+
// codex 28차 #4: tiny 키워드 하나로 거대 작업이 tiny 가 됐다 — 광범위 신호/긴 설명은 tiny 가 아니다
|
|
7736
|
+
&& (cls('Rewrite the entire compiler, replace the storage engine, and also fix one typo') || {}).tier === 'normal'
|
|
7737
|
+
&& (cls('전체 아키텍처를 갈아엎고 저장 엔진을 교체하고 오타도 수정') || {}).tier === 'normal';
|
|
7738
|
+
const weak = cls('x') || {};
|
|
7739
|
+
C4 = weak.tier === 'normal' && weak.confidence === 'low' // tiny 로 내려가면 안 된다
|
|
7740
|
+
// codex 28차 #3: --tier 를 주면 빈 작업이 통과했다 — 어떤 경로로도 빈 작업은 받지 않는다
|
|
7741
|
+
&& /"code":\s*"empty_task"/.test(R(['agents', 'route', '--tier', 'tiny', '--path', d, '--json']).o);
|
|
7742
|
+
// 사용자 명시 우선 + 잘못된 값 거부
|
|
7743
|
+
const dec = cls('결제 로직 오타', ['--tier', 'tiny']) || {};
|
|
7744
|
+
// 잘못된 --tier 는 **사유(code)까지** 단언한다 — exit 1 만 보면 검증을 없앴을 때 나는 크래시(TIER_ROLES 미정의)와
|
|
7745
|
+
// 구분되지 않아, 검증 제거 변이가 그대로 통과했다(실측 M7 생존).
|
|
7746
|
+
const badTier = R(['agents', 'route', 'x', '--tier', 'huge', '--path', d, '--json']);
|
|
7747
|
+
D1 = dec.tier === 'tiny' && dec.confidence === 'declared' && (dec.reasons || []).some(r => /의도한 것인지/.test(r))
|
|
7748
|
+
&& badTier.s === 1 && /"code":\s*"invalid_tier"/.test(badTier.o);
|
|
7749
|
+
// 토글 OFF → dispatch 거부 (사유까지)
|
|
7750
|
+
const off = R(['agents', 'route', '주문 필터 추가', '--confirm', '--path', d, '--json']);
|
|
7751
|
+
D2 = off.s === 1 && /"routing_disabled"/.test(off.o);
|
|
7752
|
+
// 토글 ON + 역할 미설정 → 거부 (판별: 토글 거부와 다른 사유여야)
|
|
7753
|
+
R(['toggle', 'set', 'difficulty-routing', 'on', '--path', d]);
|
|
7754
|
+
const nr = R(['agents', 'route', '주문 필터 추가', '--confirm', '--path', d, '--json']);
|
|
7755
|
+
D3 = nr.s === 1 && /"role_unresolved"/.test(nr.o) && !/"routing_disabled"/.test(nr.o);
|
|
7756
|
+
// 역할 설정 후 normal 은 실행 확정
|
|
7757
|
+
R(['roles', 'set', 'commander', '--provider', 'claude', '--path', d]);
|
|
7758
|
+
R(['roles', 'set', 'coder', '--provider', 'codex', '--path', d]);
|
|
7759
|
+
R(['roles', 'set', 'reviewer', '--provider', 'claude', '--path', d]);
|
|
7760
|
+
const okd = J(R(['agents', 'route', '주문 필터 추가', '--confirm', '--path', d, '--json'])) || {};
|
|
7761
|
+
D4 = okd.confirmed === true && okd.plan && okd.plan.reviewerIndependent === true;
|
|
7762
|
+
// 고위험: 승인자 없으면 fail-closed (판별: 역할은 다 설정된 상태여야 role_unresolved 가 아님이 증명된다)
|
|
7763
|
+
R(['roles', 'set', 'architect', '--provider', 'codex', '--path', d]);
|
|
7764
|
+
const hr = R(['agents', 'route', '결제 환불 로직 수정', '--confirm', '--path', d, '--json']);
|
|
7765
|
+
H1 = hr.s === 1 && /"human_approval_required"/.test(hr.o) && !/"role_unresolved"/.test(hr.o);
|
|
7766
|
+
const hr2 = J(R(['agents', 'route', '결제 환불 로직 수정', '--confirm', '--approved-by', '승인자', '--path', d, '--json'])) || {};
|
|
7767
|
+
D5 = hr2.confirmed === true;
|
|
7768
|
+
// 독립성: 구현자와 검수자가 같은 provider 면 고위험은 거부 (승인자가 있는데도 막혀야 한다 = 판별)
|
|
7769
|
+
R(['roles', 'set', 'reviewer', '--provider', 'codex', '--path', d]);
|
|
7770
|
+
const dep = R(['agents', 'route', '결제 환불 로직 수정', '--confirm', '--approved-by', '승인자', '--path', d, '--json']);
|
|
7771
|
+
H2 = dep.s === 1 && /"reviewer_not_independent"/.test(dep.o) && !/"human_approval_required"/.test(dep.o);
|
|
7772
|
+
// codex 28차 #6: 손으로 편집한 역할 설정의 **빈 provider** 가 "해석됨"으로 통과했다 —
|
|
7773
|
+
// `roles set` 은 빈 값을 안 받으므로 이 상태는 파일 편집으로만 생긴다. 그래서 파일을 직접 만들어 검증한다.
|
|
7774
|
+
const rolesFile = path.join(d, '.harness', 'agent-roles.json');
|
|
7775
|
+
const savedRoles = fs.existsSync(rolesFile) ? fs.readFileSync(rolesFile, 'utf8') : null;
|
|
7776
|
+
// 스키마는 `{roles:{...}}` 다 — 최상위에 쓰면 _loadRoles 가 {} 를 돌려줘 "역할 없음"으로 통과하고,
|
|
7777
|
+
// 빈 provider 를 검증한 것이 아니라 **셋업 실패를 통과로 읽는다**(실측으로 잡았다).
|
|
7778
|
+
fs.writeFileSync(rolesFile, JSON.stringify({ schemaVersion: 1, roles: { coder: { provider: ' ' } } }));
|
|
7779
|
+
const emptyProv = R(['agents', 'route', '버튼 여백 4px 조정', '--confirm', '--path', d, '--json']);
|
|
7780
|
+
const emptyProvJ = J(emptyProv) || {};
|
|
7781
|
+
// 셋업이 유효했는지 함께 단언: coder 가 실제로 목록에 있어야 한다(없으면 다른 이유로 막힌 것)
|
|
7782
|
+
const emptyProvOk = emptyProv.s === 1 && /"role_unresolved"/.test(emptyProv.o)
|
|
7783
|
+
&& ((emptyProvJ.plan || {}).assignments || []).some(a => a.role === 'coder' && a.resolved === false);
|
|
7784
|
+
if (savedRoles !== null) fs.writeFileSync(rolesFile, savedRoles); else fs.rmSync(rolesFile, { force: true });
|
|
7785
|
+
// 감사 기록 — codex 28차 #10: 종전엔 `total >= 8` 만 봐서 **전 항목이 {} 여도 통과**했다. 내용을 본다.
|
|
7786
|
+
const lg = J(R(['agents', 'route', '--log', '--path', d, '--json'])) || {};
|
|
7787
|
+
const ents = lg.entries || [];
|
|
7788
|
+
const auditOk = (lg.total || 0) >= 8
|
|
7789
|
+
&& ents.some(e => e.tier === 'high-risk' && e.confirmed === true && e.approvedBy === '승인자'
|
|
7790
|
+
&& Array.isArray(e.assignments) && e.assignments.some(a => a.role === 'coder' && a.provider)
|
|
7791
|
+
&& Array.isArray(e.signals) && e.signals.some(s => /^high-risk:/.test(s)))
|
|
7792
|
+
&& ents.some(e => e.confirmed === false && Array.isArray(e.refusals) && e.refusals.length > 0); // 거부된 시도도 남는다
|
|
7793
|
+
// 손상 감사로그를 조용히 덮어쓰지 않는다(codex 28차 #8)
|
|
7794
|
+
const lp = path.join(d, '.harness', 'routing-log.json');
|
|
7795
|
+
const savedLog = fs.readFileSync(lp, 'utf8');
|
|
7796
|
+
fs.writeFileSync(lp, '{"소중한":"기록"}');
|
|
7797
|
+
R(['agents', 'route', '무해한 조회', '--path', d]);
|
|
7798
|
+
const auditPreserved = fs.readFileSync(lp, 'utf8') === '{"소중한":"기록"}';
|
|
7799
|
+
fs.writeFileSync(lp, savedLog);
|
|
7800
|
+
// 정직성: 하지 않은 확인을 했다고 말하지 않고, 실행하지 않는다는 사실을 사람용·JSON·--log 모두에서 말한다
|
|
7801
|
+
const human = R(['agents', 'route', '주문 필터', '--path', d]).o;
|
|
7802
|
+
const logHuman = R(['agents', 'route', '--log', '--path', d]).o;
|
|
7803
|
+
const cj = J(R(['agents', 'route', '주문 필터 추가', '--confirm', '--path', d, '--json'])) || {};
|
|
7804
|
+
const honestOk = /확인하지 않았습니다/.test(human) && /측정값이 아닙니다/.test(human)
|
|
7805
|
+
&& !/설치·활성 여부만 확인/.test(human) // 하지 않은 확인을 했다고 말하던 옛 문구
|
|
7806
|
+
&& /확인하지 않았습니다/.test(logHuman) && /외부 CLI 를 실행하지 않습니다/.test(logHuman)
|
|
7807
|
+
&& cj.executed === false && typeof cj.confirmed === 'boolean' && !('dispatched' in cj)
|
|
7808
|
+
&& /외부 CLI 를 실행하지 않습니다/.test(JSON.stringify(cj));
|
|
7809
|
+
// 모델명 비하드코딩 — 정책 모듈에 벤더 모델 id 가 있으면 안 된다(몇 달이면 낡는다)
|
|
7810
|
+
// 경로는 CLI 기준으로 잡는다 — 검사 대상이 "지금 실행 중인 그 설치본"이어야 한다(__dirname 은 실행 위치에 따라 어긋난다).
|
|
7811
|
+
const srcOk = !/(claude-(opus|sonnet|haiku)|gpt-[45]|o[34]-mini|gemini-)/i.test(fs.readFileSync(path.join(path.dirname(CLI), '..', 'lib', 'routing.js'), 'utf8'));
|
|
7812
|
+
ok = C1 && C2 && C3 && C4 && D1 && D2 && D3 && D4 && D5 && H1 && H2 && auditOk && auditPreserved && honestOk && srcOk && emptyProvOk;
|
|
7813
|
+
if (!ok) console.log(` [route 디버그] C1=${C1} C2=${C2} C3=${C3} C4=${C4} D1=${D1} OFF거부=${D2} 역할미설정거부=${D3} normal실행=${D4} 고위험실행=${D5} 승인필요=${H1} 독립성=${H2} 감사=${auditOk} 손상보존=${auditPreserved} 정직=${honestOk} 소스=${srcOk} 빈provider=${emptyProvOk} log=${lg.total}`);
|
|
7814
|
+
} catch (e) { console.log(' [route 디버그] 예외: ' + ((e && e.message) || e)); }
|
|
7815
|
+
finally { _d.forEach(x => { try { fs.rmSync(x, { recursive: true, force: true }); } catch {} }); }
|
|
7816
|
+
console.log(ok ? '✓ B(1.36.89) agents route 난이도 라우팅: 결정적 분류(충돌시 보수적·근거부족시 normal) · 토글OFF/역할미설정/사람승인/검수독립성 4중 fail-closed · 감사기록 · 모델명 비하드코딩 · 미검증 고지' : '✗ agents route 라우팅 실패');
|
|
7817
|
+
if (!ok) failed++;
|
|
7818
|
+
}
|
|
7819
|
+
|
|
7710
7820
|
// 1.36.76 (9차 헌트 이월 3건 종결): 손상 아카이브 복원 거부 · MCP read-only 무변형 · 긴 파생 id 안전화
|
|
7711
7821
|
total++;
|
|
7712
7822
|
{
|