leerness 1.9.413 → 1.9.415
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 +41 -0
- package/README.md +5 -5
- package/bin/harness.js +65 -21
- package/lib/pure-utils.js +10 -2
- package/lib/team.js +9 -1
- package/package.json +1 -1
- package/scripts/e2e.js +59 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.415 — 2026-06-07 — 정직성 수정: handoff 보안 헤드라인 false-OK + scan/encoding/contract --json (9번째 외부평가, UR-0121)
|
|
4
|
+
|
|
5
|
+
**🚨 9번째 외부 멀티모델 리뷰(Codex GPT-5.5 + Claude Sonnet/Opus 4.8, README 미참조 클린룸)에서 발견한 "의미적 false-OK"를 수정 — leerness 정체성(정직/anti-laziness)과 정면 충돌하던 결함.**
|
|
6
|
+
|
|
7
|
+
### 배경
|
|
8
|
+
3개 모델이 깨끗한 경로에 leerness@latest 설치(README 제거) 후 직접 사용으로 객관 평가. 모든 발견을 직접 재현·검증(맹신 X)해 확정분만 수정.
|
|
9
|
+
|
|
10
|
+
### 수정 (확정 발견)
|
|
11
|
+
- **[Codex P1] handoff 보안 헤드라인 false-OK**: `handoff` 가 `.env` 가 `.gitignore` 에 있으면 무조건 "🔒 보안 OK" 출력 — **하드코딩된 시크릿이 있어도** 안전하다고 표시(scan secrets/gate 는 정확히 실패하는데 handoff 만 거짓 안심). 이제 실제 시크릿 스캔(`_collectSecretFindings`) 결과를 반영 → 커밋 대상 시크릿이 있으면 "🚨 시크릿 N건", 없을 때만 "🔒 보안 OK".
|
|
12
|
+
- **[Opus/Codex P2] `scan secrets` / `encoding check` --json 무시**: 두 명령이 `--json` 을 무시하고 텍스트만 출력(MCP/CI 파싱 불가) → 구조화 JSON + exit code 일관화.
|
|
13
|
+
- **[Opus P2] `contract verify --json` 불일치인데 exit 0**: `--json` 분기가 `process.exitCode=1` 보다 먼저 return → CI 가 계약 실패를 못 잡음. 불일치 시 exit 1.
|
|
14
|
+
|
|
15
|
+
### 검증 (회귀 0)
|
|
16
|
+
- **selftest 160→161 PASS** · **E2E 414→415 PASS**.
|
|
17
|
+
|
|
18
|
+
### 직접 재현으로 기각한 발견 (맹신 X)
|
|
19
|
+
- [Opus P1] contract verify 백틱 불릿 누락 → 이미 정상 작동(1.9.385 파서)으로 **기각**.
|
|
20
|
+
- [Sonnet] npm 패키지가 .harness 동봉 → 미동봉으로 **기각**.
|
|
21
|
+
- [Opus] route/gate/audit 동시성 손상 → Opus 자체 정정(파이프 아티팩트).
|
|
22
|
+
|
|
23
|
+
### 후속 백로그 (UR-0122~0124)
|
|
24
|
+
team add positional path 일관성 · status/health "healthy" 라벨 명확화 · contract field 범용화 · init 침습성/명령 계층화/handoff 속도/--compact 압축.
|
|
25
|
+
|
|
26
|
+
## 1.9.414 — 2026-06-07 — 에이전트 팀에 "메인 검수" 단계 (서브에이전트 검수 통합, UR-0119/0120)
|
|
27
|
+
|
|
28
|
+
**🤝 team 에 `--review`(메인 에이전트 검수 요구) 추가 — "분배(sub-agent) → 메인 검수" 흐름을 팀 정의에 일급으로 통합.**
|
|
29
|
+
|
|
30
|
+
### 배경 (사용자 체크 요청 → 갭 보강)
|
|
31
|
+
사용자 확인 요청: "서브에이전트 자동화 + 메인 에이전트 검수 과정이 구현돼 있는지". 조사 결과 분배(agents dispatch/team) + 검증(verify-claim/review) + session-workflow Step5(교차검증)는 있으나, **팀 정의에 "메인 검수 필수"가 일급으로 없었음**. team 에 review 필드를 추가해 통합.
|
|
32
|
+
|
|
33
|
+
### 구현
|
|
34
|
+
- `team add ... [--no-review]`: 팀에 `review` 필드(기본 on). `--no-review` 로 끔.
|
|
35
|
+
- `_composeTeamPlan`: review 시 멤버별 dispatch 단계 뒤에 **메인 검수 단계(reviewStep)** 추가 — `verify-claim --strict-claims` / `review --persona` 안내.
|
|
36
|
+
- `team preview`: 분배 계획 + "✔ 메인 검수 (필수)" 표시(+ --json reviewStep).
|
|
37
|
+
- `_teamHandoffReminders`: 스케줄 팀에 "· 검수필요" 표시.
|
|
38
|
+
- show/`_renderTeamsMd`: review 상태 표기.
|
|
39
|
+
|
|
40
|
+
### 검증 (회귀 0)
|
|
41
|
+
- **selftest 159→160 PASS** (_composeTeamPlan reviewStep on/off + handoff 검수필요 + team add 와이어).
|
|
42
|
+
- **E2E 413→414 PASS** (preview 메인 검수 표시 + --no-review 생략 + --json reviewStep).
|
|
43
|
+
|
|
3
44
|
## 1.9.413 — 2026-06-07 — action 명령 --json 구조화 출력 (6번째 외부평가 codex P2, UR-0101)
|
|
4
45
|
|
|
5
46
|
**🔌 자동화 일관성 완결 — task/decision/rule/lesson add 가 `--json` 을 무시하고 텍스트만 내던 것을 구조화 JSON 출력으로.**
|
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
> **AI 코딩 에이전트의 거짓 완료·중복·망각·충돌을 막아주는 검수·기억·협업 CLI 하네스.**
|
|
4
4
|
> **A CLI harness that stops AI coding agents from faking completion, duplicating work, forgetting context, and colliding.**
|
|
5
5
|
|
|
6
|
-
[](https://www.npmjs.com/package/leerness) [](https://www.npmjs.com/package/leerness) []() []() []() []() []() []()
|
|
7
7
|
|
|
8
8
|
```
|
|
9
9
|
╔══════════════════════════════════════════════════════════════╗
|
|
@@ -471,7 +471,7 @@ MIT — © leerness contributors
|
|
|
471
471
|
<!-- leerness:project-readme:start -->
|
|
472
472
|
## Leerness Project Harness
|
|
473
473
|
|
|
474
|
-
이 프로젝트는 Leerness v1.9.
|
|
474
|
+
이 프로젝트는 Leerness v1.9.415 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
|
|
475
475
|
|
|
476
476
|
### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
|
|
477
477
|
|
|
@@ -525,7 +525,7 @@ leerness memory restore decision <date|title>
|
|
|
525
525
|
|
|
526
526
|
### MCP server (외부 AI 통합)
|
|
527
527
|
|
|
528
|
-
Leerness v1.9.
|
|
528
|
+
Leerness v1.9.415는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **85개 도구**를 노출:
|
|
529
529
|
|
|
530
530
|
```jsonc
|
|
531
531
|
// 카테고리별
|
|
@@ -546,7 +546,7 @@ Leerness v1.9.413는 stdio JSON-RPC MCP server를 내장합니다 — Claude Cod
|
|
|
546
546
|
`<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
|
|
547
547
|
1) 다음 라운드 후보 선정 → 2) 코드 변경 → 3) stress-v* 신규 작성 + 누적 회귀 → 4) e2e 219/219 → 5) npm pack + git tag + GitHub release → 6) main 자동 push (1.9.140+) → 7) session close → 8) 다음 라운드 예약.
|
|
548
548
|
|
|
549
|
-
현재 누적: **70 라운드 (1.9.40 → 1.9.
|
|
549
|
+
현재 누적: **70 라운드 (1.9.40 → 1.9.415)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
|
|
550
550
|
|
|
551
551
|
### 성능 가이드 (1.9.140 측정)
|
|
552
552
|
|
|
@@ -584,6 +584,6 @@ leerness release pack --close --auto-main-push
|
|
|
584
584
|
- `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
|
|
585
585
|
- `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
|
|
586
586
|
|
|
587
|
-
Last synced by Leerness v1.9.
|
|
587
|
+
Last synced by Leerness v1.9.415: 2026-06-07
|
|
588
588
|
<!-- leerness:project-readme:end -->
|
|
589
589
|
|
package/bin/harness.js
CHANGED
|
@@ -31,7 +31,7 @@ const { _evidenceQuality, _parseEvidenceStats, _shellGuardAnalyze, _claimFileInG
|
|
|
31
31
|
// 1.9.295 (UR-0025 4단계): 정적 데이터 카탈로그 모듈 분리 (비파괴, require-based).
|
|
32
32
|
const { CAPABILITY_SURFACE, POWERFUL_COMMANDS, ADAPTERS, REUSE_CATEGORIES, REUSE_CHECKLIST, _DEFAULT_PLATFORM_CONSTRAINTS, _DEFAULT_DOMAIN_CATALOG, _LSP_LANG_PATTERNS, OPTIMISM_PATTERNS, BUILT_IN_PERSONAS, STRINGS, BUILTIN_CATALOG, ROADMAP_STATUS_LABEL, ROADMAP_STATUS_COLOR, SECRET_PATTERNS, MERGE_OVERWRITE_FILES, MINIMAL_SKIP_KEYS, REQUIRED_WORKSPACE_FILES, KEYWORD_STOPWORDS, SKILL_CATALOG_PRESETS } = require('../lib/catalogs'); // 1.9.344/368/369 (UR-0025): catalog 분리 (MERGE_OVERWRITE_FILES/MINIMAL_SKIP_KEYS 포함)
|
|
33
33
|
|
|
34
|
-
const VERSION = '1.9.
|
|
34
|
+
const VERSION = '1.9.415';
|
|
35
35
|
|
|
36
36
|
// 1.9.290 (UR-0037, Codex gpt-5.5 #4 수렴): CLI 전용 부작용은 require 시 실행하지 않는다.
|
|
37
37
|
// 이전: warning listener 제거 / NODE_OPTIONS 변경 / chcp IIFE 가 top-level 즉시 실행 → require('harness') 시 호스트 프로세스 오염.
|
|
@@ -3025,6 +3025,23 @@ function _selfTestCases() {
|
|
|
3025
3025
|
{ name: '8번째 버그헌트 (UR-0115): lazy detect --auto-track 단일 RMW 배치(O(T×N)→O(N+T)) (1.9.411)', run: () => { const src = read(__filename); const batched = src.includes("8번째 버그헌트, UR-0115") && /has\('--auto-track'\)[\s\S]{0,500}?_withLock\(progressPath\(root\), \(\) => \{[\s\S]{0,1200}?writeProgressRows/.test(src); const noPerTodoUpsert = !/for \(const t of newTodos\) \{\s*const id = nextId\(root, 'T'\);/.test(src); return batched && noPerTodoUpsert; } },
|
|
3026
3026
|
{ name: '6번째 외부평가 Opus P1 (UR-0100): list-family(decision/feature/plan/runs/team list) positional path 지원 (조용한 cwd 오독 차단) (1.9.412)', run: () => { const src = read(__filename); const L = '_resolveRoot('; const decOk = src.includes("decisionListCmd(absRoot(" + L + "args[2]))"); const planOk = src.includes("planListCmd(absRoot(" + L + "args[2]))"); const featOk = src.includes("featureListCmd(absRoot(" + L + "args[2]))"); const runsOk = src.includes("runsListCmd(absRoot(" + L + "args[2]))"); const teamOk = src.includes(L + "args[1] === 'list' ? args[2] : null)"); return decOk && planOk && featOk && runsOk && teamOk; } },
|
|
3027
3027
|
{ name: '6번째 외부평가 codex P2 (UR-0101): action 명령(task/decision/rule/lesson add) --json 구조화 출력 (1.9.413)', run: () => { const src = read(__filename); const taskJ = src.includes("log(JSON.stringify({ ok: true, id, status: arg('--status', 'requested'), request: text }))"); const decJ = src.includes("log(JSON.stringify({ ok: true, title }))"); const lesJ = src.includes("log(JSON.stringify({ ok: true, text, tag: tag || null }))"); const ruleJ = src.includes("skipped: !!result.skip"); return taskJ && decJ && lesJ && ruleJ; } },
|
|
3028
|
+
{ name: '9th 외부평가 Codex P1 (UR-0121): handoff 보안 헤드라인 실제 스캔 기반 + scan/encoding --json + contract --json exit (1.9.415)', run: () => {
|
|
3029
|
+
const src = read(__filename);
|
|
3030
|
+
const handoffWired = src.includes('_collectSecretFindings(root)') && src.includes('🚨 ' + '시크릿 ');
|
|
3031
|
+
const scanJson = src.includes('function scanSecrets(root, opts = {})') && src.includes("has('--json') || opts.json");
|
|
3032
|
+
const encJson = src.includes('function encodingCheck(root, opts = {})');
|
|
3033
|
+
const contractExit = src.includes('if (!okJson) process.exitCode = 1;');
|
|
3034
|
+
let behav = false;
|
|
3035
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_sec_'));
|
|
3036
|
+
try {
|
|
3037
|
+
fs.writeFileSync(path.join(tmp, 'c.js'), 'module.exports={apiKey:"sk-test-1234567890abcdefghijklmnopqrstuvwxyz"};');
|
|
3038
|
+
const r = _collectSecretFindings(tmp);
|
|
3039
|
+
const clean = _collectSecretFindings(fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_clean_')));
|
|
3040
|
+
behav = r.committed.length >= 1 && clean.committed.length === 0;
|
|
3041
|
+
} catch {} finally { try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} }
|
|
3042
|
+
return handoffWired && scanJson && encJson && contractExit && behav;
|
|
3043
|
+
} },
|
|
3044
|
+
{ name: '9라운드 (UR-0119/0120): team review(메인 검수) — _composeTeamPlan reviewStep + handoff 검수필요 + team add 와이어 (1.9.414)', run: () => { const m = require('../lib/pure-utils'); const on = m._composeTeamPlan({ id: 't', members: ['a', 'b'], personas: ['security'] }, '점검'); const off = m._composeTeamPlan({ id: 't', members: ['a'], review: false }, '점검'); const planOk = on.review === true && !!on.reviewStep && on.reviewStep.suggestedCommand.includes('verify-claim') && off.review === false && !off.reviewStep; const rem = m._teamHandoffReminders([{ id: 'r', schedule: 'every-session', status: 'active', members: ['a'], review: true }]); const remOk = rem.length === 1 && rem[0].includes('검수필요'); const teamSrc = read(path.join(path.dirname(__filename), '..', 'lib', 'team.js')); const wired = teamSrc.includes("review: !has('--no-review')") && teamSrc.includes('메인 검수 (필수)'); return planOk && remOk && wired; } },
|
|
3028
3045
|
{ name: 'VERSION 형식 (x.y.z)', run: () => /^\d+\.\d+\.\d+$/.test(VERSION) }
|
|
3029
3046
|
];
|
|
3030
3047
|
}
|
|
@@ -7127,30 +7144,31 @@ function _isLikelyGitignored(root, fileRel) {
|
|
|
7127
7144
|
try { gi = read(path.join(root, '.gitignore')); } catch { return false; }
|
|
7128
7145
|
return _gitignoreMatch(gi, fileRel);
|
|
7129
7146
|
}
|
|
7130
|
-
|
|
7147
|
+
// 1.9.415 (9th 외부평가 Codex P1): 시크릿 findings 수집 순수부 — scanSecrets 출력/exit 와 handoff 보안 헤드라인이 공유(단일 출처).
|
|
7148
|
+
// 기존엔 handoff 가 '.env 가 .gitignore 에 있으면 보안 OK' 로만 판단(하드코딩 시크릿 무시 → false-OK). 이제 둘 다 실제 스캔 결과를 사용.
|
|
7149
|
+
function _collectSecretFindings(root) {
|
|
7131
7150
|
root = absRoot(root);
|
|
7132
7151
|
const findings = [];
|
|
7133
|
-
// 1.9.354 (UR-0072 외부리뷰): root 가 파일이면 그 파일만
|
|
7152
|
+
// 1.9.354 (UR-0072 외부리뷰): root 가 파일이면 그 파일만 스캔. 디렉토리면 walk.
|
|
7134
7153
|
let _iter;
|
|
7135
7154
|
try { _iter = fs.statSync(root).isFile() ? [root] : walk(root); } catch { _iter = walk(root); }
|
|
7136
7155
|
for (const file of _iter) {
|
|
7137
7156
|
const ext = path.extname(file).toLowerCase();
|
|
7138
|
-
// 1.9.386 (UR-0087
|
|
7139
|
-
// extname 이 .bad/.local/.production 라 SCAN_TEXT_EXT 에 없어 통째로 스킵되던 FN → basename 으로 강제 포함.
|
|
7157
|
+
// 1.9.386 (UR-0087): env-family(.env / .env.local / .env.production …) basename 강제 포함.
|
|
7140
7158
|
const isEnvFamily = /^\.env(\.|$)/.test(path.basename(file));
|
|
7141
7159
|
if (!SCAN_TEXT_EXT.has(ext) && !isEnvFamily) continue;
|
|
7142
7160
|
let text;
|
|
7143
7161
|
try { text = read(file); } catch { continue; }
|
|
7144
7162
|
if (text.length > 1024 * 1024) continue;
|
|
7145
|
-
const fileRel = (file === root) ? path.basename(file) : rel(root, file);
|
|
7146
|
-
// 1.9.350 (UR-0060
|
|
7163
|
+
const fileRel = (file === root) ? path.basename(file) : rel(root, file);
|
|
7164
|
+
// 1.9.350 (UR-0060): leerness 자기 harness.js + secret-policy 템플릿만 제외.
|
|
7147
7165
|
if (path.resolve(file) === path.resolve(__filename) || /(^|[\\/])\.(?:harness|leerness)[\\/]secret-policy\.md$/.test(fileRel)) continue;
|
|
7148
|
-
const gitignored = _isLikelyGitignored(root, fileRel); // 1.9.365 CV-6: gitignored 면
|
|
7166
|
+
const gitignored = _isLikelyGitignored(root, fileRel); // 1.9.365 CV-6: gitignored 면 info 강등
|
|
7149
7167
|
for (const { name, re, valueGroup, requireSecretLike } of SECRET_PATTERNS) {
|
|
7150
7168
|
re.lastIndex = 0;
|
|
7151
7169
|
let m;
|
|
7152
7170
|
while ((m = re.exec(text))) {
|
|
7153
|
-
// 1.9.365 (CV-6/UR-0081):
|
|
7171
|
+
// 1.9.365 (CV-6/UR-0081): placeholder/예시 값 스킵.
|
|
7154
7172
|
if (valueGroup != null) {
|
|
7155
7173
|
const val = m[valueGroup];
|
|
7156
7174
|
if (_isPlaceholderSecret(val)) { if (re.lastIndex === m.index) re.lastIndex++; continue; }
|
|
@@ -7162,9 +7180,19 @@ function scanSecrets(root) {
|
|
|
7162
7180
|
}
|
|
7163
7181
|
}
|
|
7164
7182
|
}
|
|
7165
|
-
//
|
|
7166
|
-
|
|
7167
|
-
|
|
7183
|
+
// gitignored(.env 등 안전 보관)는 info, 커밋 대상 발견만 실패.
|
|
7184
|
+
return { findings, committed: findings.filter(f => !f.gitignored), ignored: findings.filter(f => f.gitignored) };
|
|
7185
|
+
}
|
|
7186
|
+
|
|
7187
|
+
function scanSecrets(root, opts = {}) {
|
|
7188
|
+
root = absRoot(root);
|
|
7189
|
+
const { committed, ignored } = _collectSecretFindings(root);
|
|
7190
|
+
// 1.9.415 (9th 외부평가 Opus/Codex): --json 일관성 — 기존엔 --json 무시하고 사람용 텍스트만 출력하던 FN.
|
|
7191
|
+
if (has('--json') || opts.json) {
|
|
7192
|
+
log(JSON.stringify({ version: VERSION, root, ok: committed.length === 0, count: committed.length, committed, ignored }, null, 2));
|
|
7193
|
+
if (committed.length) process.exitCode = 1;
|
|
7194
|
+
return;
|
|
7195
|
+
}
|
|
7168
7196
|
if (committed.length) {
|
|
7169
7197
|
fail(`secret patterns found: ${committed.length}`);
|
|
7170
7198
|
committed.forEach(f => log(` ${f.file}:${f.line} ${f.name} ${f.snippet}…`));
|
|
@@ -7177,7 +7205,7 @@ function scanSecrets(root) {
|
|
|
7177
7205
|
}
|
|
7178
7206
|
}
|
|
7179
7207
|
|
|
7180
|
-
function encodingCheck(root) {
|
|
7208
|
+
function encodingCheck(root, opts = {}) {
|
|
7181
7209
|
root = absRoot(root);
|
|
7182
7210
|
let warnings = 0; const findings = [];
|
|
7183
7211
|
for (const file of walk(root)) {
|
|
@@ -7206,6 +7234,12 @@ function encodingCheck(root) {
|
|
|
7206
7234
|
}
|
|
7207
7235
|
} catch {}
|
|
7208
7236
|
}
|
|
7237
|
+
// 1.9.415 (9th 외부평가 Opus/Codex): --json 일관성 — 기존엔 --json 무시하고 텍스트만 출력하던 FN.
|
|
7238
|
+
if (has('--json') || opts.json) {
|
|
7239
|
+
log(JSON.stringify({ version: VERSION, root, ok: findings.length === 0, count: findings.length, findings }, null, 2));
|
|
7240
|
+
if (warnings > 0) process.exitCode = 1;
|
|
7241
|
+
return;
|
|
7242
|
+
}
|
|
7209
7243
|
if (findings.length) {
|
|
7210
7244
|
warn(`encoding issues: ${findings.length}`);
|
|
7211
7245
|
findings.forEach(f => log(` ${f.file} ${f.issue}`));
|
|
@@ -7669,14 +7703,22 @@ function handoff(root) {
|
|
|
7669
7703
|
const j = JSON.parse(r.stdout.trim());
|
|
7670
7704
|
if (j.level) parts.push(`drift ${j.level.replace(/^[^\w]+/, '')} (${j.score})`);
|
|
7671
7705
|
} catch {}
|
|
7672
|
-
// 2) 보안 상태
|
|
7706
|
+
// 2) 보안 상태 (1.9.415, 9th 외부평가 Codex P1): 실제 시크릿 스캔 기반.
|
|
7707
|
+
// 기존엔 '.env 가 .gitignore 에 있으면 보안 OK' 로만 판단해 하드코딩 시크릿이 있어도 '🔒 보안 OK' 출력하던 false-OK.
|
|
7708
|
+
// 이제 커밋 대상 시크릿이 있으면 '🚨 시크릿 N건', 없을 때만 '보안 OK'(.env 미무시는 별도 경고).
|
|
7673
7709
|
try {
|
|
7674
|
-
const
|
|
7675
|
-
if (
|
|
7676
|
-
|
|
7677
|
-
|
|
7678
|
-
|
|
7679
|
-
|
|
7710
|
+
const sec = _collectSecretFindings(root);
|
|
7711
|
+
if (sec.committed.length) {
|
|
7712
|
+
parts.push(`🚨 시크릿 ${sec.committed.length}건`);
|
|
7713
|
+
} else {
|
|
7714
|
+
const envPath = path.join(root, '.env');
|
|
7715
|
+
if (exists(envPath)) {
|
|
7716
|
+
const giText = exists(path.join(root, '.gitignore')) ? read(path.join(root, '.gitignore')) : '';
|
|
7717
|
+
const giLines = giText.split('\n').map(l => l.trim());
|
|
7718
|
+
parts.push((giLines.includes('.env') || giLines.includes('/.env')) ? '🔒 보안 OK' : '🚨 .env 미무시');
|
|
7719
|
+
} else {
|
|
7720
|
+
parts.push('🔒 보안 OK');
|
|
7721
|
+
}
|
|
7680
7722
|
}
|
|
7681
7723
|
} catch {}
|
|
7682
7724
|
// 3) MCP 활동 누적
|
|
@@ -19792,13 +19834,15 @@ function contractVerifyCmd(specPath, implPath) {
|
|
|
19792
19834
|
}
|
|
19793
19835
|
// 출력
|
|
19794
19836
|
if (has('--json')) {
|
|
19837
|
+
const okJson = missing.length === 0 && fieldMissing.length === 0;
|
|
19795
19838
|
log(JSON.stringify({
|
|
19796
19839
|
spec: specFile, impl: implFile,
|
|
19797
19840
|
specFunctions: [...fnSpec], specFields: [...fieldSpec],
|
|
19798
19841
|
implExports: [...implExports],
|
|
19799
19842
|
missingFunctions: missing, missingFields: fieldMissing,
|
|
19800
|
-
ok:
|
|
19843
|
+
ok: okJson
|
|
19801
19844
|
}, null, 2));
|
|
19845
|
+
if (!okJson) process.exitCode = 1; // 1.9.415 (9th 외부평가 Opus P2): --json 불일치도 exit 1 (기존엔 exit 0 → CI 가 계약실패 못 잡음)
|
|
19802
19846
|
return;
|
|
19803
19847
|
}
|
|
19804
19848
|
log(`# leerness contract verify (1.9.35)`);
|
package/lib/pure-utils.js
CHANGED
|
@@ -757,6 +757,7 @@ function _renderTeamsMd(teams) {
|
|
|
757
757
|
+ `- Members: ${(t.members || []).join(', ')}\n`
|
|
758
758
|
+ `- Schedule: ${t.schedule || 'manual'}\n`
|
|
759
759
|
+ `- Deploy: ${t.deployCommand || '-'}\n`
|
|
760
|
+
+ `- Review: ${t.review !== false ? '메인 검수 필요' : '생략'}\n`
|
|
760
761
|
+ `- Status: ${t.status || 'active'}\n`;
|
|
761
762
|
}).join('');
|
|
762
763
|
return preamble + body;
|
|
@@ -773,14 +774,21 @@ function _composeTeamPlan(team, task) {
|
|
|
773
774
|
const prompt = `${effTask}${personaTag}`;
|
|
774
775
|
return { member: m, personas, dispatchPrompt: prompt, suggestedCommand: `leerness agents dispatch "${prompt}" --to ${m}` };
|
|
775
776
|
});
|
|
776
|
-
|
|
777
|
+
// 1.9.414 (UR-0119/0120): 메인 에이전트 검수 단계 — sub-agent 분배 후 메인이 산출물을 교차검증(기본 on, team.review===false 시 생략).
|
|
778
|
+
const review = t.review !== false;
|
|
779
|
+
const reviewStep = review ? {
|
|
780
|
+
type: 'review',
|
|
781
|
+
note: '메인 에이전트가 각 sub-agent 산출물을 독립 검증(교차 검수). verify-claim/contract verify/review 사용.',
|
|
782
|
+
suggestedCommand: 'leerness verify-claim <T-ID> --run-tests --strict-claims · leerness review <file> --persona ' + (personas.join(',') || 'security'),
|
|
783
|
+
} : null;
|
|
784
|
+
return { teamId: t.id || null, name: t.name || '', task: effTask, schedule: t.schedule || 'manual', memberCount: members.length, review, steps, reviewStep };
|
|
777
785
|
}
|
|
778
786
|
|
|
779
787
|
// 1.9.373 (UR-0073 Phase C): 비-manual·active 팀의 handoff 스케줄 알림 라인 (순수). 실행 트리거 아님 — 미리보기 안내만.
|
|
780
788
|
function _teamHandoffReminders(teams) {
|
|
781
789
|
return (teams || [])
|
|
782
790
|
.filter(t => t && t.schedule && t.schedule !== 'manual' && (t.status || 'active') === 'active' && t.id)
|
|
783
|
-
.map(t => `🤝 ${t.id} (${t.schedule})${Array.isArray(t.members) && t.members.length ? ' · ' + t.members.length + '명' : ''} — 미리보기: leerness team preview ${t.id}`);
|
|
791
|
+
.map(t => `🤝 ${t.id} (${t.schedule})${Array.isArray(t.members) && t.members.length ? ' · ' + t.members.length + '명' : ''}${t.review !== false ? ' · 검수필요' : ''} — 미리보기: leerness team preview ${t.id}`);
|
|
784
792
|
}
|
|
785
793
|
|
|
786
794
|
// 1.9.374 (UR-0074): 릴리스 케이던스 평가 (순수) — releases/day → 수준 + 권장. 외부리뷰 "릴리스 빈도 과다" 가시화.
|
package/lib/team.js
CHANGED
|
@@ -38,12 +38,13 @@ function teamCmd(root, sub, id, opts = {}, deps = {}) {
|
|
|
38
38
|
members: splitCsv(arg('--members', null)),
|
|
39
39
|
schedule: validSched.has(sched) ? sched : 'manual',
|
|
40
40
|
deployCommand: arg('--deploy', '') === true ? '' : arg('--deploy', ''), // 1.9.376 (Phase D): 사용자 설정 배포 명령 (실행은 게이트)
|
|
41
|
+
review: !has('--no-review'), // 1.9.414 (UR-0119/0120): 메인 에이전트 검수 요구(기본 on, --no-review 로 끔). preview/handoff 가 검수 단계 표시.
|
|
41
42
|
status: 'active',
|
|
42
43
|
createdAt: now()
|
|
43
44
|
};
|
|
44
45
|
teams.push(team);
|
|
45
46
|
_saveTeams(root, teams);
|
|
46
|
-
ok(`team 정의: ${teamId} (personas:${team.personas.length} members:${team.members.length} schedule:${team.schedule})`);
|
|
47
|
+
ok(`team 정의: ${teamId} (personas:${team.personas.length} members:${team.members.length} schedule:${team.schedule} review:${team.review ? 'on' : 'off'})`);
|
|
47
48
|
log(` ⓘ 정의 전용 — 자동 실행 없음. 목록: leerness team list`);
|
|
48
49
|
return;
|
|
49
50
|
}
|
|
@@ -58,6 +59,7 @@ function teamCmd(root, sub, id, opts = {}, deps = {}) {
|
|
|
58
59
|
log(` members: ${(t.members || []).join(', ') || '-'}`);
|
|
59
60
|
log(` schedule: ${t.schedule || 'manual'} · status: ${t.status || 'active'}`);
|
|
60
61
|
log(` deploy: ${t.deployCommand || '-'}`);
|
|
62
|
+
log(` review: ${t.review !== false ? '메인 검수 필요' : '생략'}`);
|
|
61
63
|
return;
|
|
62
64
|
}
|
|
63
65
|
if (sub === 'remove') {
|
|
@@ -83,6 +85,12 @@ function teamCmd(root, sub, id, opts = {}, deps = {}) {
|
|
|
83
85
|
log(` • ${s.member}${s.personas.length ? ' [' + s.personas.join(',') + ']' : ''}`);
|
|
84
86
|
log(` ↳ ${s.suggestedCommand}`);
|
|
85
87
|
}
|
|
88
|
+
// 1.9.414 (UR-0119/0120): 분배 후 메인 검수 단계 표시
|
|
89
|
+
if (plan.reviewStep) {
|
|
90
|
+
log(` ✔ 메인 검수 (필수)`);
|
|
91
|
+
log(` ↳ ${plan.reviewStep.note}`);
|
|
92
|
+
log(` ↳ ${plan.reviewStep.suggestedCommand}`);
|
|
93
|
+
}
|
|
86
94
|
log(`\n ⓘ dry-run — 실제 dispatch/배포 없음. 위 명령을 검토 후 직접 실행하거나, 향후 Phase C(스케줄)/D(배포)에서 게이트 적용.`);
|
|
87
95
|
return;
|
|
88
96
|
}
|
package/package.json
CHANGED
package/scripts/e2e.js
CHANGED
|
@@ -5898,5 +5898,64 @@ total++;
|
|
|
5898
5898
|
if (!ok) failed++;
|
|
5899
5899
|
}
|
|
5900
5900
|
|
|
5901
|
+
// 1.9.414 회귀 (UR-0119/0120): team review(메인 검수) — preview 가 분배 후 메인 검수 단계 표시, --no-review 시 생략
|
|
5902
|
+
total++;
|
|
5903
|
+
{
|
|
5904
|
+
let ok = false;
|
|
5905
|
+
try {
|
|
5906
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-teamreview-'));
|
|
5907
|
+
cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes', '--language', 'ko'], { encoding: 'utf8', timeout: 30000 });
|
|
5908
|
+
cp.spawnSync(process.execPath, [CLI, 'team', 'add', 'rt', '--members', 'claude,codex', '--schedule', 'every-session', '--path', d], { encoding: 'utf8', timeout: 15000 });
|
|
5909
|
+
const pv = cp.spawnSync(process.execPath, [CLI, 'team', 'preview', 'rt', '--task', '점검', '--path', d], { encoding: 'utf8', timeout: 15000 });
|
|
5910
|
+
const reviewShown = /메인 검수/.test(pv.stdout || '') && /verify-claim/.test(pv.stdout || '');
|
|
5911
|
+
const dispatchShown = /agents dispatch/.test(pv.stdout || '');
|
|
5912
|
+
cp.spawnSync(process.execPath, [CLI, 'team', 'add', 'nr', '--members', 'claude', '--no-review', '--path', d], { encoding: 'utf8', timeout: 15000 });
|
|
5913
|
+
const pv2 = cp.spawnSync(process.execPath, [CLI, 'team', 'preview', 'nr', '--path', d], { encoding: 'utf8', timeout: 15000 });
|
|
5914
|
+
const noReviewOk = !/메인 검수/.test(pv2.stdout || '');
|
|
5915
|
+
// --json 에 review/reviewStep 반영
|
|
5916
|
+
const pj = JSON.parse(cp.spawnSync(process.execPath, [CLI, 'team', 'preview', 'rt', '--path', d, '--json'], { encoding: 'utf8', timeout: 15000 }).stdout);
|
|
5917
|
+
const jsonOk = pj.review === true && !!pj.reviewStep;
|
|
5918
|
+
fs.rmSync(d, { recursive: true, force: true });
|
|
5919
|
+
ok = reviewShown && dispatchShown && noReviewOk && jsonOk;
|
|
5920
|
+
} catch {}
|
|
5921
|
+
console.log(ok ? '✓ B(1.9.414) 9라운드: team review(분배→메인 검수 단계 표시, --no-review 생략, --json reviewStep) (UR-0119/0120)' : '✗ team review 실패');
|
|
5922
|
+
if (!ok) failed++;
|
|
5923
|
+
}
|
|
5924
|
+
|
|
5925
|
+
// 1.9.415 회귀 (9th 외부평가 Codex P1/Opus P2, UR-0121): handoff 보안 헤드라인 정직화 + scan/encoding --json + contract --json exit
|
|
5926
|
+
total++;
|
|
5927
|
+
{
|
|
5928
|
+
let ok = false;
|
|
5929
|
+
try {
|
|
5930
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-honesty-'));
|
|
5931
|
+
cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes', '--language', 'ko'], { encoding: 'utf8', timeout: 30000 });
|
|
5932
|
+
fs.writeFileSync(path.join(d, 'config.js'), 'module.exports={apiKey:"sk-test-1234567890abcdefghijklmnopqrstuvwxyz",githubToken:"ghp_1234567890abcdefghijklmnopqrstuvwxyzABCD"};');
|
|
5933
|
+
fs.writeFileSync(path.join(d, 'bad.txt'), Buffer.from([0xff, 0xfe, 0x20, 0x80]));
|
|
5934
|
+
// (1) handoff 가 시크릿을 헤드라인에 정직 반영(보안 OK 아님)
|
|
5935
|
+
const ho = cp.spawnSync(process.execPath, [CLI, 'handoff', d], { encoding: 'utf8', timeout: 20000 }).stdout || '';
|
|
5936
|
+
const honestSecret = /시크릿\s*\d+건/.test(ho) && !/보안 OK/.test(ho);
|
|
5937
|
+
// (2) scan secrets --json 구조화 + exit 1
|
|
5938
|
+
const sj = cp.spawnSync(process.execPath, [CLI, 'scan', 'secrets', d, '--json'], { encoding: 'utf8', timeout: 15000 });
|
|
5939
|
+
let scanOk = false; try { const j = JSON.parse(sj.stdout); scanOk = j.ok === false && j.count >= 1 && sj.status === 1; } catch {}
|
|
5940
|
+
// (3) encoding check --json 구조화
|
|
5941
|
+
const ej = cp.spawnSync(process.execPath, [CLI, 'encoding', 'check', d, '--json'], { encoding: 'utf8', timeout: 15000 });
|
|
5942
|
+
let encOk = false; try { const j = JSON.parse(ej.stdout); encOk = j.ok === false && j.count >= 1; } catch {}
|
|
5943
|
+
// (4) contract verify --json 불일치 exit 1
|
|
5944
|
+
fs.writeFileSync(path.join(d, 's.md'), '# S\n- loginUser(id)\n- logoutUser(id)\n');
|
|
5945
|
+
fs.writeFileSync(path.join(d, 'i.js'), 'function loginUser(i){return i}\nmodule.exports={loginUser};\n');
|
|
5946
|
+
const cj = cp.spawnSync(process.execPath, [CLI, 'contract', 'verify', path.join(d, 's.md'), path.join(d, 'i.js'), '--json'], { encoding: 'utf8', timeout: 15000 });
|
|
5947
|
+
let contractOk = false; try { const j = JSON.parse(cj.stdout); contractOk = j.ok === false && cj.status === 1; } catch {}
|
|
5948
|
+
// (5) 정직성 회귀: 클린 워크스페이스는 시크릿 경고 없음
|
|
5949
|
+
const dc = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-clean-'));
|
|
5950
|
+
cp.spawnSync(process.execPath, [CLI, 'init', dc, '--yes', '--language', 'ko'], { encoding: 'utf8', timeout: 30000 });
|
|
5951
|
+
const hc = cp.spawnSync(process.execPath, [CLI, 'handoff', dc], { encoding: 'utf8', timeout: 20000 }).stdout || '';
|
|
5952
|
+
const cleanOk = !/시크릿\s*\d+건/.test(hc);
|
|
5953
|
+
fs.rmSync(d, { recursive: true, force: true }); fs.rmSync(dc, { recursive: true, force: true });
|
|
5954
|
+
ok = honestSecret && scanOk && encOk && contractOk && cleanOk;
|
|
5955
|
+
} catch {}
|
|
5956
|
+
console.log(ok ? '✓ B(1.9.415) 9th외부평가: handoff 보안 헤드라인 정직화 + scan/encoding --json + contract --json exit (UR-0121)' : '✗ honesty/--json 실패');
|
|
5957
|
+
if (!ok) failed++;
|
|
5958
|
+
}
|
|
5959
|
+
|
|
5901
5960
|
console.log(`\nE2E result: ${total - failed}/${total} passed · ${((Date.now() - _e2eStart) / 1000).toFixed(0)}s`);
|
|
5902
5961
|
if (failed > 0) process.exit(1);
|