leerness 1.9.387 → 1.9.389
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 +38 -0
- package/README.md +5 -5
- package/bin/harness.js +17 -213
- package/lib/migrate.js +123 -0
- package/lib/team.js +122 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.389 — 2026-06-06 — UR-0025 큰 핸들러 모듈화 2번째: teamCmd → lib/team.js (DI)
|
|
4
|
+
|
|
5
|
+
**🧩 두 번째 핸들러 모듈 추출 — team 서브시스템(list/add/show/remove/preview/deploy)을 lib/team.js 로 분리. migrate(1.9.388)에서 확립한 DI 패턴 재사용.**
|
|
6
|
+
|
|
7
|
+
### 배경 (UR-0025 큰 핸들러 모듈화, 사용자 승인)
|
|
8
|
+
1.9.388 migrate 추출로 DI 패턴(io import + deps 주입)을 확립. 동일 패턴으로 두 번째 응집 핸들러 teamCmd(UR-0073, ~111줄, e2e 5건 보유)를 분리.
|
|
9
|
+
|
|
10
|
+
### 구현
|
|
11
|
+
1. **lib/team.js 신규**: teamCmd(6 서브명령) 이전.
|
|
12
|
+
- 직접 require: `./io`(absRoot/log/ok/warn/fail/now) · `./pure-utils`(_composeTeamPlan/_teamDeployGate) · `./analyzers`(_shellGuardAnalyze) · node child_process.
|
|
13
|
+
- **DI 주입**: VERSION · _loadTeams · _saveTeams · _detectShellCtx · arg · has(argv 파서).
|
|
14
|
+
2. **_loadTeams/_saveTeams 는 harness 유지**: handoff team reminders(_teamHandoffReminders)도 쓰는 공유 함수 → harness 에 두고 주입만. (teamsJsonPath/teamsPath/_renderTeamsMd 도 harness 유지.)
|
|
15
|
+
3. **harness thin wrapper**: deps 구성 후 위임. dispatch · 동작 · 출력 무변경.
|
|
16
|
+
|
|
17
|
+
### 검증 (회귀 0)
|
|
18
|
+
- **selftest 134→135 PASS** (lib/team export + 위임 와이어 + lib 본문 이동(pure-utils require/_teamDeployGate/하위명령) 교차참조 + behavioral list --json). 기존 1.9.371 케이스(_saveTeams/_loadTeams/_renderTeamsMd harness 유지)도 통과.
|
|
19
|
+
- **E2E 332 유지 PASS** (team B(1.9.371/372/373/376/378) list/add/preview/deploy-gate/MCP 회귀가 CLI→wrapper→lib 경로 검증). 락 flake 시 재실행.
|
|
20
|
+
- 실측: list/add/show/preview/deploy(dry-run·gated 이중게이트) + handoff team reminders(daily 팀 노출) 보존.
|
|
21
|
+
|
|
22
|
+
## 1.9.388 — 2026-06-06 — UR-0025 큰 핸들러 모듈화: migrate audit/apply/plan → lib/migrate.js (DI)
|
|
23
|
+
|
|
24
|
+
**🧩 첫 실제 핸들러 모듈 추출 — migrate 서브시스템(audit/apply/plan)을 lib/migrate.js 로 분리(의존성 주입 ctx). lib/io.js 토대(1.9.382/383)의 첫 활용.**
|
|
25
|
+
|
|
26
|
+
### 배경 (UR-0025 큰 핸들러 모듈화, 사용자 승인)
|
|
27
|
+
1.9.382/383 에서 lib/io.js(출력+fs 프리미티브 14종)를 만든 목적은 "핸들러를 별도 lib 모듈로 분리할 토대". 이번에 그 토대로 **첫 실제 핸들러**(migrate UR-0075, ~100줄, 응집적·e2e 3건 보유)를 추출.
|
|
28
|
+
|
|
29
|
+
### 구현
|
|
30
|
+
1. **lib/migrate.js 신규**: `migrateAuditCmd` / `migrateApplyCmd` / `migratePlanCmd` 이전.
|
|
31
|
+
- I/O 프리미티브: `require('./io')`(absRoot/exists/read/log/ok/warn).
|
|
32
|
+
- harness 고유 의존은 **deps 객체로 주입(DI)**: VERSION · compareVer · REQUIRED_WORKSPACE_FILES · canonical 메모리 함수 8종(decisionsPath/…/_save*) · harnessPath(plan 의 임시폴더 init spawn 용).
|
|
33
|
+
2. **harness thin wrapper**: `_migrateDeps()` 가 deps 1회 구성 → 3개 wrapper 가 위임. **호출부(dispatch)·동작·출력 무변경**.
|
|
34
|
+
3. 새 패턴 확립: 향후 다른 핸들러도 이 DI 방식으로 lib 모듈화 가능(io import + deps 주입).
|
|
35
|
+
|
|
36
|
+
### 검증 (회귀 0)
|
|
37
|
+
- **selftest 133→134 PASS** (lib/migrate 3 exports + harness 위임 와이어 + lib 본문 이동 확인 + behavioral audit JSON). 1.9.380 케이스는 REQUIRED_WORKSPACE_FILES 소비처가 harness+lib 로 분산됨에 맞춰 교차참조 카운트로 갱신.
|
|
38
|
+
- **E2E 332 유지 PASS** (기존 migrate B(1.9.356/357/358) audit/apply/plan CLI 회귀가 CLI→wrapper→lib 경로를 그대로 검증 — 신규 케이스 불필요). 락 flake 시 재실행.
|
|
39
|
+
- 실측: audit(정합 willChange:0 / canonical-pending 감지) · apply --yes(decisions.json 백필 복원) · plan(tempInstallOk:true) · 사람용 출력 보존.
|
|
40
|
+
|
|
3
41
|
## 1.9.387 — 2026-06-06 — --json 일관성 연장: incident/runs list 빈 케이스 구조화 (UR-0088)
|
|
4
42
|
|
|
5
43
|
**🔌 `incident list` / `runs list` 의 빈 상태도 `--json` 시 `{total:0,items:[]}` 출력 — AI 에이전트가 빈 프로젝트에서 사람용 텍스트를 파싱하다 실패하던 일관성 결함 해소.**
|
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.389 하네스를 사용합니다. 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.389는 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.387는 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.389)** · 매 라운드 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.389: 2026-06-06
|
|
588
588
|
<!-- leerness:project-readme:end -->
|
|
589
589
|
|
package/bin/harness.js
CHANGED
|
@@ -29,7 +29,7 @@ const { _evidenceQuality, _parseEvidenceStats, _shellGuardAnalyze, _claimFileInG
|
|
|
29
29
|
// 1.9.295 (UR-0025 4단계): 정적 데이터 카탈로그 모듈 분리 (비파괴, require-based).
|
|
30
30
|
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 포함)
|
|
31
31
|
|
|
32
|
-
const VERSION = '1.9.
|
|
32
|
+
const VERSION = '1.9.389';
|
|
33
33
|
|
|
34
34
|
// 1.9.290 (UR-0037, Codex gpt-5.5 #4 수렴): CLI 전용 부작용은 require 시 실행하지 않는다.
|
|
35
35
|
// 이전: warning listener 제거 / NODE_OPTIONS 변경 / chcp IIFE 가 top-level 즉시 실행 → require('harness') 시 호스트 프로세스 오염.
|
|
@@ -2982,7 +2982,7 @@ function _selfTestCases() {
|
|
|
2982
2982
|
{ name: 'UR-0025: _renderWorkspaceReferenceGuide 모듈 분리 + 빌더 동작 (1.9.377)', run: () => { const m = require('../lib/pure-utils'); if (typeof _renderWorkspaceReferenceGuide !== 'function' || m._renderWorkspaceReferenceGuide !== _renderWorkspaceReferenceGuide) return false; const g = _renderWorkspaceReferenceGuide('.leerness', '9.9.9', '2026-01-01T00:00:00.000Z'); const wrapperThin = read(__filename).includes('return _renderWorkspaceReferenceGuide(dirName, VERSION, new Date().toISOString())'); return g.includes('.leerness/progress-tracker.md') && g.includes('9.9.9') && g.includes('자주 묻는 위치') && g.includes('마이그레이션 안내') && wrapperThin; } },
|
|
2983
2983
|
{ name: 'UR-0073: team MCP 도구 2종(read-only) 정의 + dispatch 와이어 (1.9.378)', run: () => { const tools = require('../lib/mcp-tools'); const src = read(__filename); const tl = tools.find(t => t.name === 'leerness_team_list'); const tp = tools.find(t => t.name === 'leerness_team_preview'); const defsOk = tl && tl.requiredTier === 'read-only' && tp && tp.requiredTier === 'read-only' && tp.inputSchema.required && tp.inputSchema.required.includes('id'); const wired = src.includes("case " + "'leerness_team_list':") && src.includes("case " + "'leerness_team_preview':") && /cliArgs = \['team', 'list'/.test(src) && /cliArgs = \['team', 'preview'/.test(src); return !!defsOk && wired; } },
|
|
2984
2984
|
{ name: 'UR-0025 심화: pulse 렌더 코어 분리 — _memorySurface + _renderPulseLine 행위 (1.9.379)', run: () => { const m = require('../lib/pure-utils'); if (typeof _memorySurface !== 'function' || typeof _renderPulseLine !== 'function' || m._memorySurface !== _memorySurface || m._renderPulseLine !== _renderPulseLine) return false; const ms = _memorySurface({ tasks: 1, decisions: 2, rules: 3, milestones: 4, lessons: 5 }) === 'T1/D2/R3/P4/L5' && _memorySurface({}) === 'T0/D0/R0/P0/L0'; const base = _renderPulseLine({ version: '1.0.0', roundCount: 7, mcpTools: 85, memorySurface: 'T0/D1/R0/P2/L0' }); const ln = base.includes('v1.0.0') && base.includes('R7') && base.includes('MCP 85') && base.includes('T0/D1/R0/P2/L0') && !base.includes('🎯') && !base.includes('abnormal'); const full = _renderPulseLine({ version: '1.0.0', roundCount: 7, mcpTools: 85, memorySurface: 'x', nextMilestone: 400, etaDays: 6, abnormalShutdown: 'high' }); const ln2 = full.includes('🎯 R400 (6d)') && full.includes('abnormal:high'); const wired = read(__filename).includes('const line = _renderPulseLine(data)') && read(__filename).includes('data.memorySurface = _memorySurface('); return ms && ln && ln2 && wired; } },
|
|
2985
|
-
{ name: 'UR-0025: REQUIRED_WORKSPACE_FILES 단일출처 — verify/migrate audit·apply 3중 중복 제거 (1.9.380)', run: () => { const c = require('../lib/catalogs'); if (REQUIRED_WORKSPACE_FILES !== c.REQUIRED_WORKSPACE_FILES) return false; const listOk = Array.isArray(c.REQUIRED_WORKSPACE_FILES) && c.REQUIRED_WORKSPACE_FILES.length === 9 && c.REQUIRED_WORKSPACE_FILES.includes('AGENTS.md') && c.REQUIRED_WORKSPACE_FILES.includes('.harness/plan.md'); const
|
|
2985
|
+
{ name: 'UR-0025: REQUIRED_WORKSPACE_FILES 단일출처 — verify/migrate audit·apply 3중 중복 제거 (1.9.380)', run: () => { const c = require('../lib/catalogs'); if (REQUIRED_WORKSPACE_FILES !== c.REQUIRED_WORKSPACE_FILES) return false; const listOk = Array.isArray(c.REQUIRED_WORKSPACE_FILES) && c.REQUIRED_WORKSPACE_FILES.length === 9 && c.REQUIRED_WORKSPACE_FILES.includes('AGENTS.md') && c.REQUIRED_WORKSPACE_FILES.includes('.harness/plan.md'); const harnessUses = (read(__filename).match(/const required = REQUIRED_WORKSPACE_FILES;/g) || []).length >= 1; const migUses = (read(path.join(path.dirname(__filename), '..', 'lib', 'migrate.js')).match(/const required = REQUIRED_WORKSPACE_FILES;/g) || []).length >= 2; return listOk && harnessUses && migUses; } },
|
|
2986
2986
|
{ name: 'UR-0025: KEYWORD_STOPWORDS 단일출처 — handoff/lessons 키워드 stopwords 2중 중복 제거 (1.9.381)', run: () => { const c = require('../lib/catalogs'); if (KEYWORD_STOPWORDS !== c.KEYWORD_STOPWORDS) return false; const setOk = c.KEYWORD_STOPWORDS instanceof Set && c.KEYWORD_STOPWORDS.has('작업') && c.KEYWORD_STOPWORDS.has('task') && !c.KEYWORD_STOPWORDS.has('고유단어') && c.KEYWORD_STOPWORDS.size >= 25; const usesConst = (read(__filename).match(/const stopwords = KEYWORD_STOPWORDS;/g) || []).length >= 2 && !/const stopwords = new Set\(\[/.test(read(__filename)); return setOk && usesConst; } },
|
|
2987
2987
|
{ name: 'UR-0025 큰핸들러토대: lib/io.js 프리미티브(log/ok/warn/fail/today/now) 모듈 분리 + 동작 (1.9.382)', run: () => { const io = require('../lib/io'); const exportsOk = ['log', 'ok', 'warn', 'fail', 'today', 'now'].every(k => typeof io[k] === 'function') && io.log === log && io.fail === fail && io.now === now; const todayOk = /^\d{4}-\d{2}-\d{2}$/.test(io.today()) && /^\d{4}-\d{2}-\d{2}T/.test(io.now()); const src = read(__filename); const moved = src.includes("require('../lib/io')") && !/^function fail\(s\) \{ log/m.test(src) && !/^function now\(\) \{ return new Date/m.test(src); let exitOk = false; const saved = process.exitCode; const _w = process.stdout.write; try { process.stdout.write = () => true; process.exitCode = 0; io.fail('probe'); exitOk = process.exitCode === 1; } finally { process.stdout.write = _w; process.exitCode = saved; } return exportsOk && todayOk && moved && exitOk; } },
|
|
2988
2988
|
{ name: 'UR-0025 큰핸들러토대: lib/io.js fs 프리미티브(read/writeUtf8/exists/mkdirp/append/rel/absRoot) 분리 + round-trip (1.9.383)', run: () => { const io = require('../lib/io'); const exp = ['absRoot', 'exists', 'read', 'readBuf', 'mkdirp', 'writeUtf8', 'append', 'rel'].every(k => typeof io[k] === 'function') && io.read === read && io.writeUtf8 === writeUtf8 && io.exists === exists; const src = read(__filename); const moved = !/^function writeUtf8\(p, s\) \{/m.test(src) && !/^function read\(p\) \{/m.test(src) && !/^function exists\(p\) \{/m.test(src); const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_io_')); let rt = false; try { const f = path.join(tmp, 'a', 'b.txt'); io.writeUtf8(f, '한글RT'); rt = io.exists(f) && io.read(f) === '한글RT' && io.rel(tmp, f) === 'a/b.txt'; } finally { try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} } return exp && moved && rt; } },
|
|
@@ -2990,6 +2990,8 @@ function _selfTestCases() {
|
|
|
2990
2990
|
{ name: '5th외부평가/UR-0086: _parseContractSpec markdown bullet 함수 감지 + 순수 추출 (1.9.385)', run: () => { const m = require('../lib/pure-utils'); if (m._parseContractSpec !== _parseContractSpec) return false; const p = _parseContractSpec('# Spec\n- add(a,b)\n* subtract(a,b)\n1. multiply(a,b)\nfunction legacy(x)\n`mentioned(`\ntick.amount\n'); const declOk = ['add', 'subtract', 'multiply', 'legacy'].every(n => p.declared.includes(n)) && p.declared.length === 4; const menOk = p.mentioned.includes('mentioned') && !p.declared.includes('mentioned'); const fieldOk = p.fields.includes('amount'); const fpOk = _parseContractSpec('- 합계 (a+b)\n- result (total)\n- foo: bar(x)\n**bold**').declared.length === 0; const src = read(__filename); const moved = src.includes('_parseContractSpec(specText)') && !/specText\.matchAll\(\/function/.test(src); return declOk && menOk && fieldOk && fpOk && moved; } },
|
|
2991
2991
|
{ name: '5th외부평가/UR-0087: _gitignoreMatch git 일치(.env↛.env.bad) + env-family 스캔 (1.9.386)', run: () => { const m = require('../lib/pure-utils'); if (m._gitignoreMatch !== _gitignoreMatch) return false; const gm = _gitignoreMatch; const semOk = gm('.env', '.env') === true && gm('.env', '.env.bad') === false && gm('.env', '.env.local') === false && gm('.env.*', '.env.bad') === true && gm('.env*', '.env') === true && gm('*.pem', 'k.pem') === true && gm('src/', 'src/a.txt') === true; const src = read(__filename); const envFamilyScan = src.includes('const isEnvFamily =') && src.includes('!SCAN_TEXT_EXT.has(ext) && !isEnvFamily'); const delegated = src.includes('return _gitignoreMatch(gi, fileRel)'); return semOk && envFamilyScan && delegated; } },
|
|
2992
2992
|
{ name: 'UR-0088 5th외부평가 일관성: incident/runs list 빈 케이스 --json 구조화 (1.9.387)', run: () => { if (typeof incidentListCmd !== 'function' || typeof runsListCmd !== 'function') return false; const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_lj_')); const save = process.argv; const _w = process.stdout.write; let io = '', ro = ''; try { fs.mkdirSync(path.join(tmp, '.harness'), { recursive: true }); process.argv = ['node', 'h', 'incident', 'list', '--json']; process.stdout.write = s => { io += s; return true; }; incidentListCmd(tmp); process.stdout.write = _w; process.argv = ['node', 'h', 'runs', 'list', '--json']; process.stdout.write = s => { ro += s; return true; }; runsListCmd(tmp); } catch {} finally { process.stdout.write = _w; process.argv = save; try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} } let ij, rj; try { ij = JSON.parse(io); rj = JSON.parse(ro); } catch {} return !!ij && ij.total === 0 && Array.isArray(ij.items) && !!rj && rj.total === 0 && Array.isArray(rj.items); } },
|
|
2993
|
+
{ name: 'UR-0025 큰핸들러 모듈화: migrate audit/apply/plan → lib/migrate.js + DI 위임 + 동작 (1.9.388)', run: () => { const m = require('../lib/migrate'); const expOk = typeof m.migrateAuditCmd === 'function' && typeof m.migrateApplyCmd === 'function' && typeof m.migratePlanCmd === 'function'; const src = read(__filename); const delegated = src.includes("require('../lib/migrate')") && src.includes('_migrate.migrateAuditCmd(root, opts, _migrateDeps())') && src.includes('_migrate.migratePlanCmd(root, opts, _migrateDeps())'); const movedToLib = read(path.join(path.dirname(__filename), '..', 'lib', 'migrate.js')).includes('leerness-plan-'); let behavOk = false; const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_mig_')); const save = process.argv; const _w = process.stdout.write; let out = ''; try { fs.mkdirSync(path.join(tmp, '.harness'), { recursive: true }); fs.writeFileSync(path.join(tmp, '.harness', 'HARNESS_VERSION'), VERSION); process.argv = ['node', 'h', 'migrate', 'audit', tmp, '--json']; process.stdout.write = s => { out += s; return true; }; migrateAuditCmd(tmp, { json: true }); } catch {} finally { process.stdout.write = _w; process.argv = save; try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} } try { const j = JSON.parse(out); behavOk = j.version === VERSION && typeof j.willChange === 'number' && Array.isArray(j.findings); } catch {} return expOk && delegated && movedToLib && behavOk; } },
|
|
2994
|
+
{ name: 'UR-0025 큰핸들러 모듈화: teamCmd → lib/team.js + DI 위임 + 동작 (1.9.389)', run: () => { const m = require('../lib/team'); const expOk = typeof m.teamCmd === 'function'; const src = read(__filename); const delegated = src.includes("require('../lib/team')") && src.includes('_team.teamCmd(root, sub, id, opts,'); const teamSrc = read(path.join(path.dirname(__filename), '..', 'lib', 'team.js')); const movedToLib = teamSrc.includes("require('./pure-utils')") && teamSrc.includes('_teamDeployGate') && teamSrc.includes('알 수 없는 team 하위명령'); let behavOk = false; const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_tm_')); const save = process.argv; const _w = process.stdout.write; let out = ''; try { fs.mkdirSync(path.join(tmp, '.harness'), { recursive: true }); process.argv = ['node', 'h', 'team', 'list', '--json']; process.stdout.write = s => { out += s; return true; }; teamCmd(tmp, 'list', undefined, { json: true }); } catch {} finally { process.stdout.write = _w; process.argv = save; try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} } try { const j = JSON.parse(out); behavOk = j.version === VERSION && j.count === 0 && Array.isArray(j.teams); } catch {} return expOk && delegated && movedToLib && behavOk; } },
|
|
2993
2995
|
{ name: 'VERSION 형식 (x.y.z)', run: () => /^\d+\.\d+\.\d+$/.test(VERSION) }
|
|
2994
2996
|
];
|
|
2995
2997
|
}
|
|
@@ -5784,114 +5786,10 @@ function _saveTeams(root, teams) {
|
|
|
5784
5786
|
writeUtf8(teamsJsonPath(root), JSON.stringify(arr, null, 2) + '\n');
|
|
5785
5787
|
writeUtf8(teamsPath(root), _renderTeamsMd(arr));
|
|
5786
5788
|
}
|
|
5787
|
-
|
|
5788
|
-
|
|
5789
|
-
|
|
5790
|
-
|
|
5791
|
-
sub = sub || 'list';
|
|
5792
|
-
if (sub === 'list') {
|
|
5793
|
-
if (json) { log(JSON.stringify({ version: VERSION, root, count: teams.length, teams }, null, 2)); return; }
|
|
5794
|
-
log(`# leerness team (1.9.371, UR-0073 Phase A) — 에이전트 팀 정의 (opt-in · 정의 전용)`);
|
|
5795
|
-
if (!teams.length) { log(' (정의된 팀 없음) — leerness team add <id> --name "..." --purpose "..." --personas a,b --members claude,codex'); return; }
|
|
5796
|
-
for (const t of teams) log(` • ${t.id}${t.name ? ' — ' + t.name : ''} [${t.status || 'active'}/${t.schedule || 'manual'}] personas:${(t.personas || []).join('|') || '-'} members:${(t.members || []).join('|') || '-'}`);
|
|
5797
|
-
log(`\n ⓘ 정의 전용 — 자동 실행 없음. 실행(리뷰/배포/블로그)은 향후 opt-in 단계.`);
|
|
5798
|
-
return;
|
|
5799
|
-
}
|
|
5800
|
-
if (sub === 'add') {
|
|
5801
|
-
const teamId = String(id || '').toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^[.\-]+|[.\-]+$/g, '');
|
|
5802
|
-
if (!teamId || teamId.includes('..')) { fail(`무효한 team id: "${id}" (영숫자/._- 만)`); return; }
|
|
5803
|
-
if (teams.some(t => t.id === teamId)) { warn(`이미 존재: ${teamId} (제거 후 재정의: leerness team remove ${teamId})`); return; }
|
|
5804
|
-
const splitCsv = v => (v && v !== true) ? String(v).split(',').map(s => s.trim()).filter(Boolean) : [];
|
|
5805
|
-
const sched = arg('--schedule', 'manual');
|
|
5806
|
-
const validSched = new Set(['manual', 'every-session', 'daily', 'weekly']);
|
|
5807
|
-
const team = {
|
|
5808
|
-
id: teamId,
|
|
5809
|
-
name: arg('--name', '') === true ? '' : arg('--name', ''),
|
|
5810
|
-
purpose: arg('--purpose', '') === true ? '' : arg('--purpose', ''),
|
|
5811
|
-
personas: splitCsv(arg('--personas', null)),
|
|
5812
|
-
members: splitCsv(arg('--members', null)),
|
|
5813
|
-
schedule: validSched.has(sched) ? sched : 'manual',
|
|
5814
|
-
deployCommand: arg('--deploy', '') === true ? '' : arg('--deploy', ''), // 1.9.376 (Phase D): 사용자 설정 배포 명령 (실행은 게이트)
|
|
5815
|
-
status: 'active',
|
|
5816
|
-
createdAt: now()
|
|
5817
|
-
};
|
|
5818
|
-
teams.push(team);
|
|
5819
|
-
_saveTeams(root, teams);
|
|
5820
|
-
ok(`team 정의: ${teamId} (personas:${team.personas.length} members:${team.members.length} schedule:${team.schedule})`);
|
|
5821
|
-
log(` ⓘ 정의 전용 — 자동 실행 없음. 목록: leerness team list`);
|
|
5822
|
-
return;
|
|
5823
|
-
}
|
|
5824
|
-
if (sub === 'show') {
|
|
5825
|
-
const t = teams.find(x => x.id === id);
|
|
5826
|
-
if (!t) { fail(`team 없음: ${id}`); return; }
|
|
5827
|
-
if (json) { log(JSON.stringify(t, null, 2)); return; }
|
|
5828
|
-
log(`# team ${t.id}`);
|
|
5829
|
-
log(` name: ${t.name || ''}`);
|
|
5830
|
-
log(` purpose: ${t.purpose || ''}`);
|
|
5831
|
-
log(` personas: ${(t.personas || []).join(', ') || '-'}`);
|
|
5832
|
-
log(` members: ${(t.members || []).join(', ') || '-'}`);
|
|
5833
|
-
log(` schedule: ${t.schedule || 'manual'} · status: ${t.status || 'active'}`);
|
|
5834
|
-
log(` deploy: ${t.deployCommand || '-'}`);
|
|
5835
|
-
return;
|
|
5836
|
-
}
|
|
5837
|
-
if (sub === 'remove') {
|
|
5838
|
-
const before = teams.length;
|
|
5839
|
-
const next = teams.filter(x => x.id !== id);
|
|
5840
|
-
if (next.length === before) { warn(`team 없음: ${id}`); return; }
|
|
5841
|
-
_saveTeams(root, next);
|
|
5842
|
-
ok(`team 제거: ${id}`);
|
|
5843
|
-
return;
|
|
5844
|
-
}
|
|
5845
|
-
// 1.9.372 (UR-0073 Phase B): team preview — dry-run 실행 계획 미리보기 (실제 dispatch/spawn/배포 없음).
|
|
5846
|
-
if (sub === 'preview') {
|
|
5847
|
-
const t = teams.find(x => x.id === id);
|
|
5848
|
-
if (!t) { fail(`team 없음: ${id}`); return; }
|
|
5849
|
-
const plan = _composeTeamPlan(t, arg('--task', null));
|
|
5850
|
-
if (json) { log(JSON.stringify({ version: VERSION, dryRun: true, ...plan }, null, 2)); return; }
|
|
5851
|
-
log(`# team preview ${t.id} (1.9.372, UR-0073 Phase B) — dry-run (실제 실행 없음)`);
|
|
5852
|
-
log(` task: ${plan.task}`);
|
|
5853
|
-
log(` schedule: ${plan.schedule} · members: ${plan.memberCount}`);
|
|
5854
|
-
if (!plan.steps.length) { warn('members 없음 — leerness team add <id> --members claude,codex 로 지정'); return; }
|
|
5855
|
-
log(` 실행 계획 (미리보기 · 자동 실행 안 함):`);
|
|
5856
|
-
for (const s of plan.steps) {
|
|
5857
|
-
log(` • ${s.member}${s.personas.length ? ' [' + s.personas.join(',') + ']' : ''}`);
|
|
5858
|
-
log(` ↳ ${s.suggestedCommand}`);
|
|
5859
|
-
}
|
|
5860
|
-
log(`\n ⓘ dry-run — 실제 dispatch/배포 없음. 위 명령을 검토 후 직접 실행하거나, 향후 Phase C(스케줄)/D(배포)에서 게이트 적용.`);
|
|
5861
|
-
return;
|
|
5862
|
-
}
|
|
5863
|
-
// 1.9.376 (UR-0073 Phase D): team deploy — 사용자 설정 deployCommand 실행. 안전: dry-run 기본 + --yes + LEERNESS_TEAM_DEPLOY=1 이중 게이트 + shell-guard.
|
|
5864
|
-
if (sub === 'deploy') {
|
|
5865
|
-
const t = teams.find(x => x.id === id);
|
|
5866
|
-
if (!t) { fail(`team 없음: ${id}`); return; }
|
|
5867
|
-
const gate = _teamDeployGate(t, { yes: has('--yes'), envOn: process.env.LEERNESS_TEAM_DEPLOY === '1' });
|
|
5868
|
-
if (json) { log(JSON.stringify({ version: VERSION, teamId: t.id, ...gate }, null, 2)); if (gate.mode !== 'execute') return; }
|
|
5869
|
-
if (gate.mode === 'no-command') { fail(`team '${t.id}' deployCommand 미설정 — leerness team add ${t.id} --deploy "<배포 명령>"`); return; }
|
|
5870
|
-
if (gate.mode === 'dry-run') {
|
|
5871
|
-
log(`# team deploy ${t.id} (1.9.376, UR-0073 Phase D) — dry-run (실행 없음)`);
|
|
5872
|
-
log(` 배포 명령: ${gate.command}`);
|
|
5873
|
-
log(` ⓘ ${gate.message}`);
|
|
5874
|
-
log(` 실행: LEERNESS_TEAM_DEPLOY=1 leerness team deploy ${t.id} --yes (셸 호환성 점검 후 실행)`);
|
|
5875
|
-
return;
|
|
5876
|
-
}
|
|
5877
|
-
if (gate.mode === 'gated') { fail(`${gate.message} — dry-run 으로 먼저 검토: leerness team deploy ${t.id}`); return; }
|
|
5878
|
-
// execute: shell-guard 정적 점검(advisory) 후 spawn
|
|
5879
|
-
try {
|
|
5880
|
-
const ctx = _detectShellCtx();
|
|
5881
|
-
const guard = _shellGuardAnalyze ? _shellGuardAnalyze(gate.command, ctx) : null;
|
|
5882
|
-
if (guard && guard.findings && guard.findings.length) {
|
|
5883
|
-
warn(`shell-guard 경고 ${guard.findings.length}건 (배포 명령): ${guard.findings.map(f => f.rule || f.kind || f).join(', ')}`);
|
|
5884
|
-
}
|
|
5885
|
-
} catch {}
|
|
5886
|
-
log(`# team deploy ${t.id} — 실행 (LEERNESS_TEAM_DEPLOY=1 + --yes)`);
|
|
5887
|
-
log(` $ ${gate.command}`);
|
|
5888
|
-
const r = cp.spawnSync(gate.command, { cwd: root, shell: true, stdio: 'inherit', timeout: 600000 });
|
|
5889
|
-
if (r.status === 0) ok(`team deploy 완료: ${t.id} (exit 0)`);
|
|
5890
|
-
else { fail(`team deploy 실패: ${t.id} (exit ${r.status})`); }
|
|
5891
|
-
return;
|
|
5892
|
-
}
|
|
5893
|
-
fail(`알 수 없는 team 하위명령: ${sub} (list|add|show|remove|preview|deploy)`);
|
|
5894
|
-
}
|
|
5789
|
+
// 1.9.389 (UR-0025 큰 핸들러 모듈화 2번째): teamCmd 핸들러를 lib/team.js 로 분리.
|
|
5790
|
+
// harness 는 deps(VERSION · 공유 저장 _loadTeams/_saveTeams · _detectShellCtx · argv 파서 arg/has)를 구성해 위임(thin wrapper). 호출부/동작 무변경.
|
|
5791
|
+
const _team = require('../lib/team');
|
|
5792
|
+
function teamCmd(root, sub, id, opts = {}) { return _team.teamCmd(root, sub, id, opts, { VERSION, _loadTeams, _saveTeams, _detectShellCtx, arg, has }); }
|
|
5895
5793
|
|
|
5896
5794
|
// 1.9.112: 전용 lessons.md (Memory Write Surface 5번째)
|
|
5897
5795
|
const lessonsPath = root => path.join(root, '.harness/lessons.md');
|
|
@@ -6771,110 +6669,16 @@ function verify(root) {
|
|
|
6771
6669
|
if (failures.length) { failures.forEach(f => fail(f)); process.exitCode = 1; } else ok('verify passed');
|
|
6772
6670
|
}
|
|
6773
6671
|
// 1.9.356 (UR-0075 Phase B): migrate audit — 비파괴 dry-run 스키마 drift 리포트(버전/ canonical JSON 백필/누락 파일). 실제 변경 X.
|
|
6774
|
-
|
|
6775
|
-
|
|
6776
|
-
|
|
6777
|
-
|
|
6778
|
-
|
|
6779
|
-
if (!projVer) findings.push({ kind: 'no-version', detail: 'HARNESS_VERSION 없음 (미초기화 또는 아주 구버전)', action: `leerness migrate --path ${root}` });
|
|
6780
|
-
else if (compareVer(projVer, VERSION) < 0) findings.push({ kind: 'version-drift', detail: `${projVer} → ${VERSION}`, action: `leerness update --yes --path ${root}` });
|
|
6781
|
-
// canonical JSON 백필 필요 (구 MD-only → decisions.json/lessons.json)
|
|
6782
|
-
if (exists(decisionsPath(root)) && !exists(decisionsJsonPath(root)) && _loadDecisions(root).length > 0) findings.push({ kind: 'canonical-pending', detail: 'decisions.md → decisions.json 백필 예정', action: 'decision add/drop 또는 migrate 시 자동' });
|
|
6783
|
-
if (exists(lessonsPath(root)) && !exists(lessonsJsonPath(root)) && _loadLessons(root).length > 0) findings.push({ kind: 'canonical-pending', detail: 'lessons.md → lessons.json 백필 예정', action: 'lesson save/drop 또는 migrate 시 자동' });
|
|
6784
|
-
// 누락 예상 파일 (현재 버전 required set 기준)
|
|
6785
|
-
const required = REQUIRED_WORKSPACE_FILES; // 1.9.380 (UR-0025): lib/catalogs 단일출처
|
|
6786
|
-
for (const f of required) if (!exists(path.join(root, f))) findings.push({ kind: 'missing-file', detail: f, action: 'migrate 가 생성' });
|
|
6787
|
-
if (opts.json) { log(JSON.stringify({ version: VERSION, root, projectVersion: projVer, willChange: findings.length, findings }, null, 2)); return; }
|
|
6788
|
-
log(`# leerness migrate audit (1.9.356, UR-0075 dry-run) — 실제 변경 없음`);
|
|
6789
|
-
log(` 대상: ${root}`);
|
|
6790
|
-
log(` 프로젝트 버전: ${projVer || '(없음)'} · 도구 버전: ${VERSION}`);
|
|
6791
|
-
if (!findings.length) { ok('마이그레이션 필요 없음 — 최신 스키마 정합'); return; }
|
|
6792
|
-
log(` 예상 변경 ${findings.length}건:`);
|
|
6793
|
-
for (const f of findings) log(` • [${f.kind}] ${f.detail}${f.action ? ` → ${f.action}` : ''}`);
|
|
6794
|
-
log(`\n 적용: leerness update --yes --path ${root} · 안전 가이드: leerness migrate --guide`);
|
|
6795
|
-
}
|
|
6796
|
-
// UR-0075 Phase C (1.9.357): migrate apply — audit가 찾은 '안전 항목'(canonical 백필)만 비파괴 적용.
|
|
6797
|
-
// 기본 dry-run(변경 없음) · --yes 로 실제 적용. version-drift/missing-file 은 apply 범위 외(수동 안내).
|
|
6798
|
-
function migrateApplyCmd(root, opts = {}) {
|
|
6799
|
-
root = absRoot(root);
|
|
6800
|
-
const apply = !!opts.yes;
|
|
6801
|
-
const applied = []; // 안전하게 적용 가능(또는 dry-run 예정): canonical 백필
|
|
6802
|
-
const skipped = []; // apply 범위 외: 수동 조치 필요
|
|
6803
|
-
// canonical-pending: MD 항목 존재 + JSON 부재 → load(MD 백필)→save(canonical JSON + MD 정규화). idempotent(UR-0053).
|
|
6804
|
-
if (exists(decisionsPath(root)) && !exists(decisionsJsonPath(root)) && _loadDecisions(root).length > 0) {
|
|
6805
|
-
if (apply) _saveDecisions(root, _loadDecisions(root));
|
|
6806
|
-
applied.push({ kind: 'canonical-backfill', detail: 'decisions.md → decisions.json' });
|
|
6807
|
-
}
|
|
6808
|
-
if (exists(lessonsPath(root)) && !exists(lessonsJsonPath(root)) && _loadLessons(root).length > 0) {
|
|
6809
|
-
if (apply) _saveLessons(root, _loadLessons(root));
|
|
6810
|
-
applied.push({ kind: 'canonical-backfill', detail: 'lessons.md → lessons.json' });
|
|
6811
|
-
}
|
|
6812
|
-
// version-drift / 누락 파일 — apply 가 안전하게 in-place 처리 불가(npm 재설치/재초기화 필요) → 수동 안내.
|
|
6813
|
-
const hvPath = path.join(root, '.harness', 'HARNESS_VERSION');
|
|
6814
|
-
const projVer = exists(hvPath) ? read(hvPath).trim() : null;
|
|
6815
|
-
if (!projVer) skipped.push({ kind: 'no-version', detail: 'HARNESS_VERSION 없음', reason: `leerness migrate --path ${root}` });
|
|
6816
|
-
else if (compareVer(projVer, VERSION) < 0) skipped.push({ kind: 'version-drift', detail: `${projVer} → ${VERSION}`, reason: `leerness update --yes --path ${root}` });
|
|
6817
|
-
const required = REQUIRED_WORKSPACE_FILES; // 1.9.380 (UR-0025): lib/catalogs 단일출처
|
|
6818
|
-
for (const f of required) if (!exists(path.join(root, f))) skipped.push({ kind: 'missing-file', detail: f, reason: 'leerness migrate / init' });
|
|
6819
|
-
if (opts.json) { log(JSON.stringify({ version: VERSION, root, dryRun: !apply, appliedCount: apply ? applied.length : 0, applied, skipped }, null, 2)); return; }
|
|
6820
|
-
log(`# leerness migrate apply (1.9.357, UR-0075 Phase C)${apply ? '' : ' — dry-run (변경 없음 · --yes 로 적용)'}`);
|
|
6821
|
-
log(` 대상: ${root}`);
|
|
6822
|
-
if (!applied.length && !skipped.length) { ok('적용할 항목 없음 — 최신 스키마 정합'); return; }
|
|
6823
|
-
if (applied.length) {
|
|
6824
|
-
log(apply ? ` ✓ 적용 ${applied.length}건 (canonical 백필 · MD→JSON, MD 정규화):` : ` 적용 예정 ${applied.length}건 (canonical 백필 · MD→JSON, MD 정규화):`);
|
|
6825
|
-
for (const a of applied) log(` • ${a.detail}`);
|
|
6826
|
-
}
|
|
6827
|
-
if (skipped.length) {
|
|
6828
|
-
log(` ⚠ 수동 필요 ${skipped.length}건 (apply 범위 외 — 안전상 자동 변경 안 함):`);
|
|
6829
|
-
for (const s of skipped) log(` • [${s.kind}] ${s.detail} → ${s.reason}`);
|
|
6830
|
-
}
|
|
6831
|
-
if (!apply && applied.length) log(`\n 적용: leerness migrate apply --path ${root} --yes`);
|
|
6832
|
-
}
|
|
6833
|
-
// UR-0075 Phase D (1.9.358): migrate plan — 임시폴더에 현재 버전을 설치(서브프로세스 격리)한 뒤
|
|
6834
|
-
// 프로젝트의 .harness 코어 관리 파일과 비교해 정확한 마이그레이션 플랜을 산출. 읽기 전용(프로젝트 미변경).
|
|
6835
|
-
// 사용자 비전("임시 폴더에 leerness 설치 후 기존 프로젝트 내용을 정확히 마이그레이션")의 진단 단계.
|
|
6836
|
-
function migratePlanCmd(root, opts = {}) {
|
|
6837
|
-
root = absRoot(root);
|
|
6838
|
-
const plan = { version: VERSION, root, projectVersion: null, versionDrift: null, canonicalPending: [], missingFiles: [], tempInstallOk: false, willChange: 0 };
|
|
6839
|
-
const hvPath = path.join(root, '.harness', 'HARNESS_VERSION');
|
|
6840
|
-
plan.projectVersion = exists(hvPath) ? read(hvPath).trim() : null;
|
|
6841
|
-
if (plan.projectVersion && compareVer(plan.projectVersion, VERSION) < 0) plan.versionDrift = `${plan.projectVersion} → ${VERSION}`;
|
|
6842
|
-
if (exists(decisionsPath(root)) && !exists(decisionsJsonPath(root)) && _loadDecisions(root).length > 0) plan.canonicalPending.push('decisions.md → decisions.json');
|
|
6843
|
-
if (exists(lessonsPath(root)) && !exists(lessonsJsonPath(root)) && _loadLessons(root).length > 0) plan.canonicalPending.push('lessons.md → lessons.json');
|
|
6844
|
-
// 임시폴더에 현재 버전 init → 생성되는 .harness 코어 파일(depth-1) 비교 (서브프로세스 격리, stdout 미오염)
|
|
6845
|
-
let tmp = null;
|
|
6846
|
-
try {
|
|
6847
|
-
tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-plan-'));
|
|
6848
|
-
const langPath = path.join(root, '.harness', 'LANGUAGE');
|
|
6849
|
-
const lang = exists(langPath) ? (read(langPath).trim() || 'ko') : 'ko';
|
|
6850
|
-
const r = cp.spawnSync(process.execPath, [__filename, 'init', tmp, '--yes', '--language', lang, '--no-banner'], { encoding: 'utf8', timeout: 60000 });
|
|
6851
|
-
plan.tempInstallOk = r.status === 0;
|
|
6852
|
-
if (plan.tempInstallOk) {
|
|
6853
|
-
const tmpHarness = path.join(tmp, '.harness');
|
|
6854
|
-
let tmpFiles = [];
|
|
6855
|
-
try { tmpFiles = fs.readdirSync(tmpHarness, { withFileTypes: true }).filter(e => e.isFile()).map(e => '.harness/' + e.name); } catch {}
|
|
6856
|
-
for (const top of ['AGENTS.md', 'CLAUDE.md']) if (exists(path.join(tmp, top))) tmpFiles.push(top);
|
|
6857
|
-
for (const f of tmpFiles) if (!exists(path.join(root, f))) plan.missingFiles.push(f);
|
|
6858
|
-
}
|
|
6859
|
-
} catch {}
|
|
6860
|
-
finally { if (tmp) { try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} } }
|
|
6861
|
-
plan.willChange = plan.missingFiles.length + plan.canonicalPending.length + (plan.versionDrift ? 1 : 0);
|
|
6862
|
-
if (opts.json) { log(JSON.stringify(plan, null, 2)); return; }
|
|
6863
|
-
log(`# leerness migrate plan (1.9.358, UR-0075 Phase D) — 임시폴더 설치 후 비교 · 읽기 전용(프로젝트 미변경)`);
|
|
6864
|
-
log(` 대상: ${root}`);
|
|
6865
|
-
log(` 프로젝트 버전: ${plan.projectVersion || '(없음)'} · 도구 버전: ${VERSION}`);
|
|
6866
|
-
if (!plan.tempInstallOk) warn('임시폴더 설치 실패 — 파일 비교 생략 (버전/canonical 만 보고)');
|
|
6867
|
-
if (!plan.willChange) { ok('마이그레이션 필요 없음 — 최신 스키마 정합'); return; }
|
|
6868
|
-
log(` 예상 변경 ${plan.willChange}건:`);
|
|
6869
|
-
if (plan.versionDrift) log(` • [version-drift] ${plan.versionDrift} → leerness update --yes --path ${root}`);
|
|
6870
|
-
for (const c of plan.canonicalPending) log(` • [canonical-pending] ${c} → leerness migrate apply --path ${root} --yes`);
|
|
6871
|
-
if (plan.missingFiles.length) {
|
|
6872
|
-
log(` • [missing-file] ${plan.missingFiles.length}건 (현재 버전이 생성하는 관리 파일 누락):`);
|
|
6873
|
-
for (const f of plan.missingFiles.slice(0, 20)) log(` - ${f}`);
|
|
6874
|
-
if (plan.missingFiles.length > 20) log(` … 외 ${plan.missingFiles.length - 20}건`);
|
|
6875
|
-
}
|
|
6876
|
-
log(`\n 전체 적용: leerness update --yes --path ${root} · canonical만: leerness migrate apply --path ${root} --yes`);
|
|
6672
|
+
// 1.9.388 (UR-0025 큰 핸들러 모듈화): migrate audit/apply/plan 핸들러를 lib/migrate.js 로 분리.
|
|
6673
|
+
// harness 는 deps(VERSION·canonical 메모리 함수·카탈로그·compareVer·harness 경로)를 1회 구성해 위임(thin wrapper). 호출부/동작 무변경.
|
|
6674
|
+
const _migrate = require('../lib/migrate');
|
|
6675
|
+
function _migrateDeps() {
|
|
6676
|
+
return { VERSION, compareVer, REQUIRED_WORKSPACE_FILES, decisionsPath, decisionsJsonPath, lessonsPath, lessonsJsonPath, _loadDecisions, _saveDecisions, _loadLessons, _saveLessons, harnessPath: __filename };
|
|
6877
6677
|
}
|
|
6678
|
+
function migrateAuditCmd(root, opts = {}) { return _migrate.migrateAuditCmd(root, opts, _migrateDeps()); }
|
|
6679
|
+
function migrateApplyCmd(root, opts = {}) { return _migrate.migrateApplyCmd(root, opts, _migrateDeps()); }
|
|
6680
|
+
function migratePlanCmd(root, opts = {}) { return _migrate.migratePlanCmd(root, opts, _migrateDeps()); }
|
|
6681
|
+
|
|
6878
6682
|
// UR-0074 (1.9.359): install-safety — 패키지 설치 안전 프로필 투명 공개 (외부리뷰 설치 신뢰성 우려 대응).
|
|
6879
6683
|
// leerness 의 핵심 안전 속성(0 런타임 의존성 · 0 install-time 스크립트)을 사실 그대로 보고 + 안전 설치 워크플로 안내.
|
|
6880
6684
|
// 회귀 가드 역할도 겸함: 누군가 런타임 deps/install hook 를 추가하면 selftest/e2e 가 실패시켜 의식적 결정을 강제.
|
package/lib/migrate.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// lib/migrate.js — 크로스버전 마이그레이션 핸들러 (UR-0075: audit / apply / plan).
|
|
2
|
+
// 1.9.388 (UR-0025 큰 핸들러 모듈화): bin/harness.js 에서 분리한 첫 실제 핸들러 모듈.
|
|
3
|
+
// - I/O 프리미티브: ./io (absRoot/exists/read/log/ok/warn).
|
|
4
|
+
// - harness 고유 의존(VERSION · canonical 메모리 함수 · REQUIRED_WORKSPACE_FILES · compareVer · harness 경로)은
|
|
5
|
+
// deps 객체로 주입(DI). harness 는 thin wrapper 로 deps 를 1회 구성해 위임 → 호출부/동작 무변경.
|
|
6
|
+
'use strict';
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
const cp = require('child_process');
|
|
11
|
+
const { absRoot, exists, read, log, ok, warn } = require('./io');
|
|
12
|
+
|
|
13
|
+
// UR-0075 Phase B (1.9.356): migrate audit — 비파괴 dry-run 스키마 drift 리포트 (실제 변경 없음).
|
|
14
|
+
function migrateAuditCmd(root, opts = {}, deps = {}) {
|
|
15
|
+
const { VERSION, compareVer, REQUIRED_WORKSPACE_FILES, decisionsPath, decisionsJsonPath, lessonsPath, lessonsJsonPath, _loadDecisions, _loadLessons } = deps;
|
|
16
|
+
root = absRoot(root);
|
|
17
|
+
const findings = [];
|
|
18
|
+
const hvPath = path.join(root, '.harness', 'HARNESS_VERSION');
|
|
19
|
+
const projVer = exists(hvPath) ? read(hvPath).trim() : null;
|
|
20
|
+
if (!projVer) findings.push({ kind: 'no-version', detail: 'HARNESS_VERSION 없음 (미초기화 또는 아주 구버전)', action: `leerness migrate --path ${root}` });
|
|
21
|
+
else if (compareVer(projVer, VERSION) < 0) findings.push({ kind: 'version-drift', detail: `${projVer} → ${VERSION}`, action: `leerness update --yes --path ${root}` });
|
|
22
|
+
// canonical JSON 백필 필요 (구 MD-only → decisions.json/lessons.json)
|
|
23
|
+
if (exists(decisionsPath(root)) && !exists(decisionsJsonPath(root)) && _loadDecisions(root).length > 0) findings.push({ kind: 'canonical-pending', detail: 'decisions.md → decisions.json 백필 예정', action: 'decision add/drop 또는 migrate 시 자동' });
|
|
24
|
+
if (exists(lessonsPath(root)) && !exists(lessonsJsonPath(root)) && _loadLessons(root).length > 0) findings.push({ kind: 'canonical-pending', detail: 'lessons.md → lessons.json 백필 예정', action: 'lesson save/drop 또는 migrate 시 자동' });
|
|
25
|
+
// 누락 예상 파일 (현재 버전 required set 기준)
|
|
26
|
+
const required = REQUIRED_WORKSPACE_FILES; // 1.9.380 (UR-0025): lib/catalogs 단일출처
|
|
27
|
+
for (const f of required) if (!exists(path.join(root, f))) findings.push({ kind: 'missing-file', detail: f, action: 'migrate 가 생성' });
|
|
28
|
+
if (opts.json) { log(JSON.stringify({ version: VERSION, root, projectVersion: projVer, willChange: findings.length, findings }, null, 2)); return; }
|
|
29
|
+
log(`# leerness migrate audit (1.9.356, UR-0075 dry-run) — 실제 변경 없음`);
|
|
30
|
+
log(` 대상: ${root}`);
|
|
31
|
+
log(` 프로젝트 버전: ${projVer || '(없음)'} · 도구 버전: ${VERSION}`);
|
|
32
|
+
if (!findings.length) { ok('마이그레이션 필요 없음 — 최신 스키마 정합'); return; }
|
|
33
|
+
log(` 예상 변경 ${findings.length}건:`);
|
|
34
|
+
for (const f of findings) log(` • [${f.kind}] ${f.detail}${f.action ? ` → ${f.action}` : ''}`);
|
|
35
|
+
log(`\n 적용: leerness update --yes --path ${root} · 안전 가이드: leerness migrate --guide`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// UR-0075 Phase C (1.9.357): migrate apply — audit가 찾은 '안전 항목'(canonical 백필)만 비파괴 적용.
|
|
39
|
+
// 기본 dry-run(변경 없음) · --yes 로 실제 적용. version-drift/missing-file 은 apply 범위 외(수동 안내).
|
|
40
|
+
function migrateApplyCmd(root, opts = {}, deps = {}) {
|
|
41
|
+
const { VERSION, compareVer, REQUIRED_WORKSPACE_FILES, decisionsPath, decisionsJsonPath, lessonsPath, lessonsJsonPath, _loadDecisions, _saveDecisions, _loadLessons, _saveLessons } = deps;
|
|
42
|
+
root = absRoot(root);
|
|
43
|
+
const apply = !!opts.yes;
|
|
44
|
+
const applied = []; // 안전하게 적용 가능(또는 dry-run 예정): canonical 백필
|
|
45
|
+
const skipped = []; // apply 범위 외: 수동 조치 필요
|
|
46
|
+
// canonical-pending: MD 항목 존재 + JSON 부재 → load(MD 백필)→save(canonical JSON + MD 정규화). idempotent(UR-0053).
|
|
47
|
+
if (exists(decisionsPath(root)) && !exists(decisionsJsonPath(root)) && _loadDecisions(root).length > 0) {
|
|
48
|
+
if (apply) _saveDecisions(root, _loadDecisions(root));
|
|
49
|
+
applied.push({ kind: 'canonical-backfill', detail: 'decisions.md → decisions.json' });
|
|
50
|
+
}
|
|
51
|
+
if (exists(lessonsPath(root)) && !exists(lessonsJsonPath(root)) && _loadLessons(root).length > 0) {
|
|
52
|
+
if (apply) _saveLessons(root, _loadLessons(root));
|
|
53
|
+
applied.push({ kind: 'canonical-backfill', detail: 'lessons.md → lessons.json' });
|
|
54
|
+
}
|
|
55
|
+
// version-drift / 누락 파일 — apply 가 안전하게 in-place 처리 불가(npm 재설치/재초기화 필요) → 수동 안내.
|
|
56
|
+
const hvPath = path.join(root, '.harness', 'HARNESS_VERSION');
|
|
57
|
+
const projVer = exists(hvPath) ? read(hvPath).trim() : null;
|
|
58
|
+
if (!projVer) skipped.push({ kind: 'no-version', detail: 'HARNESS_VERSION 없음', reason: `leerness migrate --path ${root}` });
|
|
59
|
+
else if (compareVer(projVer, VERSION) < 0) skipped.push({ kind: 'version-drift', detail: `${projVer} → ${VERSION}`, reason: `leerness update --yes --path ${root}` });
|
|
60
|
+
const required = REQUIRED_WORKSPACE_FILES; // 1.9.380 (UR-0025): lib/catalogs 단일출처
|
|
61
|
+
for (const f of required) if (!exists(path.join(root, f))) skipped.push({ kind: 'missing-file', detail: f, reason: 'leerness migrate / init' });
|
|
62
|
+
if (opts.json) { log(JSON.stringify({ version: VERSION, root, dryRun: !apply, appliedCount: apply ? applied.length : 0, applied, skipped }, null, 2)); return; }
|
|
63
|
+
log(`# leerness migrate apply (1.9.357, UR-0075 Phase C)${apply ? '' : ' — dry-run (변경 없음 · --yes 로 적용)'}`);
|
|
64
|
+
log(` 대상: ${root}`);
|
|
65
|
+
if (!applied.length && !skipped.length) { ok('적용할 항목 없음 — 최신 스키마 정합'); return; }
|
|
66
|
+
if (applied.length) {
|
|
67
|
+
log(apply ? ` ✓ 적용 ${applied.length}건 (canonical 백필 · MD→JSON, MD 정규화):` : ` 적용 예정 ${applied.length}건 (canonical 백필 · MD→JSON, MD 정규화):`);
|
|
68
|
+
for (const a of applied) log(` • ${a.detail}`);
|
|
69
|
+
}
|
|
70
|
+
if (skipped.length) {
|
|
71
|
+
log(` ⚠ 수동 필요 ${skipped.length}건 (apply 범위 외 — 안전상 자동 변경 안 함):`);
|
|
72
|
+
for (const s of skipped) log(` • [${s.kind}] ${s.detail} → ${s.reason}`);
|
|
73
|
+
}
|
|
74
|
+
if (!apply && applied.length) log(`\n 적용: leerness migrate apply --path ${root} --yes`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// UR-0075 Phase D (1.9.358): migrate plan — 임시폴더에 현재 버전을 설치(서브프로세스 격리)한 뒤
|
|
78
|
+
// 프로젝트의 .harness 코어 관리 파일과 비교해 정확한 마이그레이션 플랜을 산출. 읽기 전용(프로젝트 미변경).
|
|
79
|
+
function migratePlanCmd(root, opts = {}, deps = {}) {
|
|
80
|
+
const { VERSION, compareVer, decisionsPath, decisionsJsonPath, lessonsPath, lessonsJsonPath, _loadDecisions, _loadLessons, harnessPath } = deps;
|
|
81
|
+
root = absRoot(root);
|
|
82
|
+
const plan = { version: VERSION, root, projectVersion: null, versionDrift: null, canonicalPending: [], missingFiles: [], tempInstallOk: false, willChange: 0 };
|
|
83
|
+
const hvPath = path.join(root, '.harness', 'HARNESS_VERSION');
|
|
84
|
+
plan.projectVersion = exists(hvPath) ? read(hvPath).trim() : null;
|
|
85
|
+
if (plan.projectVersion && compareVer(plan.projectVersion, VERSION) < 0) plan.versionDrift = `${plan.projectVersion} → ${VERSION}`;
|
|
86
|
+
if (exists(decisionsPath(root)) && !exists(decisionsJsonPath(root)) && _loadDecisions(root).length > 0) plan.canonicalPending.push('decisions.md → decisions.json');
|
|
87
|
+
if (exists(lessonsPath(root)) && !exists(lessonsJsonPath(root)) && _loadLessons(root).length > 0) plan.canonicalPending.push('lessons.md → lessons.json');
|
|
88
|
+
// 임시폴더에 현재 버전 init → 생성되는 .harness 코어 파일(depth-1) 비교 (서브프로세스 격리, stdout 미오염)
|
|
89
|
+
let tmp = null;
|
|
90
|
+
try {
|
|
91
|
+
tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-plan-'));
|
|
92
|
+
const langPath = path.join(root, '.harness', 'LANGUAGE');
|
|
93
|
+
const lang = exists(langPath) ? (read(langPath).trim() || 'ko') : 'ko';
|
|
94
|
+
const r = cp.spawnSync(process.execPath, [harnessPath, 'init', tmp, '--yes', '--language', lang, '--no-banner'], { encoding: 'utf8', timeout: 60000 });
|
|
95
|
+
plan.tempInstallOk = r.status === 0;
|
|
96
|
+
if (plan.tempInstallOk) {
|
|
97
|
+
const tmpHarness = path.join(tmp, '.harness');
|
|
98
|
+
let tmpFiles = [];
|
|
99
|
+
try { tmpFiles = fs.readdirSync(tmpHarness, { withFileTypes: true }).filter(e => e.isFile()).map(e => '.harness/' + e.name); } catch {}
|
|
100
|
+
for (const top of ['AGENTS.md', 'CLAUDE.md']) if (exists(path.join(tmp, top))) tmpFiles.push(top);
|
|
101
|
+
for (const f of tmpFiles) if (!exists(path.join(root, f))) plan.missingFiles.push(f);
|
|
102
|
+
}
|
|
103
|
+
} catch {}
|
|
104
|
+
finally { if (tmp) { try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} } }
|
|
105
|
+
plan.willChange = plan.missingFiles.length + plan.canonicalPending.length + (plan.versionDrift ? 1 : 0);
|
|
106
|
+
if (opts.json) { log(JSON.stringify(plan, null, 2)); return; }
|
|
107
|
+
log(`# leerness migrate plan (1.9.358, UR-0075 Phase D) — 임시폴더 설치 후 비교 · 읽기 전용(프로젝트 미변경)`);
|
|
108
|
+
log(` 대상: ${root}`);
|
|
109
|
+
log(` 프로젝트 버전: ${plan.projectVersion || '(없음)'} · 도구 버전: ${VERSION}`);
|
|
110
|
+
if (!plan.tempInstallOk) warn('임시폴더 설치 실패 — 파일 비교 생략 (버전/canonical 만 보고)');
|
|
111
|
+
if (!plan.willChange) { ok('마이그레이션 필요 없음 — 최신 스키마 정합'); return; }
|
|
112
|
+
log(` 예상 변경 ${plan.willChange}건:`);
|
|
113
|
+
if (plan.versionDrift) log(` • [version-drift] ${plan.versionDrift} → leerness update --yes --path ${root}`);
|
|
114
|
+
for (const c of plan.canonicalPending) log(` • [canonical-pending] ${c} → leerness migrate apply --path ${root} --yes`);
|
|
115
|
+
if (plan.missingFiles.length) {
|
|
116
|
+
log(` • [missing-file] ${plan.missingFiles.length}건 (현재 버전이 생성하는 관리 파일 누락):`);
|
|
117
|
+
for (const f of plan.missingFiles.slice(0, 20)) log(` - ${f}`);
|
|
118
|
+
if (plan.missingFiles.length > 20) log(` … 외 ${plan.missingFiles.length - 20}건`);
|
|
119
|
+
}
|
|
120
|
+
log(`\n 전체 적용: leerness update --yes --path ${root} · canonical만: leerness migrate apply --path ${root} --yes`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports = { migrateAuditCmd, migrateApplyCmd, migratePlanCmd };
|
package/lib/team.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// lib/team.js — 에이전트 팀 정의 핸들러 (UR-0073: list/add/show/remove/preview/deploy).
|
|
2
|
+
// 1.9.389 (UR-0025 큰 핸들러 모듈화 2번째): bin/harness.js 에서 teamCmd 분리.
|
|
3
|
+
// - I/O 프리미티브: ./io. 순수 로직: ./pure-utils(_composeTeamPlan/_teamDeployGate). 정적분석: ./analyzers(_shellGuardAnalyze).
|
|
4
|
+
// - harness 고유 의존(VERSION · 공유 저장 함수 _loadTeams/_saveTeams · _detectShellCtx · argv 파서 arg/has)은 deps 로 주입(DI).
|
|
5
|
+
// - _loadTeams/_saveTeams 는 handoff(team reminders)도 쓰는 공유 함수라 harness 에 유지하고 주입만 받음.
|
|
6
|
+
'use strict';
|
|
7
|
+
const cp = require('child_process');
|
|
8
|
+
const { absRoot, log, ok, warn, fail, now } = require('./io');
|
|
9
|
+
const { _composeTeamPlan, _teamDeployGate } = require('./pure-utils');
|
|
10
|
+
const { _shellGuardAnalyze } = require('./analyzers');
|
|
11
|
+
|
|
12
|
+
function teamCmd(root, sub, id, opts = {}, deps = {}) {
|
|
13
|
+
const { VERSION, _loadTeams, _saveTeams, _detectShellCtx, arg, has } = deps;
|
|
14
|
+
root = absRoot(root);
|
|
15
|
+
const json = opts.json || has('--json');
|
|
16
|
+
const teams = _loadTeams(root);
|
|
17
|
+
sub = sub || 'list';
|
|
18
|
+
if (sub === 'list') {
|
|
19
|
+
if (json) { log(JSON.stringify({ version: VERSION, root, count: teams.length, teams }, null, 2)); return; }
|
|
20
|
+
log(`# leerness team (1.9.371, UR-0073 Phase A) — 에이전트 팀 정의 (opt-in · 정의 전용)`);
|
|
21
|
+
if (!teams.length) { log(' (정의된 팀 없음) — leerness team add <id> --name "..." --purpose "..." --personas a,b --members claude,codex'); return; }
|
|
22
|
+
for (const t of teams) log(` • ${t.id}${t.name ? ' — ' + t.name : ''} [${t.status || 'active'}/${t.schedule || 'manual'}] personas:${(t.personas || []).join('|') || '-'} members:${(t.members || []).join('|') || '-'}`);
|
|
23
|
+
log(`\n ⓘ 정의 전용 — 자동 실행 없음. 실행(리뷰/배포/블로그)은 향후 opt-in 단계.`);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (sub === 'add') {
|
|
27
|
+
const teamId = String(id || '').toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^[.\-]+|[.\-]+$/g, '');
|
|
28
|
+
if (!teamId || teamId.includes('..')) { fail(`무효한 team id: "${id}" (영숫자/._- 만)`); return; }
|
|
29
|
+
if (teams.some(t => t.id === teamId)) { warn(`이미 존재: ${teamId} (제거 후 재정의: leerness team remove ${teamId})`); return; }
|
|
30
|
+
const splitCsv = v => (v && v !== true) ? String(v).split(',').map(s => s.trim()).filter(Boolean) : [];
|
|
31
|
+
const sched = arg('--schedule', 'manual');
|
|
32
|
+
const validSched = new Set(['manual', 'every-session', 'daily', 'weekly']);
|
|
33
|
+
const team = {
|
|
34
|
+
id: teamId,
|
|
35
|
+
name: arg('--name', '') === true ? '' : arg('--name', ''),
|
|
36
|
+
purpose: arg('--purpose', '') === true ? '' : arg('--purpose', ''),
|
|
37
|
+
personas: splitCsv(arg('--personas', null)),
|
|
38
|
+
members: splitCsv(arg('--members', null)),
|
|
39
|
+
schedule: validSched.has(sched) ? sched : 'manual',
|
|
40
|
+
deployCommand: arg('--deploy', '') === true ? '' : arg('--deploy', ''), // 1.9.376 (Phase D): 사용자 설정 배포 명령 (실행은 게이트)
|
|
41
|
+
status: 'active',
|
|
42
|
+
createdAt: now()
|
|
43
|
+
};
|
|
44
|
+
teams.push(team);
|
|
45
|
+
_saveTeams(root, teams);
|
|
46
|
+
ok(`team 정의: ${teamId} (personas:${team.personas.length} members:${team.members.length} schedule:${team.schedule})`);
|
|
47
|
+
log(` ⓘ 정의 전용 — 자동 실행 없음. 목록: leerness team list`);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (sub === 'show') {
|
|
51
|
+
const t = teams.find(x => x.id === id);
|
|
52
|
+
if (!t) { fail(`team 없음: ${id}`); return; }
|
|
53
|
+
if (json) { log(JSON.stringify(t, null, 2)); return; }
|
|
54
|
+
log(`# team ${t.id}`);
|
|
55
|
+
log(` name: ${t.name || ''}`);
|
|
56
|
+
log(` purpose: ${t.purpose || ''}`);
|
|
57
|
+
log(` personas: ${(t.personas || []).join(', ') || '-'}`);
|
|
58
|
+
log(` members: ${(t.members || []).join(', ') || '-'}`);
|
|
59
|
+
log(` schedule: ${t.schedule || 'manual'} · status: ${t.status || 'active'}`);
|
|
60
|
+
log(` deploy: ${t.deployCommand || '-'}`);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (sub === 'remove') {
|
|
64
|
+
const before = teams.length;
|
|
65
|
+
const next = teams.filter(x => x.id !== id);
|
|
66
|
+
if (next.length === before) { warn(`team 없음: ${id}`); return; }
|
|
67
|
+
_saveTeams(root, next);
|
|
68
|
+
ok(`team 제거: ${id}`);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
// 1.9.372 (UR-0073 Phase B): team preview — dry-run 실행 계획 미리보기 (실제 dispatch/spawn/배포 없음).
|
|
72
|
+
if (sub === 'preview') {
|
|
73
|
+
const t = teams.find(x => x.id === id);
|
|
74
|
+
if (!t) { fail(`team 없음: ${id}`); return; }
|
|
75
|
+
const plan = _composeTeamPlan(t, arg('--task', null));
|
|
76
|
+
if (json) { log(JSON.stringify({ version: VERSION, dryRun: true, ...plan }, null, 2)); return; }
|
|
77
|
+
log(`# team preview ${t.id} (1.9.372, UR-0073 Phase B) — dry-run (실제 실행 없음)`);
|
|
78
|
+
log(` task: ${plan.task}`);
|
|
79
|
+
log(` schedule: ${plan.schedule} · members: ${plan.memberCount}`);
|
|
80
|
+
if (!plan.steps.length) { warn('members 없음 — leerness team add <id> --members claude,codex 로 지정'); return; }
|
|
81
|
+
log(` 실행 계획 (미리보기 · 자동 실행 안 함):`);
|
|
82
|
+
for (const s of plan.steps) {
|
|
83
|
+
log(` • ${s.member}${s.personas.length ? ' [' + s.personas.join(',') + ']' : ''}`);
|
|
84
|
+
log(` ↳ ${s.suggestedCommand}`);
|
|
85
|
+
}
|
|
86
|
+
log(`\n ⓘ dry-run — 실제 dispatch/배포 없음. 위 명령을 검토 후 직접 실행하거나, 향후 Phase C(스케줄)/D(배포)에서 게이트 적용.`);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
// 1.9.376 (UR-0073 Phase D): team deploy — 사용자 설정 deployCommand 실행. 안전: dry-run 기본 + --yes + LEERNESS_TEAM_DEPLOY=1 이중 게이트 + shell-guard.
|
|
90
|
+
if (sub === 'deploy') {
|
|
91
|
+
const t = teams.find(x => x.id === id);
|
|
92
|
+
if (!t) { fail(`team 없음: ${id}`); return; }
|
|
93
|
+
const gate = _teamDeployGate(t, { yes: has('--yes'), envOn: process.env.LEERNESS_TEAM_DEPLOY === '1' });
|
|
94
|
+
if (json) { log(JSON.stringify({ version: VERSION, teamId: t.id, ...gate }, null, 2)); if (gate.mode !== 'execute') return; }
|
|
95
|
+
if (gate.mode === 'no-command') { fail(`team '${t.id}' deployCommand 미설정 — leerness team add ${t.id} --deploy "<배포 명령>"`); return; }
|
|
96
|
+
if (gate.mode === 'dry-run') {
|
|
97
|
+
log(`# team deploy ${t.id} (1.9.376, UR-0073 Phase D) — dry-run (실행 없음)`);
|
|
98
|
+
log(` 배포 명령: ${gate.command}`);
|
|
99
|
+
log(` ⓘ ${gate.message}`);
|
|
100
|
+
log(` 실행: LEERNESS_TEAM_DEPLOY=1 leerness team deploy ${t.id} --yes (셸 호환성 점검 후 실행)`);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (gate.mode === 'gated') { fail(`${gate.message} — dry-run 으로 먼저 검토: leerness team deploy ${t.id}`); return; }
|
|
104
|
+
// execute: shell-guard 정적 점검(advisory) 후 spawn
|
|
105
|
+
try {
|
|
106
|
+
const ctx = _detectShellCtx();
|
|
107
|
+
const guard = _shellGuardAnalyze ? _shellGuardAnalyze(gate.command, ctx) : null;
|
|
108
|
+
if (guard && guard.findings && guard.findings.length) {
|
|
109
|
+
warn(`shell-guard 경고 ${guard.findings.length}건 (배포 명령): ${guard.findings.map(f => f.rule || f.kind || f).join(', ')}`);
|
|
110
|
+
}
|
|
111
|
+
} catch {}
|
|
112
|
+
log(`# team deploy ${t.id} — 실행 (LEERNESS_TEAM_DEPLOY=1 + --yes)`);
|
|
113
|
+
log(` $ ${gate.command}`);
|
|
114
|
+
const r = cp.spawnSync(gate.command, { cwd: root, shell: true, stdio: 'inherit', timeout: 600000 });
|
|
115
|
+
if (r.status === 0) ok(`team deploy 완료: ${t.id} (exit 0)`);
|
|
116
|
+
else { fail(`team deploy 실패: ${t.id} (exit ${r.status})`); }
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
fail(`알 수 없는 team 하위명령: ${sub} (list|add|show|remove|preview|deploy)`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
module.exports = { teamCmd };
|