leerness 1.9.291 → 1.9.293
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 +37 -0
- package/README.md +6 -6
- package/bin/harness.js +180 -6
- package/package.json +1 -1
- package/scripts/e2e.js +45 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.293 — 2026-06-04 — idempotency auto-fix + progress-tracker 복제 버그 근본 수정 🐛
|
|
4
|
+
|
|
5
|
+
**🔁 idempotency 위반(task/요청 중복)을 자동 회복하는 `--auto-fix` 신설 + 중복이 매 세션 누적되던 근본 버그(헤더 유실 시 전체 복제) 수정. 라이브 프로젝트 progress-tracker.md 69행→6행 복구.**
|
|
6
|
+
|
|
7
|
+
### 배경 (자기 발견 — session-close가 보고한 멱등성 위반 4→11 증가 추적)
|
|
8
|
+
세션마다 멱등성 위반이 증가(4→11)해 원인을 추적한 결과 **근본 버그** 발견: `progressHeader()`가 `|---|` 분리자를 못 찾으면(헤더 유실/손상) **파일 전체 텍스트를 헤더로 반환** → `writeProgressRows()`가 `header + rows` 로 기록하며 **기존 모든 행을 복제**. `taskAdd`/`upsertProgress`가 호출될 때마다 progress-tracker.md 가 배로 부풀며 중복 누적. 실제 라이브 파일은 헤더가 유실된 채 69행(고유 6행)까지 손상돼 있었음.
|
|
9
|
+
|
|
10
|
+
### 구현
|
|
11
|
+
1. **근본 버그 fix** — `progressHeader()` 가 분리자 부재 시 전체 텍스트 대신 **`_canonicalProgressHeader()` (frontmatter + 표 헤더 + 분리자 재구성)** 반환 → 다음 write 시 헤더 자동 복구, 복제 중단. `coreFiles` 템플릿도 동일 헬퍼로 DRY(단일 출처).
|
|
12
|
+
2. **`leerness idempotency audit --auto-fix`** — (a) 완전 동일 행 제거(순수 중복), (b) active 동일텍스트는 `status=dropped` 로 보존(id/히스토리 유지, 삭제 X), (c) user-requests open 중복 정리. 멱등(2회=no-op) · git 회복 가능.
|
|
13
|
+
3. **`drift check --auto-fix` 통합** — 기존 자동 회복 흐름(1.9.82/225/236 패턴)에 idempotency dedup 추가 → 자동 self-healing.
|
|
14
|
+
4. **라이브 복구** — 손상된 progress-tracker.md 에 auto-fix 적용: 완전중복 63행 제거 + 헤더 재구성 → 69행→6행, 멱등성 위반 0.
|
|
15
|
+
5. selftest 40→41 · e2e 237→238.
|
|
16
|
+
|
|
17
|
+
### 검증
|
|
18
|
+
- **selftest 41/41 PASS** · **E2E 238/238 PASS** (회귀 0).
|
|
19
|
+
- e2e: 헤더 유실+중복 시뮬레이션 → auto-fix → 헤더 재구성 + 완전중복 3→1 + 재검사 clean 실측.
|
|
20
|
+
- 라이브: `idempotency audit --path . --auto-fix` → "완전중복 63행 제거", 6개 실제 task(T-0001~0006) 보존, 헤더 정상 복원.
|
|
21
|
+
- MCP `leerness_idempotency_audit` 는 read-only 유지(쓰기 경로는 CLI/drift 전용 — 정책 tier 일관성).
|
|
22
|
+
|
|
23
|
+
## 1.9.292 — 2026-06-03 — UR-0031: get_project_context — MCP 시맨틱 verb (에이전트 온보딩 1콜 집약)
|
|
24
|
+
|
|
25
|
+
**🧭 GPT-5.5 "MCP-first 범용 하네스" 전략의 핵심 — 어떤 에이전트든 leerness 내부 명령을 몰라도 단 1콜로 "지금 무엇을 알아야 하는가"를 파악하는 집약 컨텍스트 verb 신설.**
|
|
26
|
+
|
|
27
|
+
### 배경
|
|
28
|
+
1.9.279에서 `leerness_state_*` 도구 설명문에 시맨틱 verb(get_project_context 등)를 표기했으나, 실제 호출 가능한 이름은 여전히 `leerness_state_*` → MCP 클라이언트가 `tools/list`에서 의도 기반 이름을 못 봄. 또한 에이전트가 작업 시작 전 컨텍스트를 모으려면 handoff/state/task/decision/rule 여러 도구를 따로 호출해야 했음.
|
|
29
|
+
|
|
30
|
+
### 구현 (UR-0031)
|
|
31
|
+
1. **`leerness context [path] [--json]` CLI 신설** — 현재 작업(in-progress task) / 미답 사용자 요청 / 최근 결정 3건 / 활성 룰 / next-actions / memory surface(진행·결정·룰·교훈) / 프로젝트 의도(project-brief Purpose)를 **단일 구조화 객체**로 집약. 기존 헬퍼(readProgressRows·readRules·_loadUserRequests·_extractDecisionBlocks·_loadNextActionQueue) 재사용 — 신규 가치(집약)이지 단일 도구 중복 아님.
|
|
32
|
+
2. **MCP `leerness_get_project_context` (80번째 도구)** — `context --json` 호출. read-only. handoff(인간용 장문)와 달리 기계 친화 lean payload. 응답: `{ version, project, currentTask, openRequests, recentDecisions, activeRules, nextActions, memory }`.
|
|
33
|
+
3. selftest 39→40 (MCP verb 등록 + CLI 디스패치 + `_mcpToolCount()≥80` 소스 정합) · e2e 236→237 (init된 프로젝트에서 집약 JSON 구조 + MCP 80 도구 실측). README MCP 배지 79→80 자동 동기화.
|
|
34
|
+
|
|
35
|
+
### 검증
|
|
36
|
+
- **selftest 40/40 PASS** · **E2E 237/237 PASS** (회귀 0).
|
|
37
|
+
- B(1.9.288) 도구수 정합 테스트가 배지-live 불일치(79 vs 80)를 즉시 검출 → readme sync로 80 정합(Codex #5 가드 정상 동작 확인).
|
|
38
|
+
- 실측: `context --json` → openRequests 3건(전략 백로그) + activeRules R-0001 + memory 집약. MCP tools/list 80개에 `leerness_get_project_context` 노출.
|
|
39
|
+
|
|
3
40
|
## 1.9.291 — 2026-06-03 — UR-0025 (2단계): 외부 에이전트 레지스트리 → lib/agent-registry.js 모듈 분리
|
|
4
41
|
|
|
5
42
|
**🧩 GPT-5.5 + Codex 두 리뷰가 공통 지적한 #1 유지보수 이슈(단일 1.2MB 파일)를 비파괴 모듈 분리로 점진 해소. 1.9.274의 lib/pure-utils.js 패턴 계승.**
|
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.293 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
|
|
475
475
|
|
|
476
476
|
### Core Commands
|
|
477
477
|
|
|
@@ -513,7 +513,7 @@ leerness memory restore decision <date|title>
|
|
|
513
513
|
|
|
514
514
|
### MCP server (외부 AI 통합)
|
|
515
515
|
|
|
516
|
-
Leerness v1.9.
|
|
516
|
+
Leerness v1.9.293는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **80개 도구**를 노출:
|
|
517
517
|
|
|
518
518
|
```jsonc
|
|
519
519
|
// 카테고리별
|
|
@@ -526,7 +526,7 @@ Leerness v1.9.291는 stdio JSON-RPC MCP server를 내장합니다 — Claude Cod
|
|
|
526
526
|
// • Workflow: session_close / agents_list / task_export / env_check / usage_stats / reuse_map / whats_new
|
|
527
527
|
|
|
528
528
|
// MCP server 실행: leerness mcp serve
|
|
529
|
-
// tools/list 응답:
|
|
529
|
+
// tools/list 응답: 80 도구
|
|
530
530
|
```
|
|
531
531
|
|
|
532
532
|
### Autonomous mode (자율 모드)
|
|
@@ -534,7 +534,7 @@ Leerness v1.9.291는 stdio JSON-RPC MCP server를 내장합니다 — Claude Cod
|
|
|
534
534
|
`<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
|
|
535
535
|
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) 다음 라운드 예약.
|
|
536
536
|
|
|
537
|
-
현재 누적: **70 라운드 (1.9.40 → 1.9.
|
|
537
|
+
현재 누적: **70 라운드 (1.9.40 → 1.9.293)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
|
|
538
538
|
|
|
539
539
|
### 성능 가이드 (1.9.140 측정)
|
|
540
540
|
|
|
@@ -572,6 +572,6 @@ leerness release pack --close --auto-main-push
|
|
|
572
572
|
- `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
|
|
573
573
|
- `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
|
|
574
574
|
|
|
575
|
-
Last synced by Leerness v1.9.
|
|
575
|
+
Last synced by Leerness v1.9.293: 2026-06-03
|
|
576
576
|
<!-- leerness:project-readme:end -->
|
|
577
577
|
|
package/bin/harness.js
CHANGED
|
@@ -10,7 +10,7 @@ const readline = require('readline');
|
|
|
10
10
|
const { _isSecretKey, compareVer, parseHarnessVersion, _classifyCJK, _riskLabel, _detectSystemLang, _parseSlashFromHelp,
|
|
11
11
|
PERMISSION_TIERS, _tierRank, _requiredTier, _policyAllows, _resolveNpmTag, _mcpJsonContent, _newRunRecord } = require('../lib/pure-utils');
|
|
12
12
|
|
|
13
|
-
const VERSION = '1.9.
|
|
13
|
+
const VERSION = '1.9.293';
|
|
14
14
|
|
|
15
15
|
// 1.9.290 (UR-0037, Codex gpt-5.5 #4 수렴): CLI 전용 부작용은 require 시 실행하지 않는다.
|
|
16
16
|
// 이전: warning listener 제거 / NODE_OPTIONS 변경 / chcp IIFE 가 top-level 즉시 실행 → require('harness') 시 호스트 프로세스 오염.
|
|
@@ -431,7 +431,7 @@ function coreFiles(root, lang = 'ko', selectedSkills = [], opts = {}) {
|
|
|
431
431
|
'.harness/skills-lock.json': skillLock(selectedSkills),
|
|
432
432
|
'.harness/project-brief.md': fm('project-brief', ['프로젝트 목적 확인','신규 기능 판단','계획 수립'], ['프로젝트 목적 변경','사용자/범위 변경'], `# Project Brief\n\n## Project\n${project}\n\n## Purpose\n- 이 프로젝트의 목적을 실제 내용으로 업데이트하세요.\n\n## Users\n-\n\n## Success Criteria\n-\n`),
|
|
433
433
|
'.harness/plan.md': fm('plan', ['작업 시작 전','새 요청 접수','범위 변경','신규 프로젝트 감지'], ['계획 추가/수정/드랍','milestone 변경','목표 변경'], `# Plan\n\n## Goal\n- 사용자 목적을 기준으로 전체 계획을 유지합니다.\n\n## Scope\n- 포함 범위를 기록합니다.\n\n## Out of Scope / Dropped\n| ID | Item | Reason | Date |\n|---|---|---|---|\n\n## Milestones\n\n### M-0001. 프로젝트 계획 정리\nStatus: planned\nProgress: 0%\n\nTasks:\n- [ ] project-brief.md를 실제 프로젝트 목적에 맞게 작성\n- [ ] context-map.md를 실제 파일 구조에 맞게 작성\n`),
|
|
434
|
-
'.harness/progress-tracker.md':
|
|
434
|
+
'.harness/progress-tracker.md': _canonicalProgressHeader() + '\n', // 1.9.293: _canonicalProgressHeader 단일 출처 (progressHeader 폴백과 동일)
|
|
435
435
|
'.harness/guideline.md': fm('guideline', ['구현 전 품질 기준 확인','계획 이행 기준 확인'], ['개발 기준 변경','검증 루틴 변경'], `# Guideline\n\n## Operating Principle\n- plan.md의 목표와 범위를 기준으로 작업합니다.\n- progress-tracker.md의 요청 상태를 기준으로 완료/미완료를 판단합니다.\n- guideline.md에는 진행률 수치를 직접 기록하지 않습니다. 진행률은 plan.md/progress-tracker.md가 단일 출처입니다.\n\n## Quality Gate\n- 변경 전 관련 route를 확인합니다 (\`leerness route <task-type>\`).\n- 변경 후 \`leerness verify\`, \`leerness audit\`, \`leerness check\`을 실행합니다.\n- 완료 선언 전 \`leerness lazy detect\`을 실행합니다.\n- 세션 종료 시 \`leerness session close\`를 실행합니다.\n`),
|
|
436
436
|
'.harness/plan-progress-boundary.md': fm('plan-progress-boundary', ['계획과 진행률이 중복될 때','작업 추적 구조 변경'], ['역할 분리 기준 변경'], `# Plan / Progress Boundary\n\n## plan.md\n- 전체 목표, milestone, 포함/제외 범위, 계획 변경 이력.\n\n## progress-tracker.md\n- 사용자 요청 단위의 상태, 증거, 다음 액션.\n- ID 규칙: T-0001부터 단조 증가. plan add 시 부여되는 ID는 plan/progress 양쪽에서 고유합니다.\n\n## guideline.md\n- plan/progress를 수행할 때 지켜야 할 실행 기준.\n`),
|
|
437
437
|
'.harness/current-state.md': fm('current-state', ['세션 시작','작업 이어받기'], ['현재 상태 변경','다음 작업 변경'], `# Current State\n\nUpdated: ${today()}\n\n## Now\n-\n\n## Next\n-\n\n## Blockers\n-\n`),
|
|
@@ -2998,6 +2998,8 @@ function _selfTestCases() {
|
|
|
2998
2998
|
{ name: 'coreFiles --minimal: 핵심 유지 + 비핵심 제외 + verify 필수 보존 (1.9.276)', run: () => { const full = coreFiles('.', 'ko', []); const min = coreFiles('.', 'ko', [], { minimal: true }); const keep = ['.harness/plan.md','.harness/progress-tracker.md','.harness/session-handoff.md','AGENTS.md','CLAUDE.md','.harness/consistency-policy.md','.harness/reuse-map.md','.harness/encoding-policy.md','.harness/secret-policy.md']; const drop = ['.cursor/rules/leerness.mdc','.harness/skill-index.md','.harness/architecture.md']; const verifyReq = ['.harness/design-system.md','.harness/protected-files.md','.harness/current-state.md']; if (!verifyReq.every(k => min[k])) return false; return Object.keys(min).length < Object.keys(full).length && keep.every(k => min[k]) && drop.every(k => !min[k]); } },
|
|
2999
2999
|
{ name: '_cliBootstrap: CLI 부작용 require.main 가드 격리 (Codex #4 UR-0037 1.9.290)', run: () => { if (typeof _cliBootstrap !== 'function' || typeof _ensureStdoutEncoding !== 'function') return false; const src = read(__filename); const guarded = /if \(require\.main === module\) _cliBootstrap\(\);/.test(src); const inFn = /function _cliBootstrap\(\)\s*\{[\s\S]*?removeAllListeners\('warning'\)[\s\S]*?NODE_OPTIONS[\s\S]*?_ensureStdoutEncoding\(\);[\s\S]*?\n\}/.test(src); const noTopIife = !/\}\)\(\);\s*\n\n\/\/ 1\.9\.184/.test(src); return guarded && inFn && noTopIife; } },
|
|
3000
3000
|
{ name: 'lib/agent-registry: EXTERNAL_AGENTS/AGENT_SLASH_COMMANDS 모듈 단일출처 분리 (UR-0025 1.9.291)', run: () => { const m = require('../lib/agent-registry'); return m.EXTERNAL_AGENTS === EXTERNAL_AGENTS && m.AGENT_SLASH_COMMANDS === AGENT_SLASH_COMMANDS && m.EXTERNAL_AGENTS.length === 10 && Object.keys(m.AGENT_SLASH_COMMANDS).length === 9 && !/const EXTERNAL_AGENTS = \[/.test(read(__filename)); } },
|
|
3001
|
+
{ name: 'get_project_context: MCP 시맨틱 verb 등록 + CLI context 디스패치 (UR-0031 1.9.292)', run: () => { const src = read(__filename); const mcpDef = /name: 'leerness_get_project_context'/.test(src); const mcpCase = /case 'leerness_get_project_context':[\s\S]*?cliArgs = \['context'/.test(src); const cliDisp = /if \(cmd === 'context'\)\s+return contextCmd/.test(src); return typeof contextCmd === 'function' && mcpDef && mcpCase && cliDisp && _mcpToolCount() >= 80; } },
|
|
3002
|
+
{ name: '_canonicalProgressHeader + idempotency auto-fix (근본 복제버그 fix 1.9.293)', run: () => { const h = _canonicalProgressHeader(); const headerOk = /leernessRole: progress-tracker/.test(h) && /\| ID \| Status \| Request \|/.test(h) && /\|---\|/.test(h); const src = read(__filename); const fnOk = typeof _autoFixIdempotency === 'function'; const noWholeTextFallback = /if \(idx < 0\) return _canonicalProgressHeader\(\);/.test(src); const driftWired = /_autoFixIdempotency\(root\)/.test(src) && /idempotency 중복/.test(src); return headerOk && fnOk && noWholeTextFallback && driftWired; } },
|
|
3001
3003
|
{ name: 'VERSION 형식 (x.y.z)', run: () => /^\d+\.\d+\.\d+$/.test(VERSION) }
|
|
3002
3004
|
];
|
|
3003
3005
|
}
|
|
@@ -5434,6 +5436,62 @@ function _runIdempotencyAudit(root) {
|
|
|
5434
5436
|
};
|
|
5435
5437
|
return audit;
|
|
5436
5438
|
}
|
|
5439
|
+
// 1.9.293: idempotency audit --auto-fix — task/user-request 중복 자동 회복.
|
|
5440
|
+
// 배경: 룰(1.9.212)/wakeup(1.9.205)은 dedup 보유하나 task/user-request 누적 중복은 탐지만 됐음(수동 drop 필요).
|
|
5441
|
+
// 안전: (1) 완전 동일 행만 제거(순수 중복), (2) active 동일-텍스트는 status=dropped 로 보존(id/히스토리 유지, 삭제 X),
|
|
5442
|
+
// (3) git-tracked 파일이라 회복 가능, (4) 멱등(2회 실행 = no-op).
|
|
5443
|
+
function _autoFixIdempotency(root) {
|
|
5444
|
+
const fixed = [];
|
|
5445
|
+
// 1) progress-tracker.md task 중복
|
|
5446
|
+
try {
|
|
5447
|
+
const pt = path.join(root, '.harness', 'progress-tracker.md');
|
|
5448
|
+
if (exists(pt)) {
|
|
5449
|
+
const header = progressHeader(root);
|
|
5450
|
+
const rows = readProgressRows(root);
|
|
5451
|
+
const seenExact = new Set();
|
|
5452
|
+
const seenActiveText = new Map();
|
|
5453
|
+
const kept = [];
|
|
5454
|
+
let removedExact = 0, droppedSameText = 0;
|
|
5455
|
+
for (const r of rows) {
|
|
5456
|
+
const exactKey = `${r.id}|${r.status}|${r.request}|${r.evidence}|${r.nextAction}|${r.updated}`;
|
|
5457
|
+
if (seenExact.has(exactKey)) { removedExact++; continue; } // 완전 동일 행 = 순수 중복 → 제거
|
|
5458
|
+
seenExact.add(exactKey);
|
|
5459
|
+
const isActive = !/^(done|dropped|blocked|completed|\[완료\]|\[드랍\]|\[보류\])/i.test(r.status || '');
|
|
5460
|
+
if (isActive && r.request && r.request.length >= 5) {
|
|
5461
|
+
if (seenActiveText.has(r.request)) {
|
|
5462
|
+
r.status = 'dropped';
|
|
5463
|
+
r.nextAction = `(auto-dedup 1.9.293 — ${seenActiveText.get(r.request)} 와 동일)`;
|
|
5464
|
+
droppedSameText++;
|
|
5465
|
+
} else {
|
|
5466
|
+
seenActiveText.set(r.request, r.id);
|
|
5467
|
+
}
|
|
5468
|
+
}
|
|
5469
|
+
kept.push(r);
|
|
5470
|
+
}
|
|
5471
|
+
if (removedExact > 0 || droppedSameText > 0) {
|
|
5472
|
+
writeProgressRows(root, header, kept);
|
|
5473
|
+
fixed.push({ kind: 'task-duplicate-request', action: 'deduped', removedExact, droppedSameText });
|
|
5474
|
+
}
|
|
5475
|
+
}
|
|
5476
|
+
} catch (e) { fixed.push({ kind: 'task-duplicate-request', action: 'error', detail: String(e.message || e) }); }
|
|
5477
|
+
// 2) user-requests.json open 중복 — 동일 텍스트 중 최초만 open 유지, 나머지 dropped(보존)
|
|
5478
|
+
try {
|
|
5479
|
+
const ur = _loadUserRequests(root);
|
|
5480
|
+
const seen = new Map();
|
|
5481
|
+
let droppedReq = 0;
|
|
5482
|
+
for (const r of ur.requests) {
|
|
5483
|
+
if (r.status !== 'open' && r.status !== 'in-progress') continue;
|
|
5484
|
+
const k = (r.text || '').trim();
|
|
5485
|
+
if (seen.has(k)) { r.status = 'dropped'; r.droppedReason = `auto-dedup-1.9.293 (== ${seen.get(k)})`; droppedReq++; }
|
|
5486
|
+
else seen.set(k, r.id);
|
|
5487
|
+
}
|
|
5488
|
+
if (droppedReq > 0) {
|
|
5489
|
+
_writeUserRequests(root, ur.requests);
|
|
5490
|
+
fixed.push({ kind: 'user-request-duplicate', action: 'dropped', count: droppedReq });
|
|
5491
|
+
}
|
|
5492
|
+
} catch (e) { fixed.push({ kind: 'user-request-duplicate', action: 'error', detail: String(e.message || e) }); }
|
|
5493
|
+
return fixed;
|
|
5494
|
+
}
|
|
5437
5495
|
function idempotencyCmd(root, sub) {
|
|
5438
5496
|
root = absRoot(root);
|
|
5439
5497
|
const isTty = process.stdout && process.stdout.isTTY;
|
|
@@ -5446,15 +5504,21 @@ function idempotencyCmd(root, sub) {
|
|
|
5446
5504
|
if (!sub || sub === 'help' || sub === '--help') {
|
|
5447
5505
|
log(`# leerness idempotency (1.9.212) — 멱등성 위반 탐지`);
|
|
5448
5506
|
log('');
|
|
5449
|
-
log(` audit
|
|
5507
|
+
log(` audit → 워크스페이스 멱등성 점검 (rules / tasks / user-requests / wakeups) (--json 가능)`);
|
|
5508
|
+
log(` audit --auto-fix → task 완전중복 행 제거 + active 동일텍스트 dropped 처리 + user-request open 중복 정리 (1.9.293)`);
|
|
5450
5509
|
log('');
|
|
5451
5510
|
log(dim(` dedup 적용 영역: ruleAdd / taskAdd (1.9.212) + _recordUserRequest (1.9.207) + _recordWakeup (1.9.205)`));
|
|
5511
|
+
log(dim(` --auto-fix 안전: 완전 동일 행만 제거 / 동일텍스트는 status=dropped 로 보존(id 유지) / git 회복 가능 / 멱등`));
|
|
5512
|
+
log(dim(` 자동화: drift check --auto-fix 가 idempotency 중복도 자동 정리 (1.9.293)`));
|
|
5452
5513
|
log(dim(` opt-out: --force 플래그로 dedup 우회 가능`));
|
|
5453
5514
|
return;
|
|
5454
5515
|
}
|
|
5455
5516
|
|
|
5456
5517
|
if (sub === 'audit') {
|
|
5457
|
-
const
|
|
5518
|
+
const autoFix = has('--auto-fix');
|
|
5519
|
+
const fixes = autoFix ? _autoFixIdempotency(root) : [];
|
|
5520
|
+
const audit = _runIdempotencyAudit(root); // auto-fix 후 상태 반영
|
|
5521
|
+
if (autoFix) audit.autoFixed = fixes;
|
|
5458
5522
|
if (has('--json')) { log(JSON.stringify(audit, null, 2)); return; }
|
|
5459
5523
|
log(cyan(`# leerness idempotency audit (1.9.212)`));
|
|
5460
5524
|
log(` audited at: ${audit.auditedAt}`);
|
|
@@ -5481,6 +5545,19 @@ function idempotencyCmd(root, sub) {
|
|
|
5481
5545
|
if (v.fix) log(dim(` fix: ${v.fix}`));
|
|
5482
5546
|
});
|
|
5483
5547
|
}
|
|
5548
|
+
if (autoFix) {
|
|
5549
|
+
log('');
|
|
5550
|
+
if (fixes.length) {
|
|
5551
|
+
log(grn(`## 🔧 auto-fix 적용 (${fixes.length})`));
|
|
5552
|
+
fixes.forEach(f => {
|
|
5553
|
+
if (f.kind === 'task-duplicate-request') log(` - [tasks] 완전중복 ${f.removedExact || 0}행 제거 · 동일텍스트 ${f.droppedSameText || 0}건 dropped 처리`);
|
|
5554
|
+
else if (f.kind === 'user-request-duplicate') log(` - [user-requests] open 중복 ${f.count || 0}건 dropped 처리`);
|
|
5555
|
+
else log(dim(` - [${f.kind}] ${f.action}${f.detail ? ' — ' + f.detail : ''}`));
|
|
5556
|
+
});
|
|
5557
|
+
} else {
|
|
5558
|
+
log(dim(' 🔧 auto-fix: 적용할 중복 없음 (이미 정합)'));
|
|
5559
|
+
}
|
|
5560
|
+
}
|
|
5484
5561
|
return;
|
|
5485
5562
|
}
|
|
5486
5563
|
|
|
@@ -5979,10 +6056,17 @@ function readProgressRows(root) {
|
|
|
5979
6056
|
}
|
|
5980
6057
|
return rows;
|
|
5981
6058
|
}
|
|
6059
|
+
// 1.9.293: progress-tracker 정식 헤더 (frontmatter + 표 헤더 + 분리자). coreFiles 와 단일 출처.
|
|
6060
|
+
function _canonicalProgressHeader() {
|
|
6061
|
+
return fm('progress-tracker', ['세션 시작', '세션 종료', '사용자 요청 상태 확인'], ['작업 상태 변경', '검증 결과 추가', '사용자 요청 드랍'],
|
|
6062
|
+
`# Progress Tracker\n\nStatus values: requested, planned, in-progress, waiting, on-hold, blocked, incomplete, done, dropped\n\n| ID | Status | Request | Evidence | Next Action | Updated |\n|---|---|---|---|---|---|\n`).trimEnd();
|
|
6063
|
+
}
|
|
5982
6064
|
function progressHeader(root) {
|
|
5983
6065
|
const text = exists(progressPath(root)) ? read(progressPath(root)) : '';
|
|
5984
6066
|
const idx = text.indexOf('|---|');
|
|
5985
|
-
|
|
6067
|
+
// 1.9.293 (근본 버그 fix): 분리자 없음(손상/헤더유실) 시 전체 텍스트 반환은 writeProgressRows 가 행을 복제 →
|
|
6068
|
+
// 세션마다 중복 누적의 원인이었음. 정식 헤더를 재구성해 반환 → 다음 write 시 자동 복구.
|
|
6069
|
+
if (idx < 0) return _canonicalProgressHeader();
|
|
5986
6070
|
return text.slice(0, text.indexOf('\n', idx)).trimEnd();
|
|
5987
6071
|
}
|
|
5988
6072
|
function writeProgressRows(root, header, rows) {
|
|
@@ -15492,6 +15576,20 @@ function driftCheckCmd(root, opts = {}) {
|
|
|
15492
15576
|
log(`⚠ delivered auto-apply 오류 (1.9.225): ${e.message}`);
|
|
15493
15577
|
}
|
|
15494
15578
|
}
|
|
15579
|
+
// 1.9.293: drift check --auto-fix 에 idempotency task/user-request 중복 자동 정리 통합
|
|
15580
|
+
// 누적 중복 task/요청이 idempotency 위반(medium)을 가중 → drift/handoff 노이즈. 안전: 완전중복 행 제거 + 동일텍스트 dropped 보존(id 유지).
|
|
15581
|
+
if (autoFix) {
|
|
15582
|
+
try {
|
|
15583
|
+
const idemFixes = _autoFixIdempotency(root);
|
|
15584
|
+
const totalFixed = idemFixes.reduce((n, f) => n + (f.removedExact || 0) + (f.droppedSameText || 0) + (f.count || 0), 0);
|
|
15585
|
+
if (totalFixed > 0) {
|
|
15586
|
+
log('');
|
|
15587
|
+
log(`🔁 --auto-fix 활성 (1.9.293) — idempotency 중복 ${totalFixed}건 자동 정리 (task/user-request dedup)`);
|
|
15588
|
+
}
|
|
15589
|
+
} catch (e) {
|
|
15590
|
+
log(`⚠ idempotency auto-fix 오류 (1.9.293): ${e.message}`);
|
|
15591
|
+
}
|
|
15592
|
+
}
|
|
15495
15593
|
// 1.9.236: drift check --auto-fix 에 release cleanup 통합 (1.9.235 회수)
|
|
15496
15594
|
// 누적된 50개+ release/* branches → abnormal-shutdown release-branch-pending 신호 가중
|
|
15497
15595
|
// 안전: keep 10 (최근 10개 유지), merged 만 삭제 (1.9.235 안전 가드)
|
|
@@ -16531,7 +16629,8 @@ function mcpServeCmd(root) {
|
|
|
16531
16629
|
{ name: 'leerness_state_start', description: '1.9.279 (UR-0031) — start_task. 새 작업 run 시작 (.leerness/runs/run-NNNN.json 생성). 인자: { path?, goal (required), agent?, model?, task? }. 응답: { started, run }.', inputSchema: { type: 'object', properties: { path: { type: 'string' }, goal: { type: 'string' }, agent: { type: 'string' }, model: { type: 'string' }, task: { type: 'string' } }, required: ['goal'] } },
|
|
16532
16630
|
{ name: 'leerness_state_record', description: '1.9.279 (UR-0031) — record_file_change / record_decision. 진행 중 run 에 변경 파일/명령/테스트/결정을 누적. 인자: { path?, filesChanged? (콤마), filesRead?, commands?, tests?, errors?, decision? }. 응답: { recorded, run }.', inputSchema: { type: 'object', properties: { path: { type: 'string' }, filesChanged: { type: 'string' }, filesRead: { type: 'string' }, commands: { type: 'string' }, tests: { type: 'string' }, errors: { type: 'string' }, decision: { type: 'string' } } } },
|
|
16533
16631
|
{ name: 'leerness_state_verify', description: '1.9.279 (UR-0031) — request_verification / verify_done. 현재 run 의 검증 결과 기록. 인자: { path?, result (required: "pass"|"fail"), note? }. 응답: { verified, result, run }.', inputSchema: { type: 'object', properties: { path: { type: 'string' }, result: { type: 'string', enum: ['pass', 'fail'] }, note: { type: 'string' } }, required: ['result'] } },
|
|
16534
|
-
{ name: 'leerness_state_handoff', description: '1.9.279 (UR-0031) — create_handoff / make_handoff. 현재 run 을 마감하고 다음 에이전트용 .leerness/handoff/latest.{md,json} 작성 (Claude→Goose→Codex 인수인계 표준). 인자: { path?, summary (required) }. 응답: { handoff, run }.', inputSchema: { type: 'object', properties: { path: { type: 'string' }, summary: { type: 'string' } }, required: ['summary'] } }
|
|
16632
|
+
{ name: 'leerness_state_handoff', description: '1.9.279 (UR-0031) — create_handoff / make_handoff. 현재 run 을 마감하고 다음 에이전트용 .leerness/handoff/latest.{md,json} 작성 (Claude→Goose→Codex 인수인계 표준). 인자: { path?, summary (required) }. 응답: { handoff, run }.', inputSchema: { type: 'object', properties: { path: { type: 'string' }, summary: { type: 'string' } }, required: ['summary'] } },
|
|
16633
|
+
{ name: 'leerness_get_project_context', description: '1.9.292 (UR-0031) — get_project_context. 외부 에이전트가 작업 시작 전 단 1콜로 "지금 무엇을 알아야 하는가"를 파악하는 집약 컨텍스트. 여러 소스(현재 작업/미답 사용자 요청/최근 결정/활성 룰/next-actions/memory surface/프로젝트 의도)를 한 번에 구조화 회수. read-only. 인자: { path? }. 응답: { version, project, currentTask, openRequests, recentDecisions, activeRules, nextActions, memory }.', inputSchema: { type: 'object', properties: { path: { type: 'string' } } } }
|
|
16535
16634
|
];
|
|
16536
16635
|
|
|
16537
16636
|
function send(obj) {
|
|
@@ -16781,6 +16880,9 @@ function mcpServeCmd(root) {
|
|
|
16781
16880
|
case 'leerness_state_handoff':
|
|
16782
16881
|
cliArgs = ['state', 'handoff', String(args.summary || ''), '--path', targetPath, '--json'];
|
|
16783
16882
|
break;
|
|
16883
|
+
case 'leerness_get_project_context':
|
|
16884
|
+
cliArgs = ['context', '--path', targetPath, '--json'];
|
|
16885
|
+
break;
|
|
16784
16886
|
default:
|
|
16785
16887
|
return send({ jsonrpc: '2.0', id, error: { code: -32601, message: `Unknown tool: ${name}` } });
|
|
16786
16888
|
}
|
|
@@ -18105,6 +18207,77 @@ function _saveRun(root, rec) { const f = _runFile(root, rec.run_id); mkdirp(path
|
|
|
18105
18207
|
function _splitList(v) { return String(v || '').split(',').map(s => s.trim()).filter(Boolean); }
|
|
18106
18208
|
|
|
18107
18209
|
// leerness state <show|start|record|verify|handoff>
|
|
18210
|
+
// 1.9.292 (UR-0031): get_project_context — 외부 에이전트 온보딩용 단일 집약 컨텍스트.
|
|
18211
|
+
// 1콜로 현재 작업/미답 요청/최근 결정/활성 룰/next-actions/memory/프로젝트 의도를 구조화 회수.
|
|
18212
|
+
// MCP leerness_get_project_context 가 이 명령(--json)을 호출. handoff(인간용 장문)와 달리 기계 친화 lean payload.
|
|
18213
|
+
function contextCmd(root, opts = {}) {
|
|
18214
|
+
root = absRoot(root);
|
|
18215
|
+
const rows = readProgressRows(root);
|
|
18216
|
+
const active = rows.find(r => r.status === 'in-progress' || r.status === '[진행]');
|
|
18217
|
+
const activeRules = readRules(root).filter(r => r.status === 'active');
|
|
18218
|
+
const openReqs = (_loadUserRequests(root).requests || []).filter(r => r.status === 'open' || r.status === 'in-progress');
|
|
18219
|
+
// 최근 결정 (last 3, 최신순) — decisionListCmd 와 동일 파서 재사용
|
|
18220
|
+
const decFile = decisionsPath(root);
|
|
18221
|
+
let recentDecisions = [];
|
|
18222
|
+
let decisionCount = 0;
|
|
18223
|
+
if (exists(decFile)) {
|
|
18224
|
+
const dtext = read(decFile);
|
|
18225
|
+
decisionCount = (dtext.match(/^### \d{4}-\d{2}-\d{2}/gm) || []).length;
|
|
18226
|
+
const blocks = _extractDecisionBlocks(dtext);
|
|
18227
|
+
recentDecisions = blocks.slice(-3).reverse().map(block => {
|
|
18228
|
+
const tm = (block.match(/^### (.+)$/m) || [])[1] || '';
|
|
18229
|
+
const dt = tm.match(/^(\d{4}-\d{2}-\d{2})\s*—\s*(.+)$/);
|
|
18230
|
+
return { date: dt ? dt[1] : null, title: (dt ? dt[2] : tm).trim().slice(0, 100) };
|
|
18231
|
+
});
|
|
18232
|
+
}
|
|
18233
|
+
const queueState = _loadNextActionQueue(root);
|
|
18234
|
+
const nextActions = (queueState.queue || []).slice(-3).reverse().map(a => ({ title: a.title, command: a.command || null }));
|
|
18235
|
+
const memory = {
|
|
18236
|
+
tasksInProgress: rows.filter(r => r.status === 'in-progress').length,
|
|
18237
|
+
decisions: decisionCount,
|
|
18238
|
+
rulesActive: activeRules.length,
|
|
18239
|
+
lessons: exists(lessonsPath(root)) ? (read(lessonsPath(root)).match(/^### \d{4}-\d{2}-\d{2}/gm) || []).length : 0
|
|
18240
|
+
};
|
|
18241
|
+
// 프로젝트 의도 (best-effort) — .harness/project-brief.md 의 ## Purpose 첫 줄
|
|
18242
|
+
let intent = null;
|
|
18243
|
+
const briefPath = path.join(root, '.harness', 'project-brief.md');
|
|
18244
|
+
if (exists(briefPath)) {
|
|
18245
|
+
const m = read(briefPath).match(/##\s*Purpose\s*\n+\s*[-*]?\s*([^\n#].*)/);
|
|
18246
|
+
if (m && !/업데이트하세요/.test(m[1])) intent = m[1].trim().slice(0, 200);
|
|
18247
|
+
}
|
|
18248
|
+
const ctx = {
|
|
18249
|
+
schemaVersion: 1,
|
|
18250
|
+
version: VERSION,
|
|
18251
|
+
project: { root, intent },
|
|
18252
|
+
currentTask: active ? { id: active.id, request: (active.request || '').slice(0, 200), status: active.status, nextAction: (active.nextAction || '').slice(0, 160) || null } : null,
|
|
18253
|
+
openRequests: { count: openReqs.length, items: openReqs.slice(0, 5).map(r => ({ id: r.id, text: (r.text || '').slice(0, 120) })) },
|
|
18254
|
+
recentDecisions,
|
|
18255
|
+
activeRules: activeRules.map(r => ({ id: r.id, trigger: r.trigger, rule: r.rule })),
|
|
18256
|
+
nextActions,
|
|
18257
|
+
memory
|
|
18258
|
+
};
|
|
18259
|
+
if (opts.json || has('--json')) { process.stdout.write(JSON.stringify(ctx, null, 2) + '\n'); return ctx; }
|
|
18260
|
+
const isTty = process.stdout && process.stdout.isTTY;
|
|
18261
|
+
const cy = s => isTty ? `\x1b[36m${s}\x1b[0m` : s;
|
|
18262
|
+
const gr = s => isTty ? `\x1b[32m${s}\x1b[0m` : s;
|
|
18263
|
+
const dm = s => isTty ? `\x1b[2m${s}\x1b[0m` : s;
|
|
18264
|
+
log(cy(`# leerness context (1.9.292) — 에이전트 온보딩 컨텍스트 (v${VERSION})`));
|
|
18265
|
+
if (intent) log(` 🎯 의도: ${intent}`);
|
|
18266
|
+
log('');
|
|
18267
|
+
if (ctx.currentTask) {
|
|
18268
|
+
log(gr(`▶ 현재 작업: ${ctx.currentTask.id} — ${ctx.currentTask.request}`));
|
|
18269
|
+
if (ctx.currentTask.nextAction) log(dm(` 다음: ${ctx.currentTask.nextAction}`));
|
|
18270
|
+
} else log(dm('▶ 현재 진행 중 작업 없음'));
|
|
18271
|
+
log('');
|
|
18272
|
+
log(`📥 미답 요청: ${ctx.openRequests.count}건`);
|
|
18273
|
+
ctx.openRequests.items.forEach(r => log(dm(` • [${r.id}] ${r.text}`)));
|
|
18274
|
+
log('');
|
|
18275
|
+
log(`🧠 메모리: 진행 ${memory.tasksInProgress} / 결정 ${memory.decisions} / 룰 ${memory.rulesActive} / 교훈 ${memory.lessons}`);
|
|
18276
|
+
if (recentDecisions.length) { log(''); log('🗂 최근 결정:'); recentDecisions.forEach(d => log(dm(` • ${d.date || '?'} — ${d.title}`))); }
|
|
18277
|
+
if (ctx.activeRules.length) { log(''); log('⚡ 활성 룰:'); ctx.activeRules.forEach(r => log(dm(` • [${r.id}] (${r.trigger}) ${r.rule}`))); }
|
|
18278
|
+
if (nextActions.length) { log(''); log('👉 다음 액션:'); nextActions.forEach(a => log(dm(` • ${a.title}${a.command ? ' → ' + a.command : ''}`))); }
|
|
18279
|
+
return ctx;
|
|
18280
|
+
}
|
|
18108
18281
|
function stateCmd(root, sub, ...args) {
|
|
18109
18282
|
root = absRoot(root || process.cwd());
|
|
18110
18283
|
const json = has('--json');
|
|
@@ -21315,6 +21488,7 @@ async function main() {
|
|
|
21315
21488
|
if (cmd === 'capabilities' || cmd === 'security-surface') return capabilitiesCmd(arg('--path', process.cwd()), { json: has('--json') });
|
|
21316
21489
|
// 1.9.278 (UR-0032): leerness state <show|start|record|verify|handoff> — .leerness/ 구조화 상태 substrate
|
|
21317
21490
|
if (cmd === 'state') return stateCmd(arg('--path', process.cwd()), args[1], ...args.slice(2));
|
|
21491
|
+
if (cmd === 'context') return contextCmd(arg('--path', process.cwd()), { json: has('--json') });
|
|
21318
21492
|
// 1.9.281 (UR-0034): leerness policy <show|set|check> — 권한 등급 (opt-in enforced)
|
|
21319
21493
|
if (cmd === 'policy') return policyCmd(arg('--path', process.cwd()), args[1], ...args.slice(2));
|
|
21320
21494
|
// 1.9.285 (UR-0023): leerness reuse-check "<기능>" — 외부 OSS 빌드 vs 재사용 결정 게이트 (오프라인)
|
package/package.json
CHANGED
package/scripts/e2e.js
CHANGED
|
@@ -3136,5 +3136,50 @@ total++;
|
|
|
3136
3136
|
if (!ok) failed++;
|
|
3137
3137
|
}
|
|
3138
3138
|
|
|
3139
|
+
// 1.9.292 회귀 (UR-0031): leerness context — get_project_context 집약 시맨틱 verb
|
|
3140
|
+
total++;
|
|
3141
|
+
{
|
|
3142
|
+
let ok = false;
|
|
3143
|
+
try {
|
|
3144
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'context', tmp, '--json'], { cwd: tmp, encoding: 'utf8', timeout: 30000 });
|
|
3145
|
+
const j = JSON.parse(r.stdout);
|
|
3146
|
+
const structOk = r.status === 0 && j.schemaVersion === 1 && !!j.version && !!j.project && ('currentTask' in j) &&
|
|
3147
|
+
j.openRequests && typeof j.openRequests.count === 'number' && Array.isArray(j.recentDecisions) &&
|
|
3148
|
+
Array.isArray(j.activeRules) && Array.isArray(j.nextActions) && j.memory && typeof j.memory.rulesActive === 'number';
|
|
3149
|
+
const h = require(path.resolve(__dirname, '..', 'bin', 'harness.js'));
|
|
3150
|
+
const mcpOk = h._mcpToolCount && h._mcpToolCount() >= 80;
|
|
3151
|
+
ok = structOk && mcpOk;
|
|
3152
|
+
} catch {}
|
|
3153
|
+
console.log(ok ? '✓ B(1.9.292) leerness context: get_project_context 집약 JSON 구조 + MCP 80 도구 (UR-0031)' : '✗ context 집약 verb 실패');
|
|
3154
|
+
if (!ok) failed++;
|
|
3155
|
+
}
|
|
3156
|
+
|
|
3157
|
+
// 1.9.293 회귀: idempotency audit --auto-fix + progressHeader 복제버그 fix
|
|
3158
|
+
total++;
|
|
3159
|
+
{
|
|
3160
|
+
let ok = false;
|
|
3161
|
+
try {
|
|
3162
|
+
const idemDir = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-idem-'));
|
|
3163
|
+
cp.spawnSync(process.execPath, [CLI, 'init', idemDir, '--yes', '--language', 'ko', '--skills', 'recommended'], { encoding: 'utf8', timeout: 30000 });
|
|
3164
|
+
const pt = path.join(idemDir, '.harness', 'progress-tracker.md');
|
|
3165
|
+
// (1) 헤더 유실 + 완전중복 + 동일텍스트 중복 시뮬레이션 (과거 손상 패턴)
|
|
3166
|
+
const dupRow = '| T-7001 | in-progress | 중복작업ABC | - | - | 2026-06-04 |';
|
|
3167
|
+
const sameText = '| T-7002 | in-progress | 중복작업ABC | - | - | 2026-06-04 |';
|
|
3168
|
+
fs.writeFileSync(pt, [dupRow, dupRow, dupRow, sameText].join('\n') + '\n', 'utf8'); // 헤더 없음(손상)
|
|
3169
|
+
// (2) auto-fix
|
|
3170
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'idempotency', 'audit', '--path', idemDir, '--auto-fix'], { encoding: 'utf8', timeout: 20000 });
|
|
3171
|
+
const after = fs.readFileSync(pt, 'utf8');
|
|
3172
|
+
const headerRestored = /\|---\|/.test(after) && /leernessRole: progress-tracker/.test(after); // 헤더 재구성
|
|
3173
|
+
const exactCollapsed = (after.match(/\| T-7001 \|/g) || []).length === 1; // 완전중복 3→1
|
|
3174
|
+
// (3) 재검사 clean
|
|
3175
|
+
const r2 = cp.spawnSync(process.execPath, [CLI, 'idempotency', 'audit', '--path', idemDir, '--json'], { encoding: 'utf8', timeout: 20000 });
|
|
3176
|
+
const audit = JSON.parse(r2.stdout);
|
|
3177
|
+
const taskDupGone = !audit.violations.some(v => v.kind === 'task-duplicate-request');
|
|
3178
|
+
ok = r.status === 0 && headerRestored && exactCollapsed && taskDupGone;
|
|
3179
|
+
} catch {}
|
|
3180
|
+
console.log(ok ? '✓ B(1.9.293) idempotency --auto-fix: 헤더 재구성 + 완전중복 제거 + dedup clean (복제버그 fix)' : '✗ idempotency auto-fix 실패');
|
|
3181
|
+
if (!ok) failed++;
|
|
3182
|
+
}
|
|
3183
|
+
|
|
3139
3184
|
console.log(`\nE2E result: ${total - failed}/${total} passed · ${((Date.now() - _e2eStart) / 1000).toFixed(0)}s`);
|
|
3140
3185
|
if (failed > 0) process.exit(1);
|