leerness 1.36.16 → 1.36.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/README.md +4 -4
- package/bin/leerness.js +126 -44
- package/lib/analyzers.js +2 -1
- package/lib/audit.js +2 -2
- package/lib/feature.js +3 -3
- package/lib/health.js +1 -1
- package/lib/pure-utils.js +3 -2
- package/lib/session-close.js +3 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.36.18 — 2026-07-14 — UR-0052: handoff --compact 단일경로 + ID 5자리+ 대응 (P2-6 주석 마스킹은 조사 후 이연)
|
|
4
|
+
|
|
5
|
+
1.36.17에서 백로그로 이연했던 UR-0052 3건 중 2건(P3-8·P1-2)을 재현·수정. P2-6(주석 마스킹)은 codex 7라운드 적대 재검 끝에 **파서 없이 안전 구현 불가**로 판단해 이연(하단 상세). 각 수정은 파일 스크립트 프로브 + selftest 케이스로 실증(맹신 X).
|
|
6
|
+
|
|
7
|
+
- **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 유의미화.
|
|
8
|
+
- **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 카운트, 날짜 정상.
|
|
9
|
+
- **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-소스 동작 유지(무회귀).
|
|
10
|
+
- **검증**: selftest 288/288(P1-2 케이스에 lib 확장 가드 포함; P2-6 케이스는 revert로 제거), P3-8/P1-2 파일 스크립트 프로브 + CLI E2E 실증, 게이트 e2e 386/386, 게시본 클린룸 재실증.
|
|
11
|
+
|
|
12
|
+
## 1.36.17 — 2026-07-14 — 렌즈-외 표면 버그헌트: 데이터손실 2 + gate FN + 아카이브 주입 + --json 에러경로 (codex, 재현확정)
|
|
13
|
+
|
|
14
|
+
게시본 1.36.16 대상, database 렌즈를 제외한 CLI 표면을 codex로 광범위 적대 헌트 → 8건 중 6건 수정(고영향 재현확정), 2건 백로그(UR-0052).
|
|
15
|
+
|
|
16
|
+
- **P1 데이터손실 — `memory restore` (decisions/lessons)**: 복원이 MD 프로젝션에만 append 하고 canonical JSON을 안 고쳐, `list`엔 안 보이고 다음 add/save가 JSON→MD 재생성으로 복원분을 영구 삭제(재현확정). → 복원 후 MD를 canonical 파서로 재파싱해 `_saveDecisions`/`_saveLessons`로 JSON 재빌드.
|
|
17
|
+
- **P2 데이터손실 — `plan remove`**: `### ` 만 블록 경계로 써서 마지막 마일스톤 뒤 사용자 h1/h2 섹션(`## Risks` 등)이 함께 삭제(재현확정). → 모든 헤딩(`#{1,3} `)을 경계로 분할, `### M-` 블록만 삭제.
|
|
18
|
+
- **P2 gate FN — `verify-claim`**: FILE_RE/`_evidenceQuality`가 basename 첫 글자를 `[A-Za-z]`로만 잡아 `123.js` 같은 숫자시작 실파일 미추출 → 없는 파일 인용한 done 주장이 vacuous-pass(재현확정, 정상 done FP 회귀 없음 확인). → 첫 글자에 숫자 허용(prev-separator 가드 유지).
|
|
19
|
+
- **P2 아카이브 주입**: lesson/decision drop이 사용자 텍스트를 archive MD에 raw 삽입 → 개행으로 `## 제거` 델리미터 위조(가짜 archive 엔트리)(재현확정). → 아카이브 필드에 `_lineSafe`.
|
|
20
|
+
- **P2 --json 에러경로**: task update/drop·lesson/decision/plan/rule not-found 6경로가 `--json`에도 평문 출력 → JSON 소비자 파싱 실패. → `failJson(has('--json'), code, msg)`.
|
|
21
|
+
- **백로그(UR-0052)**: P1-2(10k+ ID 고정폭 `\d{4}` 리더 ~20곳, 날짜 혼동 위험으로 신중한 일괄 필요), P2-6(contract verify 주석 마스킹), P3-8(handoff `--compact` 단일 워크스페이스 분기).
|
|
22
|
+
- **검증**: selftest 287/287(신규 6수정 소스가드), 데이터손실 3건 재현→수정 후 재검, 게이트 e2e 386/386.
|
|
23
|
+
|
|
3
24
|
## 1.36.16 — 2026-07-13 — 게시본 1.36.15 클린룸 codex 9건 채택: database 렌즈 정직/정밀 + selftest·병합 하드닝
|
|
4
25
|
|
|
5
26
|
게시본 1.36.13~1.36.15 누적 신규 표면(문항 8→12, cap 16)을 codex 로 적대 리뷰 → 9건(P2/P3) 전부 재현/채택. clean 3축(i18n 패리티·앵커 유일성·크래시 없음).
|
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.18 하네스를 사용합니다. 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.18는 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.16는 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.18)** · 매 라운드 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.18: 2026-07-14
|
|
242
242
|
<!-- leerness:project-readme:end -->
|
|
243
243
|
|
package/bin/leerness.js
CHANGED
|
@@ -33,7 +33,7 @@ const { _evidenceQuality, _parseEvidenceStats, _shellGuardAnalyze, _claimFileInG
|
|
|
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.18';
|
|
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,27 @@ 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
|
+
} },
|
|
2966
2987
|
{ name: '시크릿 스캐너 FN 헌트 (1.35.14): 하드코딩 자격증명 복합/JSON키 탐지 + Slack xapp + 사전단어 FP 억제 — 행위검사', run: () => {
|
|
2967
2988
|
const pats = require('../lib/catalogs').SECRET_PATTERNS;
|
|
2968
2989
|
const pu = require('../lib/pure-utils');
|
|
@@ -3732,10 +3753,10 @@ function _selfTestCases() {
|
|
|
3732
3753
|
} },
|
|
3733
3754
|
{ name: '17th 버그헌트 P2: plan add 공백제목 trim(기본값) + milestone 파서 개행 미흡수 (1.17.1)', run: () => {
|
|
3734
3755
|
const src = read(__filename);
|
|
3735
|
-
const wired = src.includes("args.slice(2).join(' ').trim() || '새 계획'") && src.includes('(M-\\d{4})\\.[ \\t]*(.+?)$');
|
|
3756
|
+
const wired = src.includes("args.slice(2).join(' ').trim() || '새 계획'") && src.includes('(M-\\d{4,})\\.[ \\t]*(.+?)$');
|
|
3736
3757
|
// 파서 동작: 공백제목 milestone 이 다음 줄 'Status:' 를 제목으로 먹지 않음
|
|
3737
3758
|
const block = '### M-0006. \nStatus: planned\nProgress: 0%\n';
|
|
3738
|
-
const m = block.match(/^### (M-\d{4})\.[ \t]*(.+?)$/m);
|
|
3759
|
+
const m = block.match(/^### (M-\d{4,})\.[ \t]*(.+?)$/m);
|
|
3739
3760
|
const safe = !m || (m[2] || '').indexOf('Status') === -1;
|
|
3740
3761
|
return wired && safe;
|
|
3741
3762
|
} },
|
|
@@ -3928,6 +3949,26 @@ function _selfTestCases() {
|
|
|
3928
3949
|
&& eq(F(['README.md']), ['docs']);
|
|
3929
3950
|
return catOk && mapOk;
|
|
3930
3951
|
} },
|
|
3952
|
+
{ name: '비-렌즈 버그헌트 수정 회귀 (게시본 1.36.16 codex): restore canonical 재빌드 + plan 헤딩경계 + FILE_RE 숫자시작 + archive/json 구조화 (소스가드)', run: () => {
|
|
3953
|
+
const src = read(__filename);
|
|
3954
|
+
// P1-1: memory restore 가 decisions/lessons canonical JSON 을 재빌드(MD-only append 아님)
|
|
3955
|
+
const restoreOk = src.includes("if (surface === 'decisions') _saveDecisions(root, _decisionsFromMd(read(activePath)))")
|
|
3956
|
+
&& src.includes("else if (surface === 'lessons') _saveLessons(root, _parseLessonEntries(read(activePath)))");
|
|
3957
|
+
// P2-4: plan remove 가 모든 헤딩(#{1,3} )을 경계로 split (사용자 h2 섹션 흡수 방지)
|
|
3958
|
+
const planOk = src.includes('const blocks = text.split(/\\n(?=#{1,3} )/);');
|
|
3959
|
+
// P2-5: FILE_RE + _evidenceQuality basename 첫 글자에 숫자 허용 (저-백슬래시 판별 조각)
|
|
3960
|
+
const fileReOk = src.includes(')?[A-Za-z0-9][');
|
|
3961
|
+
const analyzersOk = read(path.join(__dirname, '..', 'lib', 'analyzers.js')).includes('[A-Za-z0-9][\\w./');
|
|
3962
|
+
// P2-3: archive 쓰기에 _lineSafe (개행 델리미터 위조 차단)
|
|
3963
|
+
const archiveOk = src.includes('`\\n## 제거 ${today()} (target: "${_lineSafe(target)}")\\n${archiveBlocks}\\n`');
|
|
3964
|
+
// P2-7: drop/remove not-found 가 failJson (--json 에러 구조화)
|
|
3965
|
+
const jsonOk = src.includes("failJson(has('--json'), 'lesson_not_found'")
|
|
3966
|
+
&& src.includes("failJson(has('--json'), 'decision_not_found'")
|
|
3967
|
+
&& src.includes("failJson(has('--json'), 'milestone_not_found'")
|
|
3968
|
+
&& src.includes("failJson(has('--json'), 'task_not_found'")
|
|
3969
|
+
&& src.includes("failJson(has('--json'), 'rule_not_found'");
|
|
3970
|
+
return restoreOk && planOk && fileReOk && analyzersOk && archiveOk && jsonOk;
|
|
3971
|
+
} },
|
|
3931
3972
|
{ name: 'DB 렌즈 recall (dogfood FN, 1.36.4): 내용기반 감지 — 평범한 이름의 DB 모듈 잡고 산문 FP 0 (행위)', run: () => {
|
|
3932
3973
|
const T = _isDbContentText, W = _withDbDomain;
|
|
3933
3974
|
const eq = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
@@ -4781,7 +4822,7 @@ function pulseCmd(root) {
|
|
|
4781
4822
|
const decisionCount = _loadDecisions(root).length; // 1.9.339 (UR-0053): canonical 단일 진실소스
|
|
4782
4823
|
const rulesActive = readRules(root).filter(r => r.status === 'active').length;
|
|
4783
4824
|
const planText = exists(planPath(root)) ? read(planPath(root)) : '';
|
|
4784
|
-
const milestonesCnt = (planText.match(/^### M-\d{4}\./gm) || []).length;
|
|
4825
|
+
const milestonesCnt = (planText.match(/^### M-\d{4,}\./gm) || []).length;
|
|
4785
4826
|
const lessonsCount = _loadLessons(root).length;
|
|
4786
4827
|
data.memorySurface = _memorySurface({ tasks: tasksInProgress, decisions: decisionCount, rules: rulesActive, milestones: milestonesCnt, lessons: lessonsCount });
|
|
4787
4828
|
} catch {}
|
|
@@ -7259,7 +7300,7 @@ async function nextActionCmd(root, sub, ...rest) {
|
|
|
7259
7300
|
// leerness task add 호출
|
|
7260
7301
|
const taskResult = cp.spawnSync(process.execPath, [__filename, 'task', 'add', taskTitle, '--path', root], { encoding: 'utf8', timeout: 8000, env: { ...process.env, LEERNESS_INTERNAL: '1' } });
|
|
7261
7302
|
if (taskResult.status === 0) {
|
|
7262
|
-
const m = (taskResult.stdout || '').match(/T-\d{4}/);
|
|
7303
|
+
const m = (taskResult.stdout || '').match(/T-\d{4,}/);
|
|
7263
7304
|
log(` ✓ task 추가: ${m ? m[0] : '?'} — "${taskTitle}"`);
|
|
7264
7305
|
if (action.command) log(` 💡 실행 명령: ${action.command}`);
|
|
7265
7306
|
} else {
|
|
@@ -7318,9 +7359,9 @@ function _suggestNextActions(root, latestRow, keyword) {
|
|
|
7318
7359
|
try {
|
|
7319
7360
|
if (fuzzyRe && exists(planPath(root))) {
|
|
7320
7361
|
const pt = read(planPath(root));
|
|
7321
|
-
const milestones = (pt.match(/^### (M-\d{4})\..*$/gm) || []).filter(l => fuzzyRe.test(l));
|
|
7362
|
+
const milestones = (pt.match(/^### (M-\d{4,})\..*$/gm) || []).filter(l => fuzzyRe.test(l));
|
|
7322
7363
|
if (milestones.length > 0) {
|
|
7323
|
-
const m = milestones[0].match(/M-\d{4}/);
|
|
7364
|
+
const m = milestones[0].match(/M-\d{4,}/);
|
|
7324
7365
|
if (m) actions.push({ icon: '🎯', title: `plan.md milestone ${m[0]} 검증 — "${keyword}" 관련`, command: `leerness plan list --filter "${keyword}"` });
|
|
7325
7366
|
}
|
|
7326
7367
|
}
|
|
@@ -7553,7 +7594,7 @@ function readProgressRows(root) {
|
|
|
7553
7594
|
const text = exists(progressPath(root)) ? read(progressPath(root)) : '';
|
|
7554
7595
|
const rows = [];
|
|
7555
7596
|
for (const line of text.split('\n')) {
|
|
7556
|
-
if (!/^\| (?:T|M|D)-\d{4} \|/.test(line)) continue;
|
|
7597
|
+
if (!/^\| (?:T|M|D)-\d{4,} \|/.test(line)) continue;
|
|
7557
7598
|
// 1.9.399 (7번째 버그헌트 P1-A, UR-0104): 비이스케이프 파이프에서만 분리 + 셀 복원 — 사용자 텍스트의 '|'(이스케이프됨)이 컬럼을 깨지 않음.
|
|
7558
7599
|
const cells = line.split(/(?<!\\)\|/).slice(1, -1).map(s => _cellUnescape(s).trim());
|
|
7559
7600
|
if (cells.length < 6) continue;
|
|
@@ -7594,7 +7635,7 @@ function upsertProgress(root, row) {
|
|
|
7594
7635
|
});
|
|
7595
7636
|
}
|
|
7596
7637
|
|
|
7597
|
-
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 구조화
|
|
7638
|
+
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 구조화
|
|
7598
7639
|
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'); }
|
|
7599
7640
|
// 1.9.119: plan list — plan.md 의 모든 milestone (M-XXXX) 조회 (CLI + --json + MCP)
|
|
7600
7641
|
function planListCmd(root, opts = {}) {
|
|
@@ -7611,9 +7652,9 @@ function planListCmd(root, opts = {}) {
|
|
|
7611
7652
|
const text = read(pp);
|
|
7612
7653
|
const milestones = [];
|
|
7613
7654
|
// ### M-XXXX. <title> 블록 추출
|
|
7614
|
-
const blocks = text.split(/\n(?=### M-\d{4}\.)/);
|
|
7655
|
+
const blocks = text.split(/\n(?=### M-\d{4,}\.)/);
|
|
7615
7656
|
for (const b of blocks) {
|
|
7616
|
-
const headerMatch = b.match(/^### (M-\d{4})\.[ \t]*(.+?)$/m);
|
|
7657
|
+
const headerMatch = b.match(/^### (M-\d{4,})\.[ \t]*(.+?)$/m);
|
|
7617
7658
|
if (!headerMatch) continue;
|
|
7618
7659
|
const id = headerMatch[1];
|
|
7619
7660
|
const title = headerMatch[2].trim();
|
|
@@ -7717,11 +7758,13 @@ function planRemoveCmd(root, target) {
|
|
|
7717
7758
|
if (!exists(pp)) return fail('plan.md 없음');
|
|
7718
7759
|
const text = read(pp);
|
|
7719
7760
|
// milestone 블록은 "### M-XXXX. 제목" 으로 시작; "## " (Out of Scope 등) 헤더 또는 EOF 이전까지
|
|
7720
|
-
|
|
7761
|
+
// codex 버그헌트 P2: `### ` 만 경계로 쓰면 마지막 마일스톤 뒤의 사용자 h1/h2 섹션(예: `## Risks`)이
|
|
7762
|
+
// 그 마일스톤 블록에 흡수돼 함께 삭제된다. 모든 헤딩(#{1,3} )을 경계로 잘라 각 섹션을 독립 블록화.
|
|
7763
|
+
const blocks = text.split(/\n(?=#{1,3} )/);
|
|
7721
7764
|
let removed = 0;
|
|
7722
7765
|
const kept = [];
|
|
7723
7766
|
for (const b of blocks) {
|
|
7724
|
-
if (!b.startsWith('### ')) { kept.push(b); continue; }
|
|
7767
|
+
if (!b.startsWith('### ')) { kept.push(b); continue; } // 마일스톤(### M-) 블록만 삭제 후보
|
|
7725
7768
|
const headerMatch = b.match(/^### (.+)$/m);
|
|
7726
7769
|
if (!headerMatch) { kept.push(b); continue; }
|
|
7727
7770
|
const titleLine = headerMatch[1].trim();
|
|
@@ -7744,7 +7787,7 @@ function planRemoveCmd(root, target) {
|
|
|
7744
7787
|
}
|
|
7745
7788
|
kept.push(b);
|
|
7746
7789
|
}
|
|
7747
|
-
if (removed === 0) return
|
|
7790
|
+
if (removed === 0) return failJson(has('--json'), 'milestone_not_found', `매칭 milestone 없음: "${target}"`); // codex P2: --json 에러 구조화
|
|
7748
7791
|
writeUtf8(pp, kept.join('\n'));
|
|
7749
7792
|
ok(`milestone removed: ${removed}건 (보존: .harness/plan.archive.md)`);
|
|
7750
7793
|
_autoRoadmap(absRoot(root), 'data-change');
|
|
@@ -7918,7 +7961,7 @@ function taskUpdate(root, id) {
|
|
|
7918
7961
|
}
|
|
7919
7962
|
if (!_validateChoice(arg('--status', null), TASK_STATUSES, 'task status')) { process.exitCode = 1; return; } // 1.9.310 (UR-0046)
|
|
7920
7963
|
const rows = readProgressRows(root);
|
|
7921
|
-
if (!rows.find(r => r.id === id)) {
|
|
7964
|
+
if (!rows.find(r => r.id === id)) { failJson(has('--json'), 'task_not_found', `task ${id} not found in progress-tracker.md`); return; } // codex P2: --json 에러 구조화
|
|
7922
7965
|
const patch = { id };
|
|
7923
7966
|
if (arg('--status') !== null) patch.status = _normTaskStatus(arg('--status')); // 1.11.3 (UR-0178): completed/verified → done 정규화
|
|
7924
7967
|
if (arg('--evidence') !== null) patch.evidence = arg('--evidence');
|
|
@@ -7936,7 +7979,7 @@ function taskDrop(root, id) {
|
|
|
7936
7979
|
if (!id) return fail('id required');
|
|
7937
7980
|
const rows = readProgressRows(root);
|
|
7938
7981
|
// 1.9.396 (6번째 외부평가/codex P1-B): 없는 task drop 시 가짜 row(request undefined) 생성 = 데이터 손상 → task update 와 동일하게 존재 확인 후 fail(no-op).
|
|
7939
|
-
if (!rows.find(r => r.id === id)) {
|
|
7982
|
+
if (!rows.find(r => r.id === id)) { failJson(has('--json'), 'task_not_found', `task ${id} not found in progress-tracker.md`); return; } // codex P2: --json 에러 구조화
|
|
7940
7983
|
upsertProgress(root, { id, status: 'dropped', evidence: arg('--reason','사용자 요청으로 제외'), nextAction: '없음' });
|
|
7941
7984
|
ok(`task dropped: ${id}`);
|
|
7942
7985
|
_autoRoadmap(absRoot(root), 'data-change');
|
|
@@ -7963,7 +8006,7 @@ function memoryStatusCmd(root, opts = {}) {
|
|
|
7963
8006
|
const rulesPaused = rules.filter(r => r.status === 'paused').length;
|
|
7964
8007
|
// Plan
|
|
7965
8008
|
const planText = exists(planPath(root)) ? read(planPath(root)) : '';
|
|
7966
|
-
const milestones = (planText.match(/^### M-\d{4}\./gm) || []).length;
|
|
8009
|
+
const milestones = (planText.match(/^### M-\d{4,}\./gm) || []).length;
|
|
7967
8010
|
// plan milestones in-progress: progress-tracker에서 plan:M-XXXX evidence 있고 in-progress 인 row
|
|
7968
8011
|
const planInProgress = rows.filter(r => /plan:M-/.test(r.evidence || '') && r.status === 'in-progress').length;
|
|
7969
8012
|
// Lessons (UR-0058: canonical _loadLessons — JSON 단일 진실소스, MD projection fallback)
|
|
@@ -8134,6 +8177,12 @@ function memoryRestoreCmd(root, surface, target) {
|
|
|
8134
8177
|
for (const blk of restoredBlocks) {
|
|
8135
8178
|
append(activePath, '\n' + blk + '\n');
|
|
8136
8179
|
}
|
|
8180
|
+
// codex 버그헌트 P1: decisions/lessons 는 canonical JSON 이 진실소스라(list/save 가 JSON 기준),
|
|
8181
|
+
// MD 프로젝션에만 append 하면 복원분이 list 에 안 보이고 다음 add/save 가 JSON→MD 재생성으로 영구 삭제.
|
|
8182
|
+
// 방금 복원된 MD 를 canonical 파서로 재파싱해 JSON 을 재빌드 → 복원분이 후속 write 에도 생존.
|
|
8183
|
+
if (surface === 'decisions') _saveDecisions(root, _decisionsFromMd(read(activePath)));
|
|
8184
|
+
else if (surface === 'lessons') _saveLessons(root, _parseLessonEntries(read(activePath)));
|
|
8185
|
+
// (plan 은 plan.md 가 진실소스라 append 로 충분)
|
|
8137
8186
|
// archive 재작성 — 모두 제거되면 파일 비움 (헤더만 남김 또는 삭제)
|
|
8138
8187
|
if (kept.length === 0) {
|
|
8139
8188
|
// archive 헤더만 남겨도 의미 있음 — 향후 다시 사용 가능
|
|
@@ -8198,19 +8247,21 @@ function lessonDropCmd(root, target) {
|
|
|
8198
8247
|
const kept = [];
|
|
8199
8248
|
const removed = [];
|
|
8200
8249
|
const all = _loadLessons(root);
|
|
8201
|
-
if (!all.length) return
|
|
8250
|
+
if (!all.length) return failJson(has('--json'), 'no_lessons', 'lessons 없음'); // codex P2: --json 에러 구조화
|
|
8202
8251
|
for (const l of all) {
|
|
8203
8252
|
if (l.date === target || (l.text || '').includes(target)) removed.push(l);
|
|
8204
8253
|
else kept.push(l);
|
|
8205
8254
|
}
|
|
8206
|
-
if (!removed.length) return
|
|
8255
|
+
if (!removed.length) return failJson(has('--json'), 'lesson_not_found', `매칭 lesson 없음: "${target}"`);
|
|
8207
8256
|
// archive 보존 — lessons.archive.md 에 projection MD 블록 형태로 추가
|
|
8208
8257
|
const archivePath = path.join(root, '.harness/lessons.archive.md');
|
|
8209
8258
|
const archiveHeader = exists(archivePath) ? '' : '# Lessons archive\n\n';
|
|
8259
|
+
// codex 버그헌트 P2: 사용자 텍스트(l.text/l.tag/target)를 archive MD 에 raw 삽입하면 개행으로
|
|
8260
|
+
// `## 제거 …` 델리미터를 위조해 가짜 archive 엔트리를 만들 수 있다(archive list/restore 오염). _lineSafe 로 개행 무력화.
|
|
8210
8261
|
const archiveBlocks = removed.map(l =>
|
|
8211
|
-
`\n### ${l.date}\n- Lesson: ${l.text}\n${l.tag ? `- Tag: ${l.tag}\n` : ''}`
|
|
8262
|
+
`\n### ${_lineSafe(l.date)}\n- Lesson: ${_lineSafe(l.text)}\n${l.tag ? `- Tag: ${_lineSafe(l.tag)}\n` : ''}`
|
|
8212
8263
|
).join('');
|
|
8213
|
-
append(archivePath, archiveHeader + `\n## 제거 ${today()} (target: "${target}")\n${archiveBlocks}\n`);
|
|
8264
|
+
append(archivePath, archiveHeader + `\n## 제거 ${today()} (target: "${_lineSafe(target)}")\n${archiveBlocks}\n`);
|
|
8214
8265
|
_saveLessons(root, kept);
|
|
8215
8266
|
ok(`lesson dropped: ${removed.length}건 (보존: .harness/lessons.archive.md)`);
|
|
8216
8267
|
_autoRoadmap(absRoot(root), 'data-change');
|
|
@@ -8287,22 +8338,23 @@ function decisionDropCmd(root, target) {
|
|
|
8287
8338
|
if (!target) return fail('decision drop <date|title-substring> 필요. 예: leerness decision drop "2026-05-20" 또는 leerness decision drop "PostgreSQL"');
|
|
8288
8339
|
// 1.9.339 (UR-0053): canonical JSON 기준 drop (date 또는 title substring 매칭) — JSON+MD projection 동시 갱신.
|
|
8289
8340
|
const all = _loadDecisions(root);
|
|
8290
|
-
if (!all.length) return
|
|
8341
|
+
if (!all.length) return failJson(has('--json'), 'no_decisions', 'decisions 없음'); // codex P2: --json 에러 구조화
|
|
8291
8342
|
const kept = [];
|
|
8292
8343
|
const removed = [];
|
|
8293
8344
|
for (const d of all) {
|
|
8294
8345
|
if (d.date === target || (d.title || '').includes(target)) removed.push(d);
|
|
8295
8346
|
else kept.push(d);
|
|
8296
8347
|
}
|
|
8297
|
-
if (!removed.length) return
|
|
8348
|
+
if (!removed.length) return failJson(has('--json'), 'decision_not_found', `매칭 decision 없음: "${target}"`);
|
|
8298
8349
|
// archive 보존 (projection MD 블록 형태)
|
|
8299
8350
|
const archivePath = path.join(root, '.harness/decisions.archive.md');
|
|
8300
8351
|
const archiveHeader = exists(archivePath) ? '' : '# Decisions archive\n\n';
|
|
8352
|
+
// codex 버그헌트 P2: 사용자 필드 raw 삽입 → 개행으로 `## 제거` 델리미터 위조 가능. _lineSafe 로 무력화.
|
|
8301
8353
|
const archiveBlocks = removed.map(d => {
|
|
8302
|
-
const head = d.date ? `${d.date} — ${d.title}` : d.title;
|
|
8303
|
-
return `\n### ${head}\n- Decision: ${d.decision || ''}\n- Reason: ${d.reason || ''}\n- Alternatives: ${d.alternatives || ''}\n- Impact: ${d.impact || ''}\n`;
|
|
8354
|
+
const head = d.date ? `${_lineSafe(d.date)} — ${_lineSafe(d.title)}` : _lineSafe(d.title);
|
|
8355
|
+
return `\n### ${head}\n- Decision: ${_lineSafe(d.decision || '')}\n- Reason: ${_lineSafe(d.reason || '')}\n- Alternatives: ${_lineSafe(d.alternatives || '')}\n- Impact: ${_lineSafe(d.impact || '')}\n`;
|
|
8304
8356
|
}).join('');
|
|
8305
|
-
append(archivePath, archiveHeader + `\n## 제거 ${today()} (target: "${target}")\n${archiveBlocks}\n`);
|
|
8357
|
+
append(archivePath, archiveHeader + `\n## 제거 ${today()} (target: "${_lineSafe(target)}")\n${archiveBlocks}\n`);
|
|
8306
8358
|
_saveDecisions(root, kept);
|
|
8307
8359
|
ok(`decision dropped: ${removed.length}건 (보존: .harness/decisions.archive.md)`);
|
|
8308
8360
|
_autoRoadmap(absRoot(root), 'data-change');
|
|
@@ -8363,10 +8415,10 @@ function _jaccard(a, b) {
|
|
|
8363
8415
|
function taskRelink(root) {
|
|
8364
8416
|
root = absRoot(root);
|
|
8365
8417
|
const planText = exists(planPath(root)) ? read(planPath(root)) : '';
|
|
8366
|
-
const milestones = [...planText.matchAll(/^### (M-\d{4})\.[ \t]*(.+?)$/gm)]
|
|
8418
|
+
const milestones = [...planText.matchAll(/^### (M-\d{4,})\.[ \t]*(.+?)$/gm)]
|
|
8367
8419
|
.map(m => ({ id: m[1], text: m[2].trim() }));
|
|
8368
8420
|
const rows = readProgressRows(root);
|
|
8369
|
-
const linkedM = new Set(rows.map(r => (r.evidence.match(/M-\d{4}/) || [])[0]).filter(Boolean));
|
|
8421
|
+
const linkedM = new Set(rows.map(r => (r.evidence.match(/M-\d{4,}/) || [])[0]).filter(Boolean));
|
|
8370
8422
|
const orphanM = milestones.filter(m => !linkedM.has(m.id));
|
|
8371
8423
|
if (!orphanM.length) return ok('미연결 milestone 없음');
|
|
8372
8424
|
|
|
@@ -8414,7 +8466,7 @@ function taskFixEvidence(root) {
|
|
|
8414
8466
|
!r.evidence ||
|
|
8415
8467
|
/^\s*$/.test(r.evidence) ||
|
|
8416
8468
|
/^(user-request|-)$/.test(r.evidence) ||
|
|
8417
|
-
/^plan:M-\d{4}\s*$/.test(r.evidence)
|
|
8469
|
+
/^plan:M-\d{4,}\s*$/.test(r.evidence)
|
|
8418
8470
|
)
|
|
8419
8471
|
);
|
|
8420
8472
|
if (!candidates.length) return ok('갱신 후보 없음 (모든 done row가 검증 키워드 보유)');
|
|
@@ -8426,7 +8478,7 @@ function taskFixEvidence(root) {
|
|
|
8426
8478
|
for (const r of candidates) {
|
|
8427
8479
|
let newEv = setAll;
|
|
8428
8480
|
if (preserveLink) {
|
|
8429
|
-
const m = (r.evidence || '').match(/plan:M-\d{4}/);
|
|
8481
|
+
const m = (r.evidence || '').match(/plan:M-\d{4,}/);
|
|
8430
8482
|
if (m && !newEv.includes(m[0])) {
|
|
8431
8483
|
newEv = `${setAll} (${m[0]})`;
|
|
8432
8484
|
preserved++;
|
|
@@ -8742,7 +8794,7 @@ function lazyDetect(root, opts = {}) {
|
|
|
8742
8794
|
for (const r of rows) {
|
|
8743
8795
|
if (!/^(done|completed|verified)$/i.test(r.status || '')) continue;
|
|
8744
8796
|
const _ev = (r.evidence || '').trim();
|
|
8745
|
-
const _trivial = !_ev || /^(user-request|n\/?a|tbd|todo|wip|none|nil|x|-+|\.+|\?+)$/i.test(_ev) || /^plan:M-\d{4}$/i.test(_ev);
|
|
8797
|
+
const _trivial = !_ev || /^(user-request|n\/?a|tbd|todo|wip|none|nil|x|-+|\.+|\?+)$/i.test(_ev) || /^plan:M-\d{4,}$/i.test(_ev);
|
|
8746
8798
|
if (_trivial) {
|
|
8747
8799
|
issues++; _warn(`done row without verifiable evidence: ${r.id} (${r.request})`,
|
|
8748
8800
|
{ kind: 'evidence_missing', severity: 'warn', taskId: r.id, request: r.request });
|
|
@@ -8826,7 +8878,7 @@ function lazyDetect(root, opts = {}) {
|
|
|
8826
8878
|
_withLock(progressPath(root), () => {
|
|
8827
8879
|
const header = progressHeader(root);
|
|
8828
8880
|
const rows2 = readProgressRows(root);
|
|
8829
|
-
let maxT = 0; const idRe = /\bT-(\d{4})\b/g;
|
|
8881
|
+
let maxT = 0; const idRe = /\bT-(\d{4,})\b/g;
|
|
8830
8882
|
const scanSrc = (exists(planPath(root)) ? read(planPath(root)) : '') + '\n' + rows2.map(r => r.id).join('\n');
|
|
8831
8883
|
let mm; while ((mm = idRe.exec(scanSrc))) maxT = Math.max(maxT, Number(mm[1]));
|
|
8832
8884
|
for (const t of newTodos) {
|
|
@@ -8999,7 +9051,7 @@ function handoff(root) {
|
|
|
8999
9051
|
const rules = readRules(root);
|
|
9000
9052
|
const rulesActive = rules.filter(r => r.status === 'active').length;
|
|
9001
9053
|
const planText = exists(planPath(root)) ? read(planPath(root)) : '';
|
|
9002
|
-
const milestones = (planText.match(/^### M-\d{4}\./gm) || []).length;
|
|
9054
|
+
const milestones = (planText.match(/^### M-\d{4,}\./gm) || []).length;
|
|
9003
9055
|
const lessonsCount = _loadLessons(root).length;
|
|
9004
9056
|
// 1.9.130: archive 카운트 통합
|
|
9005
9057
|
const archiveCountsH = { decisions: 0, lessons: 0, plan: 0, total: 0 };
|
|
@@ -9190,6 +9242,32 @@ function handoff(root) {
|
|
|
9190
9242
|
log(JSON.stringify(result, null, 2));
|
|
9191
9243
|
return;
|
|
9192
9244
|
}
|
|
9245
|
+
// 1.36.18 (UR-0052 P3-8): 단일 워크스페이스 --compact 단축. 종전엔 --compact 가 섹션 몇 개만 억제하고
|
|
9246
|
+
// 본문(session-handoff/progress/decisions…) 186줄을 전량 출력 → 문서상 "~500자 1줄 요약"인 --compact 가
|
|
9247
|
+
// 단일 경로에서 사실상 무효(멀티 워크스페이스 _handoffWorkspace 에만 compact 존재). MCP handoff·REPL preview·
|
|
9248
|
+
// agent-mode 등 내부 호출자 3곳이 단일 경로에 --compact 를 쓰며 186줄을 받아 preview 첫 3줄이 무의미(헤더/날짜/이름)했음.
|
|
9249
|
+
// subprocess 없이(REPL 8s timeout·MCP 친화) 파일 로더만으로 핵심 신호 압축 후 return — _handoffWorkspace compact 형식과 정합.
|
|
9250
|
+
if (has('--compact')) {
|
|
9251
|
+
const rows = readProgressRows(root);
|
|
9252
|
+
const byStatus = {};
|
|
9253
|
+
for (const r of rows) byStatus[r.status] = (byStatus[r.status] || 0) + 1;
|
|
9254
|
+
const done = byStatus['done'] || 0, wip = byStatus['in-progress'] || 0, blocked = byStatus['blocked'] || 0;
|
|
9255
|
+
const pct = rows.length ? Math.round(done / rows.length * 100) : 0;
|
|
9256
|
+
const decN = _loadDecisions(root).length;
|
|
9257
|
+
const rulesActive = readRules(root).filter(r => r.status === 'active').length;
|
|
9258
|
+
const lessN = _loadLessons(root).length;
|
|
9259
|
+
const planText = exists(planPath(root)) ? read(planPath(root)) : '';
|
|
9260
|
+
const mileN = (planText.match(/^### M-\d{4,}\./gm) || []).length;
|
|
9261
|
+
const openReq = (_loadUserRequests(root).requests || []).filter(r => r.status === 'open' || r.status === 'in-progress').length;
|
|
9262
|
+
const nx = rows.find(r => r.status === 'in-progress') || rows.find(r => r.status === 'planned') || null;
|
|
9263
|
+
const flags = [];
|
|
9264
|
+
if (openReq) flags.push(`📥 미답 ${openReq}`);
|
|
9265
|
+
if (blocked) flags.push(`🚫 blocked ${blocked}`);
|
|
9266
|
+
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 개행 주입 차단
|
|
9267
|
+
if (nx) log(_lineSafe(`다음: ${nx.id} [${nx.status}] ${nx.nextAction || nx.request || ''}`).slice(0, 200));
|
|
9268
|
+
log('핵심 규칙: 의존성0 · 한국어주석 · UTF-8noBOM · reuse-map등록 · anti-lazy-work · verify-claim자동검수');
|
|
9269
|
+
return;
|
|
9270
|
+
}
|
|
9193
9271
|
const sections = [];
|
|
9194
9272
|
function block(label, p) {
|
|
9195
9273
|
if (!exists(p)) return;
|
|
@@ -9287,7 +9365,7 @@ function handoff(root) {
|
|
|
9287
9365
|
const decisions = _loadDecisions(root).length; // 1.9.339 (UR-0053): canonical 단일 진실소스
|
|
9288
9366
|
const rulesActive = readRules(root).filter(r => r.status === 'active').length;
|
|
9289
9367
|
const planText = exists(planPath(root)) ? read(planPath(root)) : '';
|
|
9290
|
-
const planMilestones = (planText.match(/^### M-\d{4}\./gm) || []).length;
|
|
9368
|
+
const planMilestones = (planText.match(/^### M-\d{4,}\./gm) || []).length;
|
|
9291
9369
|
const lessons = _loadLessons(root).length;
|
|
9292
9370
|
parts.push(`🧠 mem T${inProgressTasks}/D${decisions}/R${rulesActive}/P${planMilestones}/L${lessons}`);
|
|
9293
9371
|
} catch {}
|
|
@@ -10971,7 +11049,10 @@ function verifyClaimCmd(root, taskId, opts = {}) {
|
|
|
10971
11049
|
// 1.36.10 (선재 백로그): Windows 백슬래시 경로 지원 — 'src\\inventory.mjs' 가 'inventory.mjs' 로,
|
|
10972
11050
|
// 'db\\migrations\\001_create.sql' 이 'create.sql' 로 잘려 추출돼 존재 검사가 false-fail 하던 결함.
|
|
10973
11051
|
// 구분자에 \\ 허용 + 추출 후 / 로 정규화(하류의 git diff 비교·렌즈 매핑은 / 기준).
|
|
10974
|
-
|
|
11052
|
+
// codex 버그헌트 P2: basename 첫 글자를 [A-Za-z] 로만 잡아 '123.js'·'2fa.ts' 같은 숫자시작 실파일이
|
|
11053
|
+
// 추출 안 돼 존재검사가 vacuous-pass → 없는 파일 인용한 done 주장이 통과(gate FN). 첫 글자에 숫자 허용.
|
|
11054
|
+
// (prev-separator 가드가 UNC/드라이브/URL 꼬리 오탐은 계속 차단하므로 FP 억제 유지)
|
|
11055
|
+
const FILE_RE = new RegExp(`(?:[A-Za-z][A-Za-z0-9_-]*[\\/\\\\])?[A-Za-z0-9][\\w./\\\\-]*\\.(?:${FILE_EXTS})\\b`, 'g');
|
|
10975
11056
|
// 1.36.11 (클린룸 P1-B): 매치 직전 문자가 경로 구분자면 더 긴 경로의 "꼬리"만 잡은 것(UNC \\\\server\\..., 드라이브 X:\\...,
|
|
10976
11057
|
// 숫자 시작 선두 세그먼트 등) — 잘린 꼬리를 루트-상대 경로로 검증하면 무관한 로컬 파일이 통과(우회성). 그런 매치는 버린다.
|
|
10977
11058
|
const filePatterns = [];
|
|
@@ -13749,9 +13830,9 @@ function _brainstormFor(root, topic) {
|
|
|
13749
13830
|
const planFile_brainstorm = planPath(root);
|
|
13750
13831
|
if (exists(planFile_brainstorm)) {
|
|
13751
13832
|
const planText = read(planFile_brainstorm);
|
|
13752
|
-
const milestoneBlocks = planText.split(/\n(?=### M-\d{4}\.)/);
|
|
13833
|
+
const milestoneBlocks = planText.split(/\n(?=### M-\d{4,}\.)/);
|
|
13753
13834
|
for (const b of milestoneBlocks) {
|
|
13754
|
-
const m = b.match(/^### (M-\d{4})\.[ \t]*(.+?)$/m);
|
|
13835
|
+
const m = b.match(/^### (M-\d{4,})\.[ \t]*(.+?)$/m);
|
|
13755
13836
|
if (m && matches(b)) {
|
|
13756
13837
|
const idx = planText.indexOf(b);
|
|
13757
13838
|
const lineNo = idx >= 0 ? planText.slice(0, idx).split('\n').length : 0;
|
|
@@ -14044,9 +14125,9 @@ function brainstormCmd(root, topic) {
|
|
|
14044
14125
|
const planFile_b2 = planPath(root);
|
|
14045
14126
|
if (exists(planFile_b2)) {
|
|
14046
14127
|
const planText = read(planFile_b2);
|
|
14047
|
-
const milestoneBlocks = planText.split(/\n(?=### M-\d{4}\.)/);
|
|
14128
|
+
const milestoneBlocks = planText.split(/\n(?=### M-\d{4,}\.)/);
|
|
14048
14129
|
for (const b of milestoneBlocks) {
|
|
14049
|
-
const m = b.match(/^### (M-\d{4})\.[ \t]*(.+?)$/m);
|
|
14130
|
+
const m = b.match(/^### (M-\d{4,})\.[ \t]*(.+?)$/m);
|
|
14050
14131
|
if (m && matches(b)) {
|
|
14051
14132
|
const idx = planText.indexOf(b);
|
|
14052
14133
|
const lineNo = idx >= 0 ? planText.slice(0, idx).split('\n').length : 0;
|
|
@@ -14158,7 +14239,7 @@ function _roadmapData(root) {
|
|
|
14158
14239
|
const milestones = _roadmapParseMilestones(exists(planPath(root)) ? read(planPath(root)) : '');
|
|
14159
14240
|
const tasks = readProgressRows(root).map(t => ({
|
|
14160
14241
|
...t,
|
|
14161
|
-
milestones: Array.from(String(t.evidence || '').matchAll(/M-\d{4}/g)).map(m => m[0])
|
|
14242
|
+
milestones: Array.from(String(t.evidence || '').matchAll(/M-\d{4,}/g)).map(m => m[0])
|
|
14162
14243
|
}));
|
|
14163
14244
|
// skills
|
|
14164
14245
|
const skills = [];
|
|
@@ -14174,7 +14255,7 @@ function _roadmapData(root) {
|
|
|
14174
14255
|
const rulesT = exists(rulesPath(root)) ? read(rulesPath(root)) : '';
|
|
14175
14256
|
const rules = [];
|
|
14176
14257
|
for (const line of rulesT.split('\n')) {
|
|
14177
|
-
if (!/^\| R-\d{4} \|/.test(line)) continue;
|
|
14258
|
+
if (!/^\| R-\d{4,} \|/.test(line)) continue;
|
|
14178
14259
|
const cells = line.split('|').slice(1, -1).map(s => s.trim());
|
|
14179
14260
|
if (cells.length < 6) continue;
|
|
14180
14261
|
rules.push({ id: cells[0], trigger: cells[1], rule: cells[2], status: cells[4], lastVerified: cells[5] });
|
|
@@ -14468,7 +14549,7 @@ function readRules(root) {
|
|
|
14468
14549
|
if (!exists(f)) return [];
|
|
14469
14550
|
const rules = [];
|
|
14470
14551
|
for (const line of read(f).split('\n')) {
|
|
14471
|
-
if (!/^\| R-\d{4} \|/.test(line)) continue;
|
|
14552
|
+
if (!/^\| R-\d{4,} \|/.test(line)) continue;
|
|
14472
14553
|
// 1.9.399 (7번째 버그헌트 P1-A, UR-0104): 비이스케이프 파이프 분리 + 복원 — rule 텍스트의 '|'가 컬럼 밀림/멱등성 무력화를 못 일으킴.
|
|
14473
14554
|
const cells = line.split(/(?<!\\)\|/).slice(1, -1).map(s => _cellUnescape(s).trim());
|
|
14474
14555
|
if (cells.length < 6) continue;
|
|
@@ -14487,7 +14568,7 @@ function nextRuleId(root) {
|
|
|
14487
14568
|
const rules = readRules(root);
|
|
14488
14569
|
let max = 0;
|
|
14489
14570
|
for (const r of rules) {
|
|
14490
|
-
const m = r.id.match(/^R-(\d{4})$/);
|
|
14571
|
+
const m = r.id.match(/^R-(\d{4,})$/);
|
|
14491
14572
|
if (m) max = Math.max(max, Number(m[1]));
|
|
14492
14573
|
}
|
|
14493
14574
|
// 1.11.3 (14th 버그헌트 P2, UR-0180): 아카이브의 R-id 도 카운트 — 기존엔 활성 rules 만 스캔해 rule remove 후 같은 R-id 가 다른 룰에 재사용됐음(아카이브 ID 충돌).
|
|
@@ -14547,7 +14628,7 @@ function ruleRemove(root, id) {
|
|
|
14547
14628
|
if (!id) return fail('id required');
|
|
14548
14629
|
const rules = readRules(root);
|
|
14549
14630
|
const i = rules.findIndex(r => r.id === id);
|
|
14550
|
-
if (i < 0) return
|
|
14631
|
+
if (i < 0) return failJson(has('--json'), 'rule_not_found', `rule not found: ${id}`); // codex P2: --json 에러 구조화
|
|
14551
14632
|
const removed = rules.splice(i, 1)[0];
|
|
14552
14633
|
writeRules(root, rules);
|
|
14553
14634
|
const archive = exists(rulesArchivePath(root)) ? read(rulesArchivePath(root)) : '# Rules archive\n\n| ID | Trigger | Rule | Added | Status | Removed |\n|---|---|---|---|---|---|\n';
|
|
@@ -20114,6 +20195,7 @@ function contractVerifyCmd(specPath, implPath) {
|
|
|
20114
20195
|
// 대신 정적 소스 분석 — module.exports = { foo, bar } / exports.foo = ... / module.exports.foo = ... 패턴 grep.
|
|
20115
20196
|
const implSrc = read(implFile);
|
|
20116
20197
|
// 1.9.429 (UR-0129): 브레이스 균형 top-level 키(멀티라인 module.exports 안전) + ESM export 인식 — pure-utils 단일출처
|
|
20198
|
+
// 1.36.18 (UR-0052 P2-6 이연): 주석 마스킹은 7라운드 codex 재검 결과 파서 없이 안전 구현 불가로 판단해 미적용(하단 참조).
|
|
20117
20199
|
const implExports = new Set(_parseImplExports(implSrc));
|
|
20118
20200
|
// 검사: spec 강선언(function 시그니처 + markdown bullet) 함수 중 impl exports에 없는 것.
|
|
20119
20201
|
// 1.9.385 (UR-0086): 기존 `specText.includes('function '+fn)` 가드는 bullet/backtick 추출명을 무력화하던 잠재 FN.
|
package/lib/analyzers.js
CHANGED
|
@@ -36,7 +36,8 @@ function _shellGuardAnalyze(cmd, ctx) {
|
|
|
36
36
|
}
|
|
37
37
|
function _evidenceQuality(evidence) {
|
|
38
38
|
const e = String(evidence || '');
|
|
39
|
-
|
|
39
|
+
// codex 버그헌트 P2: basename 첫 글자에 숫자 허용 ('123.js' 등 숫자시작 실파일) — bin FILE_RE 와 정합.
|
|
40
|
+
const hasFile = /(?:[A-Za-z][\w-]*[\/\\])?[A-Za-z0-9][\w./\\-]*\.(?:js|ts|tsx|jsx|mjs|cjs|py|go|rs|rb|kt|cs|gd|java|php|swift|c|cpp|h|html|css|scss|vue|svelte|json|yaml|yml|toml|md|sql|prisma|sh)\b/i.test(e);
|
|
40
41
|
const hasTest = /(\d+)\s*(?:\/\s*\d+\s*)?(?:통과|passed|passing|개\s*테스트)|\btests?\b\s*[:=]?\s*\d|Tests?:\s*\d|\b\d+\s*tests?\b/i.test(e);
|
|
41
42
|
const hasLog = /Exit\s*[:=]|exit\s*code|Command\s*[:=]|npm\s+(?:test|run)|pytest|cargo\s+test|go\s+test/i.test(e);
|
|
42
43
|
const missing = [];
|
package/lib/audit.js
CHANGED
|
@@ -62,11 +62,11 @@ function audit(root, opts = {}, deps = {}) {
|
|
|
62
62
|
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
63
|
else ok(`reuse-map.md has ${reuseLines} entries`);
|
|
64
64
|
const planText = exists(planPath(root)) ? read(planPath(root)) : '';
|
|
65
|
-
const milestoneIds = Array.from(planText.matchAll(/^### (M-\d{4})\./gm)).map(m => m[1]);
|
|
65
|
+
const milestoneIds = Array.from(planText.matchAll(/^### (M-\d{4,})\./gm)).map(m => m[1]);
|
|
66
66
|
const rows = readProgressRows(root);
|
|
67
67
|
// 1.9.6 수정: 한 row에 여러 plan:M-XXXX 링크가 있어도 모두 인식 (matchAll로 전부 추출)
|
|
68
68
|
const linkedMs = new Set(
|
|
69
|
-
rows.flatMap(r => Array.from(String(r.evidence || '').matchAll(/M-\d{4}/g), m => m[0]))
|
|
69
|
+
rows.flatMap(r => Array.from(String(r.evidence || '').matchAll(/M-\d{4,}/g), m => m[0]))
|
|
70
70
|
);
|
|
71
71
|
const missingFromProgress = milestoneIds.filter(m => !linkedMs.has(m));
|
|
72
72
|
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}) 인식.
|
|
@@ -1251,7 +1252,7 @@ function _featureGraphTemplate() {
|
|
|
1251
1252
|
function _parseFeatureGraph(text) {
|
|
1252
1253
|
if (!text) return [];
|
|
1253
1254
|
const nodes = [];
|
|
1254
|
-
const re = /^## (F-\d{4})\s+(.+?)\s*$/gm;
|
|
1255
|
+
const re = /^## (F-\d{4,})\s+(.+?)\s*$/gm;
|
|
1255
1256
|
const positions = [];
|
|
1256
1257
|
let m;
|
|
1257
1258
|
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}`;
|