leerness 1.36.29 → 1.36.31
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 +21 -0
- package/README.md +4 -4
- package/bin/leerness.js +91 -14
- package/lib/graph.js +100 -2
- package/lib/team.js +18 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.36.31 — 2026-07-15 — 동시성 직렬화 — 병렬 에이전트의 lost update 차단 (codex 미검토표면 헌트 이연분 #1/#2/#5/#7)
|
|
4
|
+
|
|
5
|
+
1.36.28 헌트에서 이연했던 동시성 P1 계열. 기존 `_withLock`(원자 배타 생성 + stale 회수 + 재진입 안전)을 재사용 — 새 락 발명 아님.
|
|
6
|
+
|
|
7
|
+
- **#1 P1 — 병렬 `state start` run 유실/충돌**: 두 병렬 start 가 같은 run-0001 을 배정해 한쪽 goal 이 덮이거나 EPERM 크래시(codex 12-trial 실측 6~9/12 발화). → stateCmd 변경 sub(start/record/verify/handoff) 전체를 state.json 락으로 직렬화(load→카운터→run 저장→state 저장이 한 임계구역). 실측: 병렬 8 start → run 파일 8/8·고유 goal 8/8·EPERM 0.
|
|
8
|
+
- **#2 P1 — 병렬 `team add` 무성 유실**: 20 병렬 add 전부 exit 0 인데 16 팀만 잔존(4건 무성 소실). → 락 안 재로드→중복검사→추가→저장 + Windows 일시 EPERM(락 밖 병렬 읽기가 파일을 잠깐 여는 사이 원자 rename 실패) 소규모 재시도. 실측: **20/20 잔존 ×2회**.
|
|
9
|
+
- **#5 P2 — record↔handoff 경합 stale handoff**: 같은 state.json 락으로 함께 직렬화. 실측 6-trial 에러 0.
|
|
10
|
+
- **#7 P2 — pre-wake-audit 테이블형 in-progress 미인식**: `status: in-progress`(YAML형)만 매치해 CLI 가 생성하는 canonical 테이블(`| T-XXXX | in-progress |`)을 못 봄 — session-resume 과 동일 정규식 추가(YAML형 호환 유지). 실측: 3일 stale 테이블 픽스처에서 `stale-in-progress` 감지.
|
|
11
|
+
- **검증**: selftest 301/301, codex 원 재현 스크립트 재실행(4건 전부 ✓, #2 는 2회 반복), 게이트 e2e, 게시본 클린룸. 이로써 1.36.28 헌트 10건 중 **9건 처리**(잔여: state 손상 시 quarantine 심화 — 저가치).
|
|
12
|
+
|
|
13
|
+
## 1.36.30 — 2026-07-15 — 온톨로지 그래프 기본 활성 + roadmap 통합 + 기능 토글 (사용자 요청)
|
|
14
|
+
|
|
15
|
+
`leerness.html`(온톨로지 그래프)가 **컨트롤 타워**가 된다: install/session-close 자동생성 대상이 roadmap.html → leerness.html 로 전환(기본 활성), roadmap 기능은 그래프 파일의 탭으로 통합, 그래프 뷰에서 leerness 기능(gate 등)을 토글.
|
|
16
|
+
|
|
17
|
+
- **탭 3종**: 그래프(기존 force-directed) | **로드맵**(milestone 진행률바 + 상태별 task + skills/rules — roadmap.html 기능 흡수, 수동 `leerness roadmap` 은 호환 유지) | **⚙ 토글**(스위치 UI).
|
|
18
|
+
- **`leerness toggle list|set <id> on|off`** + `.harness/toggles.json` — 레지스트리: `gate` / `lens` / `auto-graph` / `delegation-brief`. 기본 전부 ON.
|
|
19
|
+
- **AI 준수 배선**: gate·lens 는 토글 OFF 시 "🎛 토글 OFF — 건너뜀" 명시 후 exit 0(조용한 무시 아님, --json 은 `skipped:true`) · `delegation-brief` OFF 면 dispatch/multi 가 브리프 미접두 · **handoff 헤드라인에 `🎛 토글 OFF: …` 노출** — AI 가 세션 시작 시 상태를 보고 준수.
|
|
20
|
+
- **⚙ 탭 정직성**: 정적 HTML 은 파일 쓰기가 불가 — 스위치 클릭 시 적용 명령(`leerness toggle set …`)을 표시+복사 버튼으로 제공하고 UI 에 명시.
|
|
21
|
+
- **canonical feature 그래프 인식** (codex 미검토표면 #9 편입): `leerness feature add/link` 가 만드는 `## F-XXXX` 블록을 그래프 노드/엣지로 렌더 — 종전엔 ad-hoc "A -> B" 라인만 파싱해 CLI 생성 그래프가 HTML 에 0 노드였다.
|
|
22
|
+
- **검증**: selftest 300/300(토글 저장/로드+기본 ON+레지스트리 4종+HTML 탭·데이터·임베드 JS 문법+배선 5종), 실측 6종(init 자동생성 전환 / 탭 임베드 / toggle CLI / gate·lens 스킵 / handoff 🎛 / brief 토글 0↔1 / canonical feature 노드), 게이트 e2e, 게시본 클린룸.
|
|
23
|
+
|
|
3
24
|
## 1.36.29 — 2026-07-15 — `agents multi` 위임 브리프 접두 — 백그라운드 AI 도 leerness 프로토콜을 받도록 (사용자 보고)
|
|
4
25
|
|
|
5
26
|
사용자 질문("백그라운드에서 호출된 AI 가 leerness 를 참조하는가?")에 대한 조사 결과: `agents dispatch` 는 1.35.6 부터 "[leerness 위임 프로토콜]" 브리프(handoff 적재→task 등록→evidence→verify-claim→session close)를 자동 접두하지만, **`agents multi`(--execute 실 spawn 포함)는 원문 그대로 위임** — 병렬 백그라운드 AI 들이 프로토콜을 받지 못하는 실제 갭이었다.
|
package/README.md
CHANGED
|
@@ -121,7 +121,7 @@ MIT
|
|
|
121
121
|
<!-- leerness:project-readme:start -->
|
|
122
122
|
## Leerness Project Harness
|
|
123
123
|
|
|
124
|
-
이 프로젝트는 Leerness v1.36.
|
|
124
|
+
이 프로젝트는 Leerness v1.36.31 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
|
|
125
125
|
|
|
126
126
|
### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
|
|
127
127
|
|
|
@@ -175,7 +175,7 @@ leerness memory restore decision <date|title>
|
|
|
175
175
|
|
|
176
176
|
### MCP server (외부 AI 통합)
|
|
177
177
|
|
|
178
|
-
Leerness v1.36.
|
|
178
|
+
Leerness v1.36.31는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **86개 도구**를 노출:
|
|
179
179
|
|
|
180
180
|
```jsonc
|
|
181
181
|
// 카테고리별
|
|
@@ -196,7 +196,7 @@ Leerness v1.36.29는 stdio JSON-RPC MCP server를 내장합니다 — Claude Cod
|
|
|
196
196
|
`<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
|
|
197
197
|
1) 다음 라운드 후보 선정 → 2) 코드 변경 → 3) stress-v* 신규 작성 + 누적 회귀 → 4) e2e 219/219 → 5) npm pack + git tag + GitHub release → 6) main 자동 push → 7) session close → 8) 다음 라운드 예약.
|
|
198
198
|
|
|
199
|
-
현재 누적: **70 라운드 (1.9.40 → 1.36.
|
|
199
|
+
현재 누적: **70 라운드 (1.9.40 → 1.36.31)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
|
|
200
200
|
|
|
201
201
|
### 성능 가이드
|
|
202
202
|
|
|
@@ -234,6 +234,6 @@ leerness release pack --close --auto-main-push
|
|
|
234
234
|
- `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
|
|
235
235
|
- `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
|
|
236
236
|
|
|
237
|
-
Last synced by Leerness v1.36.
|
|
237
|
+
Last synced by Leerness v1.36.31: 2026-07-15
|
|
238
238
|
<!-- leerness:project-readme:end -->
|
|
239
239
|
|
package/bin/leerness.js
CHANGED
|
@@ -34,7 +34,7 @@ const { CAPABILITY_SURFACE, POWERFUL_COMMANDS, ADAPTERS, REUSE_CATEGORIES, REUSE
|
|
|
34
34
|
const { tokenizeForRank: _tokenizeForRank, expandQuery: _expandQuery, scoreHits: _scoreHits, suggestTerms: _suggestTerms } = require('../lib/search-core'); // 1.36.23: memory search 랭킹 코어(순수·0-deps)
|
|
35
35
|
const { findCorruptedStateJson: _findCorruptedStateJson } = require('../lib/state-integrity'); // 1.36.1 (클린룸 리뷰 FN): .harness/*.json 상태 무결성 (audit/health/check 공유)
|
|
36
36
|
|
|
37
|
-
const VERSION = '1.36.
|
|
37
|
+
const VERSION = '1.36.31';
|
|
38
38
|
|
|
39
39
|
// 1.9.290 (UR-0037, Codex gpt-5.5 #4 수렴): CLI 전용 부작용은 require 시 실행하지 않는다.
|
|
40
40
|
// 이전: warning listener 제거 / NODE_OPTIONS 변경 / chcp IIFE 가 top-level 즉시 실행 → require('harness') 시 호스트 프로세스 오염.
|
|
@@ -2949,7 +2949,7 @@ function _selfTestCases() {
|
|
|
2949
2949
|
const briefOk = typeof b === 'string' && b.includes('leerness handoff .') && b.includes('task add') && b.includes('verify-claim') && b.includes('session close') && b.includes('--evidence') && !b.includes('`') && !b.includes('$') && !b.includes('"');
|
|
2950
2950
|
// (2) dispatch 와이어: lib/agents.js 가 --raw 옵트아웃으로 브리프를 접두하고, DI 로 _harnessBrief 를 받음.
|
|
2951
2951
|
const agentsSrc = read(path.join(__dirname, '..', 'lib', 'agents.js'));
|
|
2952
|
-
const wired = /has\('--raw'\)[\s\S]{0,120}_harnessBrief\(\) \+ task/.test(agentsSrc) &&
|
|
2952
|
+
const wired = /has\('--raw'\)[\s\S]{0,120}_harnessBrief\(\) \+ task/.test(agentsSrc) && read(__filename).includes("_harnessBrief: _tgl.toggleOn(root, 'delegation-brief')"); // 1.36.30: delegation-brief 토글 연동 형태 (리터럴 매치)
|
|
2953
2953
|
// (3) bench stdin hang 수정: codex exec 가 열린 stdin 파이프에서 EOF 대기(실측) → spawn stdio ignore.
|
|
2954
2954
|
const stdinFixed = agentsSrc.includes("{ shell: true, stdio: ['ignore', 'pipe', 'pipe'] }");
|
|
2955
2955
|
// (4) 1.36.29 (사용자 보고): multi 도 브리프 접두 — --execute 실 spawn(_cliChat) + 명령 목록/JSON 3지점 전부 briefTask.
|
|
@@ -3072,6 +3072,52 @@ function _selfTestCases() {
|
|
|
3072
3072
|
const owOk = p._managedMerge('x.md', next, prev, '.', new Set(['x.md'])) === next;
|
|
3073
3073
|
return survived && idem && legacyOk && cleanOk && owOk;
|
|
3074
3074
|
} },
|
|
3075
|
+
{ name: '온톨로지 그래프 통합+토글 (1.36.30, 사용자 요청): 탭 3종(그래프/로드맵/⚙)+기본 자동생성 전환+toggle CLI+gate/lens/brief 준수 — 행위검사', run: () => {
|
|
3076
|
+
const tg = require('../lib/toggles');
|
|
3077
|
+
if (typeof tg.toggleCmd !== 'function' || typeof tg.loadToggles !== 'function') return false;
|
|
3078
|
+
const regOk = ['gate', 'lens', 'auto-graph', 'delegation-brief'].every(k => !!tg.TOGGLE_REGISTRY[k]);
|
|
3079
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_tgl_'));
|
|
3080
|
+
let behavOk = false, htmlOk = false;
|
|
3081
|
+
try {
|
|
3082
|
+
fs.mkdirSync(path.join(tmp, '.harness'), { recursive: true });
|
|
3083
|
+
// 토글 저장/로드 행위 + 기본 ON
|
|
3084
|
+
const d0 = tg.loadToggles(tmp);
|
|
3085
|
+
const defOn = Object.values(d0).every(v => v === true);
|
|
3086
|
+
tg.saveToggles(tmp, Object.assign({}, d0, { gate: false }));
|
|
3087
|
+
const offOk = tg.loadToggles(tmp).gate === false && tg.toggleOn(tmp, 'lens') === true;
|
|
3088
|
+
behavOk = defOn && offOk;
|
|
3089
|
+
// 그래프 HTML: 탭 3종 + roadmap/toggles 데이터 + 임베드 JS 문법
|
|
3090
|
+
const m = require('../lib/graph');
|
|
3091
|
+
const out = path.join(tmp, 'leerness.html');
|
|
3092
|
+
m.graphHtmlCmd(tmp, { _roadmapData, _loadDecisions, _loadLessons, _parseFeatureGraph, _loadToggles: tg.loadToggles, _toggleRegistry: tg.TOGGLE_REGISTRY, quiet: true }, out);
|
|
3093
|
+
const html = fs.readFileSync(out, 'utf8');
|
|
3094
|
+
const o = '<scr' + 'ipt>', c = '</scr' + 'ipt>';
|
|
3095
|
+
const js = html.slice(html.indexOf(o) + o.length, html.lastIndexOf(c));
|
|
3096
|
+
let synOk = false; try { new Function(js); synOk = true; } catch {}
|
|
3097
|
+
htmlOk = html.includes('data-v="roadmap"') && html.includes('data-v="toggles"') && html.includes('toggleRegistry') && synOk;
|
|
3098
|
+
} catch { behavOk = false; } finally { try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} }
|
|
3099
|
+
// 배선 소스가드: 자동생성이 leerness.html(온톨로지)로 전환 + gate/lens 토글 준수 + toggle dispatch
|
|
3100
|
+
const s = read(__filename);
|
|
3101
|
+
const wired = s.includes('온톨로지 그래프 자동 갱신 (' + '${trigger})')
|
|
3102
|
+
&& s.includes("_tgl.toggleOn(root, 'gate')") && s.includes("_tgl.toggleOn(root, 'lens')")
|
|
3103
|
+
&& s.includes("cmd === 'toggle'") && s.includes("_tgl.toggleOn(root, 'delegation-brief')");
|
|
3104
|
+
return regOk && behavOk && htmlOk && wired;
|
|
3105
|
+
} },
|
|
3106
|
+
{ name: '동시성 직렬화 (1.36.31, codex #1/#2/#5/#7): state 변경 락 + team add 락내 재로드 + EPERM 재시도 + pre-wake 테이블 매처 — 소스가드+행위', run: () => {
|
|
3107
|
+
const s = read(__filename);
|
|
3108
|
+
// #1/#5: stateCmd 변경 sub 가 state.json 락으로 재귀 직렬화 (재진입 가드 포함 — 무한재귀 방지 형태)
|
|
3109
|
+
const stateLock = s.includes("const _mut = ['start', 'record', 'verify', 'handoff'].includes(sub || '')")
|
|
3110
|
+
&& s.includes("_withLock(path.join(_leernessStateDir(root), 'state.json'), () => stateCmd(root, sub, ...args))")
|
|
3111
|
+
&& s.includes("!_heldLocks.has(path.join(_leernessStateDir(root), 'state.json') + '.lock')");
|
|
3112
|
+
// #2: team add 가 락 안 재로드→중복검사→저장 + EPERM 재시도
|
|
3113
|
+
const teamSrc = read(path.join(path.dirname(__filename), '..', 'lib', 'team.js'));
|
|
3114
|
+
const teamLock = teamSrc.includes('const cur = _loadTeams(root);') && teamSrc.includes("deps._withLock(require('path').join(root, '.harness', 'teams.json'), _doAdd)") && teamSrc.includes("e.code === 'EPERM'");
|
|
3115
|
+
const teamWired = s.includes('_detectShellCtx, arg, has, _withLock }');
|
|
3116
|
+
// #7: pre-wake 매처가 테이블형+YAML형 모두 (행위)
|
|
3117
|
+
const both = ('| T-0001 | in-progress | x |\nstatus: in-progress\n'.match(/^\|\s*T-\d{4,}\s*\|\s*in-progress\s*\|/gm) || []).length === 1;
|
|
3118
|
+
const pwWired = /_runPreWakeAudit[\s\S]{0,2200}\^\\\|\\s\*T-\\d\{4,\}\\s\*\\\|\\s\*in-progress/.test(s);
|
|
3119
|
+
return stateLock && teamLock && teamWired && both && pwWired;
|
|
3120
|
+
} },
|
|
3075
3121
|
{ name: 'debug 렌즈 (1.36.27, obra/superpowers systematic-debugging): 자기질문 6문항 ko/en 락스텝 + affects 유효 + route bugfix 힌트 + 파일매핑 미확장 — 행위검사', run: () => {
|
|
3076
3122
|
const d = LENS_CATALOG.debug;
|
|
3077
3123
|
if (!d) return false;
|
|
@@ -4442,7 +4488,7 @@ function _selfTestCases() {
|
|
|
4442
4488
|
const rowsEn = sc.includes("t('- 없음', '- none')") && sc.includes('_retroOneLine(agg, uiLang)');
|
|
4443
4489
|
const retroEn = bin.includes('function _retroOneLine(agg, lang)') && bin.includes('`done ${done}/${total}') && bin.includes('decisions ${agg.decisionBlocks} accumulated');
|
|
4444
4490
|
const roadmapEn = bin.includes('roadmap.html auto-updated (${trigger})');
|
|
4445
|
-
const koPreserved = bin.includes('완료 ${done}/${total}') && bin.includes('
|
|
4491
|
+
const koPreserved = bin.includes('완료 ${done}/${total}') && bin.includes('온톨로지 그래프 자동 갱신 (${trigger})') && sc.includes("t('- 없음', '- none')"); // ko 인자 보존
|
|
4446
4492
|
return rowsEn && retroEn && roadmapEn && koPreserved;
|
|
4447
4493
|
} },
|
|
4448
4494
|
{ name: 'CLI 영어화 Phase 9 (1.25.2, UR-0010): health 진단 영어/한국어 보존 + uiLang 주입 (소스 가드)', run: () => {
|
|
@@ -5404,6 +5450,11 @@ function lensCmd(domain, opts = {}) {
|
|
|
5404
5450
|
if (domain != null) domain = String(domain).trim().toLowerCase();
|
|
5405
5451
|
// 1.19.3: 내장 + 프로젝트 커스텀(.harness/quality-lenses.json) 병합 catalog.
|
|
5406
5452
|
const root = opts.root || arg('--path', process.cwd());
|
|
5453
|
+
// 1.36.30: 토글 준수 (⚙ 탭 / leerness toggle set lens off)
|
|
5454
|
+
if (!_tgl.toggleOn(root, 'lens')) {
|
|
5455
|
+
if (jsonMode) { log(JSON.stringify({ ok: true, skipped: true, reason: 'toggle_off', hint: 'leerness toggle set lens on' })); return; }
|
|
5456
|
+
log('🎛 lens 토글 OFF — 건너뜀 (재활성: leerness toggle set lens on)'); return;
|
|
5457
|
+
}
|
|
5407
5458
|
const L = _uiLang(root); const t = (ko, en) => (L === 'en' ? en : ko); // 1.24.2 (UR-0010 Phase 8): lens 영어 opt-in
|
|
5408
5459
|
const catalog = _effectiveLensCatalog(root);
|
|
5409
5460
|
if (domain && !catalog[domain]) {
|
|
@@ -5698,7 +5749,10 @@ function _runPreWakeAudit(root) {
|
|
|
5698
5749
|
const stat = fs.statSync(ptPath);
|
|
5699
5750
|
const ageMin = Math.floor((Date.now() - stat.mtimeMs) / 60000);
|
|
5700
5751
|
// in-progress 태그가 있는데 24h+ 갱신 없음
|
|
5701
|
-
|
|
5752
|
+
// 1.36.31 (codex 미검토표면 #7): CLI 가 생성하는 canonical 테이블형(| T-XXXX | in-progress |)을 못 보던 매처 —
|
|
5753
|
+
// session-resume 의 테이블 정규식과 동일하게 인식(구 YAML 형도 호환 유지).
|
|
5754
|
+
const inProgressCount = (content.match(/^\|\s*T-\d{4,}\s*\|\s*in-progress\s*\|/gm) || []).length
|
|
5755
|
+
+ (content.match(/status:\s*in-progress/gi) || []).length;
|
|
5702
5756
|
if (inProgressCount > 0 && ageMin > 24 * 60) {
|
|
5703
5757
|
audit.findings.warning.push({
|
|
5704
5758
|
kind: 'stale-in-progress',
|
|
@@ -7875,7 +7929,8 @@ function _saveTeams(root, teams) {
|
|
|
7875
7929
|
// 1.9.389 (UR-0025 큰 핸들러 모듈화 2번째): teamCmd 핸들러를 lib/team.js 로 분리.
|
|
7876
7930
|
// harness 는 deps(VERSION · 공유 저장 _loadTeams/_saveTeams · _detectShellCtx · argv 파서 arg/has)를 구성해 위임(thin wrapper). 호출부/동작 무변경.
|
|
7877
7931
|
const _team = require('../lib/team');
|
|
7878
|
-
|
|
7932
|
+
const _tgl = require('../lib/toggles'); // 1.36.30: 기능 토글 (그래프 ⚙ 탭 연동 — gate/lens/auto-graph/delegation-brief)
|
|
7933
|
+
function teamCmd(root, sub, id, opts = {}) { return _team.teamCmd(root, sub, id, opts, { VERSION, _loadTeams, _saveTeams, _detectShellCtx, arg, has, _withLock }); } // 1.36.31: add 경합 락
|
|
7879
7934
|
|
|
7880
7935
|
// 1.9.112: 전용 lessons.md (Memory Write Surface 5번째)
|
|
7881
7936
|
const lessonsPath = root => path.join(root, '.harness/lessons.md');
|
|
@@ -9692,6 +9747,12 @@ function handoff(root) {
|
|
|
9692
9747
|
else if (briefGap) parts.push(t('📋 project-brief 미작성', '📋 project-brief unfilled'));
|
|
9693
9748
|
else if (planGap) parts.push(t('📋 plan Goal 미작성', '📋 plan Goal unfilled'));
|
|
9694
9749
|
} catch {}
|
|
9750
|
+
// 1.36.30: 기능 토글 OFF 헤드라인 — AI 가 이 상태를 보고 해당 기능을 스킵 준수 (그래프 ⚙ 탭과 동일 상태)
|
|
9751
|
+
try {
|
|
9752
|
+
const _tgs = _tgl.loadToggles(root);
|
|
9753
|
+
const _offs = Object.keys(_tgs).filter(k => _tgs[k] === false);
|
|
9754
|
+
if (_offs.length) parts.push(t(`🎛 토글 OFF: ${_offs.join(',')}`, `🎛 toggles OFF: ${_offs.join(',')}`));
|
|
9755
|
+
} catch {}
|
|
9695
9756
|
// 3) MCP 활동 누적
|
|
9696
9757
|
try {
|
|
9697
9758
|
const stats = _readUsageStats(root);
|
|
@@ -13573,7 +13634,7 @@ function _dispatchCommand(agentId, task, writeMode, model) {
|
|
|
13573
13634
|
|
|
13574
13635
|
const _agents = require('../lib/agents');
|
|
13575
13636
|
// 1.9.424 (UR-0025/UR-0125 큰 핸들러 모듈화 9번째): agentsCmd → lib/agents.js (DI 위임, rest→array)
|
|
13576
|
-
function agentsCmd(root, sub, ...args) { return _agents.agentsCmd(root, sub, args, { VERSION, has, arg, _agentSlashHint, _allProviders, _checkAgent, _cliChat, _dispatchCommand, _harnessBrief, _loadEnvFile, _normalizeRole, _policyEnforce, _readUserProviders, _recommendAgent, _recordRun, _resolveRole, _shellQuoteArg, lessonsPath, taskLogPath }); }
|
|
13637
|
+
function agentsCmd(root, sub, ...args) { return _agents.agentsCmd(root, sub, args, { VERSION, has, arg, _agentSlashHint, _allProviders, _checkAgent, _cliChat, _dispatchCommand, _harnessBrief: _tgl.toggleOn(root, 'delegation-brief') ? _harnessBrief : undefined, _loadEnvFile, _normalizeRole, _policyEnforce, _readUserProviders, _recommendAgent, _recordRun, _resolveRole, _shellQuoteArg, lessonsPath, taskLogPath }); } // 1.36.30: delegation-brief 토글 OFF 면 브리프 미접두(=--raw 경로)
|
|
13577
13638
|
|
|
13578
13639
|
function personaCmd(root, sub, idOrName, ...rest) {
|
|
13579
13640
|
root = absRoot(root || process.cwd());
|
|
@@ -13766,7 +13827,12 @@ async function selfCheck(root) {
|
|
|
13766
13827
|
// 1.9.2: 게이트 5종 한번에 실행 (verify + audit + scan secrets + encoding check + lazy detect).
|
|
13767
13828
|
function gate(root) {
|
|
13768
13829
|
root = absRoot(root);
|
|
13769
|
-
const jsonMode = has('--json');
|
|
13830
|
+
const jsonMode = has('--json');
|
|
13831
|
+
// 1.36.30: 토글 준수 — 그래프 ⚙ 탭/`leerness toggle set gate off` 시 스킵을 명시하고 exit 0 (조용한 무시 아님).
|
|
13832
|
+
if (!_tgl.toggleOn(root, 'gate')) {
|
|
13833
|
+
if (jsonMode) { log(JSON.stringify({ ok: true, skipped: true, reason: 'toggle_off', hint: 'leerness toggle set gate on' })); return; }
|
|
13834
|
+
log('🎛 gate 토글 OFF — 건너뜀 (재활성: leerness toggle set gate on)'); return;
|
|
13835
|
+
} // 외부리뷰 C2: --json 일관성 — 이전엔 텍스트 헤더+단계별 JSON 혼재로 파싱 불가. 단일 객체로 집계.
|
|
13770
13836
|
const checks = [];
|
|
13771
13837
|
let bad = 0;
|
|
13772
13838
|
// 1.33.3 (verify-claim+CI gate 슬라이스 강화): --claims opt-in — 모든 done 주장을 정밀 per-claim 검증(verify-claim --all)으로 추가(6번째). 기본 5체크는 무변경(기존 어댑터 회귀 0).
|
|
@@ -14871,11 +14937,14 @@ function _autoRoadmap(root, trigger) {
|
|
|
14871
14937
|
const cfg = _autoRoadmapConfig(root);
|
|
14872
14938
|
if (!cfg.enabled) return false;
|
|
14873
14939
|
if (trigger === 'data-change' && !cfg.onEveryChange) return false;
|
|
14874
|
-
|
|
14875
|
-
|
|
14876
|
-
|
|
14877
|
-
|
|
14878
|
-
|
|
14940
|
+
// 1.36.30 (사용자 요청): 자동 생성 대상을 roadmap.html → leerness.html(온톨로지 그래프 + 로드맵 탭 통합)로 전환.
|
|
14941
|
+
// roadmap.html 기능은 그래프 파일의 "로드맵" 탭에 통합됨. 수동 `leerness roadmap` 은 호환 유지.
|
|
14942
|
+
// 토글 auto-graph OFF 시 생성 안 함(⚙ 탭/`leerness toggle`).
|
|
14943
|
+
if (!_tgl.toggleOn(root, 'auto-graph')) return false;
|
|
14944
|
+
const outFile = path.resolve(cfg.outFile || path.join(root, 'leerness.html'));
|
|
14945
|
+
const s = _graph.graphHtmlCmd(root, { _roadmapData, _loadDecisions, _loadLessons, _parseFeatureGraph, _loadToggles: _tgl.loadToggles, _toggleRegistry: _tgl.TOGGLE_REGISTRY, quiet: true }, outFile);
|
|
14946
|
+
const _en = _uiLang(root) === 'en';
|
|
14947
|
+
log(_en ? `✓ ontology graph auto-updated (${trigger}) — ${rel(root, outFile)} (graph+roadmap+toggles)` : `✓ 온톨로지 그래프 자동 갱신 (${trigger}) — ${rel(root, outFile)} (그래프+로드맵+토글 탭)`);
|
|
14879
14948
|
return true;
|
|
14880
14949
|
} catch (e) {
|
|
14881
14950
|
warn((_uiLang(root) === 'en' ? 'roadmap auto-update failed: ' : 'roadmap 자동 갱신 실패: ') + (e && e.message ? e.message : e));
|
|
@@ -18941,6 +19010,13 @@ function contextCmd(root, opts = {}) {
|
|
|
18941
19010
|
}
|
|
18942
19011
|
function stateCmd(root, sub, ...args) {
|
|
18943
19012
|
root = absRoot(root || process.cwd());
|
|
19013
|
+
// 1.36.31 (codex 미검토표면 #1/#5): 변경 sub 는 state.json 락으로 전체 직렬화 — 병렬 start 가 같은 run-0001 을
|
|
19014
|
+
// 배정/덮어쓰거나(카운터 lost update), record↔handoff 경합으로 stale handoff 가 나오던 것을 차단.
|
|
19015
|
+
// load→카운터→run 저장→state 저장이 한 임계구역. _withLock 은 재진입 안전 + stale 락 자동 회수.
|
|
19016
|
+
const _mut = ['start', 'record', 'verify', 'handoff'].includes(sub || '');
|
|
19017
|
+
if (_mut && !_heldLocks.has(path.join(_leernessStateDir(root), 'state.json') + '.lock')) {
|
|
19018
|
+
return _withLock(path.join(_leernessStateDir(root), 'state.json'), () => stateCmd(root, sub, ...args));
|
|
19019
|
+
}
|
|
18944
19020
|
const json = has('--json');
|
|
18945
19021
|
sub = sub || 'show';
|
|
18946
19022
|
|
|
@@ -21194,14 +21270,14 @@ function doctorCmd(opts = {}) { return _diag.doctorCmd(opts, { VERSION, uiLang:
|
|
|
21194
21270
|
function whichCmd() { return _diag.whichCmd({ VERSION, uiLang: _uiLang(arg('--path', process.cwd())), has, harnessPath: __filename }); }
|
|
21195
21271
|
// 1.34.3 (T-0077): `graph --html` → lib/graph.js 온톨로지 HTML(leerness.html) 생성기 위임. 데이터는 in-process 로더 주입(자식 프로세스 셸링 X).
|
|
21196
21272
|
const _graph = require('../lib/graph');
|
|
21197
|
-
function graphHtmlCmd(root) { return _graph.graphHtmlCmd(root, { _roadmapData, _loadDecisions, _loadLessons, has, arg }); }
|
|
21273
|
+
function graphHtmlCmd(root) { return _graph.graphHtmlCmd(root, { _roadmapData, _loadDecisions, _loadLessons, _parseFeatureGraph, _loadToggles: _tgl.loadToggles, _toggleRegistry: _tgl.TOGGLE_REGISTRY, has, arg }); } // 1.36.30: 로드맵 탭 + ⚙ 토글 탭 + canonical feature
|
|
21198
21274
|
// 1.34.4 (T-0077 후속): handoff 시 leerness.html 자동 재생성 — opt-in(LEERNESS_AUTO_GRAPH=1, 기본 OFF / "Always-Off Opt-In"). 사용자 비전 "자동으로 작성되게" 충족. 비치명(try/catch) · 기본경로 무영향.
|
|
21199
21275
|
function _maybeAutoGraph(root) {
|
|
21200
21276
|
if (process.env.LEERNESS_AUTO_GRAPH !== '1') return;
|
|
21201
21277
|
try {
|
|
21202
21278
|
const r0 = absRoot(root || process.cwd());
|
|
21203
21279
|
if (!exists(path.join(r0, '.harness'))) return;
|
|
21204
|
-
const s = _graph.graphHtmlCmd(r0, { _roadmapData, _loadDecisions, _loadLessons, quiet: true });
|
|
21280
|
+
const s = _graph.graphHtmlCmd(r0, { _roadmapData, _loadDecisions, _loadLessons, _parseFeatureGraph, _loadToggles: _tgl.loadToggles, _toggleRegistry: _tgl.TOGGLE_REGISTRY, quiet: true });
|
|
21205
21281
|
if (!has('--json') && !has('--quiet') && !has('--compact')) log(`📊 ontology graph auto-regenerated: leerness.html (${s.nodes} nodes · ${s.edges} links) — LEERNESS_AUTO_GRAPH=1`); // --json 출력 오염 방지
|
|
21206
21282
|
} catch {}
|
|
21207
21283
|
}
|
|
@@ -21550,6 +21626,7 @@ async function main() {
|
|
|
21550
21626
|
if (cmd === 'milestones') return milestonesCmd(_resolveRoot(args[1]));
|
|
21551
21627
|
// 1.9.231: leerness pulse — 한 줄 종합 요약 (10 핵심 지표)
|
|
21552
21628
|
if (cmd === 'pulse') return pulseCmd(_resolveRoot(args[1]));
|
|
21629
|
+
if (cmd === 'toggle') return _tgl.toggleCmd(arg('--path', process.cwd()), args[1], args[2], args[3], { has, VERSION }); // 1.36.30: 기능 토글 (그래프 ⚙ 탭 연동)
|
|
21553
21630
|
if (cmd === 'lens') return lensCmd(args[1]); // 1.18.3 (UR-0003): 분야별 자기질문 품질 렌즈
|
|
21554
21631
|
// 1.9.233: leerness commands — 카테고리화된 전체 CLI 명령 목록
|
|
21555
21632
|
if (cmd === 'commands') return commandsCmd(arg('--path', process.cwd()));
|
package/lib/graph.js
CHANGED
|
@@ -39,9 +39,33 @@ canvas{position:fixed;inset:0;top:46px}
|
|
|
39
39
|
#panel .x{position:absolute;top:12px;right:14px;color:var(--mut);cursor:pointer;font-size:16px}
|
|
40
40
|
#hint{position:fixed;bottom:12px;left:16px;color:var(--mut);font-size:11px;opacity:.7}
|
|
41
41
|
#empty{position:fixed;inset:0;display:none;place-items:center;color:var(--mut);text-align:center}
|
|
42
|
+
#tabs{display:flex;gap:2px;border:1px solid var(--line);border-radius:8px;padding:2px}
|
|
43
|
+
.tab{padding:4px 12px;border-radius:6px;cursor:pointer;font-size:11px;color:var(--mut);user-select:none}
|
|
44
|
+
.tab.on{background:#1a2230;color:#fff}
|
|
45
|
+
.view{position:fixed;inset:0;top:46px;overflow:auto;background:var(--bg);display:none;padding:22px 26px;z-index:5}
|
|
46
|
+
.view.on{display:block}
|
|
47
|
+
.ms{border:1px solid var(--line);border-radius:10px;padding:14px 16px;margin:0 0 12px;background:var(--panel)}
|
|
48
|
+
.ms h3{margin:0 0 8px;font-size:13px;color:#fff}
|
|
49
|
+
.bar{height:7px;border-radius:5px;background:#1a2230;overflow:hidden;margin:6px 0}
|
|
50
|
+
.bar i{display:block;height:100%;background:var(--brand)}
|
|
51
|
+
.tk{display:flex;gap:8px;align-items:baseline;padding:3px 0;font-size:12px}
|
|
52
|
+
.tk .st{width:86px;flex:none;font-size:10px;text-transform:uppercase;letter-spacing:.04em}
|
|
53
|
+
.sec{color:var(--mut);font-size:11px;text-transform:uppercase;letter-spacing:.07em;margin:20px 0 8px}
|
|
54
|
+
.tgrow{display:flex;align-items:center;gap:14px;border:1px solid var(--line);border-radius:10px;padding:14px 16px;margin:0 0 10px;background:var(--panel)}
|
|
55
|
+
.sw2{position:relative;width:40px;height:22px;flex:none;border-radius:100px;background:#30363d;cursor:pointer;transition:background .15s}
|
|
56
|
+
.sw2.on{background:var(--brand)}
|
|
57
|
+
.sw2 i{position:absolute;top:3px;left:3px;width:16px;height:16px;border-radius:50%;background:#fff;transition:left .15s}
|
|
58
|
+
.sw2.on i{left:21px}
|
|
59
|
+
.tgname{font-weight:700;color:#fff;width:150px;flex:none}
|
|
60
|
+
.tgdesc{color:var(--mut);font-size:11px;flex:1}
|
|
61
|
+
.cmd{display:none;margin-top:8px;background:#0b0f14;border:1px solid var(--line);border-radius:7px;padding:8px 10px;font-size:11px;color:#7ee787}
|
|
62
|
+
.cmd.show{display:flex;align-items:center;gap:10px}
|
|
63
|
+
.cmd button{background:#1a2230;border:1px solid var(--line);color:var(--txt);border-radius:5px;padding:3px 9px;cursor:pointer;font:inherit;font-size:10px}
|
|
64
|
+
.note{color:var(--mut);font-size:11px;margin:14px 0 18px;line-height:1.6}
|
|
42
65
|
</style></head><body>
|
|
43
66
|
<div id="bar">
|
|
44
67
|
<div class="ttl"><span class="dot"></span><span id="proj">leerness</span><span style="color:var(--mut);font-weight:400">/ ontology</span></div>
|
|
68
|
+
<div id="tabs"><span class="tab on" data-v="graph">그래프</span><span class="tab" data-v="roadmap">로드맵</span><span class="tab" data-v="toggles">⚙ 토글</span></div>
|
|
45
69
|
<span class="stat" id="stat"></span>
|
|
46
70
|
<input id="search" placeholder="search nodes…" autocomplete="off">
|
|
47
71
|
<div id="chips"></div>
|
|
@@ -50,6 +74,8 @@ canvas{position:fixed;inset:0;top:46px}
|
|
|
50
74
|
<div id="panel"><span class="x" onclick="closePanel()">✕</span><div id="pbody"></div></div>
|
|
51
75
|
<div id="empty">No nodes — run <b>leerness handoff .</b> to populate the harness, then regenerate.</div>
|
|
52
76
|
<div id="hint">drag node · scroll zoom · drag bg pan · click node → details · search+Enter jump · f / dblclick fit · p export PNG · Esc close</div>
|
|
77
|
+
<div id="vroadmap" class="view"></div>
|
|
78
|
+
<div id="vtoggles" class="view"></div>
|
|
53
79
|
<script>
|
|
54
80
|
var DATA = /*__DATA__*/null;
|
|
55
81
|
var COLORS={task:'#58a6ff',plan:'#d29922',decision:'#39d0d8',lesson:'#e3b341',rule:'#bc8cff',skill:'#2dd4bf',feature:'#6e7681'};
|
|
@@ -144,6 +170,55 @@ document.getElementById('search').addEventListener('keydown',function(ev){ if(ev
|
|
|
144
170
|
function exportPng(){ try{ var t=document.createElement('canvas'); t.width=cv.width; t.height=cv.height; var tx=t.getContext('2d'); tx.fillStyle='#0a0d12'; tx.fillRect(0,0,t.width,t.height); tx.drawImage(cv,0,0); var a=document.createElement('a'); a.download='leerness-graph.png'; a.href=t.toDataURL('image/png'); a.click(); }catch(e){} }
|
|
145
171
|
window.addEventListener('keydown',function(ev){ if(ev.target&&ev.target.tagName==='INPUT')return; if(ev.key==='f'||ev.key==='F'){_fit=true;fitView();} else if(ev.key==='p'||ev.key==='P'){exportPng();} else if(ev.key==='Escape'){closePanel();} });
|
|
146
172
|
cv.addEventListener('dblclick',function(ev){ if(!hit(ev.offsetX,ev.offsetY)){_fit=true;fitView();} });
|
|
173
|
+
|
|
174
|
+
// ── 탭 (1.36.30): 그래프 | 로드맵 | ⚙ 토글 ──
|
|
175
|
+
var _views={roadmap:document.getElementById('vroadmap'),toggles:document.getElementById('vtoggles')};
|
|
176
|
+
document.querySelectorAll('.tab').forEach(function(t){t.onclick=function(){
|
|
177
|
+
document.querySelectorAll('.tab').forEach(function(x){x.classList.toggle('on',x===t);});
|
|
178
|
+
var v=t.getAttribute('data-v');
|
|
179
|
+
Object.keys(_views).forEach(function(k){_views[k].classList.toggle('on',k===v);});
|
|
180
|
+
document.getElementById('hint').style.display=(v==='graph')?'':'none';
|
|
181
|
+
if(v!=='graph')closePanel();
|
|
182
|
+
};});
|
|
183
|
+
|
|
184
|
+
// 로드맵 뷰 — DATA.roadmap (roadmap.html 기능 통합)
|
|
185
|
+
(function(){
|
|
186
|
+
var rd=(DATA&&DATA.roadmap)||{};var el=_views.roadmap;var h='';
|
|
187
|
+
var ms=rd.milestones||[],tk=rd.tasks||[];
|
|
188
|
+
h+='<div class="sec">Milestones ('+ms.length+')</div>';
|
|
189
|
+
if(!ms.length)h+='<div class="note">milestone 없음 — leerness plan add "제목" 으로 추가</div>';
|
|
190
|
+
ms.forEach(function(m){var pg=parseInt(m.progress,10)||0;
|
|
191
|
+
h+='<div class="ms"><h3>'+esc(m.id)+' · '+esc(m.title)+' <span style="color:var(--mut);font-weight:400">'+esc(m.status||'')+' '+pg+'%</span></h3>'
|
|
192
|
+
+'<div class="bar"><i style="width:'+Math.min(100,pg)+'%"></i></div>'
|
|
193
|
+
+(m.nextAction?'<div style="color:var(--mut);font-size:11px">next: '+esc(m.nextAction)+'</div>':'')+'</div>';});
|
|
194
|
+
var byS={};tk.forEach(function(t){(byS[t.status||'requested']=byS[t.status||'requested']||[]).push(t);});
|
|
195
|
+
h+='<div class="sec">Tasks ('+tk.length+')</div>';
|
|
196
|
+
['in-progress','blocked','waiting','planned','requested','done','verified'].forEach(function(s){var arr=byS[s];if(!arr||!arr.length)return;
|
|
197
|
+
h+='<div class="ms"><h3 style="color:'+(STATUSCOL[s]||'#8b949e')+'">'+s+' ('+arr.length+')</h3>';
|
|
198
|
+
arr.slice(0,40).forEach(function(t){h+='<div class="tk"><span class="st" style="color:'+(STATUSCOL[s]||'#8b949e')+'">'+esc(t.id)+'</span><span>'+esc(t.request)+'</span></div>';});
|
|
199
|
+
if(arr.length>40)h+='<div class="note">… +'+(arr.length-40)+'</div>';h+='</div>';});
|
|
200
|
+
if(rd.skills&&rd.skills.length){h+='<div class="sec">Skills ('+rd.skills.length+')</div><div class="ms">'+rd.skills.map(function(s){return '<div class="tk"><span>'+esc(s.name||s.id||s)+'</span></div>';}).join('')+'</div>';}
|
|
201
|
+
if(rd.rules&&rd.rules.length){h+='<div class="sec">Active Rules</div><div class="ms">'+rd.rules.filter(function(r){return r.status==='active';}).map(function(r){return '<div class="tk"><span>'+esc(r.rule||r.text||'')+'</span></div>';}).join('')+'</div>';}
|
|
202
|
+
el.innerHTML=h;
|
|
203
|
+
})();
|
|
204
|
+
|
|
205
|
+
// ⚙ 토글 뷰 — DATA.toggles (생성 시점 상태). 정적 파일이라 실제 적용은 CLI — 스위치 클릭 시 적용 명령 표시+복사.
|
|
206
|
+
(function(){
|
|
207
|
+
var tg=(DATA&&DATA.toggles)||{};var reg=(DATA&&DATA.toggleRegistry)||{};var el=_views.toggles;
|
|
208
|
+
var h='<div class="note">스위치를 누르면 적용 명령이 나타납니다 — 이 파일은 정적 HTML 이라 <b>실제 적용은 아래 명령을 터미널(또는 AI)에게 실행</b>시키세요. AI 는 handoff 헤드라인의 🎛 상태를 자동 준수합니다.</div>';
|
|
209
|
+
Object.keys(reg).forEach(function(id){var on=tg[id]!==false;var m=reg[id]||{};
|
|
210
|
+
h+='<div class="tgrow" id="tg-'+id+'"><div class="sw2'+(on?' on':'')+'" onclick="tgFlip(\''+id+'\')"><i></i></div>'
|
|
211
|
+
+'<div class="tgname">'+esc(id)+'</div><div class="tgdesc">'+esc(m.desc||'')+'<br><span style="opacity:.7">영향: '+esc(m.affects||'')+'</span>'
|
|
212
|
+
+'<div class="cmd" id="cmd-'+id+'"><span id="cmdtext-'+id+'"></span><button onclick="tgCopy(\''+id+'\')">복사</button></div></div></div>';});
|
|
213
|
+
el.innerHTML=h;
|
|
214
|
+
window._tg=tg;
|
|
215
|
+
window.tgFlip=function(id){var cur=window._tg[id]!==false;var nv=!cur;window._tg[id]=nv;
|
|
216
|
+
var row=document.getElementById('tg-'+id);row.querySelector('.sw2').classList.toggle('on',nv);
|
|
217
|
+
var c=document.getElementById('cmd-'+id);c.classList.add('show');
|
|
218
|
+
document.getElementById('cmdtext-'+id).textContent='leerness toggle set '+id+' '+(nv?'on':'off');};
|
|
219
|
+
window.tgCopy=function(id){var t=document.getElementById('cmdtext-'+id).textContent;
|
|
220
|
+
try{navigator.clipboard.writeText(t);}catch(e){var ta=document.createElement('textarea');ta.value=t;document.body.appendChild(ta);ta.select();document.execCommand('copy');ta.remove();}};
|
|
221
|
+
})();
|
|
147
222
|
</script></body></html>`;
|
|
148
223
|
|
|
149
224
|
const _txt = v => (v == null ? '' : String(v));
|
|
@@ -190,17 +265,40 @@ function buildGraphData(root, deps = {}) {
|
|
|
190
265
|
const fg = path.join(root, '.harness', 'feature-graph.md');
|
|
191
266
|
if (exists(fg)) {
|
|
192
267
|
try {
|
|
193
|
-
|
|
268
|
+
const fgText = read(fg);
|
|
269
|
+
for (const line of fgText.split(/\r?\n/)) {
|
|
194
270
|
const m = line.match(/([\w./-]+)\s*(?:->|→|depends on|uses)\s*([\w./-]+)/i);
|
|
195
271
|
if (m) { const a = 'feat:' + m[1], b = 'feat:' + m[2]; add({ id: a, type: 'feature', status: '', label: _trunc(m[1], 40), detail: { feature: m[1] } }); add({ id: b, type: 'feature', status: '', label: _trunc(m[2], 40), detail: { feature: m[2] } }); linkIds(a, b, 'feature'); }
|
|
196
272
|
}
|
|
273
|
+
// 1.36.30 (codex 미검토표면 #9): CLI 가 생성하는 canonical 형식(## F-XXXX 블록 + depends-on/affects 필드)도 인식.
|
|
274
|
+
// 종전엔 ad-hoc "A -> B" 라인만 파싱해 leerness feature add/link 로 만든 그래프가 HTML 에 0 노드였다.
|
|
275
|
+
if (typeof deps._parseFeatureGraph === 'function') {
|
|
276
|
+
const pf = deps._parseFeatureGraph(fgText) || [];
|
|
277
|
+
for (const f of pf) {
|
|
278
|
+
add({ id: f.id, type: 'feature', status: f.status || '', label: _trunc(f.title || f.id, 48), detail: { title: _txt(f.title), status: _txt(f.status), files: (f.files || []).join(', '), errorModes: (f.errorModes || []).join(', ') } });
|
|
279
|
+
}
|
|
280
|
+
for (const f of pf) {
|
|
281
|
+
for (const d of f.dependsOn || []) linkIds(f.id, d, 'feature');
|
|
282
|
+
for (const a of f.affects || []) linkIds(f.id, a, 'feature');
|
|
283
|
+
for (const c of f.coChangesWith || []) linkIds(f.id, c, 'feature');
|
|
284
|
+
}
|
|
285
|
+
}
|
|
197
286
|
} catch {}
|
|
198
287
|
}
|
|
199
288
|
|
|
200
289
|
for (const n of nodes) delete n._ms; // 내부 보조 필드 임베드 제외
|
|
201
290
|
const counts = {};
|
|
202
291
|
for (const n of nodes) counts[n.type] = (counts[n.type] || 0) + 1;
|
|
203
|
-
|
|
292
|
+
// 1.36.30: 로드맵 탭 데이터(roadmap.html 기능 통합) + 토글 상태(⚙ 탭) 임베드.
|
|
293
|
+
const roadmap = {
|
|
294
|
+
milestones: (rd.milestones || []).map(m => ({ id: m.id, title: _trunc(m.title, 90), status: m.status, progress: m.progress, nextAction: _trunc(m.nextAction, 120) })),
|
|
295
|
+
tasks: (rd.tasks || []).map(t => ({ id: t.id, request: _trunc(t.request, 110), status: t.status })),
|
|
296
|
+
skills: (rd.skills || []).map(s => ({ name: _trunc(s.name || s.id || s, 60) })),
|
|
297
|
+
rules: (rd.rules || []).map(r => ({ rule: _trunc(r.rule || r.text || '', 110), status: r.status })),
|
|
298
|
+
};
|
|
299
|
+
const toggles = (typeof deps._loadToggles === 'function' ? deps._loadToggles(root) : {}) || {};
|
|
300
|
+
const toggleRegistry = deps._toggleRegistry || {};
|
|
301
|
+
return { project: rd.project || path.basename(root), version: rd.version || '', root, counts, nodes, edges, roadmap, toggles, toggleRegistry };
|
|
204
302
|
}
|
|
205
303
|
|
|
206
304
|
// `leerness graph --html [path] [--out file] [--json]` 핸들러.
|
package/lib/team.js
CHANGED
|
@@ -42,8 +42,24 @@ function teamCmd(root, sub, id, opts = {}, deps = {}) {
|
|
|
42
42
|
status: 'active',
|
|
43
43
|
createdAt: now()
|
|
44
44
|
};
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
// 1.36.31 (codex 미검토표면 #2): 병렬 team add 가 서로의 등록을 덮어쓰던 lost update — 락 안에서 재로드→중복검사→추가→저장.
|
|
46
|
+
const _doAdd = () => {
|
|
47
|
+
const cur = _loadTeams(root);
|
|
48
|
+
if (cur.some(t => t.id === teamId)) { warn(`이미 존재: ${teamId} (제거 후 재정의: leerness team remove ${teamId})`); return false; }
|
|
49
|
+
cur.push(team);
|
|
50
|
+
// Windows: 락 밖의 병렬 _loadTeams 읽기가 파일을 잠깐 열어둔 사이 원자 rename 이 EPERM(일시적) — 짧은 재시도.
|
|
51
|
+
let _lastErr = null;
|
|
52
|
+
for (let k = 0; k < 4; k++) {
|
|
53
|
+
try { _saveTeams(root, cur); _lastErr = null; break; }
|
|
54
|
+
catch (e) { _lastErr = e; if (e && e.code === 'EPERM') { const t0 = Date.now(); while (Date.now() - t0 < 25) {} continue; } throw e; }
|
|
55
|
+
}
|
|
56
|
+
if (_lastErr) throw _lastErr;
|
|
57
|
+
return true;
|
|
58
|
+
};
|
|
59
|
+
const _added = (typeof deps._withLock === 'function')
|
|
60
|
+
? deps._withLock(require('path').join(root, '.harness', 'teams.json'), _doAdd)
|
|
61
|
+
: _doAdd();
|
|
62
|
+
if (!_added) return;
|
|
47
63
|
ok(`team 정의: ${teamId} (personas:${team.personas.length} members:${team.members.length} schedule:${team.schedule} review:${team.review ? 'on' : 'off'})`);
|
|
48
64
|
log(` ⓘ 정의 전용 — 자동 실행 없음. 목록: leerness team list`);
|
|
49
65
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leerness",
|
|
3
|
-
"version": "1.36.
|
|
3
|
+
"version": "1.36.31",
|
|
4
4
|
"description": "The AI-coding operations layer that makes \"done\" require evidence — persistent memory, evidence-gated completion checks, and clean handoffs for any AI agent (Claude Code, Codex, Cursor). State lives as plain files in your repo. CLI + MCP, 0 runtime dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"leerness",
|