leerness 1.36.28 → 1.36.30

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 CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.36.30 — 2026-07-15 — 온톨로지 그래프 기본 활성 + roadmap 통합 + 기능 토글 (사용자 요청)
4
+
5
+ `leerness.html`(온톨로지 그래프)가 **컨트롤 타워**가 된다: install/session-close 자동생성 대상이 roadmap.html → leerness.html 로 전환(기본 활성), roadmap 기능은 그래프 파일의 탭으로 통합, 그래프 뷰에서 leerness 기능(gate 등)을 토글.
6
+
7
+ - **탭 3종**: 그래프(기존 force-directed) | **로드맵**(milestone 진행률바 + 상태별 task + skills/rules — roadmap.html 기능 흡수, 수동 `leerness roadmap` 은 호환 유지) | **⚙ 토글**(스위치 UI).
8
+ - **`leerness toggle list|set <id> on|off`** + `.harness/toggles.json` — 레지스트리: `gate` / `lens` / `auto-graph` / `delegation-brief`. 기본 전부 ON.
9
+ - **AI 준수 배선**: gate·lens 는 토글 OFF 시 "🎛 토글 OFF — 건너뜀" 명시 후 exit 0(조용한 무시 아님, --json 은 `skipped:true`) · `delegation-brief` OFF 면 dispatch/multi 가 브리프 미접두 · **handoff 헤드라인에 `🎛 토글 OFF: …` 노출** — AI 가 세션 시작 시 상태를 보고 준수.
10
+ - **⚙ 탭 정직성**: 정적 HTML 은 파일 쓰기가 불가 — 스위치 클릭 시 적용 명령(`leerness toggle set …`)을 표시+복사 버튼으로 제공하고 UI 에 명시.
11
+ - **canonical feature 그래프 인식** (codex 미검토표면 #9 편입): `leerness feature add/link` 가 만드는 `## F-XXXX` 블록을 그래프 노드/엣지로 렌더 — 종전엔 ad-hoc "A -> B" 라인만 파싱해 CLI 생성 그래프가 HTML 에 0 노드였다.
12
+ - **검증**: selftest 300/300(토글 저장/로드+기본 ON+레지스트리 4종+HTML 탭·데이터·임베드 JS 문법+배선 5종), 실측 6종(init 자동생성 전환 / 탭 임베드 / toggle CLI / gate·lens 스킵 / handoff 🎛 / brief 토글 0↔1 / canonical feature 노드), 게이트 e2e, 게시본 클린룸.
13
+
14
+ ## 1.36.29 — 2026-07-15 — `agents multi` 위임 브리프 접두 — 백그라운드 AI 도 leerness 프로토콜을 받도록 (사용자 보고)
15
+
16
+ 사용자 질문("백그라운드에서 호출된 AI 가 leerness 를 참조하는가?")에 대한 조사 결과: `agents dispatch` 는 1.35.6 부터 "[leerness 위임 프로토콜]" 브리프(handoff 적재→task 등록→evidence→verify-claim→session close)를 자동 접두하지만, **`agents multi`(--execute 실 spawn 포함)는 원문 그대로 위임** — 병렬 백그라운드 AI 들이 프로토콜을 받지 못하는 실제 갭이었다.
17
+
18
+ - **multi 3지점 브리프 접두**: `--execute` 실 spawn(`_cliChat`) + 명령 목록 출력 + `--json` commands. dispatch 와 동일 규칙(`--raw` 로 원문 위임). 로그/`_recordRun` 의 task 표시는 원문 유지(기록 정직성).
19
+ - 참조 경로 정리(조사 확정): ① dispatch/multi 프롬프트 접두(이번에 완성) ② AGENTS.md 자동 로드(codex 실측 준수 — cwd 가 루트가 아니거나 미지원 CLI 면 무력해서 접두가 안전망) ③ orchestrate 는 자체 JSON 계약 컨텍스트 포함.
20
+ - **검증**: selftest 299/299(브리프 4단계 + dispatch 와이어 + **multi 3지점 와이어**), 실측(multi 출력에 프로토콜 1회 포함 · `--raw` 0회), 게이트 e2e, 게시본 클린룸.
21
+
3
22
  ## 1.36.28 — 2026-07-15 — 미검토 표면 헌트: 데이터 유실 클래스(손상 스토어 클로버 · URL 덮어쓰기) + 정확성(impact 전이 · alias 경계)
4
23
 
5
24
  rotate-review-targets 교훈 적용 — 최근 잘 검토된 표면(lens/memory/plan/requests/skill)을 **제외**하고 미검토 표면(state/team/api-skill/wakeup/intent/feature)을 codex 로 QA 헌트. 10 findings 중 결정적 5건을 맹신 X(전부 재현) 후, 데이터 유실 클래스 + 저위험 순수 교정 4건을 수정. 동시성 3건(크로스프로세스 락)은 0-deps rabbit-hole 위험이라 정직하게 이연.
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.28 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
124
+ 이 프로젝트는 Leerness v1.36.30 하네스를 사용합니다. 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.28는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **86개 도구**를 노출:
178
+ Leerness v1.36.30는 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.28는 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.28)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
199
+ 현재 누적: **70 라운드 (1.9.40 → 1.36.30)** · 매 라운드 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.28: 2026-07-15
237
+ Last synced by Leerness v1.36.30: 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.28';
37
+ const VERSION = '1.36.30';
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,10 +2949,14 @@ 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) && /_harnessBrief,/.test(read(__filename));
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
- return briefOk && wired && stdinFixed;
2955
+ // (4) 1.36.29 (사용자 보고): multi 도 브리프 접두 — --execute 실 spawn(_cliChat) + 명령 목록/JSON 3지점 전부 briefTask.
2956
+ const multiWired = agentsSrc.includes('_cliChat(root, def.id, briefTask,')
2957
+ && agentsSrc.includes('_dispatchCommand(x.def.id, briefTask, writeMode)')
2958
+ && agentsSrc.includes('_dispatchCommand(def.id, briefTask, writeMode)');
2959
+ return briefOk && wired && stdinFixed && multiWired;
2956
2960
  } },
2957
2961
  { name: '19th GPT5.5평가 (UR-0013): _parseTestStdout 러너 인식(jest/tap/spec/비율) + declared-pass 부풀림 3경로 게이팅 + json ok/reasons (1.35.7)', run: () => {
2958
2962
  // (1) 파서 행위: 기존 4형식 보존 + spec 리포터(패턴 6) 신규 — denom = pass + fail.
@@ -3068,6 +3072,37 @@ function _selfTestCases() {
3068
3072
  const owOk = p._managedMerge('x.md', next, prev, '.', new Set(['x.md'])) === next;
3069
3073
  return survived && idem && legacyOk && cleanOk && owOk;
3070
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
+ } },
3071
3106
  { name: 'debug 렌즈 (1.36.27, obra/superpowers systematic-debugging): 자기질문 6문항 ko/en 락스텝 + affects 유효 + route bugfix 힌트 + 파일매핑 미확장 — 행위검사', run: () => {
3072
3107
  const d = LENS_CATALOG.debug;
3073
3108
  if (!d) return false;
@@ -4438,7 +4473,7 @@ function _selfTestCases() {
4438
4473
  const rowsEn = sc.includes("t('- 없음', '- none')") && sc.includes('_retroOneLine(agg, uiLang)');
4439
4474
  const retroEn = bin.includes('function _retroOneLine(agg, lang)') && bin.includes('`done ${done}/${total}') && bin.includes('decisions ${agg.decisionBlocks} accumulated');
4440
4475
  const roadmapEn = bin.includes('roadmap.html auto-updated (${trigger})');
4441
- const koPreserved = bin.includes('완료 ${done}/${total}') && bin.includes('roadmap.html 자동 갱신 (${trigger})') && sc.includes("t('- 없음', '- none')"); // ko 인자 보존
4476
+ const koPreserved = bin.includes('완료 ${done}/${total}') && bin.includes('온톨로지 그래프 자동 갱신 (${trigger})') && sc.includes("t('- 없음', '- none')"); // ko 인자 보존
4442
4477
  return rowsEn && retroEn && roadmapEn && koPreserved;
4443
4478
  } },
4444
4479
  { name: 'CLI 영어화 Phase 9 (1.25.2, UR-0010): health 진단 영어/한국어 보존 + uiLang 주입 (소스 가드)', run: () => {
@@ -5400,6 +5435,11 @@ function lensCmd(domain, opts = {}) {
5400
5435
  if (domain != null) domain = String(domain).trim().toLowerCase();
5401
5436
  // 1.19.3: 내장 + 프로젝트 커스텀(.harness/quality-lenses.json) 병합 catalog.
5402
5437
  const root = opts.root || arg('--path', process.cwd());
5438
+ // 1.36.30: 토글 준수 (⚙ 탭 / leerness toggle set lens off)
5439
+ if (!_tgl.toggleOn(root, 'lens')) {
5440
+ if (jsonMode) { log(JSON.stringify({ ok: true, skipped: true, reason: 'toggle_off', hint: 'leerness toggle set lens on' })); return; }
5441
+ log('🎛 lens 토글 OFF — 건너뜀 (재활성: leerness toggle set lens on)'); return;
5442
+ }
5403
5443
  const L = _uiLang(root); const t = (ko, en) => (L === 'en' ? en : ko); // 1.24.2 (UR-0010 Phase 8): lens 영어 opt-in
5404
5444
  const catalog = _effectiveLensCatalog(root);
5405
5445
  if (domain && !catalog[domain]) {
@@ -7871,6 +7911,7 @@ function _saveTeams(root, teams) {
7871
7911
  // 1.9.389 (UR-0025 큰 핸들러 모듈화 2번째): teamCmd 핸들러를 lib/team.js 로 분리.
7872
7912
  // harness 는 deps(VERSION · 공유 저장 _loadTeams/_saveTeams · _detectShellCtx · argv 파서 arg/has)를 구성해 위임(thin wrapper). 호출부/동작 무변경.
7873
7913
  const _team = require('../lib/team');
7914
+ const _tgl = require('../lib/toggles'); // 1.36.30: 기능 토글 (그래프 ⚙ 탭 연동 — gate/lens/auto-graph/delegation-brief)
7874
7915
  function teamCmd(root, sub, id, opts = {}) { return _team.teamCmd(root, sub, id, opts, { VERSION, _loadTeams, _saveTeams, _detectShellCtx, arg, has }); }
7875
7916
 
7876
7917
  // 1.9.112: 전용 lessons.md (Memory Write Surface 5번째)
@@ -9688,6 +9729,12 @@ function handoff(root) {
9688
9729
  else if (briefGap) parts.push(t('📋 project-brief 미작성', '📋 project-brief unfilled'));
9689
9730
  else if (planGap) parts.push(t('📋 plan Goal 미작성', '📋 plan Goal unfilled'));
9690
9731
  } catch {}
9732
+ // 1.36.30: 기능 토글 OFF 헤드라인 — AI 가 이 상태를 보고 해당 기능을 스킵 준수 (그래프 ⚙ 탭과 동일 상태)
9733
+ try {
9734
+ const _tgs = _tgl.loadToggles(root);
9735
+ const _offs = Object.keys(_tgs).filter(k => _tgs[k] === false);
9736
+ if (_offs.length) parts.push(t(`🎛 토글 OFF: ${_offs.join(',')}`, `🎛 toggles OFF: ${_offs.join(',')}`));
9737
+ } catch {}
9691
9738
  // 3) MCP 활동 누적
9692
9739
  try {
9693
9740
  const stats = _readUsageStats(root);
@@ -13569,7 +13616,7 @@ function _dispatchCommand(agentId, task, writeMode, model) {
13569
13616
 
13570
13617
  const _agents = require('../lib/agents');
13571
13618
  // 1.9.424 (UR-0025/UR-0125 큰 핸들러 모듈화 9번째): agentsCmd → lib/agents.js (DI 위임, rest→array)
13572
- 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 }); }
13619
+ 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 경로)
13573
13620
 
13574
13621
  function personaCmd(root, sub, idOrName, ...rest) {
13575
13622
  root = absRoot(root || process.cwd());
@@ -13762,7 +13809,12 @@ async function selfCheck(root) {
13762
13809
  // 1.9.2: 게이트 5종 한번에 실행 (verify + audit + scan secrets + encoding check + lazy detect).
13763
13810
  function gate(root) {
13764
13811
  root = absRoot(root);
13765
- const jsonMode = has('--json'); // 외부리뷰 C2: --json 일관성 — 이전엔 텍스트 헤더+단계별 JSON 혼재로 파싱 불가. 단일 객체로 집계.
13812
+ const jsonMode = has('--json');
13813
+ // 1.36.30: 토글 준수 — 그래프 ⚙ 탭/`leerness toggle set gate off` 시 스킵을 명시하고 exit 0 (조용한 무시 아님).
13814
+ if (!_tgl.toggleOn(root, 'gate')) {
13815
+ if (jsonMode) { log(JSON.stringify({ ok: true, skipped: true, reason: 'toggle_off', hint: 'leerness toggle set gate on' })); return; }
13816
+ log('🎛 gate 토글 OFF — 건너뜀 (재활성: leerness toggle set gate on)'); return;
13817
+ } // 외부리뷰 C2: --json 일관성 — 이전엔 텍스트 헤더+단계별 JSON 혼재로 파싱 불가. 단일 객체로 집계.
13766
13818
  const checks = [];
13767
13819
  let bad = 0;
13768
13820
  // 1.33.3 (verify-claim+CI gate 슬라이스 강화): --claims opt-in — 모든 done 주장을 정밀 per-claim 검증(verify-claim --all)으로 추가(6번째). 기본 5체크는 무변경(기존 어댑터 회귀 0).
@@ -14867,11 +14919,14 @@ function _autoRoadmap(root, trigger) {
14867
14919
  const cfg = _autoRoadmapConfig(root);
14868
14920
  if (!cfg.enabled) return false;
14869
14921
  if (trigger === 'data-change' && !cfg.onEveryChange) return false;
14870
- const outFile = path.resolve(cfg.outFile || path.join(root, 'roadmap.html'));
14871
- const data = _roadmapData(root);
14872
- writeUtf8(outFile, _roadmapHTML(data));
14873
- const _en = _uiLang(root) === 'en'; // 1.24.1 (UR-0010): roadmap 로그 en
14874
- log(_en ? `✓ roadmap.html auto-updated (${trigger}) ${rel(root, outFile)}` : `✓ roadmap.html 자동 갱신 (${trigger}) — ${rel(root, outFile)}`);
14922
+ // 1.36.30 (사용자 요청): 자동 생성 대상을 roadmap.html leerness.html(온톨로지 그래프 + 로드맵 탭 통합)로 전환.
14923
+ // roadmap.html 기능은 그래프 파일의 "로드맵" 탭에 통합됨. 수동 `leerness roadmap` 은 호환 유지.
14924
+ // 토글 auto-graph OFF 시 생성 안 함( 탭/`leerness toggle`).
14925
+ if (!_tgl.toggleOn(root, 'auto-graph')) return false;
14926
+ const outFile = path.resolve(cfg.outFile || path.join(root, 'leerness.html'));
14927
+ const s = _graph.graphHtmlCmd(root, { _roadmapData, _loadDecisions, _loadLessons, _parseFeatureGraph, _loadToggles: _tgl.loadToggles, _toggleRegistry: _tgl.TOGGLE_REGISTRY, quiet: true }, outFile);
14928
+ const _en = _uiLang(root) === 'en';
14929
+ log(_en ? `✓ ontology graph auto-updated (${trigger}) — ${rel(root, outFile)} (graph+roadmap+toggles)` : `✓ 온톨로지 그래프 자동 갱신 (${trigger}) — ${rel(root, outFile)} (그래프+로드맵+토글 탭)`);
14875
14930
  return true;
14876
14931
  } catch (e) {
14877
14932
  warn((_uiLang(root) === 'en' ? 'roadmap auto-update failed: ' : 'roadmap 자동 갱신 실패: ') + (e && e.message ? e.message : e));
@@ -21190,14 +21245,14 @@ function doctorCmd(opts = {}) { return _diag.doctorCmd(opts, { VERSION, uiLang:
21190
21245
  function whichCmd() { return _diag.whichCmd({ VERSION, uiLang: _uiLang(arg('--path', process.cwd())), has, harnessPath: __filename }); }
21191
21246
  // 1.34.3 (T-0077): `graph --html` → lib/graph.js 온톨로지 HTML(leerness.html) 생성기 위임. 데이터는 in-process 로더 주입(자식 프로세스 셸링 X).
21192
21247
  const _graph = require('../lib/graph');
21193
- function graphHtmlCmd(root) { return _graph.graphHtmlCmd(root, { _roadmapData, _loadDecisions, _loadLessons, has, arg }); }
21248
+ 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
21194
21249
  // 1.34.4 (T-0077 후속): handoff 시 leerness.html 자동 재생성 — opt-in(LEERNESS_AUTO_GRAPH=1, 기본 OFF / "Always-Off Opt-In"). 사용자 비전 "자동으로 작성되게" 충족. 비치명(try/catch) · 기본경로 무영향.
21195
21250
  function _maybeAutoGraph(root) {
21196
21251
  if (process.env.LEERNESS_AUTO_GRAPH !== '1') return;
21197
21252
  try {
21198
21253
  const r0 = absRoot(root || process.cwd());
21199
21254
  if (!exists(path.join(r0, '.harness'))) return;
21200
- const s = _graph.graphHtmlCmd(r0, { _roadmapData, _loadDecisions, _loadLessons, quiet: true });
21255
+ const s = _graph.graphHtmlCmd(r0, { _roadmapData, _loadDecisions, _loadLessons, _parseFeatureGraph, _loadToggles: _tgl.loadToggles, _toggleRegistry: _tgl.TOGGLE_REGISTRY, quiet: true });
21201
21256
  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 출력 오염 방지
21202
21257
  } catch {}
21203
21258
  }
@@ -21546,6 +21601,7 @@ async function main() {
21546
21601
  if (cmd === 'milestones') return milestonesCmd(_resolveRoot(args[1]));
21547
21602
  // 1.9.231: leerness pulse — 한 줄 종합 요약 (10 핵심 지표)
21548
21603
  if (cmd === 'pulse') return pulseCmd(_resolveRoot(args[1]));
21604
+ if (cmd === 'toggle') return _tgl.toggleCmd(arg('--path', process.cwd()), args[1], args[2], args[3], { has, VERSION }); // 1.36.30: 기능 토글 (그래프 ⚙ 탭 연동)
21549
21605
  if (cmd === 'lens') return lensCmd(args[1]); // 1.18.3 (UR-0003): 분야별 자기질문 품질 렌즈
21550
21606
  // 1.9.233: leerness commands — 카테고리화된 전체 CLI 명령 목록
21551
21607
  if (cmd === 'commands') return commandsCmd(arg('--path', process.cwd()));
package/lib/agents.js CHANGED
@@ -74,6 +74,9 @@ function agentsCmd(root, sub, args = [], deps = {}) {
74
74
  if (sub === 'multi') {
75
75
  const task = _taskArg();
76
76
  if (!task) { fail('multi "<task>" 또는 --task 필요'); return process.exit(1); }
77
+ // 1.36.29 (사용자 보고): dispatch 만 위임 브리프를 접두하고 multi(--execute 실 spawn 포함)는 원문 그대로 위임돼
78
+ // 백그라운드 AI 가 leerness 프로토콜(handoff 적재/task 등록/evidence/session close)을 받지 못했다. dispatch 와 동일 규칙: --raw 로 옵트아웃.
79
+ const briefTask = (has('--raw') || typeof _harnessBrief !== 'function') ? task : (_harnessBrief() + task);
77
80
  const onlyArg = arg('--only', null); // 'claude,codex' 처럼 콤마 구분 — 활성 중에서 추가 필터
78
81
  const writeMode = has('--write');
79
82
  const execute = has('--execute'); // 1.9.156: 명령 출력 → 실제 spawn + consensus 합의
@@ -109,7 +112,7 @@ function agentsCmd(root, sub, args = [], deps = {}) {
109
112
  // 병렬 _cliChat 호출 (sandbox 자동: runCommandSafe + env scrub + observability)
110
113
  const results = await Promise.all(ready.map(async ({ def }) => {
111
114
  const start = Date.now();
112
- const r = await _cliChat(root, def.id, task, { timeout });
115
+ const r = await _cliChat(root, def.id, briefTask, { timeout });
113
116
  return {
114
117
  agent: def.id,
115
118
  elapsed: Date.now() - start,
@@ -203,7 +206,7 @@ function agentsCmd(root, sub, args = [], deps = {}) {
203
206
  log(JSON.stringify({
204
207
  task, count: ready.length,
205
208
  agents: ready.map(x => ({ id: x.def.id, version: x.status.version })),
206
- commands: ready.map(x => _dispatchCommand(x.def.id, task, writeMode)),
209
+ commands: ready.map(x => _dispatchCommand(x.def.id, briefTask, writeMode)),
207
210
  // 1.9.266 (UR-0021 2단계): 각 에이전트 슬래시 명령 힌트 — sub-agent 가 알맞은 슬래시 사용
208
211
  slashCommands: ready.reduce((acc, x) => { const h = _agentSlashHint(root, x.def.id); if (h) acc[x.def.id] = { invoke: h.invoke, commands: h.commands.map(c => c.cmd) }; return acc; }, {})
209
212
  }, null, 2));
@@ -219,7 +222,7 @@ function agentsCmd(root, sub, args = [], deps = {}) {
219
222
  for (const { def, status } of ready) {
220
223
  log(`### [${def.id}] (v${status.version || '?'})`);
221
224
  log('```sh');
222
- log(_dispatchCommand(def.id, task, writeMode));
225
+ log(_dispatchCommand(def.id, briefTask, writeMode));
223
226
  log('```');
224
227
  // 1.9.266 (UR-0021 2단계): 에이전트별 슬래시 명령 힌트
225
228
  try {
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
- for (const line of read(fg).split(/\r?\n/)) {
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
- return { project: rd.project || path.basename(root), version: rd.version || '', root, counts, nodes, edges };
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/toggles.js ADDED
@@ -0,0 +1,70 @@
1
+ // lib/toggles.js — 기능 토글 (1.36.30, 사용자 요청): 온톨로지 그래프 뷰의 스위치와 연동되는 기능 on/off 스토어.
2
+ // .harness/toggles.json 에 저장. AI/CLI 는 여기 상태를 읽어 준수한다(gate/lens 스킵, 위임 브리프 생략, 자동 그래프).
3
+ // 설계: 기본 전부 ON(파일 없으면 결손 아님). 손상 파일은 SAVE 시 덮어쓰기 거부(1.36.28 _assertStoreParsable 패턴).
4
+ 'use strict';
5
+ const path = require('path');
6
+ const { absRoot, exists, read, writeUtf8, mkdirp, log, ok, fail } = require('./io');
7
+
8
+ // 토글 레지스트리 — id: { ko 설명, 영향 명령 }. 새 토글은 여기에 추가.
9
+ const TOGGLE_REGISTRY = {
10
+ 'gate': { desc: '완료 게이트 (verify+audit+scan+encoding+lazy 통합 점검)', affects: 'leerness gate' },
11
+ 'lens': { desc: '품질 렌즈 자기질문 (완료 선언 전 분야별 점검)', affects: 'leerness lens' },
12
+ 'auto-graph': { desc: '온톨로지 그래프(leerness.html) 자동 갱신 (install/session-close)', affects: 'install · session close' },
13
+ 'delegation-brief': { desc: '백그라운드 AI 위임 시 leerness 프로토콜 브리프 자동 접두', affects: 'agents dispatch · agents multi' },
14
+ };
15
+
16
+ function _togglesPath(root) { return path.join(absRoot(root), '.harness', 'toggles.json'); }
17
+
18
+ // 전체 토글 상태 로드 — 파일 없으면 전부 기본 ON. 손상 파일은 읽기에선 기본값(비변경 경로 resilient).
19
+ function loadToggles(root) {
20
+ const out = {};
21
+ for (const id of Object.keys(TOGGLE_REGISTRY)) out[id] = true;
22
+ const f = _togglesPath(root);
23
+ if (!exists(f)) return out;
24
+ try {
25
+ const j = JSON.parse(read(f));
26
+ for (const [k, v] of Object.entries(j || {})) if (k in out) out[k] = v !== false;
27
+ } catch {}
28
+ return out;
29
+ }
30
+
31
+ // 단일 토글 조회 헬퍼 — 호출부 한 줄용.
32
+ function toggleOn(root, id) { return loadToggles(root)[id] !== false; }
33
+
34
+ function saveToggles(root, toggles) {
35
+ const f = _togglesPath(root);
36
+ // 1.36.28 패턴: 손상 파일 덮어쓰기 거부 — 단 토글은 전량 기본값 복원 가능하니 손상 시 재생성 허용이 사용자 친화적.
37
+ // (여기의 데이터는 bool 4개뿐 — 유실 비용이 0 에 가깝고, 거부하면 토글 자체가 잠긴다. 의도적 예외.)
38
+ mkdirp(path.dirname(f));
39
+ writeUtf8(f, JSON.stringify(toggles, null, 2) + '\n');
40
+ }
41
+
42
+ // `leerness toggle [list|set <id> on|off] [--json]`
43
+ function toggleCmd(root, sub, id, val, deps = {}) {
44
+ const { has, VERSION } = deps;
45
+ root = absRoot(root);
46
+ const json = has && has('--json');
47
+ const cur = loadToggles(root);
48
+ if (!sub || sub === 'list') {
49
+ if (json) { log(JSON.stringify({ version: VERSION, toggles: cur, registry: TOGGLE_REGISTRY }, null, 2)); return; }
50
+ log(`# leerness toggle — 기능 토글 (온톨로지 그래프 뷰 ⚙ 탭과 연동)`);
51
+ for (const [k, meta] of Object.entries(TOGGLE_REGISTRY)) {
52
+ log(` ${cur[k] ? '🟢 ON ' : '⚪ OFF'} ${k.padEnd(17)} ${meta.desc} [${meta.affects}]`);
53
+ }
54
+ log(`\n 변경: leerness toggle set <id> on|off · 그래프 뷰: leerness graph --html → leerness.html 의 ⚙ 탭`);
55
+ return;
56
+ }
57
+ if (sub === 'set') {
58
+ if (!TOGGLE_REGISTRY[id]) { fail(`알 수 없는 토글: ${id} (가능: ${Object.keys(TOGGLE_REGISTRY).join(', ')})`); process.exitCode = 1; return; }
59
+ const on = String(val).toLowerCase();
60
+ if (on !== 'on' && on !== 'off') { fail(`값은 on|off (받음: ${val})`); process.exitCode = 1; return; }
61
+ cur[id] = on === 'on';
62
+ saveToggles(root, cur);
63
+ if (json) { log(JSON.stringify({ ok: true, id, value: cur[id], toggles: cur }, null, 2)); return; }
64
+ ok(`toggle ${id} = ${on.toUpperCase()}${cur[id] ? '' : ' — 관련 명령이 스킵 동작으로 전환됩니다'}`);
65
+ return;
66
+ }
67
+ fail(`알 수 없는 하위명령: ${sub} (가능: list, set)`); process.exitCode = 1;
68
+ }
69
+
70
+ module.exports = { TOGGLE_REGISTRY, loadToggles, toggleOn, saveToggles, toggleCmd, _togglesPath };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.36.28",
3
+ "version": "1.36.30",
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",