leerness 1.36.17 → 1.36.19
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 +19 -0
- package/README.md +4 -4
- package/bin/leerness.js +106 -29
- package/lib/audit.js +11 -2
- package/lib/feature.js +3 -3
- package/lib/health.js +1 -1
- package/lib/pure-utils.js +29 -3
- package/lib/session-close.js +3 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.36.19 — 2026-07-14 — 전략 앵커(project-brief/plan Goal) 미작성 감지 — 인계 AI가 프로젝트 맥락을 못 받던 근본원인 표면화 (실사용 7 프로젝트 dogfood)
|
|
4
|
+
|
|
5
|
+
**현장 관찰**: leerness를 오래 적용한 실프로젝트에서, 인계받은 AI(codex 등)가 "leerness를 참조하나 프로젝트 맥락을 이해 못 하고" 작업하는 현상이 보고됨. 7개 실사용 프로젝트를 dogfood 조사한 결과 근본원인이 드러남 — **동적 상태(current-state 7/7, decisions)는 잘 유지되나, 정체성 앵커인 `project-brief.md` Purpose(5/7 미작성)와 `plan.md` Goal/Scope(7/7 미작성)이 템플릿 placeholder 그대로 방치됨**. 27개 결정을 쌓은 프로젝트조차 brief는 빈칸. 그 결과 인계 AI는 "최근 무슨 작업을 했는지"(동적)는 받지만 "이 프로젝트가 무엇이고 범위가 어디까지인지"(정체성)를 못 받아, 최근 스레드만 이어가며 전체 프레임을 놓침. 게다가 leerness 스스로 이 갭을 전혀 경고하지 않았음(감지 부재).
|
|
6
|
+
|
|
7
|
+
- **전략 앵커 미작성 감지 (신규)**: pure-utils에 `_briefUnfilled`/`_planGoalUnfilled` 순수 detector 추가 — `## Purpose`/`## Goal` 섹션이 템플릿 placeholder 문구이거나 실내용 0(빈 bullet)이면 미작성으로 판정. bullet/문단 무관(문단형 실작성도 정상 인식), 섹션 자체가 없으면 미플래그(FP 회피). 7 프로젝트 ground-truth + 컨트롤(placeholder/빈칸/bullet작성/문단작성/섹션없음)로 판별 정밀 검증.
|
|
8
|
+
- **`audit` finding 2종**: project-brief Purpose 미작성 → `project_brief_unfilled` 경고("인계받는 AI가 프로젝트 목적/맥락을 못 받습니다"), plan Goal 미작성 → `plan_goal_unfilled` 경고. `<!-- leerness:na -->` 마커로 의도적 스킵 가능(기존 design-system/reuse-map 관례와 동일).
|
|
9
|
+
- **handoff 헤드라인 표면화**: 세션 시작 헤드라인에 `📋 project-brief 미작성` / `📋 plan Goal 미작성` / `📋 정체성앵커 미작성 (brief+plan)` 노출 — 파묻힌 init task(T-0001) 대신 매 세션 가시화. 채워진 프로젝트엔 무신호(무노이즈).
|
|
10
|
+
- **검증**: selftest 289/289(신규 앵커 감지 행위검사 — placeholder/빈칸/bullet/문단/섹션없음 5판별 + audit/handoff 배선 가드), 실프로젝트 실증(Adzento=brief+plan 경고, cafe-macro/view-work=brief populated·plan만), 게이트 e2e, 게시본 클린룸.
|
|
11
|
+
- **이연(다음 라운드, codex fresh-surface QA)**: #7 `--json`이 mutation 전 early-return(`reuse autodetect`/`release cleanup`/`env encoding` `--apply --json`이 apply를 보고하나 실제 미적용) + 실패 시 exit 0, #8 CRLF `plan.md` tasks:[] & 잘못된 JSON 스키마가 `--json` 경로에서 crash(구조화 에러 아님), #6 `copyRec` lstatSync 심링크 미처리. 재현→수정 예정.
|
|
12
|
+
|
|
13
|
+
## 1.36.18 — 2026-07-14 — UR-0052: handoff --compact 단일경로 + ID 5자리+ 대응 (P2-6 주석 마스킹은 조사 후 이연)
|
|
14
|
+
|
|
15
|
+
1.36.17에서 백로그로 이연했던 UR-0052 3건 중 2건(P3-8·P1-2)을 재현·수정. P2-6(주석 마스킹)은 codex 7라운드 적대 재검 끝에 **파서 없이 안전 구현 불가**로 판단해 이연(하단 상세). 각 수정은 파일 스크립트 프로브 + selftest 케이스로 실증(맹신 X).
|
|
16
|
+
|
|
17
|
+
- **P3-8 handoff `--compact` 단일 워크스페이스 무효**: `--compact`가 멀티(`_handoffWorkspace`)에만 있어 단일 `handoff . --compact`는 섹션 몇 개만 억제하고 본문 186줄 전량 출력 → 문서상 "~500자 요약"과 불일치. MCP handoff·REPL preview·agent-mode 등 내부 호출자 3곳이 단일 경로에 `--compact`를 써 preview 첫 3줄이 헤더/날짜/이름으로 무의미했음(재현: 186줄). → 단일 `handoff()`에 compact 단축 추가(subprocess 없이 파일 로더만으로 프로젝트/진척/메모리요약/다음액션/미답요청 3줄 압축, `_lineSafe`로 projectName 개행 주입 차단). 실측 186→3줄, `--json`은 여전히 full, preview 유의미화.
|
|
18
|
+
- **P1-2 ID 리더 5자리+ 대응**: T-/M-/D-/R-/F- ID를 `\d{4}`(정확히 4자리)로 읽어, 10,000번째 ID(T-10000)부터 앞 4자리("1000")만 잡아 truncation/충돌(예: T-10000 뒤 `task add`가 T-1001 재발급)(재현확정). → letter-prefix ID의 `\d{4}`를 `\d{4,}`로 확장(bin 28·pure-utils 2·lib 9 = `feature.js`/`audit.js`/`health.js`/`session-close.js` 포함 — codex가 이 lib 누락을 지적). 판별자 `(?!-)`로 날짜 연도(`\d{4}-`)는 exactly-4 보존(불변 assert). `\d{4,}`는 4자리 ID도 매치해 무회귀. 실측: T-10000 뒤 add→**T-10001**, `feature show F-10000` 수용, 밀스톤 M-10000 카운트, 날짜 정상.
|
|
19
|
+
- **P2-6 이연(조사 완료, 미채택)**: `contract verify`가 raw 소스를 훑어 주석처리된 `// module.exports = { fn }`을 실 구현으로 카운트하는 FN은 실재한다. 그러나 이를 고치려면 스캔 전 "주석만" 지워야 하고, JS에서 주석 경계를 정확히 찾으려면 정규식-리터럴/나눗셈/문자열/템플릿 보간(임의 코드)을 모두 렉싱해야 한다. 의존성 0(파서 금지) 제약 하에 손수 만든 마스커 3종(주석+문자열+정규식 마스킹 → 주석+문자열만 원문보존 → 줄머리 주석만)을 codex가 **7라운드 연속** 유효 JS에서 깨뜨렸다 — 특히 실코드를 지우는 **거짓 FAIL**(정당 계약을 CI에서 차단): `if(x) /[/*]/`의 `/*`를 유령 블록주석으로, `${/[}]/.test(x)?`\n/* ${f.field} */\n`:''}`의 정규식 `}`를 보간 종료로 오계수해 중첩 템플릿의 실 필드를 삭제 등. CI 게이트에서 거짓 FAIL(정당 계약 차단) > 거짓 PASS(주석 export 카운트)이므로, **니치한 FN을 위해 현실적 거짓 FAIL 위험을 들이는 것은 순손해**로 판단하고 미채택. 근본 해법은 실 JS 파서(제약 위반)라 이연. contract verify는 기존 raw-소스 동작 유지(무회귀).
|
|
20
|
+
- **검증**: selftest 288/288(P1-2 케이스에 lib 확장 가드 포함; P2-6 케이스는 revert로 제거), P3-8/P1-2 파일 스크립트 프로브 + CLI E2E 실증, 게이트 e2e 386/386, 게시본 클린룸 재실증.
|
|
21
|
+
|
|
3
22
|
## 1.36.17 — 2026-07-14 — 렌즈-외 표면 버그헌트: 데이터손실 2 + gate FN + 아카이브 주입 + --json 에러경로 (codex, 재현확정)
|
|
4
23
|
|
|
5
24
|
게시본 1.36.16 대상, database 렌즈를 제외한 CLI 표면을 codex로 광범위 적대 헌트 → 8건 중 6건 수정(고영향 재현확정), 2건 백로그(UR-0052).
|
package/README.md
CHANGED
|
@@ -125,7 +125,7 @@ MIT
|
|
|
125
125
|
<!-- leerness:project-readme:start -->
|
|
126
126
|
## Leerness Project Harness
|
|
127
127
|
|
|
128
|
-
이 프로젝트는 Leerness v1.36.
|
|
128
|
+
이 프로젝트는 Leerness v1.36.19 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
|
|
129
129
|
|
|
130
130
|
### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
|
|
131
131
|
|
|
@@ -179,7 +179,7 @@ leerness memory restore decision <date|title>
|
|
|
179
179
|
|
|
180
180
|
### MCP server (외부 AI 통합)
|
|
181
181
|
|
|
182
|
-
Leerness v1.36.
|
|
182
|
+
Leerness v1.36.19는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **86개 도구**를 노출:
|
|
183
183
|
|
|
184
184
|
```jsonc
|
|
185
185
|
// 카테고리별
|
|
@@ -200,7 +200,7 @@ Leerness v1.36.17는 stdio JSON-RPC MCP server를 내장합니다 — Claude Cod
|
|
|
200
200
|
`<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
|
|
201
201
|
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) 다음 라운드 예약.
|
|
202
202
|
|
|
203
|
-
현재 누적: **70 라운드 (1.9.40 → 1.36.
|
|
203
|
+
현재 누적: **70 라운드 (1.9.40 → 1.36.19)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
|
|
204
204
|
|
|
205
205
|
### 성능 가이드 (1.9.140 측정)
|
|
206
206
|
|
|
@@ -238,6 +238,6 @@ leerness release pack --close --auto-main-push
|
|
|
238
238
|
- `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
|
|
239
239
|
- `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
|
|
240
240
|
|
|
241
|
-
Last synced by Leerness v1.36.
|
|
241
|
+
Last synced by Leerness v1.36.19: 2026-07-14
|
|
242
242
|
<!-- leerness:project-readme:end -->
|
|
243
243
|
|
package/bin/leerness.js
CHANGED
|
@@ -26,14 +26,14 @@ const { _isSecretKey, _isPlaceholderSecret, _looksSecretLike, _mergeLines, _merg
|
|
|
26
26
|
_migrationGuideText, _parseContractSpec, _gitignoreMatch,
|
|
27
27
|
_featureGraphTemplate, _parseFeatureGraph, _nextFeatureId, _featureBlock, _featureImpactBfs,
|
|
28
28
|
_parseChangelogBetween, _cellSafe, _cellUnescape, _lineSafe, _parseLimit, _parseAddTitle, _parseImplExports, _taskPositionalPath, _completionClaimAllowed, _minorKey, _shouldPublishNpm,
|
|
29
|
-
_matchTool, _parsePackageJsonDeps, _parseRequirementsTxt, _buildGlossary, _renderGlossaryMd } = require('../lib/pure-utils'); // 1.9.318~1.11.4 (UR-0025/.../0007 glossary): 순수 유틸 모듈 분리
|
|
29
|
+
_matchTool, _parsePackageJsonDeps, _parseRequirementsTxt, _buildGlossary, _renderGlossaryMd, _briefUnfilled, _planGoalUnfilled } = require('../lib/pure-utils'); // 1.9.318~1.11.4 (UR-0025/.../0007 glossary): 순수 유틸 모듈 분리
|
|
30
30
|
// 1.9.304 (UR-0025): 순수 분석/검증 함수 모듈 분리.
|
|
31
31
|
const { _evidenceQuality, _parseEvidenceStats, _shellGuardAnalyze, _claimFileInGit, _epistemicHonestyCheck } = require('../lib/analyzers');
|
|
32
32
|
// 1.9.295 (UR-0025 4단계): 정적 데이터 카탈로그 모듈 분리 (비파괴, require-based).
|
|
33
33
|
const { CAPABILITY_SURFACE, POWERFUL_COMMANDS, ADAPTERS, REUSE_CATEGORIES, REUSE_CHECKLIST, _DEFAULT_PLATFORM_CONSTRAINTS, _DEFAULT_DOMAIN_CATALOG, _TOOL_CATALOG, _LSP_LANG_PATTERNS, OPTIMISM_PATTERNS, BUILT_IN_PERSONAS, STRINGS, BUILTIN_CATALOG, ROADMAP_STATUS_LABEL, ROADMAP_STATUS_COLOR, SECRET_PATTERNS, MERGE_OVERWRITE_FILES, MINIMAL_SKIP_KEYS, REQUIRED_WORKSPACE_FILES, KEYWORD_STOPWORDS, SKILL_CATALOG_PRESETS } = require('../lib/catalogs'); // 1.9.344/368/369 (UR-0025): catalog 분리 · 1.11.4 (UR-0007): _TOOL_CATALOG
|
|
34
34
|
const { findCorruptedStateJson: _findCorruptedStateJson } = require('../lib/state-integrity'); // 1.36.1 (클린룸 리뷰 FN): .harness/*.json 상태 무결성 (audit/health/check 공유)
|
|
35
35
|
|
|
36
|
-
const VERSION = '1.36.
|
|
36
|
+
const VERSION = '1.36.19';
|
|
37
37
|
|
|
38
38
|
// 1.9.290 (UR-0037, Codex gpt-5.5 #4 수렴): CLI 전용 부작용은 require 시 실행하지 않는다.
|
|
39
39
|
// 이전: warning listener 제거 / NODE_OPTIONS 변경 / chcp IIFE 가 top-level 즉시 실행 → require('harness') 시 호스트 프로세스 오염.
|
|
@@ -2963,6 +2963,43 @@ function _selfTestCases() {
|
|
|
2963
2963
|
const realDecl = p._parseContractSpec('function realFn(){}\n').declared.includes('realFn'); // 회귀: 펜스 밖 선언 유지
|
|
2964
2964
|
return bracket && dotStill && fenceExcluded && realDecl;
|
|
2965
2965
|
} },
|
|
2966
|
+
{ name: 'ID 리더 5자리+ 대응 (1.36.18, UR-0052 P1-2): \\d{4}→\\d{4,} — 10k+ ID(T-10000) truncation 방지, 날짜 연도는 exactly-4 보존 — 행위검사', run: () => {
|
|
2967
|
+
// 행위: 5자리 ID 전체 캡처(구 \d{4}는 앞 4자리만 → truncation/충돌), 4자리 ID 무회귀, 날짜 연도 exactly-4 유지.
|
|
2968
|
+
const taskRe = /\bT-(\d{4,})\b/;
|
|
2969
|
+
const idFive = (taskRe.exec('T-12345') || [])[1] === '12345'; // 5자리 전체
|
|
2970
|
+
const idFour = (taskRe.exec('T-0001') || [])[1] === '0001'; // 회귀: 4자리 유지
|
|
2971
|
+
const ruleFive = /^R-(\d{4,})$/.test('R-99999') && /^R-(\d{4,})$/.test('R-0001');
|
|
2972
|
+
const mileFive = /^### M-\d{4,}\./m.test('### M-10000. big') && /^### M-\d{4,}\./m.test('### M-0004. small');
|
|
2973
|
+
const dateExact4 = /^\d{4}-\d{2}-\d{2}$/.test('2026-07-14') && !/^\d{4}-\d{2}-\d{2}$/.test('20260-7-14'); // 연도는 정확히 4자리
|
|
2974
|
+
// 소스가드: 핵심 리더가 실제로 \d{4,} 로 넓혀졌고, 날짜 패턴은 \d{4}- 로 보존.
|
|
2975
|
+
const s = read(__filename);
|
|
2976
|
+
const readersWidened = s.includes('/\\bT-(\\d{4,})\\b/g') && s.includes('/^R-(\\d{4,})$/') && s.includes('### M-\\d{4,}\\.');
|
|
2977
|
+
const dateKept = s.includes('\\d{4}-\\d{2}-\\d{2}'); // 날짜 연도 미확장 확인
|
|
2978
|
+
// codex F2/F5 회귀가드: lib 리더도 함께 넓혀졌는지(feature/audit/health/session-close 누락 방지) — 없으면 5자리 ID 가 이 표면에서만 truncation.
|
|
2979
|
+
const dir = require('path').dirname(__dirname) + '/lib';
|
|
2980
|
+
const libWidened = read(dir + '/feature.js').includes('F-\\d{4,}$')
|
|
2981
|
+
&& read(dir + '/audit.js').includes('M-\\d{4,}')
|
|
2982
|
+
&& read(dir + '/health.js').includes('M-\\d{4,}')
|
|
2983
|
+
&& read(dir + '/session-close.js').includes('M-\\d{4,}');
|
|
2984
|
+
const libNoStale = !read(dir + '/feature.js').includes('F-\\d{4}$') && !read(dir + '/audit.js').includes('(M-\\d{4})');
|
|
2985
|
+
return idFive && idFour && ruleFive && mileFive && dateExact4 && readersWidened && dateKept && libWidened && libNoStale;
|
|
2986
|
+
} },
|
|
2987
|
+
{ name: '전략 앵커 미작성 감지 (1.36.19, 실사용 7 프로젝트 dogfood): project-brief Purpose / plan Goal placeholder·빈칸 감지 + 실작성(bullet/문단) 무오탐 — 행위검사', run: () => {
|
|
2988
|
+
const p = require('../lib/pure-utils');
|
|
2989
|
+
if (typeof p._briefUnfilled !== 'function' || typeof p._planGoalUnfilled !== 'function') return false;
|
|
2990
|
+
const phBrief = p._briefUnfilled('# Project Brief\n## Purpose\n- 이 프로젝트의 목적을 실제 내용으로 업데이트하세요.\n## Users\n-\n'); // 템플릿 placeholder
|
|
2991
|
+
const emptyBrief = p._briefUnfilled('## Purpose\n- \n## Users\n-\n'); // 빈 bullet
|
|
2992
|
+
const filledBullet = !p._briefUnfilled('## Purpose\n- 소셜 발행 자동화 MCP 서버\n'); // 실작성(bullet) 무오탐
|
|
2993
|
+
const filledPara = !p._briefUnfilled('## Purpose\n코드베이스 변화를 실시간 가시화한다\n'); // 실작성(문단) 무오탐
|
|
2994
|
+
const noSection = !p._briefUnfilled('# Project Brief\n## Project\nfoo\n'); // Purpose 섹션 없음 → 미플래그(FP 회피)
|
|
2995
|
+
const phPlan = p._planGoalUnfilled('# Plan\n## Goal\n- 사용자 목적을 기준으로 전체 계획을 유지합니다.\n## Scope\n-\n'); // plan 템플릿
|
|
2996
|
+
const filledPlan = !p._planGoalUnfilled('## Goal\n- 카드뉴스 발행 자동화 완성\n'); // 실작성 plan 무오탐
|
|
2997
|
+
// audit + handoff 배선 가드
|
|
2998
|
+
const s = read(__filename); const au = require('fs').readFileSync(require('path').join(require('path').dirname(__dirname), 'lib', 'audit.js'), 'utf8');
|
|
2999
|
+
const wired = au.includes("_finding('project_brief_unfilled'") && au.includes("_finding('plan_goal_unfilled'")
|
|
3000
|
+
&& s.includes('📋 project-brief 미작성');
|
|
3001
|
+
return phBrief && emptyBrief && filledBullet && filledPara && noSection && phPlan && filledPlan && wired;
|
|
3002
|
+
} },
|
|
2966
3003
|
{ name: '시크릿 스캐너 FN 헌트 (1.35.14): 하드코딩 자격증명 복합/JSON키 탐지 + Slack xapp + 사전단어 FP 억제 — 행위검사', run: () => {
|
|
2967
3004
|
const pats = require('../lib/catalogs').SECRET_PATTERNS;
|
|
2968
3005
|
const pu = require('../lib/pure-utils');
|
|
@@ -3732,10 +3769,10 @@ function _selfTestCases() {
|
|
|
3732
3769
|
} },
|
|
3733
3770
|
{ name: '17th 버그헌트 P2: plan add 공백제목 trim(기본값) + milestone 파서 개행 미흡수 (1.17.1)', run: () => {
|
|
3734
3771
|
const src = read(__filename);
|
|
3735
|
-
const wired = src.includes("args.slice(2).join(' ').trim() || '새 계획'") && src.includes('(M-\\d{4})\\.[ \\t]*(.+?)$');
|
|
3772
|
+
const wired = src.includes("args.slice(2).join(' ').trim() || '새 계획'") && src.includes('(M-\\d{4,})\\.[ \\t]*(.+?)$');
|
|
3736
3773
|
// 파서 동작: 공백제목 milestone 이 다음 줄 'Status:' 를 제목으로 먹지 않음
|
|
3737
3774
|
const block = '### M-0006. \nStatus: planned\nProgress: 0%\n';
|
|
3738
|
-
const m = block.match(/^### (M-\d{4})\.[ \t]*(.+?)$/m);
|
|
3775
|
+
const m = block.match(/^### (M-\d{4,})\.[ \t]*(.+?)$/m);
|
|
3739
3776
|
const safe = !m || (m[2] || '').indexOf('Status') === -1;
|
|
3740
3777
|
return wired && safe;
|
|
3741
3778
|
} },
|
|
@@ -4801,7 +4838,7 @@ function pulseCmd(root) {
|
|
|
4801
4838
|
const decisionCount = _loadDecisions(root).length; // 1.9.339 (UR-0053): canonical 단일 진실소스
|
|
4802
4839
|
const rulesActive = readRules(root).filter(r => r.status === 'active').length;
|
|
4803
4840
|
const planText = exists(planPath(root)) ? read(planPath(root)) : '';
|
|
4804
|
-
const milestonesCnt = (planText.match(/^### M-\d{4}\./gm) || []).length;
|
|
4841
|
+
const milestonesCnt = (planText.match(/^### M-\d{4,}\./gm) || []).length;
|
|
4805
4842
|
const lessonsCount = _loadLessons(root).length;
|
|
4806
4843
|
data.memorySurface = _memorySurface({ tasks: tasksInProgress, decisions: decisionCount, rules: rulesActive, milestones: milestonesCnt, lessons: lessonsCount });
|
|
4807
4844
|
} catch {}
|
|
@@ -7279,7 +7316,7 @@ async function nextActionCmd(root, sub, ...rest) {
|
|
|
7279
7316
|
// leerness task add 호출
|
|
7280
7317
|
const taskResult = cp.spawnSync(process.execPath, [__filename, 'task', 'add', taskTitle, '--path', root], { encoding: 'utf8', timeout: 8000, env: { ...process.env, LEERNESS_INTERNAL: '1' } });
|
|
7281
7318
|
if (taskResult.status === 0) {
|
|
7282
|
-
const m = (taskResult.stdout || '').match(/T-\d{4}/);
|
|
7319
|
+
const m = (taskResult.stdout || '').match(/T-\d{4,}/);
|
|
7283
7320
|
log(` ✓ task 추가: ${m ? m[0] : '?'} — "${taskTitle}"`);
|
|
7284
7321
|
if (action.command) log(` 💡 실행 명령: ${action.command}`);
|
|
7285
7322
|
} else {
|
|
@@ -7338,9 +7375,9 @@ function _suggestNextActions(root, latestRow, keyword) {
|
|
|
7338
7375
|
try {
|
|
7339
7376
|
if (fuzzyRe && exists(planPath(root))) {
|
|
7340
7377
|
const pt = read(planPath(root));
|
|
7341
|
-
const milestones = (pt.match(/^### (M-\d{4})\..*$/gm) || []).filter(l => fuzzyRe.test(l));
|
|
7378
|
+
const milestones = (pt.match(/^### (M-\d{4,})\..*$/gm) || []).filter(l => fuzzyRe.test(l));
|
|
7342
7379
|
if (milestones.length > 0) {
|
|
7343
|
-
const m = milestones[0].match(/M-\d{4}/);
|
|
7380
|
+
const m = milestones[0].match(/M-\d{4,}/);
|
|
7344
7381
|
if (m) actions.push({ icon: '🎯', title: `plan.md milestone ${m[0]} 검증 — "${keyword}" 관련`, command: `leerness plan list --filter "${keyword}"` });
|
|
7345
7382
|
}
|
|
7346
7383
|
}
|
|
@@ -7573,7 +7610,7 @@ function readProgressRows(root) {
|
|
|
7573
7610
|
const text = exists(progressPath(root)) ? read(progressPath(root)) : '';
|
|
7574
7611
|
const rows = [];
|
|
7575
7612
|
for (const line of text.split('\n')) {
|
|
7576
|
-
if (!/^\| (?:T|M|D)-\d{4} \|/.test(line)) continue;
|
|
7613
|
+
if (!/^\| (?:T|M|D)-\d{4,} \|/.test(line)) continue;
|
|
7577
7614
|
// 1.9.399 (7번째 버그헌트 P1-A, UR-0104): 비이스케이프 파이프에서만 분리 + 셀 복원 — 사용자 텍스트의 '|'(이스케이프됨)이 컬럼을 깨지 않음.
|
|
7578
7615
|
const cells = line.split(/(?<!\\)\|/).slice(1, -1).map(s => _cellUnescape(s).trim());
|
|
7579
7616
|
if (cells.length < 6) continue;
|
|
@@ -7614,7 +7651,7 @@ function upsertProgress(root, row) {
|
|
|
7614
7651
|
});
|
|
7615
7652
|
}
|
|
7616
7653
|
|
|
7617
|
-
function planShow(root) { const p = planPath(root); const has_ = exists(p); const content = has_ ? read(p) : ''; if (has('--json')) { const milestones = (content.match(/^### (M-\d{4})\b.*$/gm) || []).map(l => l.replace(/^###\s*/, '').trim()); log(JSON.stringify({ exists: has_, milestones, raw: content }, null, 2)); return; } log(has_ ? content : 'plan.md not found'); } // 1.9.428 (UR-0128): plan show --json 구조화
|
|
7654
|
+
function planShow(root) { const p = planPath(root); const has_ = exists(p); const content = has_ ? read(p) : ''; if (has('--json')) { const milestones = (content.match(/^### (M-\d{4,})\b.*$/gm) || []).map(l => l.replace(/^###\s*/, '').trim()); log(JSON.stringify({ exists: has_, milestones, raw: content }, null, 2)); return; } log(has_ ? content : 'plan.md not found'); } // 1.9.428 (UR-0128): plan show --json 구조화
|
|
7618
7655
|
function planInit(root) { const goal = arg('--goal', ''); if (!exists(planPath(root))) return install(root); append(planPath(root), `\n## User Goal\n- ${goal || '사용자 목적을 작성하세요.'}\n`); ok('plan goal appended'); }
|
|
7619
7656
|
// 1.9.119: plan list — plan.md 의 모든 milestone (M-XXXX) 조회 (CLI + --json + MCP)
|
|
7620
7657
|
function planListCmd(root, opts = {}) {
|
|
@@ -7631,9 +7668,9 @@ function planListCmd(root, opts = {}) {
|
|
|
7631
7668
|
const text = read(pp);
|
|
7632
7669
|
const milestones = [];
|
|
7633
7670
|
// ### M-XXXX. <title> 블록 추출
|
|
7634
|
-
const blocks = text.split(/\n(?=### M-\d{4}\.)/);
|
|
7671
|
+
const blocks = text.split(/\n(?=### M-\d{4,}\.)/);
|
|
7635
7672
|
for (const b of blocks) {
|
|
7636
|
-
const headerMatch = b.match(/^### (M-\d{4})\.[ \t]*(.+?)$/m);
|
|
7673
|
+
const headerMatch = b.match(/^### (M-\d{4,})\.[ \t]*(.+?)$/m);
|
|
7637
7674
|
if (!headerMatch) continue;
|
|
7638
7675
|
const id = headerMatch[1];
|
|
7639
7676
|
const title = headerMatch[2].trim();
|
|
@@ -7985,7 +8022,7 @@ function memoryStatusCmd(root, opts = {}) {
|
|
|
7985
8022
|
const rulesPaused = rules.filter(r => r.status === 'paused').length;
|
|
7986
8023
|
// Plan
|
|
7987
8024
|
const planText = exists(planPath(root)) ? read(planPath(root)) : '';
|
|
7988
|
-
const milestones = (planText.match(/^### M-\d{4}\./gm) || []).length;
|
|
8025
|
+
const milestones = (planText.match(/^### M-\d{4,}\./gm) || []).length;
|
|
7989
8026
|
// plan milestones in-progress: progress-tracker에서 plan:M-XXXX evidence 있고 in-progress 인 row
|
|
7990
8027
|
const planInProgress = rows.filter(r => /plan:M-/.test(r.evidence || '') && r.status === 'in-progress').length;
|
|
7991
8028
|
// Lessons (UR-0058: canonical _loadLessons — JSON 단일 진실소스, MD projection fallback)
|
|
@@ -8394,10 +8431,10 @@ function _jaccard(a, b) {
|
|
|
8394
8431
|
function taskRelink(root) {
|
|
8395
8432
|
root = absRoot(root);
|
|
8396
8433
|
const planText = exists(planPath(root)) ? read(planPath(root)) : '';
|
|
8397
|
-
const milestones = [...planText.matchAll(/^### (M-\d{4})\.[ \t]*(.+?)$/gm)]
|
|
8434
|
+
const milestones = [...planText.matchAll(/^### (M-\d{4,})\.[ \t]*(.+?)$/gm)]
|
|
8398
8435
|
.map(m => ({ id: m[1], text: m[2].trim() }));
|
|
8399
8436
|
const rows = readProgressRows(root);
|
|
8400
|
-
const linkedM = new Set(rows.map(r => (r.evidence.match(/M-\d{4}/) || [])[0]).filter(Boolean));
|
|
8437
|
+
const linkedM = new Set(rows.map(r => (r.evidence.match(/M-\d{4,}/) || [])[0]).filter(Boolean));
|
|
8401
8438
|
const orphanM = milestones.filter(m => !linkedM.has(m.id));
|
|
8402
8439
|
if (!orphanM.length) return ok('미연결 milestone 없음');
|
|
8403
8440
|
|
|
@@ -8445,7 +8482,7 @@ function taskFixEvidence(root) {
|
|
|
8445
8482
|
!r.evidence ||
|
|
8446
8483
|
/^\s*$/.test(r.evidence) ||
|
|
8447
8484
|
/^(user-request|-)$/.test(r.evidence) ||
|
|
8448
|
-
/^plan:M-\d{4}\s*$/.test(r.evidence)
|
|
8485
|
+
/^plan:M-\d{4,}\s*$/.test(r.evidence)
|
|
8449
8486
|
)
|
|
8450
8487
|
);
|
|
8451
8488
|
if (!candidates.length) return ok('갱신 후보 없음 (모든 done row가 검증 키워드 보유)');
|
|
@@ -8457,7 +8494,7 @@ function taskFixEvidence(root) {
|
|
|
8457
8494
|
for (const r of candidates) {
|
|
8458
8495
|
let newEv = setAll;
|
|
8459
8496
|
if (preserveLink) {
|
|
8460
|
-
const m = (r.evidence || '').match(/plan:M-\d{4}/);
|
|
8497
|
+
const m = (r.evidence || '').match(/plan:M-\d{4,}/);
|
|
8461
8498
|
if (m && !newEv.includes(m[0])) {
|
|
8462
8499
|
newEv = `${setAll} (${m[0]})`;
|
|
8463
8500
|
preserved++;
|
|
@@ -8773,7 +8810,7 @@ function lazyDetect(root, opts = {}) {
|
|
|
8773
8810
|
for (const r of rows) {
|
|
8774
8811
|
if (!/^(done|completed|verified)$/i.test(r.status || '')) continue;
|
|
8775
8812
|
const _ev = (r.evidence || '').trim();
|
|
8776
|
-
const _trivial = !_ev || /^(user-request|n\/?a|tbd|todo|wip|none|nil|x|-+|\.+|\?+)$/i.test(_ev) || /^plan:M-\d{4}$/i.test(_ev);
|
|
8813
|
+
const _trivial = !_ev || /^(user-request|n\/?a|tbd|todo|wip|none|nil|x|-+|\.+|\?+)$/i.test(_ev) || /^plan:M-\d{4,}$/i.test(_ev);
|
|
8777
8814
|
if (_trivial) {
|
|
8778
8815
|
issues++; _warn(`done row without verifiable evidence: ${r.id} (${r.request})`,
|
|
8779
8816
|
{ kind: 'evidence_missing', severity: 'warn', taskId: r.id, request: r.request });
|
|
@@ -8857,7 +8894,7 @@ function lazyDetect(root, opts = {}) {
|
|
|
8857
8894
|
_withLock(progressPath(root), () => {
|
|
8858
8895
|
const header = progressHeader(root);
|
|
8859
8896
|
const rows2 = readProgressRows(root);
|
|
8860
|
-
let maxT = 0; const idRe = /\bT-(\d{4})\b/g;
|
|
8897
|
+
let maxT = 0; const idRe = /\bT-(\d{4,})\b/g;
|
|
8861
8898
|
const scanSrc = (exists(planPath(root)) ? read(planPath(root)) : '') + '\n' + rows2.map(r => r.id).join('\n');
|
|
8862
8899
|
let mm; while ((mm = idRe.exec(scanSrc))) maxT = Math.max(maxT, Number(mm[1]));
|
|
8863
8900
|
for (const t of newTodos) {
|
|
@@ -9030,7 +9067,7 @@ function handoff(root) {
|
|
|
9030
9067
|
const rules = readRules(root);
|
|
9031
9068
|
const rulesActive = rules.filter(r => r.status === 'active').length;
|
|
9032
9069
|
const planText = exists(planPath(root)) ? read(planPath(root)) : '';
|
|
9033
|
-
const milestones = (planText.match(/^### M-\d{4}\./gm) || []).length;
|
|
9070
|
+
const milestones = (planText.match(/^### M-\d{4,}\./gm) || []).length;
|
|
9034
9071
|
const lessonsCount = _loadLessons(root).length;
|
|
9035
9072
|
// 1.9.130: archive 카운트 통합
|
|
9036
9073
|
const archiveCountsH = { decisions: 0, lessons: 0, plan: 0, total: 0 };
|
|
@@ -9221,6 +9258,32 @@ function handoff(root) {
|
|
|
9221
9258
|
log(JSON.stringify(result, null, 2));
|
|
9222
9259
|
return;
|
|
9223
9260
|
}
|
|
9261
|
+
// 1.36.18 (UR-0052 P3-8): 단일 워크스페이스 --compact 단축. 종전엔 --compact 가 섹션 몇 개만 억제하고
|
|
9262
|
+
// 본문(session-handoff/progress/decisions…) 186줄을 전량 출력 → 문서상 "~500자 1줄 요약"인 --compact 가
|
|
9263
|
+
// 단일 경로에서 사실상 무효(멀티 워크스페이스 _handoffWorkspace 에만 compact 존재). MCP handoff·REPL preview·
|
|
9264
|
+
// agent-mode 등 내부 호출자 3곳이 단일 경로에 --compact 를 쓰며 186줄을 받아 preview 첫 3줄이 무의미(헤더/날짜/이름)했음.
|
|
9265
|
+
// subprocess 없이(REPL 8s timeout·MCP 친화) 파일 로더만으로 핵심 신호 압축 후 return — _handoffWorkspace compact 형식과 정합.
|
|
9266
|
+
if (has('--compact')) {
|
|
9267
|
+
const rows = readProgressRows(root);
|
|
9268
|
+
const byStatus = {};
|
|
9269
|
+
for (const r of rows) byStatus[r.status] = (byStatus[r.status] || 0) + 1;
|
|
9270
|
+
const done = byStatus['done'] || 0, wip = byStatus['in-progress'] || 0, blocked = byStatus['blocked'] || 0;
|
|
9271
|
+
const pct = rows.length ? Math.round(done / rows.length * 100) : 0;
|
|
9272
|
+
const decN = _loadDecisions(root).length;
|
|
9273
|
+
const rulesActive = readRules(root).filter(r => r.status === 'active').length;
|
|
9274
|
+
const lessN = _loadLessons(root).length;
|
|
9275
|
+
const planText = exists(planPath(root)) ? read(planPath(root)) : '';
|
|
9276
|
+
const mileN = (planText.match(/^### M-\d{4,}\./gm) || []).length;
|
|
9277
|
+
const openReq = (_loadUserRequests(root).requests || []).filter(r => r.status === 'open' || r.status === 'in-progress').length;
|
|
9278
|
+
const nx = rows.find(r => r.status === 'in-progress') || rows.find(r => r.status === 'planned') || null;
|
|
9279
|
+
const flags = [];
|
|
9280
|
+
if (openReq) flags.push(`📥 미답 ${openReq}`);
|
|
9281
|
+
if (blocked) flags.push(`🚫 blocked ${blocked}`);
|
|
9282
|
+
log(_lineSafe(`leerness compact: ${detectProjectName(root)} · ${done}/${rows.length}(${pct}%) done · WIP ${wip} · T${wip}/D${decN}/R${rulesActive}/P${mileN}/L${lessN}${flags.length ? ' · ' + flags.join(' · ') : ''}`)); // 1.36.18 (codex F4): projectName 개행 주입 차단
|
|
9283
|
+
if (nx) log(_lineSafe(`다음: ${nx.id} [${nx.status}] ${nx.nextAction || nx.request || ''}`).slice(0, 200));
|
|
9284
|
+
log('핵심 규칙: 의존성0 · 한국어주석 · UTF-8noBOM · reuse-map등록 · anti-lazy-work · verify-claim자동검수');
|
|
9285
|
+
return;
|
|
9286
|
+
}
|
|
9224
9287
|
const sections = [];
|
|
9225
9288
|
function block(label, p) {
|
|
9226
9289
|
if (!exists(p)) return;
|
|
@@ -9278,6 +9341,19 @@ function handoff(root) {
|
|
|
9278
9341
|
}
|
|
9279
9342
|
}
|
|
9280
9343
|
} catch {}
|
|
9344
|
+
// 2b) 전략 앵커 미작성 (1.36.19, 실사용 7 프로젝트 dogfood): project-brief Purpose(5/7)·plan Goal(7/7)이 템플릿 그대로
|
|
9345
|
+
// 방치돼 인계받는 AI 가 "프로젝트가 무엇인지/범위"를 못 받던 근본원인 → 세션시작 헤드라인에 표면화(파묻힌 init task 대신 가시화).
|
|
9346
|
+
try {
|
|
9347
|
+
const _naM = '<!-- leerness:na';
|
|
9348
|
+
const _bf = path.join(root, '.harness/project-brief.md');
|
|
9349
|
+
const _briefTxt = exists(_bf) ? read(_bf) : '';
|
|
9350
|
+
const _planTxt = exists(planPath(root)) ? read(planPath(root)) : '';
|
|
9351
|
+
const briefGap = !_briefTxt.includes(_naM) && _briefUnfilled(_briefTxt);
|
|
9352
|
+
const planGap = !!_planTxt && !_planTxt.includes(_naM) && _planGoalUnfilled(_planTxt);
|
|
9353
|
+
if (briefGap && planGap) parts.push(t('📋 정체성앵커 미작성 (brief+plan)', '📋 identity anchors unfilled (brief+plan)'));
|
|
9354
|
+
else if (briefGap) parts.push(t('📋 project-brief 미작성', '📋 project-brief unfilled'));
|
|
9355
|
+
else if (planGap) parts.push(t('📋 plan Goal 미작성', '📋 plan Goal unfilled'));
|
|
9356
|
+
} catch {}
|
|
9281
9357
|
// 3) MCP 활동 누적
|
|
9282
9358
|
try {
|
|
9283
9359
|
const stats = _readUsageStats(root);
|
|
@@ -9318,7 +9394,7 @@ function handoff(root) {
|
|
|
9318
9394
|
const decisions = _loadDecisions(root).length; // 1.9.339 (UR-0053): canonical 단일 진실소스
|
|
9319
9395
|
const rulesActive = readRules(root).filter(r => r.status === 'active').length;
|
|
9320
9396
|
const planText = exists(planPath(root)) ? read(planPath(root)) : '';
|
|
9321
|
-
const planMilestones = (planText.match(/^### M-\d{4}\./gm) || []).length;
|
|
9397
|
+
const planMilestones = (planText.match(/^### M-\d{4,}\./gm) || []).length;
|
|
9322
9398
|
const lessons = _loadLessons(root).length;
|
|
9323
9399
|
parts.push(`🧠 mem T${inProgressTasks}/D${decisions}/R${rulesActive}/P${planMilestones}/L${lessons}`);
|
|
9324
9400
|
} catch {}
|
|
@@ -13783,9 +13859,9 @@ function _brainstormFor(root, topic) {
|
|
|
13783
13859
|
const planFile_brainstorm = planPath(root);
|
|
13784
13860
|
if (exists(planFile_brainstorm)) {
|
|
13785
13861
|
const planText = read(planFile_brainstorm);
|
|
13786
|
-
const milestoneBlocks = planText.split(/\n(?=### M-\d{4}\.)/);
|
|
13862
|
+
const milestoneBlocks = planText.split(/\n(?=### M-\d{4,}\.)/);
|
|
13787
13863
|
for (const b of milestoneBlocks) {
|
|
13788
|
-
const m = b.match(/^### (M-\d{4})\.[ \t]*(.+?)$/m);
|
|
13864
|
+
const m = b.match(/^### (M-\d{4,})\.[ \t]*(.+?)$/m);
|
|
13789
13865
|
if (m && matches(b)) {
|
|
13790
13866
|
const idx = planText.indexOf(b);
|
|
13791
13867
|
const lineNo = idx >= 0 ? planText.slice(0, idx).split('\n').length : 0;
|
|
@@ -14078,9 +14154,9 @@ function brainstormCmd(root, topic) {
|
|
|
14078
14154
|
const planFile_b2 = planPath(root);
|
|
14079
14155
|
if (exists(planFile_b2)) {
|
|
14080
14156
|
const planText = read(planFile_b2);
|
|
14081
|
-
const milestoneBlocks = planText.split(/\n(?=### M-\d{4}\.)/);
|
|
14157
|
+
const milestoneBlocks = planText.split(/\n(?=### M-\d{4,}\.)/);
|
|
14082
14158
|
for (const b of milestoneBlocks) {
|
|
14083
|
-
const m = b.match(/^### (M-\d{4})\.[ \t]*(.+?)$/m);
|
|
14159
|
+
const m = b.match(/^### (M-\d{4,})\.[ \t]*(.+?)$/m);
|
|
14084
14160
|
if (m && matches(b)) {
|
|
14085
14161
|
const idx = planText.indexOf(b);
|
|
14086
14162
|
const lineNo = idx >= 0 ? planText.slice(0, idx).split('\n').length : 0;
|
|
@@ -14192,7 +14268,7 @@ function _roadmapData(root) {
|
|
|
14192
14268
|
const milestones = _roadmapParseMilestones(exists(planPath(root)) ? read(planPath(root)) : '');
|
|
14193
14269
|
const tasks = readProgressRows(root).map(t => ({
|
|
14194
14270
|
...t,
|
|
14195
|
-
milestones: Array.from(String(t.evidence || '').matchAll(/M-\d{4}/g)).map(m => m[0])
|
|
14271
|
+
milestones: Array.from(String(t.evidence || '').matchAll(/M-\d{4,}/g)).map(m => m[0])
|
|
14196
14272
|
}));
|
|
14197
14273
|
// skills
|
|
14198
14274
|
const skills = [];
|
|
@@ -14208,7 +14284,7 @@ function _roadmapData(root) {
|
|
|
14208
14284
|
const rulesT = exists(rulesPath(root)) ? read(rulesPath(root)) : '';
|
|
14209
14285
|
const rules = [];
|
|
14210
14286
|
for (const line of rulesT.split('\n')) {
|
|
14211
|
-
if (!/^\| R-\d{4} \|/.test(line)) continue;
|
|
14287
|
+
if (!/^\| R-\d{4,} \|/.test(line)) continue;
|
|
14212
14288
|
const cells = line.split('|').slice(1, -1).map(s => s.trim());
|
|
14213
14289
|
if (cells.length < 6) continue;
|
|
14214
14290
|
rules.push({ id: cells[0], trigger: cells[1], rule: cells[2], status: cells[4], lastVerified: cells[5] });
|
|
@@ -14502,7 +14578,7 @@ function readRules(root) {
|
|
|
14502
14578
|
if (!exists(f)) return [];
|
|
14503
14579
|
const rules = [];
|
|
14504
14580
|
for (const line of read(f).split('\n')) {
|
|
14505
|
-
if (!/^\| R-\d{4} \|/.test(line)) continue;
|
|
14581
|
+
if (!/^\| R-\d{4,} \|/.test(line)) continue;
|
|
14506
14582
|
// 1.9.399 (7번째 버그헌트 P1-A, UR-0104): 비이스케이프 파이프 분리 + 복원 — rule 텍스트의 '|'가 컬럼 밀림/멱등성 무력화를 못 일으킴.
|
|
14507
14583
|
const cells = line.split(/(?<!\\)\|/).slice(1, -1).map(s => _cellUnescape(s).trim());
|
|
14508
14584
|
if (cells.length < 6) continue;
|
|
@@ -14521,7 +14597,7 @@ function nextRuleId(root) {
|
|
|
14521
14597
|
const rules = readRules(root);
|
|
14522
14598
|
let max = 0;
|
|
14523
14599
|
for (const r of rules) {
|
|
14524
|
-
const m = r.id.match(/^R-(\d{4})$/);
|
|
14600
|
+
const m = r.id.match(/^R-(\d{4,})$/);
|
|
14525
14601
|
if (m) max = Math.max(max, Number(m[1]));
|
|
14526
14602
|
}
|
|
14527
14603
|
// 1.11.3 (14th 버그헌트 P2, UR-0180): 아카이브의 R-id 도 카운트 — 기존엔 활성 rules 만 스캔해 rule remove 후 같은 R-id 가 다른 룰에 재사용됐음(아카이브 ID 충돌).
|
|
@@ -20148,6 +20224,7 @@ function contractVerifyCmd(specPath, implPath) {
|
|
|
20148
20224
|
// 대신 정적 소스 분석 — module.exports = { foo, bar } / exports.foo = ... / module.exports.foo = ... 패턴 grep.
|
|
20149
20225
|
const implSrc = read(implFile);
|
|
20150
20226
|
// 1.9.429 (UR-0129): 브레이스 균형 top-level 키(멀티라인 module.exports 안전) + ESM export 인식 — pure-utils 단일출처
|
|
20227
|
+
// 1.36.18 (UR-0052 P2-6 이연): 주석 마스킹은 7라운드 codex 재검 결과 파서 없이 안전 구현 불가로 판단해 미적용(하단 참조).
|
|
20151
20228
|
const implExports = new Set(_parseImplExports(implSrc));
|
|
20152
20229
|
// 검사: spec 강선언(function 시그니처 + markdown bullet) 함수 중 impl exports에 없는 것.
|
|
20153
20230
|
// 1.9.385 (UR-0086): 기존 `specText.includes('function '+fn)` 가드는 bullet/backtick 추출명을 무력화하던 잠재 FN.
|
package/lib/audit.js
CHANGED
|
@@ -7,6 +7,7 @@ const path = require('path');
|
|
|
7
7
|
const { log, ok, warn, fail, failJson, today, now, absRoot, exists, read, readBuf, mkdirp, writeUtf8, append, rel } = require('./io');
|
|
8
8
|
const { SECRET_PATTERNS } = require('./catalogs');
|
|
9
9
|
const { findCorruptedStateJson } = require('./state-integrity'); // 1.36.1 (클린룸 리뷰 FN): 상태 JSON 무결성
|
|
10
|
+
const { _briefUnfilled, _planGoalUnfilled } = require('./pure-utils'); // 1.36.19: 전략 앵커(project-brief/plan Goal) 미작성 감지
|
|
10
11
|
|
|
11
12
|
function audit(root, opts = {}, deps = {}) {
|
|
12
13
|
const { VERSION, arg, has, planPath, readProgressRows, currentStatePath, handoffPath, envDiff, _readFeatureGraph, _matchAPISkills, _listAPISkills, _collectSecretFindings } = deps;
|
|
@@ -61,12 +62,20 @@ function audit(root, opts = {}, deps = {}) {
|
|
|
61
62
|
if (reuse.includes(naMarker)) ok('reuse-map.md marked NA (skipped)');
|
|
62
63
|
else if (reuseLines === 0) { warnings++; warn('reuse-map.md is empty (consider populating known reusable elements)'); _finding('reuse_map_empty', 'warn', 'reuse-map.md is empty'); }
|
|
63
64
|
else ok(`reuse-map.md has ${reuseLines} entries`);
|
|
65
|
+
// 1.36.19 (실사용 7 프로젝트 dogfood): 전략 앵커 미작성 — 동적 상태는 유지되나 project-brief Purpose(5/7)·plan Goal(7/7)이
|
|
66
|
+
// 템플릿 그대로라 인계받는 AI 가 "프로젝트가 무엇인지/범위"를 못 받음(관찰된 근본원인). naMarker 로 의도적 스킵 가능.
|
|
67
|
+
const briefFile = path.join(root, '.harness/project-brief.md');
|
|
68
|
+
const briefTxt = exists(briefFile) ? read(briefFile) : '';
|
|
69
|
+
if (briefTxt.includes(naMarker)) ok('project-brief.md marked NA (skipped)');
|
|
70
|
+
else if (_briefUnfilled(briefTxt)) { warnings++; warn('project-brief.md Purpose 미작성(템플릿 그대로) — 인계받는 AI가 프로젝트 목적/맥락을 못 받습니다 (project-brief.md 채우기)'); _finding('project_brief_unfilled', 'warn', 'project-brief.md Purpose still placeholder/empty — AI handoffs lack project context'); }
|
|
71
|
+
else ok('project-brief.md Purpose populated');
|
|
64
72
|
const planText = exists(planPath(root)) ? read(planPath(root)) : '';
|
|
65
|
-
|
|
73
|
+
if (planText && !planText.includes(naMarker) && _planGoalUnfilled(planText)) { warnings++; warn('plan.md Goal 미작성(템플릿 그대로) — 프로젝트 목표/범위가 비어 인계 AI가 방향을 못 잡습니다 (plan.md Goal/Scope 채우기)'); _finding('plan_goal_unfilled', 'warn', 'plan.md Goal still placeholder/empty'); }
|
|
74
|
+
const milestoneIds = Array.from(planText.matchAll(/^### (M-\d{4,})\./gm)).map(m => m[1]);
|
|
66
75
|
const rows = readProgressRows(root);
|
|
67
76
|
// 1.9.6 수정: 한 row에 여러 plan:M-XXXX 링크가 있어도 모두 인식 (matchAll로 전부 추출)
|
|
68
77
|
const linkedMs = new Set(
|
|
69
|
-
rows.flatMap(r => Array.from(String(r.evidence || '').matchAll(/M-\d{4}/g), m => m[0]))
|
|
78
|
+
rows.flatMap(r => Array.from(String(r.evidence || '').matchAll(/M-\d{4,}/g), m => m[0]))
|
|
70
79
|
);
|
|
71
80
|
const missingFromProgress = milestoneIds.filter(m => !linkedMs.has(m));
|
|
72
81
|
if (missingFromProgress.length) {
|
package/lib/feature.js
CHANGED
|
@@ -41,7 +41,7 @@ function featureAddCmd(root, title, deps = {}) {
|
|
|
41
41
|
function featureLinkCmd(root, fromId, deps = {}) {
|
|
42
42
|
const { _readFeatureGraph, _writeFeatureGraph, arg } = deps;
|
|
43
43
|
root = absRoot(root);
|
|
44
|
-
if (!fromId || !/^F-\d{4}$/.test(fromId)) return fail('feature link: 첫 인자는 F-XXXX 형식 ID');
|
|
44
|
+
if (!fromId || !/^F-\d{4,}$/.test(fromId)) return fail('feature link: 첫 인자는 F-XXXX 형식 ID');
|
|
45
45
|
const { nodes } = _readFeatureGraph(root);
|
|
46
46
|
const node = nodes.find(n => n.id === fromId);
|
|
47
47
|
if (!node) return fail(`feature ${fromId} 없음 — feature add 먼저`);
|
|
@@ -60,7 +60,7 @@ function featureImpactCmd(root, fromId, deps = {}) {
|
|
|
60
60
|
const { _readFeatureGraph, has } = deps;
|
|
61
61
|
root = absRoot(root);
|
|
62
62
|
const _j = has('--json'); // 1.9.398 (UR-0099): --json 에러 구조화
|
|
63
|
-
if (!fromId || !/^F-\d{4}$/.test(fromId)) return failJson(_j, 'bad_id', 'feature impact: F-XXXX ID 필요');
|
|
63
|
+
if (!fromId || !/^F-\d{4,}$/.test(fromId)) return failJson(_j, 'bad_id', 'feature impact: F-XXXX ID 필요');
|
|
64
64
|
const { nodes } = _readFeatureGraph(root);
|
|
65
65
|
const node = nodes.find(n => n.id === fromId);
|
|
66
66
|
if (!node) return failJson(_j, 'not_found', `feature ${fromId} 없음`);
|
|
@@ -103,7 +103,7 @@ function featureShowCmd(root, fromId, deps = {}) {
|
|
|
103
103
|
const { _readFeatureGraph, has } = deps;
|
|
104
104
|
root = absRoot(root);
|
|
105
105
|
const _j = has('--json'); // 1.9.398 (UR-0099): --json 에러 구조화
|
|
106
|
-
if (!fromId || !/^F-\d{4}$/.test(fromId)) return failJson(_j, 'bad_id', 'feature show: F-XXXX ID 필요');
|
|
106
|
+
if (!fromId || !/^F-\d{4,}$/.test(fromId)) return failJson(_j, 'bad_id', 'feature show: F-XXXX ID 필요');
|
|
107
107
|
const { nodes } = _readFeatureGraph(root);
|
|
108
108
|
const node = nodes.find(n => n.id === fromId);
|
|
109
109
|
if (!node) return failJson(_j, 'not_found', `feature ${fromId} 없음`);
|
package/lib/health.js
CHANGED
|
@@ -95,7 +95,7 @@ function healthCmd(root, deps = {}) {
|
|
|
95
95
|
const rules = readRules(root);
|
|
96
96
|
const rulesActive = rules.filter(r => r.status === 'active').length;
|
|
97
97
|
const planText = exists(planPath(root)) ? read(planPath(root)) : '';
|
|
98
|
-
const milestones = (planText.match(/^### M-\d{4}\./gm) || []).length;
|
|
98
|
+
const milestones = (planText.match(/^### M-\d{4,}\./gm) || []).length;
|
|
99
99
|
const lessonsCount = _loadLessons(root).length;
|
|
100
100
|
out.memorySurface = {
|
|
101
101
|
tasks: { inProgress: tasksInProgress, total: rows.length, byStatus: tasksByStatus },
|
package/lib/pure-utils.js
CHANGED
|
@@ -322,7 +322,7 @@ function _roadmapParseMilestones(text) {
|
|
|
322
322
|
const s = String(text || '');
|
|
323
323
|
const out = [];
|
|
324
324
|
// 1.9.352 (UR-0068 외부리뷰): 다음 milestone 직전까지 block 한정 — 이전 구현은 slice(m.index) 로 다음 milestone 의 Status/Progress 를 누출했음
|
|
325
|
-
const matches = [...s.matchAll(/^### (M-\d{4})\.[ \t]*(.+?)$/gm)]; // 17th 버그헌트 P2: \s* 가 개행 흡수해 빈 제목 milestone 이 다음 줄(Status:)을 제목으로 먹던 것 차단
|
|
325
|
+
const matches = [...s.matchAll(/^### (M-\d{4,})\.[ \t]*(.+?)$/gm)]; // 17th 버그헌트 P2: \s* 가 개행 흡수해 빈 제목 milestone 이 다음 줄(Status:)을 제목으로 먹던 것 차단
|
|
326
326
|
for (let i = 0; i < matches.length; i++) {
|
|
327
327
|
const m = matches[i];
|
|
328
328
|
const end = i + 1 < matches.length ? matches[i + 1].index : s.length;
|
|
@@ -933,6 +933,7 @@ function _renderPulseLine(data) {
|
|
|
933
933
|
return line;
|
|
934
934
|
}
|
|
935
935
|
|
|
936
|
+
|
|
936
937
|
// 1.9.429 (10th 외부평가 UR-0129): impl 소스에서 export 식별자 추출.
|
|
937
938
|
// 브레이스 균형으로 module.exports={...} 의 top-level 키만(함수 본문/중첩객체 안전 — 멀티라인 첫키만 버그 수정)
|
|
938
939
|
// + exports.foo + ESM(export function/const/let/var/class, export {a, b as c}) 인식.
|
|
@@ -1042,8 +1043,33 @@ function _renderGlossaryMd(entries, opts = {}) {
|
|
|
1042
1043
|
return s + GLOSSARY_END + '\n';
|
|
1043
1044
|
}
|
|
1044
1045
|
|
|
1046
|
+
// 1.36.19 (실사용 7 프로젝트 dogfood): 전략 앵커(project-brief Purpose / plan Goal) 미작성 감지.
|
|
1047
|
+
// 현장 관찰 — 동적 상태(current-state/decisions)는 7/7 유지되나 정체성 앵커는 brief 5/7·plan 7/7 이 템플릿 placeholder
|
|
1048
|
+
// 그대로 방치됨(27개 결정 쌓은 프로젝트조차 brief 빈칸). 인계받은 AI 가 "프로젝트가 무엇인지"를 못 받는 근본원인 →
|
|
1049
|
+
// audit/handoff 가 표면화하도록 순수 detector. bullet/문단 무관, 빈 섹션·placeholder 문구를 모두 미작성으로 판정.
|
|
1050
|
+
// 섹션 자체가 없으면(구조 미상) 미플래그(FP 회피). `<!-- leerness:na -->` 스킵은 캘러(audit)가 처리.
|
|
1051
|
+
function _mdSectionBody(content, heading) {
|
|
1052
|
+
const lines = String(content || '').split('\n');
|
|
1053
|
+
const h = new RegExp('^##\\s*' + heading.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\s*$', 'i');
|
|
1054
|
+
const i = lines.findIndex(l => h.test(l));
|
|
1055
|
+
if (i < 0) return null;
|
|
1056
|
+
const body = [];
|
|
1057
|
+
for (let j = i + 1; j < lines.length; j++) { if (/^##\s/.test(lines[j])) break; body.push(lines[j]); }
|
|
1058
|
+
return body.join('\n');
|
|
1059
|
+
}
|
|
1060
|
+
const _BRIEF_PLACEHOLDER_RE = /실제\s*(?:내용|목적)\s*으로\s*업데이트|describe .*purpose here|update .*with (?:the )?real/i;
|
|
1061
|
+
const _PLAN_GOAL_PLACEHOLDER_RE = /전체\s*계획을\s*유지합니다|사용자\s*목적을\s*기준으로|state your goal here|maintain the overall plan/i;
|
|
1062
|
+
function _sectionUnfilled(content, heading, placeholderRe) {
|
|
1063
|
+
const body = _mdSectionBody(content, heading);
|
|
1064
|
+
if (body == null) return false; // 섹션 없음 → 미플래그
|
|
1065
|
+
if (placeholderRe.test(body)) return true; // 템플릿 문구 그대로
|
|
1066
|
+
return !body.split('\n').some(l => l.replace(/^\s*[-*]\s*/, '').trim().length > 0); // 실내용(bullet/문단) 0 → 빈칸
|
|
1067
|
+
}
|
|
1068
|
+
function _briefUnfilled(content) { return _sectionUnfilled(content, 'Purpose', _BRIEF_PLACEHOLDER_RE); }
|
|
1069
|
+
function _planGoalUnfilled(content) { return _sectionUnfilled(content, 'Goal', _PLAN_GOAL_PLACEHOLDER_RE); }
|
|
1070
|
+
|
|
1045
1071
|
module.exports = {
|
|
1046
|
-
_parseImplExports,
|
|
1072
|
+
_parseImplExports, _briefUnfilled, _planGoalUnfilled, _mdSectionBody,
|
|
1047
1073
|
_matchTool, _parsePackageJsonDeps, _parseRequirementsTxt, _buildGlossary, _renderGlossaryMd, GLOSSARY_START, GLOSSARY_END,
|
|
1048
1074
|
_isSecretKey, compareVer, parseHarnessVersion,
|
|
1049
1075
|
_isPlaceholderSecret, _looksSecretLike,
|
|
@@ -1251,7 +1277,7 @@ function _featureGraphTemplate() {
|
|
|
1251
1277
|
function _parseFeatureGraph(text) {
|
|
1252
1278
|
if (!text) return [];
|
|
1253
1279
|
const nodes = [];
|
|
1254
|
-
const re = /^## (F-\d{4})\s+(.+?)\s*$/gm;
|
|
1280
|
+
const re = /^## (F-\d{4,})\s+(.+?)\s*$/gm;
|
|
1255
1281
|
const positions = [];
|
|
1256
1282
|
let m;
|
|
1257
1283
|
while ((m = re.exec(text)) !== null) positions.push({ id: m[1], title: m[2], start: m.index });
|
package/lib/session-close.js
CHANGED
|
@@ -33,7 +33,7 @@ function sessionClose(root, opts = {}, deps = {}) {
|
|
|
33
33
|
jsonResult.recommendedDirection = (buckets['in-progress'][0]?.request) || (buckets['planned'][0]?.request) || (buckets['requested'][0]?.request) || null;
|
|
34
34
|
jsonResult.nextExactStep = (buckets['in-progress'][0]?.nextAction) || (buckets['planned'][0]?.nextAction) || (buckets['requested'][0]?.nextAction) || null;
|
|
35
35
|
// 1.12.3 (14th 버그헌트 P3, UR-0183): 마감 시 완료 정직성 advisory — done 인데 evidence 가 비었거나 placeholder 인 task 노출(차단 X, 정직성 환기). lazy detect 의 done_no_evidence 휴리스틱과 동일.
|
|
36
|
-
const _doneNoEvidence = (buckets['done'] || []).filter(r => !r.evidence || /^(\s*|user-request|-)$/.test(r.evidence) || /^plan:M-\d{4}\s*$/.test(r.evidence));
|
|
36
|
+
const _doneNoEvidence = (buckets['done'] || []).filter(r => !r.evidence || /^(\s*|user-request|-)$/.test(r.evidence) || /^plan:M-\d{4,}\s*$/.test(r.evidence));
|
|
37
37
|
jsonResult.completionHonesty = { doneTotal: (buckets['done'] || []).length, doneWithoutEvidence: _doneNoEvidence.length, ids: _doneNoEvidence.slice(0, 5).map(r => r.id) };
|
|
38
38
|
if (_doneNoEvidence.length) log(t(` ⚠ 완료 정직성: done ${_doneNoEvidence.length}건 evidence 없음/placeholder (${_doneNoEvidence.slice(0, 3).map(r => r.id).join(', ')}) — verify-claim 권장 (advisory)`, ` ⚠ completion honesty: ${_doneNoEvidence.length} done with no/placeholder evidence (${_doneNoEvidence.slice(0, 3).map(r => r.id).join(', ')}) — verify-claim recommended (advisory)`));
|
|
39
39
|
// 1.17.6 (UR-0049 마감 정합): done 의 미해소 낙관 의심 재확인 — verify-claim 을 건너뛴 거짓 주장(evidence 에 API/DB 주장 있는데 코드 흔적 없음)이
|
|
@@ -283,7 +283,7 @@ function sessionClose(root, opts = {}, deps = {}) {
|
|
|
283
283
|
const rules0 = readRules(root);
|
|
284
284
|
const rulesActive0 = rules0.filter(r => r.status === 'active').length;
|
|
285
285
|
const planText0 = exists(planPath(root)) ? read(planPath(root)) : '';
|
|
286
|
-
const milestones0 = (planText0.match(/^### M-\d{4}\./gm) || []).length;
|
|
286
|
+
const milestones0 = (planText0.match(/^### M-\d{4,}\./gm) || []).length;
|
|
287
287
|
const lessonsCount0 = _loadLessons(root).length;
|
|
288
288
|
// 1.9.130: archive 카운트 통합
|
|
289
289
|
const archiveCountsS = { decisions: 0, lessons: 0, plan: 0, total: 0 };
|
|
@@ -620,7 +620,7 @@ function sessionClose(root, opts = {}, deps = {}) {
|
|
|
620
620
|
const dCnt = _loadDecisions(root).length; // 1.9.339 (UR-0053): canonical 단일 진실소스
|
|
621
621
|
const rActive = readRules(root).filter(r => r.status === 'active').length;
|
|
622
622
|
const planText = exists(planPath(root)) ? read(planPath(root)) : '';
|
|
623
|
-
const pCnt = (planText.match(/^### M-\d{4}\./gm) || []).length;
|
|
623
|
+
const pCnt = (planText.match(/^### M-\d{4,}\./gm) || []).length;
|
|
624
624
|
const lCnt = _loadLessons(root).length;
|
|
625
625
|
const mem = `T${tIn}/D${dCnt}/R${rActive}/P${pCnt}/L${lCnt}`;
|
|
626
626
|
let pulseLine = `📍 v${VERSION} · 🔄 R${rh.roundCount} · 🧠 ${mem}`;
|