leerness 1.36.17 → 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 +9 -0
- package/README.md +4 -4
- package/bin/leerness.js +76 -28
- 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,14 @@
|
|
|
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
|
+
|
|
3
12
|
## 1.36.17 — 2026-07-14 — 렌즈-외 표면 버그헌트: 데이터손실 2 + gate FN + 아카이브 주입 + --json 에러경로 (codex, 재현확정)
|
|
4
13
|
|
|
5
14
|
게시본 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.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.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.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
|
} },
|
|
@@ -4801,7 +4822,7 @@ function pulseCmd(root) {
|
|
|
4801
4822
|
const decisionCount = _loadDecisions(root).length; // 1.9.339 (UR-0053): canonical 단일 진실소스
|
|
4802
4823
|
const rulesActive = readRules(root).filter(r => r.status === 'active').length;
|
|
4803
4824
|
const planText = exists(planPath(root)) ? read(planPath(root)) : '';
|
|
4804
|
-
const milestonesCnt = (planText.match(/^### M-\d{4}\./gm) || []).length;
|
|
4825
|
+
const milestonesCnt = (planText.match(/^### M-\d{4,}\./gm) || []).length;
|
|
4805
4826
|
const lessonsCount = _loadLessons(root).length;
|
|
4806
4827
|
data.memorySurface = _memorySurface({ tasks: tasksInProgress, decisions: decisionCount, rules: rulesActive, milestones: milestonesCnt, lessons: lessonsCount });
|
|
4807
4828
|
} catch {}
|
|
@@ -7279,7 +7300,7 @@ async function nextActionCmd(root, sub, ...rest) {
|
|
|
7279
7300
|
// leerness task add 호출
|
|
7280
7301
|
const taskResult = cp.spawnSync(process.execPath, [__filename, 'task', 'add', taskTitle, '--path', root], { encoding: 'utf8', timeout: 8000, env: { ...process.env, LEERNESS_INTERNAL: '1' } });
|
|
7281
7302
|
if (taskResult.status === 0) {
|
|
7282
|
-
const m = (taskResult.stdout || '').match(/T-\d{4}/);
|
|
7303
|
+
const m = (taskResult.stdout || '').match(/T-\d{4,}/);
|
|
7283
7304
|
log(` ✓ task 추가: ${m ? m[0] : '?'} — "${taskTitle}"`);
|
|
7284
7305
|
if (action.command) log(` 💡 실행 명령: ${action.command}`);
|
|
7285
7306
|
} else {
|
|
@@ -7338,9 +7359,9 @@ function _suggestNextActions(root, latestRow, keyword) {
|
|
|
7338
7359
|
try {
|
|
7339
7360
|
if (fuzzyRe && exists(planPath(root))) {
|
|
7340
7361
|
const pt = read(planPath(root));
|
|
7341
|
-
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));
|
|
7342
7363
|
if (milestones.length > 0) {
|
|
7343
|
-
const m = milestones[0].match(/M-\d{4}/);
|
|
7364
|
+
const m = milestones[0].match(/M-\d{4,}/);
|
|
7344
7365
|
if (m) actions.push({ icon: '🎯', title: `plan.md milestone ${m[0]} 검증 — "${keyword}" 관련`, command: `leerness plan list --filter "${keyword}"` });
|
|
7345
7366
|
}
|
|
7346
7367
|
}
|
|
@@ -7573,7 +7594,7 @@ function readProgressRows(root) {
|
|
|
7573
7594
|
const text = exists(progressPath(root)) ? read(progressPath(root)) : '';
|
|
7574
7595
|
const rows = [];
|
|
7575
7596
|
for (const line of text.split('\n')) {
|
|
7576
|
-
if (!/^\| (?:T|M|D)-\d{4} \|/.test(line)) continue;
|
|
7597
|
+
if (!/^\| (?:T|M|D)-\d{4,} \|/.test(line)) continue;
|
|
7577
7598
|
// 1.9.399 (7번째 버그헌트 P1-A, UR-0104): 비이스케이프 파이프에서만 분리 + 셀 복원 — 사용자 텍스트의 '|'(이스케이프됨)이 컬럼을 깨지 않음.
|
|
7578
7599
|
const cells = line.split(/(?<!\\)\|/).slice(1, -1).map(s => _cellUnescape(s).trim());
|
|
7579
7600
|
if (cells.length < 6) continue;
|
|
@@ -7614,7 +7635,7 @@ function upsertProgress(root, row) {
|
|
|
7614
7635
|
});
|
|
7615
7636
|
}
|
|
7616
7637
|
|
|
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 구조화
|
|
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 구조화
|
|
7618
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'); }
|
|
7619
7640
|
// 1.9.119: plan list — plan.md 의 모든 milestone (M-XXXX) 조회 (CLI + --json + MCP)
|
|
7620
7641
|
function planListCmd(root, opts = {}) {
|
|
@@ -7631,9 +7652,9 @@ function planListCmd(root, opts = {}) {
|
|
|
7631
7652
|
const text = read(pp);
|
|
7632
7653
|
const milestones = [];
|
|
7633
7654
|
// ### M-XXXX. <title> 블록 추출
|
|
7634
|
-
const blocks = text.split(/\n(?=### M-\d{4}\.)/);
|
|
7655
|
+
const blocks = text.split(/\n(?=### M-\d{4,}\.)/);
|
|
7635
7656
|
for (const b of blocks) {
|
|
7636
|
-
const headerMatch = b.match(/^### (M-\d{4})\.[ \t]*(.+?)$/m);
|
|
7657
|
+
const headerMatch = b.match(/^### (M-\d{4,})\.[ \t]*(.+?)$/m);
|
|
7637
7658
|
if (!headerMatch) continue;
|
|
7638
7659
|
const id = headerMatch[1];
|
|
7639
7660
|
const title = headerMatch[2].trim();
|
|
@@ -7985,7 +8006,7 @@ function memoryStatusCmd(root, opts = {}) {
|
|
|
7985
8006
|
const rulesPaused = rules.filter(r => r.status === 'paused').length;
|
|
7986
8007
|
// Plan
|
|
7987
8008
|
const planText = exists(planPath(root)) ? read(planPath(root)) : '';
|
|
7988
|
-
const milestones = (planText.match(/^### M-\d{4}\./gm) || []).length;
|
|
8009
|
+
const milestones = (planText.match(/^### M-\d{4,}\./gm) || []).length;
|
|
7989
8010
|
// plan milestones in-progress: progress-tracker에서 plan:M-XXXX evidence 있고 in-progress 인 row
|
|
7990
8011
|
const planInProgress = rows.filter(r => /plan:M-/.test(r.evidence || '') && r.status === 'in-progress').length;
|
|
7991
8012
|
// Lessons (UR-0058: canonical _loadLessons — JSON 단일 진실소스, MD projection fallback)
|
|
@@ -8394,10 +8415,10 @@ function _jaccard(a, b) {
|
|
|
8394
8415
|
function taskRelink(root) {
|
|
8395
8416
|
root = absRoot(root);
|
|
8396
8417
|
const planText = exists(planPath(root)) ? read(planPath(root)) : '';
|
|
8397
|
-
const milestones = [...planText.matchAll(/^### (M-\d{4})\.[ \t]*(.+?)$/gm)]
|
|
8418
|
+
const milestones = [...planText.matchAll(/^### (M-\d{4,})\.[ \t]*(.+?)$/gm)]
|
|
8398
8419
|
.map(m => ({ id: m[1], text: m[2].trim() }));
|
|
8399
8420
|
const rows = readProgressRows(root);
|
|
8400
|
-
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));
|
|
8401
8422
|
const orphanM = milestones.filter(m => !linkedM.has(m.id));
|
|
8402
8423
|
if (!orphanM.length) return ok('미연결 milestone 없음');
|
|
8403
8424
|
|
|
@@ -8445,7 +8466,7 @@ function taskFixEvidence(root) {
|
|
|
8445
8466
|
!r.evidence ||
|
|
8446
8467
|
/^\s*$/.test(r.evidence) ||
|
|
8447
8468
|
/^(user-request|-)$/.test(r.evidence) ||
|
|
8448
|
-
/^plan:M-\d{4}\s*$/.test(r.evidence)
|
|
8469
|
+
/^plan:M-\d{4,}\s*$/.test(r.evidence)
|
|
8449
8470
|
)
|
|
8450
8471
|
);
|
|
8451
8472
|
if (!candidates.length) return ok('갱신 후보 없음 (모든 done row가 검증 키워드 보유)');
|
|
@@ -8457,7 +8478,7 @@ function taskFixEvidence(root) {
|
|
|
8457
8478
|
for (const r of candidates) {
|
|
8458
8479
|
let newEv = setAll;
|
|
8459
8480
|
if (preserveLink) {
|
|
8460
|
-
const m = (r.evidence || '').match(/plan:M-\d{4}/);
|
|
8481
|
+
const m = (r.evidence || '').match(/plan:M-\d{4,}/);
|
|
8461
8482
|
if (m && !newEv.includes(m[0])) {
|
|
8462
8483
|
newEv = `${setAll} (${m[0]})`;
|
|
8463
8484
|
preserved++;
|
|
@@ -8773,7 +8794,7 @@ function lazyDetect(root, opts = {}) {
|
|
|
8773
8794
|
for (const r of rows) {
|
|
8774
8795
|
if (!/^(done|completed|verified)$/i.test(r.status || '')) continue;
|
|
8775
8796
|
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);
|
|
8797
|
+
const _trivial = !_ev || /^(user-request|n\/?a|tbd|todo|wip|none|nil|x|-+|\.+|\?+)$/i.test(_ev) || /^plan:M-\d{4,}$/i.test(_ev);
|
|
8777
8798
|
if (_trivial) {
|
|
8778
8799
|
issues++; _warn(`done row without verifiable evidence: ${r.id} (${r.request})`,
|
|
8779
8800
|
{ kind: 'evidence_missing', severity: 'warn', taskId: r.id, request: r.request });
|
|
@@ -8857,7 +8878,7 @@ function lazyDetect(root, opts = {}) {
|
|
|
8857
8878
|
_withLock(progressPath(root), () => {
|
|
8858
8879
|
const header = progressHeader(root);
|
|
8859
8880
|
const rows2 = readProgressRows(root);
|
|
8860
|
-
let maxT = 0; const idRe = /\bT-(\d{4})\b/g;
|
|
8881
|
+
let maxT = 0; const idRe = /\bT-(\d{4,})\b/g;
|
|
8861
8882
|
const scanSrc = (exists(planPath(root)) ? read(planPath(root)) : '') + '\n' + rows2.map(r => r.id).join('\n');
|
|
8862
8883
|
let mm; while ((mm = idRe.exec(scanSrc))) maxT = Math.max(maxT, Number(mm[1]));
|
|
8863
8884
|
for (const t of newTodos) {
|
|
@@ -9030,7 +9051,7 @@ function handoff(root) {
|
|
|
9030
9051
|
const rules = readRules(root);
|
|
9031
9052
|
const rulesActive = rules.filter(r => r.status === 'active').length;
|
|
9032
9053
|
const planText = exists(planPath(root)) ? read(planPath(root)) : '';
|
|
9033
|
-
const milestones = (planText.match(/^### M-\d{4}\./gm) || []).length;
|
|
9054
|
+
const milestones = (planText.match(/^### M-\d{4,}\./gm) || []).length;
|
|
9034
9055
|
const lessonsCount = _loadLessons(root).length;
|
|
9035
9056
|
// 1.9.130: archive 카운트 통합
|
|
9036
9057
|
const archiveCountsH = { decisions: 0, lessons: 0, plan: 0, total: 0 };
|
|
@@ -9221,6 +9242,32 @@ function handoff(root) {
|
|
|
9221
9242
|
log(JSON.stringify(result, null, 2));
|
|
9222
9243
|
return;
|
|
9223
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
|
+
}
|
|
9224
9271
|
const sections = [];
|
|
9225
9272
|
function block(label, p) {
|
|
9226
9273
|
if (!exists(p)) return;
|
|
@@ -9318,7 +9365,7 @@ function handoff(root) {
|
|
|
9318
9365
|
const decisions = _loadDecisions(root).length; // 1.9.339 (UR-0053): canonical 단일 진실소스
|
|
9319
9366
|
const rulesActive = readRules(root).filter(r => r.status === 'active').length;
|
|
9320
9367
|
const planText = exists(planPath(root)) ? read(planPath(root)) : '';
|
|
9321
|
-
const planMilestones = (planText.match(/^### M-\d{4}\./gm) || []).length;
|
|
9368
|
+
const planMilestones = (planText.match(/^### M-\d{4,}\./gm) || []).length;
|
|
9322
9369
|
const lessons = _loadLessons(root).length;
|
|
9323
9370
|
parts.push(`🧠 mem T${inProgressTasks}/D${decisions}/R${rulesActive}/P${planMilestones}/L${lessons}`);
|
|
9324
9371
|
} catch {}
|
|
@@ -13783,9 +13830,9 @@ function _brainstormFor(root, topic) {
|
|
|
13783
13830
|
const planFile_brainstorm = planPath(root);
|
|
13784
13831
|
if (exists(planFile_brainstorm)) {
|
|
13785
13832
|
const planText = read(planFile_brainstorm);
|
|
13786
|
-
const milestoneBlocks = planText.split(/\n(?=### M-\d{4}\.)/);
|
|
13833
|
+
const milestoneBlocks = planText.split(/\n(?=### M-\d{4,}\.)/);
|
|
13787
13834
|
for (const b of milestoneBlocks) {
|
|
13788
|
-
const m = b.match(/^### (M-\d{4})\.[ \t]*(.+?)$/m);
|
|
13835
|
+
const m = b.match(/^### (M-\d{4,})\.[ \t]*(.+?)$/m);
|
|
13789
13836
|
if (m && matches(b)) {
|
|
13790
13837
|
const idx = planText.indexOf(b);
|
|
13791
13838
|
const lineNo = idx >= 0 ? planText.slice(0, idx).split('\n').length : 0;
|
|
@@ -14078,9 +14125,9 @@ function brainstormCmd(root, topic) {
|
|
|
14078
14125
|
const planFile_b2 = planPath(root);
|
|
14079
14126
|
if (exists(planFile_b2)) {
|
|
14080
14127
|
const planText = read(planFile_b2);
|
|
14081
|
-
const milestoneBlocks = planText.split(/\n(?=### M-\d{4}\.)/);
|
|
14128
|
+
const milestoneBlocks = planText.split(/\n(?=### M-\d{4,}\.)/);
|
|
14082
14129
|
for (const b of milestoneBlocks) {
|
|
14083
|
-
const m = b.match(/^### (M-\d{4})\.[ \t]*(.+?)$/m);
|
|
14130
|
+
const m = b.match(/^### (M-\d{4,})\.[ \t]*(.+?)$/m);
|
|
14084
14131
|
if (m && matches(b)) {
|
|
14085
14132
|
const idx = planText.indexOf(b);
|
|
14086
14133
|
const lineNo = idx >= 0 ? planText.slice(0, idx).split('\n').length : 0;
|
|
@@ -14192,7 +14239,7 @@ function _roadmapData(root) {
|
|
|
14192
14239
|
const milestones = _roadmapParseMilestones(exists(planPath(root)) ? read(planPath(root)) : '');
|
|
14193
14240
|
const tasks = readProgressRows(root).map(t => ({
|
|
14194
14241
|
...t,
|
|
14195
|
-
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])
|
|
14196
14243
|
}));
|
|
14197
14244
|
// skills
|
|
14198
14245
|
const skills = [];
|
|
@@ -14208,7 +14255,7 @@ function _roadmapData(root) {
|
|
|
14208
14255
|
const rulesT = exists(rulesPath(root)) ? read(rulesPath(root)) : '';
|
|
14209
14256
|
const rules = [];
|
|
14210
14257
|
for (const line of rulesT.split('\n')) {
|
|
14211
|
-
if (!/^\| R-\d{4} \|/.test(line)) continue;
|
|
14258
|
+
if (!/^\| R-\d{4,} \|/.test(line)) continue;
|
|
14212
14259
|
const cells = line.split('|').slice(1, -1).map(s => s.trim());
|
|
14213
14260
|
if (cells.length < 6) continue;
|
|
14214
14261
|
rules.push({ id: cells[0], trigger: cells[1], rule: cells[2], status: cells[4], lastVerified: cells[5] });
|
|
@@ -14502,7 +14549,7 @@ function readRules(root) {
|
|
|
14502
14549
|
if (!exists(f)) return [];
|
|
14503
14550
|
const rules = [];
|
|
14504
14551
|
for (const line of read(f).split('\n')) {
|
|
14505
|
-
if (!/^\| R-\d{4} \|/.test(line)) continue;
|
|
14552
|
+
if (!/^\| R-\d{4,} \|/.test(line)) continue;
|
|
14506
14553
|
// 1.9.399 (7번째 버그헌트 P1-A, UR-0104): 비이스케이프 파이프 분리 + 복원 — rule 텍스트의 '|'가 컬럼 밀림/멱등성 무력화를 못 일으킴.
|
|
14507
14554
|
const cells = line.split(/(?<!\\)\|/).slice(1, -1).map(s => _cellUnescape(s).trim());
|
|
14508
14555
|
if (cells.length < 6) continue;
|
|
@@ -14521,7 +14568,7 @@ function nextRuleId(root) {
|
|
|
14521
14568
|
const rules = readRules(root);
|
|
14522
14569
|
let max = 0;
|
|
14523
14570
|
for (const r of rules) {
|
|
14524
|
-
const m = r.id.match(/^R-(\d{4})$/);
|
|
14571
|
+
const m = r.id.match(/^R-(\d{4,})$/);
|
|
14525
14572
|
if (m) max = Math.max(max, Number(m[1]));
|
|
14526
14573
|
}
|
|
14527
14574
|
// 1.11.3 (14th 버그헌트 P2, UR-0180): 아카이브의 R-id 도 카운트 — 기존엔 활성 rules 만 스캔해 rule remove 후 같은 R-id 가 다른 룰에 재사용됐음(아카이브 ID 충돌).
|
|
@@ -20148,6 +20195,7 @@ function contractVerifyCmd(specPath, implPath) {
|
|
|
20148
20195
|
// 대신 정적 소스 분석 — module.exports = { foo, bar } / exports.foo = ... / module.exports.foo = ... 패턴 grep.
|
|
20149
20196
|
const implSrc = read(implFile);
|
|
20150
20197
|
// 1.9.429 (UR-0129): 브레이스 균형 top-level 키(멀티라인 module.exports 안전) + ESM export 인식 — pure-utils 단일출처
|
|
20198
|
+
// 1.36.18 (UR-0052 P2-6 이연): 주석 마스킹은 7라운드 codex 재검 결과 파서 없이 안전 구현 불가로 판단해 미적용(하단 참조).
|
|
20151
20199
|
const implExports = new Set(_parseImplExports(implSrc));
|
|
20152
20200
|
// 검사: spec 강선언(function 시그니처 + markdown bullet) 함수 중 impl exports에 없는 것.
|
|
20153
20201
|
// 1.9.385 (UR-0086): 기존 `specText.includes('function '+fn)` 가드는 bullet/backtick 추출명을 무력화하던 잠재 FN.
|
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}`;
|