leerness 1.36.181 → 1.36.182
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 +4 -0
- package/README.md +4 -4
- package/bin/leerness.js +13 -10
- package/package.json +1 -1
- package/scripts/e2e.js +3 -2
- package/scripts/i18n-next-cluster-probe.js +50 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.36.182 — 2026-08-31
|
|
4
|
+
|
|
5
|
+
- T-0162: English-mode milestones now contains zero Hangul across stored, environment, and explicit locale paths while preserving Korean and locale-independent JSON contracts. The 39-command aggregate leakage ratchet tightens exactly from 38 to 34 lines. External Codex review findings are covered by UTC-midnight-safe ETA comparison and real CLI coverage of the 500+ terminal branch.
|
|
6
|
+
|
|
3
7
|
## 1.36.181 — 2026-08-30
|
|
4
8
|
|
|
5
9
|
- T-0161: English-mode release cadence, idempotency audit, plan list, and round-history now contain zero Hangul across stored, environment, and explicit locale paths. A dedicated edge-state probe preserves Korean output, legacy plan sentinels, locale-independent auto-fix JSON/state writes, and the 39-command leakage ratchet tightens exactly from 58 to 38 lines so improvements cannot mask regressions elsewhere.
|
package/README.md
CHANGED
|
@@ -138,7 +138,7 @@ MIT
|
|
|
138
138
|
<!-- leerness:project-readme:start -->
|
|
139
139
|
## Leerness Project Harness
|
|
140
140
|
|
|
141
|
-
이 프로젝트는 Leerness v1.36.
|
|
141
|
+
이 프로젝트는 Leerness v1.36.182 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
|
|
142
142
|
|
|
143
143
|
### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
|
|
144
144
|
|
|
@@ -192,7 +192,7 @@ leerness memory restore decision <date|title>
|
|
|
192
192
|
|
|
193
193
|
### MCP server (외부 AI 통합)
|
|
194
194
|
|
|
195
|
-
Leerness v1.36.
|
|
195
|
+
Leerness v1.36.182는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **89개 도구**를 노출:
|
|
196
196
|
|
|
197
197
|
```jsonc
|
|
198
198
|
// 카테고리별
|
|
@@ -213,7 +213,7 @@ Leerness v1.36.181는 stdio JSON-RPC MCP server를 내장합니다 — Claude Co
|
|
|
213
213
|
`<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
|
|
214
214
|
1) 다음 라운드 후보 선정 → 2) 코드 변경 → 3) 회귀 테스트 갱신 → 4) 전체 e2e 스위트 통과 → 5) npm publish + git tag → 6) main push → 7) session close → 8) 다음 라운드 예약.
|
|
215
215
|
|
|
216
|
-
현재 누적: **v1.9.x → 1.36.
|
|
216
|
+
현재 누적: **v1.9.x → 1.36.182 릴리스 태그 이력** (수백 라운드) · _reports/는 비공개 보존.
|
|
217
217
|
|
|
218
218
|
### 성능 가이드
|
|
219
219
|
|
|
@@ -251,5 +251,5 @@ leerness release pack --close --auto-main-push
|
|
|
251
251
|
- `.leerness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
|
|
252
252
|
- `.leerness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
|
|
253
253
|
|
|
254
|
-
Last synced by Leerness v1.36.
|
|
254
|
+
Last synced by Leerness v1.36.182: 2026-08-31
|
|
255
255
|
<!-- leerness:project-readme:end -->
|
package/bin/leerness.js
CHANGED
|
@@ -49,7 +49,7 @@ const {
|
|
|
49
49
|
migrateLegacyWorkspace,
|
|
50
50
|
} = require('../lib/workspace-dir');
|
|
51
51
|
|
|
52
|
-
const VERSION = '1.36.
|
|
52
|
+
const VERSION = '1.36.182';
|
|
53
53
|
|
|
54
54
|
// MCP lifecycle 주소 표식은 현재 CLI 호출 한 번에만 유효하다. CLI bootstrap에서 즉시 env에서
|
|
55
55
|
// 떼어 두어 `--no-record`/hook처럼 presence 기록 함수에 도달하지 않는 경로도 후속 child에 유출하지 않는다.
|
|
@@ -3408,6 +3408,7 @@ function _computeMilestones(root) {
|
|
|
3408
3408
|
|
|
3409
3409
|
function milestonesCmd(root) {
|
|
3410
3410
|
root = absRoot(root);
|
|
3411
|
+
const _L = _uiLang(root); const t = (ko, en) => (_L === 'en' ? en : ko);
|
|
3411
3412
|
const isTty = process.stdout && process.stdout.isTTY;
|
|
3412
3413
|
const cy = s => isTty ? `\x1b[36m${s}\x1b[0m` : s;
|
|
3413
3414
|
const gr = s => isTty ? `\x1b[32m${s}\x1b[0m` : s;
|
|
@@ -3416,29 +3417,31 @@ function milestonesCmd(root) {
|
|
|
3416
3417
|
const data = _computeMilestones(root);
|
|
3417
3418
|
if (data.gitHistoryState !== 'available') _throwGitHistoryUnavailable(data.gitHistoryError);
|
|
3418
3419
|
if (has('--json')) { log(JSON.stringify(data, null, 2)); return; }
|
|
3419
|
-
log(cy(`# leerness milestones (1.9.229) — 도달 마일스톤 + 다음 ETA`));
|
|
3420
|
+
log(cy(t(`# leerness milestones (1.9.229) — 도달 마일스톤 + 다음 ETA`, `# leerness milestones (1.9.229) — reached milestones + next ETA`)));
|
|
3420
3421
|
log('');
|
|
3421
|
-
log(` 📦 총 라운드: ${gr(String(data.totalRounds))}`);
|
|
3422
|
+
log(t(` 📦 총 라운드: ${gr(String(data.totalRounds))}`, ` 📦 total rounds: ${gr(String(data.totalRounds))}`));
|
|
3422
3423
|
if (data.baselineAt) log(` 📍 baseline: ${(data.baselineAt || '').split(' ')[0]}`);
|
|
3423
|
-
if (data.avgRoundsPerDay) log(` 📊 평균: ${gr(data.avgRoundsPerDay + ' rounds/day')}`);
|
|
3424
|
+
if (data.avgRoundsPerDay) log(t(` 📊 평균: ${gr(data.avgRoundsPerDay + ' rounds/day')}`, ` 📊 average: ${gr(data.avgRoundsPerDay + ' rounds/day')}`));
|
|
3424
3425
|
log('');
|
|
3425
3426
|
if (data.reached.length === 0) {
|
|
3426
|
-
log(dm(` (도달 마일스톤 없음 — 첫 R25 마일스톤 진행 중)`));
|
|
3427
|
+
log(dm(t(` (도달 마일스톤 없음 — 첫 R25 마일스톤 진행 중)`, ` (no milestones reached — progressing toward the first R25 milestone)`)));
|
|
3427
3428
|
} else {
|
|
3428
|
-
log(` 도달 마일스톤 ${data.reached.length}
|
|
3429
|
+
log(t(` 도달 마일스톤 ${data.reached.length}개:`, ` ${data.reached.length} milestone${data.reached.length === 1 ? '' : 's'} reached:`));
|
|
3429
3430
|
for (const m of data.reached) {
|
|
3430
3431
|
log(` ✓ R${m.milestone} ${gr(`(v${m.version}, ${m.reachedAt}, +${m.daysFromBaseline}d)`)}`);
|
|
3431
3432
|
}
|
|
3432
3433
|
}
|
|
3433
3434
|
log('');
|
|
3434
3435
|
if (data.next) {
|
|
3435
|
-
log(yl(` 🎯 다음 마일스톤: R${data.next.milestone}`));
|
|
3436
|
-
log(` • ${data.next.roundsRemaining} 라운드
|
|
3436
|
+
log(yl(t(` 🎯 다음 마일스톤: R${data.next.milestone}`, ` 🎯 next milestone: R${data.next.milestone}`)));
|
|
3437
|
+
log(t(` • ${data.next.roundsRemaining} 라운드 남음`, ` • ${data.next.roundsRemaining} rounds remaining`));
|
|
3437
3438
|
if (data.next.etaDays != null) {
|
|
3438
|
-
log(` • ETA: ${data.next.etaDate} (~${data.next.etaDays}일 후, 현재 속도 기준)`);
|
|
3439
|
+
log(t(` • ETA: ${data.next.etaDate} (~${data.next.etaDays}일 후, 현재 속도 기준)`, ` • ETA: ${data.next.etaDate} (~${data.next.etaDays} days from now, based on current pace)`));
|
|
3439
3440
|
}
|
|
3440
3441
|
} else {
|
|
3441
|
-
log(data && (data.roundCount || data.totalRounds)
|
|
3442
|
+
log(data && (data.roundCount || data.totalRounds)
|
|
3443
|
+
? gr(t(` 🎉 모든 마일스톤 달성 (500+)`, ` 🎉 all milestones reached (500+)`))
|
|
3444
|
+
: dm(t(` (이 저장소 계보의 릴리스 태그 이력 없음)`, ` (no release-tag history for this repository lineage)`))); // 1.36.38 (#4): 이력 0 인데 달성 축하 금지
|
|
3442
3445
|
}
|
|
3443
3446
|
}
|
|
3444
3447
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leerness",
|
|
3
|
-
"version": "1.36.
|
|
3
|
+
"version": "1.36.182",
|
|
4
4
|
"description": "The AI-coding operations layer that makes \"done\" require evidence — persistent memory, evidence-gated completion checks, and clean handoffs for any AI agent (Claude Code, Codex, Cursor). State lives as plain files in your repo. CLI + MCP, 0 runtime dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"leerness",
|
package/scripts/e2e.js
CHANGED
|
@@ -11030,7 +11030,8 @@ total++;
|
|
|
11030
11030
|
// 1.36.174: provider list 영어 모드의 요약/안내 두 줄을 영어화해 104로 조임(39개 명령 실측).
|
|
11031
11031
|
// 1.36.180: agents list(18) + insights(17) + toggle list(11) 우선 표면을 영어화해 58로 조임.
|
|
11032
11032
|
// 1.36.181: 공동 최상위 release cadence/idempotency audit/plan list/round-history(각 5)를 영어화해 38로 조임.
|
|
11033
|
-
|
|
11033
|
+
// 1.36.182: milestones 빈 이력 표면 4줄을 영어화해 34로 조임. 도달/ETA 분기도 전용 probe가 고정한다.
|
|
11034
|
+
const BASELINE = 34;
|
|
11034
11035
|
dbg.baseline = BASELINE;
|
|
11035
11036
|
// 합계가 우연히 낮아진 뒤 다른 명령의 회귀가 그 여유를 소비하지 못하게 exact 로 잠근다.
|
|
11036
11037
|
// 번역이 더 진행되면 실측값과 BASELINE을 같은 변경에서 함께 낮춰야 한다.
|
|
@@ -11038,7 +11039,7 @@ total++;
|
|
|
11038
11039
|
// 이번 라운드가 고친 표면은 **0 이어야** 한다 — 래칫과 별개로 회귀를 직접 막는다.
|
|
11039
11040
|
const cmdOut = String(R(['commands']).stdout || '');
|
|
11040
11041
|
dbg.commandsClean = cmdOut.split('\n').filter(l => HANGUL.test(l)).length === 0;
|
|
11041
|
-
dbg.nextClusterClean = ['release cadence', 'idempotency audit', 'plan list', 'round-history']
|
|
11042
|
+
dbg.nextClusterClean = ['release cadence', 'idempotency audit', 'plan list', 'round-history', 'milestones']
|
|
11042
11043
|
.every(cmd => measured.get(cmd) === 0);
|
|
11043
11044
|
// 한국어 사용자가 보던 것은 **바뀌면 안 된다**. 영어화하면서 `개` 단위를 양쪽에서 없애는 회귀를 냈고
|
|
11044
11045
|
// 직전 커밋과의 바이트 비교가 잡았다. 여기서는 한국어 출력이 여전히 한국어 관례를 지키는지 단언한다.
|
|
@@ -31,12 +31,13 @@ function run(args, env = baseEnv) {
|
|
|
31
31
|
});
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
function git(args, extraEnv = {}) {
|
|
34
|
+
function git(args, extraEnv = {}, input) {
|
|
35
35
|
return cp.spawnSync('git', args, {
|
|
36
36
|
cwd: project,
|
|
37
37
|
env: { ...baseEnv, ...extraEnv },
|
|
38
38
|
encoding: 'utf8',
|
|
39
39
|
timeout: 30000,
|
|
40
|
+
input,
|
|
40
41
|
});
|
|
41
42
|
}
|
|
42
43
|
|
|
@@ -78,6 +79,17 @@ function normalizedJson(label, result) {
|
|
|
78
79
|
throw new Error(`${label} did not return one JSON document: ${error.message}`);
|
|
79
80
|
}
|
|
80
81
|
if (parsed && typeof parsed === 'object') delete parsed.auditedAt;
|
|
82
|
+
// Milestone ETA is intentionally based on the wall clock. English and Korean
|
|
83
|
+
// probes run in separate processes, so they can legitimately straddle UTC
|
|
84
|
+
// midnight. Validate the public date shape, then remove only that volatility
|
|
85
|
+
// before comparing the locale-independent payloads.
|
|
86
|
+
if (parsed && parsed.next && Object.prototype.hasOwnProperty.call(parsed.next, 'etaDate')) {
|
|
87
|
+
const etaDate = parsed.next.etaDate;
|
|
88
|
+
if (etaDate !== null && !/^\d{4}-\d{2}-\d{2}$/.test(String(etaDate))) {
|
|
89
|
+
throw new Error(`${label} returned an invalid milestone etaDate: ${etaDate}`);
|
|
90
|
+
}
|
|
91
|
+
if (etaDate !== null) parsed.next.etaDate = '<clock-derived-date>';
|
|
92
|
+
}
|
|
81
93
|
return parsed;
|
|
82
94
|
}
|
|
83
95
|
|
|
@@ -92,6 +104,7 @@ const surfaces = [
|
|
|
92
104
|
['idempotency audit', ['idempotency', 'audit', '--path', project]],
|
|
93
105
|
['plan list', ['plan', 'list', '--path', project]],
|
|
94
106
|
['round-history', ['round-history', '--path', project]],
|
|
107
|
+
['milestones', ['milestones', '--path', project]],
|
|
95
108
|
];
|
|
96
109
|
|
|
97
110
|
try {
|
|
@@ -206,8 +219,19 @@ try {
|
|
|
206
219
|
requireGitSuccess('git tag second snapshot', git(['tag', '-a', 'v1.0.1', '-m', 'v1.0.1'], {
|
|
207
220
|
GIT_COMMITTER_DATE: secondDate,
|
|
208
221
|
}));
|
|
222
|
+
// Reach R25 without manufacturing another 23 commits. Annotated tags have
|
|
223
|
+
// independent creation times, which is exactly the evidence milestones
|
|
224
|
+
// consumes, and exercise reached/next/ETA rendering in one small fixture.
|
|
225
|
+
for (let i = 2; i < 25; i++) {
|
|
226
|
+
const version = `v1.0.${i}`;
|
|
227
|
+
const tagDate = `2026-08-29T00:${String(i).padStart(2, '0')}:00+09:00`;
|
|
228
|
+
requireGitSuccess(`git tag ${version}`, git(['tag', '-a', version, '-m', version], {
|
|
229
|
+
GIT_COMMITTER_DATE: tagDate,
|
|
230
|
+
}));
|
|
231
|
+
}
|
|
209
232
|
requireNoHangul('release cadence with measured history', run(['release', 'cadence', '--path', project]));
|
|
210
233
|
requireNoHangul('round-history with tags', run(['round-history', '--path', project]));
|
|
234
|
+
requireNoHangul('milestones with reached history', run(['milestones', '--path', project]));
|
|
211
235
|
|
|
212
236
|
// Machine payloads remain the canonical, locale-independent contract. The
|
|
213
237
|
// idempotency timestamp is intentionally volatile and is normalized only
|
|
@@ -226,6 +250,7 @@ try {
|
|
|
226
250
|
['idempotency audit', ['위반 발견', '중복 룰']],
|
|
227
251
|
['plan list', ['완료기준(Done-When)', 'Tasks:', '완료)']],
|
|
228
252
|
['round-history', ['자율 라운드 통계', '누적 라운드', '최근 10 tags']],
|
|
253
|
+
['milestones', ['도달 마일스톤', '총 라운드', '다음 마일스톤', '라운드 남음']],
|
|
229
254
|
]);
|
|
230
255
|
for (const [label, args] of surfaces) {
|
|
231
256
|
const output = requireSuccess(`${label} Korean control`, run(args, koEnv));
|
|
@@ -233,7 +258,30 @@ try {
|
|
|
233
258
|
if (missing.length) throw new Error(`${label} Korean control lost anchors: ${missing.join(', ')}`);
|
|
234
259
|
}
|
|
235
260
|
|
|
236
|
-
|
|
261
|
+
// Exercise the terminal 500+ renderer without spawning another 475 `git tag`
|
|
262
|
+
// processes. Lightweight semver refs are valid release tags and one atomic
|
|
263
|
+
// update-ref process gives the real CLI a 500-tag history.
|
|
264
|
+
const headResult = git(['rev-parse', 'HEAD']);
|
|
265
|
+
requireGitSuccess('git resolve terminal milestone fixture', headResult);
|
|
266
|
+
const head = String(headResult.stdout || '').trim();
|
|
267
|
+
const refs = [];
|
|
268
|
+
for (let i = 25; i < 500; i++) refs.push(`create refs/tags/v1.0.${i} ${head}`);
|
|
269
|
+
requireGitSuccess('git create terminal milestone fixture', git(['update-ref', '--stdin'], {}, refs.join('\n') + '\n'));
|
|
270
|
+
const terminalEn = requireNoHangul('milestones all reached (English)', run(['milestones', '--path', project, '--language', 'en']));
|
|
271
|
+
if (!terminalEn.includes('all milestones reached (500+)')) {
|
|
272
|
+
throw new Error('milestones English terminal branch lost its 500+ completion marker');
|
|
273
|
+
}
|
|
274
|
+
const terminalKo = requireSuccess('milestones all reached (Korean)', run(['milestones', '--path', project], koEnv));
|
|
275
|
+
if (!terminalKo.includes('모든 마일스톤 달성 (500+)')) {
|
|
276
|
+
throw new Error('milestones Korean terminal branch lost its 500+ completion marker');
|
|
277
|
+
}
|
|
278
|
+
const terminalJson = normalizedJson('milestones terminal JSON', run(['milestones', '--path', project, '--json']));
|
|
279
|
+
if (terminalJson.totalRounds !== 500 || terminalJson.next !== null
|
|
280
|
+
|| !Array.isArray(terminalJson.reached) || terminalJson.reached.at(-1)?.milestone !== 500) {
|
|
281
|
+
throw new Error('milestones terminal JSON did not preserve the 500-round contract');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
console.log('✓ Five next-cluster English surfaces contain no Hangul across locale paths and edge states; 5/5 Korean controls and JSON contracts remain intact');
|
|
237
285
|
} catch (error) {
|
|
238
286
|
console.error(`✗ ${error && error.message ? error.message : error}`);
|
|
239
287
|
process.exitCode = 1;
|