leerness 1.30.0 → 1.32.0
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 +199 -0
- package/README.md +4 -4
- package/bin/leerness.js +314 -103
- package/lib/audit.js +17 -1
- package/lib/catalogs.js +23 -22
- package/lib/pure-utils.js +15 -4
- package/package.json +1 -1
- package/scripts/e2e.js +244 -7
package/lib/audit.js
CHANGED
|
@@ -8,7 +8,7 @@ const { log, ok, warn, fail, failJson, today, now, absRoot, exists, read, readBu
|
|
|
8
8
|
const { SECRET_PATTERNS } = require('./catalogs');
|
|
9
9
|
|
|
10
10
|
function audit(root, opts = {}, deps = {}) {
|
|
11
|
-
const { VERSION, arg, has, planPath, readProgressRows, currentStatePath, handoffPath, envDiff, _readFeatureGraph, _matchAPISkills, _listAPISkills } = deps;
|
|
11
|
+
const { VERSION, arg, has, planPath, readProgressRows, currentStatePath, handoffPath, envDiff, _readFeatureGraph, _matchAPISkills, _listAPISkills, _collectSecretFindings } = deps;
|
|
12
12
|
root = absRoot(root);
|
|
13
13
|
let warnings = 0, failures = 0;
|
|
14
14
|
// 1.9.35 개선 #5: --fix 옵션 — 자동 수정 가능한 항목 적용
|
|
@@ -192,6 +192,22 @@ function audit(root, opts = {}, deps = {}) {
|
|
|
192
192
|
}
|
|
193
193
|
} catch {}
|
|
194
194
|
}
|
|
195
|
+
// 1.30.1 (14th 외부리뷰 F1): 커밋된 시크릿(_collectSecretFindings.committed)을 failure 로 승격 — scan secrets 와 일관.
|
|
196
|
+
// 기존엔 .gitignore 패턴/.env 동기화만 검사해 소스에 노출된 실 시크릿(AWS/GitHub 등)을 통과시키고 healthy:true 를 반환하던 정직성 갭
|
|
197
|
+
// (audit 기반 CI 게이트가 노출 시크릿을 통과). gitignored 보관 시크릿은 _collectSecretFindings 가 committed 에서 제외(FP 0). 끄기: --no-secret-scan.
|
|
198
|
+
if (!has('--no-secret-scan') && typeof _collectSecretFindings === 'function') {
|
|
199
|
+
try {
|
|
200
|
+
const { committed } = _collectSecretFindings(root);
|
|
201
|
+
if (committed && committed.length) {
|
|
202
|
+
failures++;
|
|
203
|
+
fail(`커밋된 시크릿 ${committed.length}건 발견 (소스 노출) — leerness scan secrets 로 상세 확인`);
|
|
204
|
+
committed.slice(0, 4).forEach(f => log(` ${f.file}:${f.line} ${f.name}`));
|
|
205
|
+
_finding('committed_secret', 'fail', '커밋된 시크릿 발견 (소스 노출)', { count: committed.length, sample: committed.slice(0, 10).map(f => ({ file: f.file, line: f.line, name: f.name })) });
|
|
206
|
+
} else {
|
|
207
|
+
ok('커밋된 시크릿 없음 (소스 스캔, 1.30.1)');
|
|
208
|
+
}
|
|
209
|
+
} catch {}
|
|
210
|
+
}
|
|
195
211
|
// 1.9.71: .env / .env.example 동기화 감사 (--no-env-check로 끄기)
|
|
196
212
|
if (!has('--no-env-check')) {
|
|
197
213
|
try {
|
package/lib/catalogs.js
CHANGED
|
@@ -3,22 +3,23 @@
|
|
|
3
3
|
// 런타임 변형 없음 — capabilitiesCmd/adapterCmd/_reuseDetect/reuseCheckCmd 소비처 모두 읽기 전용.
|
|
4
4
|
'use strict';
|
|
5
5
|
|
|
6
|
+
// 1.31.3 (UR-0010): desc/optOut/note 는 ko 기본 + *En 영어 변형 (capabilities --language en 렌더가 *En||원본 사용).
|
|
6
7
|
const CAPABILITY_SURFACE = {
|
|
7
|
-
filesystem: { risk: 'low', desc: '.harness/ 메타파일 생성·갱신, 변경 전 .harness/archive/ 자동 백업. 소스코드는 직접 수정 안 함.', optOut: '핵심 동작 (백업으로 보호)' },
|
|
8
|
-
network: { risk: 'low', desc: 'npm 최신 버전 비교(update --check)만. 그 외 외부 URL 자동 fetch 안 함.', optOut: 'LEERNESS_OFFLINE=1' },
|
|
9
|
-
childProcess: { risk: 'medium', desc: 'git(명시 명령 시 status/commit/push), npm test(verify-code), 외부 CLI --version 감지. 셸 spawn.', optOut: 'verify 계열 한정 · 외부 CLI 는 opt-in' },
|
|
10
|
-
externalAgents: { risk: 'medium', desc: 'agents dispatch/multi — 외부 AI CLI(claude/codex/agy/grok/copilot) 호출. 기본은 명령 텍스트만 생성, multi --execute 시 실제 spawn.', optOut: 'LEERNESS_ENABLE_* 미설정 시 비활성 (기본 off)' },
|
|
11
|
-
automationBridges: { risk: 'high', desc: 'web(playwright)/pc(robotjs)/lsp(typescript) 브리지 — opt-in 의존성. pc 는 마우스/키보드 제어(full 권한).', optOut: '의존성 미설치 시 비활성 (기본 off, 명시 설치 필요)' },
|
|
12
|
-
claudeHook: { risk: 'low', desc: 'init 시 .claude/settings.local.json 에 SessionStart hook(update --check) 설치.', optOut: 'leerness init . --no-auto-update' }
|
|
8
|
+
filesystem: { risk: 'low', desc: '.harness/ 메타파일 생성·갱신, 변경 전 .harness/archive/ 자동 백업. 소스코드는 직접 수정 안 함.', descEn: 'Create/update .harness/ metadata files, auto-backup to .harness/archive/ before changes. Does not modify source code directly.', optOut: '핵심 동작 (백업으로 보호)', optOutEn: 'core behavior (protected by backups)' },
|
|
9
|
+
network: { risk: 'low', desc: 'npm 최신 버전 비교(update --check)만. 그 외 외부 URL 자동 fetch 안 함.', descEn: 'Only npm latest-version comparison (update --check). No other external URL is fetched automatically.', optOut: 'LEERNESS_OFFLINE=1', optOutEn: 'LEERNESS_OFFLINE=1' },
|
|
10
|
+
childProcess: { risk: 'medium', desc: 'git(명시 명령 시 status/commit/push), npm test(verify-code), 외부 CLI --version 감지. 셸 spawn.', descEn: 'git (status/commit/push on explicit commands), npm test (verify-code), external CLI --version detection. Shell spawn.', optOut: 'verify 계열 한정 · 외부 CLI 는 opt-in', optOutEn: 'limited to verify family · external CLIs are opt-in' },
|
|
11
|
+
externalAgents: { risk: 'medium', desc: 'agents dispatch/multi — 외부 AI CLI(claude/codex/agy/grok/copilot) 호출. 기본은 명령 텍스트만 생성, multi --execute 시 실제 spawn.', descEn: 'agents dispatch/multi — calls external AI CLIs (claude/codex/agy/grok/copilot). By default only generates command text; spawns for real on multi --execute.', optOut: 'LEERNESS_ENABLE_* 미설정 시 비활성 (기본 off)', optOutEn: 'disabled unless LEERNESS_ENABLE_* is set (off by default)' },
|
|
12
|
+
automationBridges: { risk: 'high', desc: 'web(playwright)/pc(robotjs)/lsp(typescript) 브리지 — opt-in 의존성. pc 는 마우스/키보드 제어(full 권한).', descEn: 'web(playwright)/pc(robotjs)/lsp(typescript) bridges — opt-in dependencies. pc controls mouse/keyboard (full access).', optOut: '의존성 미설치 시 비활성 (기본 off, 명시 설치 필요)', optOutEn: 'disabled when the dependency is not installed (off by default, explicit install required)' },
|
|
13
|
+
claudeHook: { risk: 'low', desc: 'init 시 .claude/settings.local.json 에 SessionStart hook(update --check) 설치.', descEn: 'On init, installs a SessionStart hook (update --check) into .claude/settings.local.json.', optOut: 'leerness init . --no-auto-update', optOutEn: 'leerness init . --no-auto-update' }
|
|
13
14
|
};
|
|
14
15
|
const POWERFUL_COMMANDS = [
|
|
15
|
-
{ cmd: 'init', note: '.harness/ 50+ 파일 + .claude hook 생성 (변경 전 백업)' },
|
|
16
|
-
{ cmd: 'update --yes', note: '자동 마이그레이션 — 메타파일 갱신' },
|
|
17
|
-
{ cmd: 'agents multi --execute', note: '외부 AI CLI 실제 spawn (병렬 실행)' },
|
|
18
|
-
{ cmd: 'release publish / sync-main', note: 'git push + npm publish + GitHub release' },
|
|
19
|
-
{ cmd: 'pc <click|type|...>', note: '마우스/키보드 제어 (robotjs, full 권한)' },
|
|
20
|
-
{ cmd: 'web <...>', note: '헤드리스 브라우저 자동화 (playwright)' },
|
|
21
|
-
{ cmd: 'setup-agents', note: '외부 CLI 활성화 + 자동 설치 시도' }
|
|
16
|
+
{ cmd: 'init', note: '.harness/ 50+ 파일 + .claude hook 생성 (변경 전 백업)', noteEn: 'creates .harness/ 50+ files + .claude hook (backup before changes)' },
|
|
17
|
+
{ cmd: 'update --yes', note: '자동 마이그레이션 — 메타파일 갱신', noteEn: 'auto migration — updates metadata files' },
|
|
18
|
+
{ cmd: 'agents multi --execute', note: '외부 AI CLI 실제 spawn (병렬 실행)', noteEn: 'actually spawns external AI CLIs (parallel execution)' },
|
|
19
|
+
{ cmd: 'release publish / sync-main', note: 'git push + npm publish + GitHub release', noteEn: 'git push + npm publish + GitHub release' },
|
|
20
|
+
{ cmd: 'pc <click|type|...>', note: '마우스/키보드 제어 (robotjs, full 권한)', noteEn: 'mouse/keyboard control (robotjs, full access)' },
|
|
21
|
+
{ cmd: 'web <...>', note: '헤드리스 브라우저 자동화 (playwright)', noteEn: 'headless browser automation (playwright)' },
|
|
22
|
+
{ cmd: 'setup-agents', note: '외부 CLI 활성화 + 자동 설치 시도', noteEn: 'enables external CLIs + attempts auto-install' }
|
|
22
23
|
];
|
|
23
24
|
const ADAPTERS = {
|
|
24
25
|
claude: { label: 'Anthropic Claude Code', keys: ['CLAUDE.md', '.claude/commands/handoff.md', '.claude/commands/session-close.md', '.claude/commands/audit.md', '.claude/commands/lazy-detect.md', '.claude/commands/update.md', '.claude/skills/leerness.md'], mcp: true },
|
|
@@ -66,8 +67,8 @@ const _DEFAULT_PLATFORM_CONSTRAINTS = {
|
|
|
66
67
|
docs: 'https://stripe.com/docs/rate-limits',
|
|
67
68
|
constraints: [
|
|
68
69
|
{ kind: 'rate-limit', detail: 'read: 100 req/s, write: 100 req/s (live mode), test mode: 25 req/s' },
|
|
69
|
-
{ kind: 'idempotency', detail: 'Idempotency-Key 헤더 24h 유지 — 중복 결제 방지 필수' },
|
|
70
|
-
{ kind: 'webhook', detail: 'webhook 서명 검증 필수 (Stripe-Signature header + endpoint secret)' }
|
|
70
|
+
{ kind: 'idempotency', detail: 'Idempotency-Key 헤더 24h 유지 — 중복 결제 방지 필수', detailEn: 'keep Idempotency-Key header for 24h — required to prevent duplicate charges' },
|
|
71
|
+
{ kind: 'webhook', detail: 'webhook 서명 검증 필수 (Stripe-Signature header + endpoint secret)', detailEn: 'verify the webhook signature (Stripe-Signature header + endpoint secret) — required' }
|
|
71
72
|
]
|
|
72
73
|
},
|
|
73
74
|
openai: {
|
|
@@ -75,8 +76,8 @@ const _DEFAULT_PLATFORM_CONSTRAINTS = {
|
|
|
75
76
|
docs: 'https://platform.openai.com/docs/guides/rate-limits',
|
|
76
77
|
constraints: [
|
|
77
78
|
{ kind: 'rate-limit', detail: 'tier-based: Free 3 RPM / Tier 1 500 RPM / Tier 5 10,000 RPM' },
|
|
78
|
-
{ kind: 'token-limit', detail: 'TPM (tokens/min) 별도 — 큰 입력 시 RPM 도달 전 차단 가능' },
|
|
79
|
-
{ kind: 'cost', detail: 'gpt-4: $30/$60 per 1M input/output tokens — 대량 호출 전 비용 추정 필수' }
|
|
79
|
+
{ kind: 'token-limit', detail: 'TPM (tokens/min) 별도 — 큰 입력 시 RPM 도달 전 차단 가능', detailEn: 'TPM (tokens/min) is separate — large inputs can hit it before RPM' },
|
|
80
|
+
{ kind: 'cost', detail: 'gpt-4: $30/$60 per 1M input/output tokens — 대량 호출 전 비용 추정 필수', detailEn: 'gpt-4: $30/$60 per 1M input/output tokens — estimate cost before high volume' }
|
|
80
81
|
]
|
|
81
82
|
},
|
|
82
83
|
anthropic: {
|
|
@@ -84,7 +85,7 @@ const _DEFAULT_PLATFORM_CONSTRAINTS = {
|
|
|
84
85
|
docs: 'https://docs.anthropic.com/claude/reference/rate-limits',
|
|
85
86
|
constraints: [
|
|
86
87
|
{ kind: 'rate-limit', detail: 'tier-based: Free 5 RPM / Tier 1 50 RPM / Tier 4 4,000 RPM' },
|
|
87
|
-
{ kind: 'context-window', detail: 'claude-sonnet 200K context, claude-opus 200K, 1M tier 별도' },
|
|
88
|
+
{ kind: 'context-window', detail: 'claude-sonnet 200K context, claude-opus 200K, 1M tier 별도', detailEn: 'claude-sonnet 200K context, claude-opus 200K, 1M tier separate' },
|
|
88
89
|
{ kind: 'cost', detail: 'sonnet: $3/$15 per 1M tokens (1M context tier 2x)' }
|
|
89
90
|
]
|
|
90
91
|
},
|
|
@@ -94,15 +95,15 @@ const _DEFAULT_PLATFORM_CONSTRAINTS = {
|
|
|
94
95
|
constraints: [
|
|
95
96
|
{ kind: 'rate-limit', detail: 'authenticated: 5,000 req/hr, unauthenticated: 60 req/hr' },
|
|
96
97
|
{ kind: 'rate-limit', detail: 'search API: 30 req/min (authenticated)' },
|
|
97
|
-
{ kind: 'secondary', detail: 'secondary rate limit — concurrent + content creation 별도 가드' }
|
|
98
|
+
{ kind: 'secondary', detail: 'secondary rate limit — concurrent + content creation 별도 가드', detailEn: 'secondary rate limit — guard concurrent + content-creation separately' }
|
|
98
99
|
]
|
|
99
100
|
},
|
|
100
101
|
discord: {
|
|
101
102
|
aliases: ['discord', 'discord api', 'discord bot'],
|
|
102
103
|
docs: 'https://discord.com/developers/docs/topics/rate-limits',
|
|
103
104
|
constraints: [
|
|
104
|
-
{ kind: 'rate-limit', detail: 'global: 50 req/s, per-route 별도' },
|
|
105
|
-
{ kind: 'invalid', detail: '10,000 invalid req/10min → 1h ban 위험' }
|
|
105
|
+
{ kind: 'rate-limit', detail: 'global: 50 req/s, per-route 별도', detailEn: 'global: 50 req/s, per-route separate' },
|
|
106
|
+
{ kind: 'invalid', detail: '10,000 invalid req/10min → 1h ban 위험', detailEn: '10,000 invalid req/10min → risk of a 1h ban' }
|
|
106
107
|
]
|
|
107
108
|
},
|
|
108
109
|
twitter: {
|
|
@@ -110,7 +111,7 @@ const _DEFAULT_PLATFORM_CONSTRAINTS = {
|
|
|
110
111
|
docs: 'https://developer.twitter.com/en/docs/twitter-api/rate-limits',
|
|
111
112
|
constraints: [
|
|
112
113
|
{ kind: 'rate-limit', detail: 'tier-based: Free 1,500 posts/month, Basic 50,000 posts/month' },
|
|
113
|
-
{ kind: 'auth', detail: 'OAuth 2.0 PKCE 필수 (user context), App-only는 별도 endpoint' }
|
|
114
|
+
{ kind: 'auth', detail: 'OAuth 2.0 PKCE 필수 (user context), App-only는 별도 endpoint', detailEn: 'OAuth 2.0 PKCE required (user context); App-only uses a separate endpoint' }
|
|
114
115
|
]
|
|
115
116
|
}
|
|
116
117
|
}
|
package/lib/pure-utils.js
CHANGED
|
@@ -462,7 +462,8 @@ function _parseSkillMd(text) {
|
|
|
462
462
|
}
|
|
463
463
|
|
|
464
464
|
// 1.9.333 (UR-0025 심층): 순수 플랫폼 제약 매칭 — catalog + 텍스트 → 매칭 플랫폼/제약/제안 (fs 의존 0, catalog 주입).
|
|
465
|
-
|
|
465
|
+
// 1.31.2 (UR-0010): optional lang ('en') → 영어 suggestion. 기본 'ko' (무회귀, selftest 2-arg 호출 보존).
|
|
466
|
+
function _matchConstraints(catalog, text, lang) {
|
|
466
467
|
if (!text || typeof text !== 'string' || !catalog || !catalog.platforms) return { matched: [], suggestions: [] };
|
|
467
468
|
const lower = text.toLowerCase();
|
|
468
469
|
const matched = [];
|
|
@@ -474,7 +475,9 @@ function _matchConstraints(catalog, text) {
|
|
|
474
475
|
const suggestions = [];
|
|
475
476
|
const generic = /\bapi\b|연동|integration|호출|rate|limit|quota|webhook/i.test(text);
|
|
476
477
|
if (generic && matched.length === 0) {
|
|
477
|
-
suggestions.push(
|
|
478
|
+
suggestions.push(lang === 'en'
|
|
479
|
+
? 'generic API-integration keywords detected — run leerness constraints list to review the pre-registered platform catalog'
|
|
480
|
+
: '일반적 API 연동 키워드 감지 — leerness constraints list 로 사전 등록된 플랫폼 catalog 확인 권장');
|
|
478
481
|
}
|
|
479
482
|
return { matched, suggestions, totalPlatforms: Object.keys(catalog.platforms).length };
|
|
480
483
|
}
|
|
@@ -807,10 +810,18 @@ function _composeTeamPlan(team, task) {
|
|
|
807
810
|
}
|
|
808
811
|
|
|
809
812
|
// 1.9.373 (UR-0073 Phase C): 비-manual·active 팀의 handoff 스케줄 알림 라인 (순수). 실행 트리거 아님 — 미리보기 안내만.
|
|
810
|
-
|
|
813
|
+
// 1.31.3 (UR-0010): optional lang ('en') → 영어 라벨. 기본 'ko' (무회귀, 1-arg 호출 보존).
|
|
814
|
+
function _teamHandoffReminders(teams, lang) {
|
|
815
|
+
const en = lang === 'en';
|
|
811
816
|
return (teams || [])
|
|
812
817
|
.filter(t => t && t.schedule && t.schedule !== 'manual' && (t.status || 'active') === 'active' && t.id)
|
|
813
|
-
.map(t =>
|
|
818
|
+
.map(t => {
|
|
819
|
+
const n = Array.isArray(t.members) ? t.members.length : 0;
|
|
820
|
+
const memberPart = n ? (en ? ` · ${n} member${n === 1 ? '' : 's'}` : ` · ${n}명`) : '';
|
|
821
|
+
const reviewPart = t.review !== false ? (en ? ' · review needed' : ' · 검수필요') : '';
|
|
822
|
+
const preview = en ? 'preview' : '미리보기';
|
|
823
|
+
return `🤝 ${t.id} (${t.schedule})${memberPart}${reviewPart} — ${preview}: leerness team preview ${t.id}`;
|
|
824
|
+
});
|
|
814
825
|
}
|
|
815
826
|
|
|
816
827
|
// 1.9.374 (UR-0074): 릴리스 케이던스 평가 (순수) — releases/day → 수준 + 권장. 외부리뷰 "릴리스 빈도 과다" 가시화.
|
package/package.json
CHANGED
package/scripts/e2e.js
CHANGED
|
@@ -830,13 +830,14 @@ total++;
|
|
|
830
830
|
{
|
|
831
831
|
// agents dispatch — 활성 미충족 시 거부
|
|
832
832
|
const env = { ...process.env, LEERNESS_ENABLE_CODEX: '0' };
|
|
833
|
-
|
|
833
|
+
// 1.30.2: timeout 10s→30s flake 하드닝(1.9.375 계열) — 전체 e2e 부하(수백 spawn) 하에서 짧은 타임아웃이 간헐 빈-stdout→오판.
|
|
834
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'agents', 'dispatch', 'test task', '--to', 'codex'], { encoding: 'utf8', timeout: 30000, env });
|
|
834
835
|
const okBlocked = r.status !== 0 && /비활성|disabled|not-installed/i.test(r.stdout);
|
|
835
836
|
// --to 누락 거부
|
|
836
|
-
const r2 = cp.spawnSync(process.execPath, [CLI, 'agents', 'dispatch', 'test'], { encoding: 'utf8', timeout:
|
|
837
|
+
const r2 = cp.spawnSync(process.execPath, [CLI, 'agents', 'dispatch', 'test'], { encoding: 'utf8', timeout: 30000 });
|
|
837
838
|
const okNoTarget = r2.status !== 0 && /--to.*필요/.test(r2.stdout + r2.stderr);
|
|
838
839
|
// 알 수 없는 agent 거부
|
|
839
|
-
const r3 = cp.spawnSync(process.execPath, [CLI, 'agents', 'dispatch', 'test', '--to', 'jedi'], { encoding: 'utf8', timeout:
|
|
840
|
+
const r3 = cp.spawnSync(process.execPath, [CLI, 'agents', 'dispatch', 'test', '--to', 'jedi'], { encoding: 'utf8', timeout: 30000 });
|
|
840
841
|
const okBadAgent = r3.status !== 0 && /알 수 없는 agent/.test(r3.stdout + r3.stderr);
|
|
841
842
|
const ok = okBlocked && okNoTarget && okBadAgent;
|
|
842
843
|
console.log(ok ? '✓ B(1.9.30) agents dispatch: env=0/--to 누락/잘못된 agent 모두 거부' : `✗ dispatch 실패 (block=${okBlocked} noT=${okNoTarget} bad=${okBadAgent})`);
|
|
@@ -882,7 +883,7 @@ total++;
|
|
|
882
883
|
total++;
|
|
883
884
|
{
|
|
884
885
|
// --version --banner: LEERNESS ASCII + 신규 슬로건 (1.9.144+ "AI 에이전트 검수·기억·드리프트 방지 하네스")
|
|
885
|
-
const r = cp.spawnSync(process.execPath, [CLI, '--version', '--banner'], { encoding: 'utf8', timeout:
|
|
886
|
+
const r = cp.spawnSync(process.execPath, [CLI, '--version', '--banner'], { encoding: 'utf8', timeout: 30000, env: { ...process.env, TERM: 'dumb' } });
|
|
886
887
|
const ok = r.status === 0
|
|
887
888
|
&& /╔═+╗/.test(r.stdout)
|
|
888
889
|
&& /███████╗/.test(r.stdout)
|
|
@@ -6041,6 +6042,229 @@ total++;
|
|
|
6041
6042
|
if (!ok) failed++;
|
|
6042
6043
|
}
|
|
6043
6044
|
|
|
6045
|
+
// 1.30.1 회귀 (14th 외부리뷰 F1+F2): audit/handoff 보안요약이 커밋된 시크릿을 정직하게 노출.
|
|
6046
|
+
// F1: audit 가 _collectSecretFindings 콘텐츠 스캔을 돌려 committed 시크릿을 failure 로 승격(scan secrets 와 일관) — gitignored 는 무영향(FP 0).
|
|
6047
|
+
// F2: handoff 🔒 보안요약 섹션이 .env 없어도 committed 시크릿을 노출(envExists 단독 게이팅 제거).
|
|
6048
|
+
total++;
|
|
6049
|
+
{
|
|
6050
|
+
let ok = false;
|
|
6051
|
+
try {
|
|
6052
|
+
const H = /[가-힣]/;
|
|
6053
|
+
// (F1) un-gitignored .env + 실 시크릿 → audit healthy:false exit1
|
|
6054
|
+
const d1 = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-f1bad-'));
|
|
6055
|
+
cp.spawnSync(process.execPath, [CLI, 'init', d1, '--yes', '--language', 'ko'], { encoding: 'utf8', timeout: 30000 });
|
|
6056
|
+
fs.writeFileSync(path.join(d1, '.gitignore'), 'node_modules/\n');
|
|
6057
|
+
fs.writeFileSync(path.join(d1, '.env'), 'AWS=AKIAJQXMP7RZ2KL9WXYZ\nGH=ghp_aZ9bY8cX7dW6eV5fU4gT3hS2iR1jQ0kP9oN8\n');
|
|
6058
|
+
const a1 = cp.spawnSync(process.execPath, [CLI, 'audit', d1, '--json'], { encoding: 'utf8', timeout: 20000 });
|
|
6059
|
+
let f1bad = false; try { const j = JSON.parse(a1.stdout); f1bad = j.healthy === false && a1.status === 1 && j.findings.some(x => x.kind === 'committed_secret'); } catch {}
|
|
6060
|
+
// (F1-noFP) gitignored .env + 시크릿 → audit healthy:true (no false-positive)
|
|
6061
|
+
const d2 = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-f1ok-'));
|
|
6062
|
+
cp.spawnSync(process.execPath, [CLI, 'init', d2, '--yes', '--language', 'ko'], { encoding: 'utf8', timeout: 30000 });
|
|
6063
|
+
fs.writeFileSync(path.join(d2, '.gitignore'), '.env\n.env.local\n.env.production\n.env.*.local\n*.pem\ncredentials.json\nnode_modules/\n');
|
|
6064
|
+
fs.writeFileSync(path.join(d2, '.env'), 'AWS=AKIAJQXMP7RZ2KL9WXYZ\n');
|
|
6065
|
+
const a2 = cp.spawnSync(process.execPath, [CLI, 'audit', d2, '--json'], { encoding: 'utf8', timeout: 20000 });
|
|
6066
|
+
let f1ok = false; try { const j = JSON.parse(a2.stdout); f1ok = j.healthy === true && a2.status === 0; } catch {}
|
|
6067
|
+
// (F2) committed secret in config.js, NO .env → handoff 보안요약 섹션이 노출(ko) + en 영어(섹션 한글 0)
|
|
6068
|
+
const d3 = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-f2-'));
|
|
6069
|
+
cp.spawnSync(process.execPath, [CLI, 'init', d3, '--yes', '--language', 'ko'], { encoding: 'utf8', timeout: 30000 });
|
|
6070
|
+
fs.writeFileSync(path.join(d3, '.gitignore'), 'node_modules/\n');
|
|
6071
|
+
fs.writeFileSync(path.join(d3, 'config.js'), 'const k="AKIAJQXMP7RZ2KL9WXYZ";\nconst g="ghp_aZ9bY8cX7dW6eV5fU4gT3hS2iR1jQ0kP9oN8";\n');
|
|
6072
|
+
const hoKo = (cp.spawnSync(process.execPath, [CLI, 'handoff', d3], { encoding: 'utf8', timeout: 25000 }).stdout) || '';
|
|
6073
|
+
const f2ko = /🔒\s*보안 요약/.test(hoKo) && /커밋된 시크릿/.test(hoKo) && /config\.js/.test(hoKo);
|
|
6074
|
+
const d4 = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-f2en-'));
|
|
6075
|
+
cp.spawnSync(process.execPath, [CLI, 'init', d4, '--yes', '--language', 'en'], { encoding: 'utf8', timeout: 30000 });
|
|
6076
|
+
fs.writeFileSync(path.join(d4, '.gitignore'), 'node_modules/\n');
|
|
6077
|
+
fs.writeFileSync(path.join(d4, 'config.js'), 'const k="AKIAJQXMP7RZ2KL9WXYZ";\n');
|
|
6078
|
+
const hoEn = (cp.spawnSync(process.execPath, [CLI, 'handoff', d4], { encoding: 'utf8', timeout: 25000 }).stdout) || '';
|
|
6079
|
+
const enSecLines = hoEn.split('\n').filter(l => /Security summary|committed secret/i.test(l));
|
|
6080
|
+
const f2en = /Security summary/.test(hoEn) && /committed secret/i.test(hoEn) && enSecLines.length >= 1 && !enSecLines.some(l => H.test(l));
|
|
6081
|
+
[d1, d2, d3, d4].forEach(d => { try { fs.rmSync(d, { recursive: true, force: true }); } catch {} });
|
|
6082
|
+
ok = f1bad && f1ok && f2ko && f2en;
|
|
6083
|
+
} catch {}
|
|
6084
|
+
console.log(ok ? '✓ B(1.30.1) 14th외부리뷰 F1+F2: audit committed-secret→failure(scan 일관, gitignored FP0) + handoff 보안요약이 committed 시크릿 노출(ko/en)' : '✗ 보안 정직성 F1+F2 가드 실패');
|
|
6085
|
+
if (!ok) failed++;
|
|
6086
|
+
}
|
|
6087
|
+
|
|
6088
|
+
// 1.30.2 회귀 (#157 사용자명시, 하위 프로젝트 방향 — 외부AI+Claude 교차검토 → 방향 C): parent detect 가 상위 leerness 부모를 탐지(read-only) + handoff 헤드라인 노출 + 자동 적용 안 함.
|
|
6089
|
+
total++;
|
|
6090
|
+
{
|
|
6091
|
+
let ok = false;
|
|
6092
|
+
try {
|
|
6093
|
+
const par = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-par-'));
|
|
6094
|
+
cp.spawnSync(process.execPath, [CLI, 'init', par, '--yes', '--language', 'ko'], { encoding: 'utf8', timeout: 30000 });
|
|
6095
|
+
const sub = path.join(par, 'sub');
|
|
6096
|
+
cp.spawnSync(process.execPath, [CLI, 'init', sub, '--yes', '--language', 'ko'], { encoding: 'utf8', timeout: 30000 });
|
|
6097
|
+
// (1) parent detect --json from sub → parent detected, applied:false, assetCount≥1
|
|
6098
|
+
const pj = cp.spawnSync(process.execPath, [CLI, 'parent', 'detect', '--path', sub, '--json'], { encoding: 'utf8', timeout: 15000 });
|
|
6099
|
+
let detectOk = false; try { const j = JSON.parse(pj.stdout); detectOk = j.applied === false && j.parent && j.parent.workspaceDir === '.harness' && j.parent.assetCount >= 1; } catch {}
|
|
6100
|
+
// (2) parent detect from standalone → null
|
|
6101
|
+
const alone = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-alone-'));
|
|
6102
|
+
cp.spawnSync(process.execPath, [CLI, 'init', alone, '--yes', '--language', 'ko'], { encoding: 'utf8', timeout: 30000 });
|
|
6103
|
+
const aj = cp.spawnSync(process.execPath, [CLI, 'parent', 'detect', '--path', alone, '--json'], { encoding: 'utf8', timeout: 15000 });
|
|
6104
|
+
let aloneOk = false; try { const j = JSON.parse(aj.stdout); aloneOk = j.parent === null; } catch {}
|
|
6105
|
+
// (3) handoff headline from sub shows 🔗 부모 프로젝트 (미적용); en shows "not applied"
|
|
6106
|
+
const hoKo = (cp.spawnSync(process.execPath, [CLI, 'handoff', '--path', sub], { encoding: 'utf8', timeout: 25000 }).stdout) || '';
|
|
6107
|
+
const hoEn = (cp.spawnSync(process.execPath, [CLI, 'handoff', '--path', sub, '--language', 'en'], { encoding: 'utf8', timeout: 25000 }).stdout) || '';
|
|
6108
|
+
const headlineOk = /🔗 부모 프로젝트.*미적용/.test(hoKo) && /🔗 parent project.*not applied/.test(hoEn);
|
|
6109
|
+
// (4) read-only: parent detect 가 sub 에 아무 파일도 쓰지 않음(adopt 미구현)
|
|
6110
|
+
const before = fs.readdirSync(sub).sort().join(',');
|
|
6111
|
+
cp.spawnSync(process.execPath, [CLI, 'parent', 'detect', '--path', sub], { encoding: 'utf8', timeout: 15000 });
|
|
6112
|
+
const after = fs.readdirSync(sub).sort().join(',');
|
|
6113
|
+
const readOnlyOk = before === after;
|
|
6114
|
+
[par, alone].forEach(d => { try { fs.rmSync(d, { recursive: true, force: true }); } catch {} });
|
|
6115
|
+
ok = detectOk && aloneOk && headlineOk && readOnlyOk;
|
|
6116
|
+
} catch {}
|
|
6117
|
+
console.log(ok ? '✓ B(1.30.2) #157 하위프로젝트: parent detect(상위 leerness 탐지·--json applied:false) + 독립 null + handoff 헤드라인 🔗(ko/en, 미적용) + read-only' : '✗ parent detect 가드 실패');
|
|
6118
|
+
if (!ok) failed++;
|
|
6119
|
+
}
|
|
6120
|
+
|
|
6121
|
+
// 1.30.3 회귀 (#158 사용자명시): parent adopt 게이트형 적용 — dry-run 기본(쓰기 0) + --apply(사용자 명시) 시에만 자식-로컬 참조 기록 + 자식 design-system.md 무변경(비파괴) + handoff 헤드라인 adopted 반영.
|
|
6122
|
+
total++;
|
|
6123
|
+
{
|
|
6124
|
+
let ok = false;
|
|
6125
|
+
try {
|
|
6126
|
+
const par = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-adopt-'));
|
|
6127
|
+
cp.spawnSync(process.execPath, [CLI, 'init', par, '--yes', '--language', 'ko'], { encoding: 'utf8', timeout: 30000 });
|
|
6128
|
+
const sub = path.join(par, 'sub');
|
|
6129
|
+
cp.spawnSync(process.execPath, [CLI, 'init', sub, '--yes', '--language', 'ko'], { encoding: 'utf8', timeout: 30000 });
|
|
6130
|
+
const childDs = path.join(sub, '.harness', 'design-system.md');
|
|
6131
|
+
const childDsBefore = fs.readFileSync(childDs, 'utf8');
|
|
6132
|
+
const inherited = path.join(sub, '.harness', 'inherited-from-parent.md');
|
|
6133
|
+
const link = path.join(sub, '.harness', 'PARENT_LINK.json');
|
|
6134
|
+
// (1) DRY-RUN: 쓰기 0
|
|
6135
|
+
cp.spawnSync(process.execPath, [CLI, 'parent', 'adopt', '--path', sub], { encoding: 'utf8', timeout: 15000 });
|
|
6136
|
+
const dryNoWrite = !fs.existsSync(inherited) && !fs.existsSync(link);
|
|
6137
|
+
// (2) --apply: 참조파일+마커 기록, 자식 design-system.md 무변경
|
|
6138
|
+
cp.spawnSync(process.execPath, [CLI, 'parent', 'adopt', '--apply', '--path', sub], { encoding: 'utf8', timeout: 15000 });
|
|
6139
|
+
const wrote = fs.existsSync(inherited) && fs.existsSync(link);
|
|
6140
|
+
const childUnchanged = fs.readFileSync(childDs, 'utf8') === childDsBefore;
|
|
6141
|
+
let linkOk = false; try { const j = JSON.parse(fs.readFileSync(link, 'utf8')); linkOk = !!j.parentRoot && Array.isArray(j.adoptedKinds) && j.adoptedKinds.length >= 1; } catch {}
|
|
6142
|
+
// (3) handoff 헤드라인 adopted 반영(ko/en)
|
|
6143
|
+
const hoKo = (cp.spawnSync(process.execPath, [CLI, 'handoff', '--path', sub], { encoding: 'utf8', timeout: 25000 }).stdout) || '';
|
|
6144
|
+
const hoEn = (cp.spawnSync(process.execPath, [CLI, 'handoff', '--path', sub, '--language', 'en'], { encoding: 'utf8', timeout: 25000 }).stdout) || '';
|
|
6145
|
+
const headlineOk = /🔗 부모 프로젝트.*adopted/.test(hoKo) && /🔗 parent project.*adopted/.test(hoEn);
|
|
6146
|
+
// (4) --json applied:true on apply
|
|
6147
|
+
const aj = cp.spawnSync(process.execPath, [CLI, 'parent', 'adopt', '--apply', '--json', '--path', sub], { encoding: 'utf8', timeout: 15000 });
|
|
6148
|
+
let jsonOk = false; try { const j = JSON.parse(aj.stdout); jsonOk = j.applied === true && typeof j.inheritedPath === 'string'; } catch {}
|
|
6149
|
+
fs.rmSync(par, { recursive: true, force: true });
|
|
6150
|
+
ok = dryNoWrite && wrote && childUnchanged && linkOk && headlineOk && jsonOk;
|
|
6151
|
+
} catch {}
|
|
6152
|
+
console.log(ok ? '✓ B(1.30.3) #158 parent adopt: dry-run 쓰기0 + --apply 참조파일/마커 + 자식 design-system 무변경(비파괴) + handoff adopted(ko/en) + --json applied:true' : '✗ parent adopt 가드 실패');
|
|
6153
|
+
if (!ok) failed++;
|
|
6154
|
+
}
|
|
6155
|
+
|
|
6156
|
+
// 1.30.4 회귀 (#155 / 14th리뷰 F5+F6+F7): add류 cli-ux 일관성 — decision/lesson dedup + rule/lesson 빈입력 --json 구조화 + task/rule bogus subcommand 토큰 명시.
|
|
6157
|
+
total++;
|
|
6158
|
+
{
|
|
6159
|
+
let ok = false;
|
|
6160
|
+
try {
|
|
6161
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-f567-'));
|
|
6162
|
+
cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes', '--language', 'ko'], { encoding: 'utf8', timeout: 30000 });
|
|
6163
|
+
const run = (args) => cp.spawnSync(process.execPath, [CLI, ...args, '--path', d], { encoding: 'utf8', timeout: 15000 });
|
|
6164
|
+
const isJson = (s) => { try { JSON.parse((s||'').trim()); return true; } catch { return false; } };
|
|
6165
|
+
// F5 dedup: decision/lesson 동일 입력 2회 → 1 copy, --force → 2
|
|
6166
|
+
run(['decision', 'add', 'dupdec']); run(['decision', 'add', 'dupdec']);
|
|
6167
|
+
const decCount = ((run(['decision', 'list']).stdout || '').match(/dupdec/g) || []).length;
|
|
6168
|
+
run(['lesson', 'save', 'duples']); run(['lesson', 'save', 'duples']);
|
|
6169
|
+
const lesCount = ((run(['lesson', 'list']).stdout || '').match(/duples/g) || []).length;
|
|
6170
|
+
run(['decision', 'add', 'dupdec', '--force']);
|
|
6171
|
+
const decForce = ((run(['decision', 'list']).stdout || '').match(/dupdec/g) || []).length;
|
|
6172
|
+
const f5 = decCount === 1 && lesCount === 1 && decForce === 2;
|
|
6173
|
+
// F6 빈입력 --json 구조화 + exit1 (성공경로도 JSON 유지)
|
|
6174
|
+
const ra = run(['rule', 'add', '', '--json']); const ls = run(['lesson', 'save', '', '--json']);
|
|
6175
|
+
const raOk = run(['rule', 'add', '룰F6', '--json']); const lsOk = run(['lesson', 'save', '레슨F6', '--json']);
|
|
6176
|
+
const f6 = isJson(ra.stdout) && /empty_title/.test(ra.stdout) && ra.status === 1
|
|
6177
|
+
&& isJson(ls.stdout) && /empty_text/.test(ls.stdout) && ls.status === 1
|
|
6178
|
+
&& isJson(raOk.stdout) && isJson(lsOk.stdout);
|
|
6179
|
+
// F7 bogus subcommand → 잘못된 토큰 명시 + exit1 (유효 하위명령 무회귀)
|
|
6180
|
+
const tf = run(['task', 'frobnicate']); const rf = run(['rule', 'frobnicate']);
|
|
6181
|
+
const f7 = /task 하위명령: frobnicate/.test(tf.stdout + tf.stderr) && tf.status === 1
|
|
6182
|
+
&& /rule 하위명령: frobnicate/.test(rf.stdout + rf.stderr) && rf.status === 1
|
|
6183
|
+
&& run(['task', 'list']).status === 0 && run(['rule', 'list']).status === 0;
|
|
6184
|
+
fs.rmSync(d, { recursive: true, force: true });
|
|
6185
|
+
ok = f5 && f6 && f7;
|
|
6186
|
+
} catch {}
|
|
6187
|
+
console.log(ok ? '✓ B(1.30.4) #155 cli-ux 일관성: decision/lesson dedup(--force 우회) + rule/lesson 빈입력 --json 구조화(exit1) + task/rule bogus subcommand 토큰 명시' : '✗ cli-ux 일관성 F5+F6+F7 가드 실패');
|
|
6188
|
+
if (!ok) failed++;
|
|
6189
|
+
}
|
|
6190
|
+
|
|
6191
|
+
// 1.31.1 회귀 (UR-0010): install-safety 출력 영어 opt-in — en 한글 0 + ko 보존 + 셸-무관 가드(npx --yes/PowerShell/no npm_config prefix) 양 언어 보존.
|
|
6192
|
+
total++;
|
|
6193
|
+
{
|
|
6194
|
+
let ok = false;
|
|
6195
|
+
try {
|
|
6196
|
+
const H = /[가-힣]/;
|
|
6197
|
+
const en = (cp.spawnSync(process.execPath, [CLI, 'install-safety', '--language', 'en'], { encoding: 'utf8', timeout: 15000 }).stdout) || '';
|
|
6198
|
+
const ko = (cp.spawnSync(process.execPath, [CLI, 'install-safety'], { encoding: 'utf8', timeout: 15000 }).stdout) || '';
|
|
6199
|
+
const enOk = /install safety profile/.test(en) && !H.test(en);
|
|
6200
|
+
const koOk = /설치 안전 프로필/.test(ko);
|
|
6201
|
+
let guardOk = false;
|
|
6202
|
+
try { const j = JSON.parse(cp.spawnSync(process.execPath, [CLI, 'install-safety', '--json', '--language', 'en'], { encoding: 'utf8', timeout: 15000 }).stdout); guardOk = j.safeInstall.filter(x => x.includes('npx --yes')).length >= 2 && j.hardeningNote.includes('PowerShell') && !j.safeInstall.some(x => /^npm_config_\w+=/.test(String(x).trim())); } catch {}
|
|
6203
|
+
ok = enOk && koOk && guardOk;
|
|
6204
|
+
} catch {}
|
|
6205
|
+
console.log(ok ? '✓ B(1.31.1) UR-0010: install-safety en 영어(한글 0) + ko 보존 + 셸-무관 가드(npx --yes/PowerShell/no npm_config prefix) 양 언어 보존' : '✗ install-safety 영어화 가드 실패');
|
|
6206
|
+
if (!ok) failed++;
|
|
6207
|
+
}
|
|
6208
|
+
|
|
6209
|
+
// 1.31.2 회귀 (UR-0010): constraints 영어화 — list/check 라벨 + 카탈로그 detailEn + suggestion 영어 / ko 보존 / 매칭(한국어 alias) 무회귀.
|
|
6210
|
+
total++;
|
|
6211
|
+
{
|
|
6212
|
+
let ok = false;
|
|
6213
|
+
try {
|
|
6214
|
+
const H = /[가-힣]/;
|
|
6215
|
+
const cap = (args) => (cp.spawnSync(process.execPath, [CLI, ...args], { encoding: 'utf8', timeout: 15000 }).stdout) || '';
|
|
6216
|
+
const listEn = cap(['constraints', 'list', '--language', 'en']);
|
|
6217
|
+
const listKo = cap(['constraints', 'list']);
|
|
6218
|
+
const chkEn = cap(['constraints', 'check', 'stripe payment api integration', '--language', 'en']);
|
|
6219
|
+
const noMatchEn = cap(['constraints', 'check', 'build a generic api integration widget xyz', '--language', 'en']);
|
|
6220
|
+
// EN: zero Hangul + English catalog detail + English suggestion/labels surfaced
|
|
6221
|
+
const enOk = !H.test(listEn) && /duplicate charges/.test(listEn) && /no platform matched/.test(noMatchEn)
|
|
6222
|
+
&& /review constraints before building/.test(chkEn) && !H.test(chkEn) && !H.test(noMatchEn)
|
|
6223
|
+
&& /review the pre-registered platform catalog/.test(noMatchEn);
|
|
6224
|
+
// KO preserved: Korean catalog detail still present in default output
|
|
6225
|
+
const koOk = H.test(listKo) && /필수|별도/.test(listKo) && /매칭된 플랫폼 없음|플랫폼 매칭/.test(cap(['constraints', 'check', '일반 api 연동 위젯 xyz']));
|
|
6226
|
+
// matching unaffected: Korean alias still matches stripe (--json)
|
|
6227
|
+
let matchOk = false;
|
|
6228
|
+
try { const j = JSON.parse(cap(['constraints', 'check', 'stripe 결제 api 연동', '--json'])); matchOk = (j.matched || []).some(m => m.platform === 'stripe'); } catch {}
|
|
6229
|
+
ok = enOk && koOk && matchOk;
|
|
6230
|
+
} catch {}
|
|
6231
|
+
console.log(ok ? '✓ B(1.31.2) UR-0010: constraints en 영어(list/check 라벨+카탈로그 detailEn+suggestion, 한글 0) + ko 보존 + 한국어 alias 매칭 무회귀' : '✗ constraints 영어화 가드 실패');
|
|
6232
|
+
if (!ok) failed++;
|
|
6233
|
+
}
|
|
6234
|
+
|
|
6235
|
+
// 1.31.3 회귀 (UR-0010): capabilities(권한·보안 표면) 영어화(라벨+카탈로그 descEn/optOutEn/noteEn+principles) + team reminder 본문 영어화 / ko 보존.
|
|
6236
|
+
total++;
|
|
6237
|
+
{
|
|
6238
|
+
let ok = false;
|
|
6239
|
+
try {
|
|
6240
|
+
const H = /[가-힣]/;
|
|
6241
|
+
const cap = (args) => (cp.spawnSync(process.execPath, [CLI, ...args], { encoding: 'utf8', timeout: 15000 }).stdout) || '';
|
|
6242
|
+
const capEn = cap(['capabilities', '--language', 'en']);
|
|
6243
|
+
const capKo = cap(['capabilities']);
|
|
6244
|
+
const capJsonEn = cap(['capabilities', '--json', '--language', 'en']);
|
|
6245
|
+
// EN: zero Hangul + English catalog desc/note + English labels; KO: preserved; JSON: English principles
|
|
6246
|
+
const capOk = !H.test(capEn) && /metadata files|external AI CLIs|mouse\/keyboard/.test(capEn)
|
|
6247
|
+
&& /Permission surface/.test(capEn) && /Principles/.test(capEn)
|
|
6248
|
+
&& H.test(capKo) && /권한 표면|백업/.test(capKo)
|
|
6249
|
+
&& /0 runtime dependencies/.test(capJsonEn);
|
|
6250
|
+
// team reminder body en/ko via handoff (full wiring: pure lang + caller _uiLang)
|
|
6251
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-team-i18n-'));
|
|
6252
|
+
cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes'], { encoding: 'utf8', timeout: 30000 });
|
|
6253
|
+
cp.spawnSync(process.execPath, [CLI, 'team', 'add', 'nightly', '--members', 'a,b', '--schedule', 'every-session', '--path', d], { encoding: 'utf8', timeout: 15000 });
|
|
6254
|
+
const hoEn = (cp.spawnSync(process.execPath, [CLI, 'handoff', d, '--language', 'en'], { encoding: 'utf8', timeout: 30000 }).stdout) || '';
|
|
6255
|
+
const hoKo = (cp.spawnSync(process.execPath, [CLI, 'handoff', d], { encoding: 'utf8', timeout: 30000 }).stdout) || '';
|
|
6256
|
+
const teamEn = hoEn.split('\n').filter(l => /🤝|team preview nightly/.test(l));
|
|
6257
|
+
const teamKo = hoKo.split('\n').filter(l => /🤝|team preview nightly/.test(l));
|
|
6258
|
+
const teamOk = teamEn.length >= 2 && !teamEn.some(l => H.test(l))
|
|
6259
|
+
&& teamEn.some(l => /2 members/.test(l)) && teamEn.some(l => /review needed/.test(l)) && teamEn.some(l => /preview:/.test(l))
|
|
6260
|
+
&& teamKo.some(l => /2명/.test(l)) && teamKo.some(l => /검수필요/.test(l)) && teamKo.some(l => /미리보기/.test(l));
|
|
6261
|
+
try { fs.rmSync(d, { recursive: true, force: true }); } catch {}
|
|
6262
|
+
ok = capOk && teamOk;
|
|
6263
|
+
} catch {}
|
|
6264
|
+
console.log(ok ? '✓ B(1.31.3) UR-0010: capabilities en 영어(표면 desc/optOut/note/principles, 한글 0) + ko 보존 + team reminder 본문 en/ko(handoff 전체배선)' : '✗ capabilities/team reminder 영어화 가드 실패');
|
|
6265
|
+
if (!ok) failed++;
|
|
6266
|
+
}
|
|
6267
|
+
|
|
6044
6268
|
// 1.9.430 (10th 외부평가 UR-0130): health 보안 CRITICAL(커밋 시크릿)은 --strict 없이도 exit 1(CI 게이트). 클린은 exit 0.
|
|
6045
6269
|
total++;
|
|
6046
6270
|
{
|
|
@@ -6297,9 +6521,22 @@ total++;
|
|
|
6297
6521
|
const agEnOk = /CLI agent slash commands/.test(agEn) && agEnLines.length >= 2 && !agEnLines.some(l => H.test(l));
|
|
6298
6522
|
const agKoOk = /에이전트 슬래시/.test(agKo);
|
|
6299
6523
|
fs.rmSync(da, { recursive: true, force: true });
|
|
6300
|
-
|
|
6301
|
-
|
|
6302
|
-
|
|
6524
|
+
// ⑫ (1.30.5 #156 F3+F4) handoff 본문 워크플로 가이드 + 메모리 변동 en 영어(섹션 한글 0) + ko 보존 · verify-claim/optimism-check 미입력 에러 en/ko.
|
|
6525
|
+
const df3 = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-i18n-f3-'));
|
|
6526
|
+
cp.spawnSync(process.execPath, [CLI, 'init', df3, '--yes', '--language', 'en'], { encoding: 'utf8', timeout: 30000 });
|
|
6527
|
+
const hf3En = out(cp.spawnSync(process.execPath, [CLI, 'handoff', df3], { encoding: 'utf8', timeout: 25000 }));
|
|
6528
|
+
const wfLines = hf3En.split('\n').filter(l => /Session workflow|Analyze request|sub-agent work|to disable:/.test(l));
|
|
6529
|
+
const f3En = /Session workflow/.test(hf3En) && wfLines.length >= 3 && !wfLines.some(l => H.test(l));
|
|
6530
|
+
const df3ko = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-i18n-f3k-'));
|
|
6531
|
+
cp.spawnSync(process.execPath, [CLI, 'init', df3ko, '--yes', '--language', 'ko'], { encoding: 'utf8', timeout: 30000 });
|
|
6532
|
+
const f3Ko = /세션 워크플로 6단계/.test(out(cp.spawnSync(process.execPath, [CLI, 'handoff', df3ko], { encoding: 'utf8', timeout: 25000 })));
|
|
6533
|
+
const vcEn = out(cp.spawnSync(process.execPath, [CLI, 'verify-claim', '--path', df3, '--language', 'en'], { encoding: 'utf8', timeout: 15000 }));
|
|
6534
|
+
const vcKo = out(cp.spawnSync(process.execPath, [CLI, 'verify-claim', '--path', df3ko], { encoding: 'utf8', timeout: 15000 }));
|
|
6535
|
+
const f4 = /required\. ex:/.test(vcEn) && !H.test(vcEn) && /필요\. 예:/.test(vcKo);
|
|
6536
|
+
[df3, df3ko].forEach(d => { try { fs.rmSync(d, { recursive: true, force: true }); } catch {} });
|
|
6537
|
+
ok = lensKoOk && lensEnOk && noLeak && stOk && healthOk && driftOk && doctorOk && hoEnOk && hoKoOk && edEnOk && edKoOk && shEnOk && shKoOk && agEnOk && agKoOk && f3En && f3Ko && f4;
|
|
6538
|
+
} catch {}
|
|
6539
|
+
console.log(ok ? '✓ B(1.25.1/1.25.2/1.27.2/1.28.2/1.29.1/1.29.2/1.29.3/1.29.4/1.30.5) i18n 행위: --language en 런타임 영어(lens/health/drift/doctor/handoff보안요약/env-detect/shell-guard/agent-slash/워크플로가이드/verify-claim) + ko 기본 보존 + --language positional 무누출 + status 에러 en/ko (UR-0010)' : '✗ i18n 행위 회귀 가드 실패');
|
|
6303
6540
|
if (!ok) failed++;
|
|
6304
6541
|
}
|
|
6305
6542
|
|