leerness 1.36.178 → 1.36.179
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 +16 -9
- package/package.json +5 -4
- package/scripts/parent-detect-selftest-probe.js +104 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.36.179 — 2026-08-30
|
|
4
|
+
|
|
5
|
+
- T-0091: parent-detect selftest를 OS 임시 디렉터리 상위의 외부 .leerness와 격리하고, 두 번째 temp 생성 실패 시 첫 fixture를 정리하도록 셋업 수명을 보강. 동일한 rule-add selftest 형제 경로와 결정론적 회귀 probe를 fast/core/full 게이트에 연결.
|
|
6
|
+
|
|
3
7
|
## 1.36.178 — 2026-08-29
|
|
4
8
|
|
|
5
9
|
- CI: env encoding-check --apply 전체 E2E를 Windows BOM 교체와 POSIX byte-exact no-op 계약으로 분기해 v1.36.177 Linux/Node 22의 466/467 실패를 수정.
|
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.179 하네스를 사용합니다. 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.179는 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.178는 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.179 릴리스 태그 이력** (수백 라운드) · _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.179: 2026-08-30
|
|
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.179';
|
|
53
53
|
|
|
54
54
|
// MCP lifecycle 주소 표식은 현재 CLI 호출 한 번에만 유효하다. CLI bootstrap에서 즉시 env에서
|
|
55
55
|
// 떼어 두어 `--no-record`/hook처럼 presence 기록 함수에 도달하지 않는 경로도 후속 child에 유출하지 않는다.
|
|
@@ -5283,10 +5283,11 @@ function _selfTestCases() {
|
|
|
5283
5283
|
// (b) positional path 가 root 로 쓰이고 cwd 는 오염되지 않는지 확인한다.
|
|
5284
5284
|
const m = require('../lib/pure-utils');
|
|
5285
5285
|
const u = m._parseAddTitle(['rule', 'add', '세션', '점검', '--trigger', 'every-session', '/p'], 2) === '세션 점검';
|
|
5286
|
-
|
|
5287
|
-
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_rulecwd_'));
|
|
5286
|
+
let proj = null, cwd = null;
|
|
5288
5287
|
let wired = false;
|
|
5289
5288
|
try {
|
|
5289
|
+
proj = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_ruleproj_'));
|
|
5290
|
+
cwd = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_rulecwd_'));
|
|
5290
5291
|
for (const d of [proj, cwd]) { fs.mkdirSync(path.join(d, '.leerness'), { recursive: true }); fs.writeFileSync(path.join(d, '.leerness', 'HARNESS_VERSION'), VERSION); }
|
|
5291
5292
|
const r = cp.spawnSync(process.execPath, [__filename, 'rule', 'add', '세션', '점검', '--trigger', 'every-session', proj, '--json'], { encoding: 'utf8', cwd, timeout: 30000, env: { ...process.env, LEERNESS_INTERNAL: '1', LEERNESS_NO_BANNER: '1' } });
|
|
5292
5293
|
const line = (r.stdout || '').split(/\r?\n/).map(x => x.trim()).filter(x => x.startsWith('{')).pop();
|
|
@@ -5294,7 +5295,7 @@ function _selfTestCases() {
|
|
|
5294
5295
|
const titleOk = j.ok === true && j.rule === '세션 점검' && j.trigger === 'every-session';
|
|
5295
5296
|
const rootOk = fs.existsSync(path.join(proj, '.leerness', 'rules.md')) && !fs.existsSync(path.join(cwd, '.leerness', 'rules.md'));
|
|
5296
5297
|
wired = titleOk && rootOk;
|
|
5297
|
-
} catch (e) { wired = false; } finally { for (const d of [proj, cwd]) { try { fs.rmSync(d, { recursive: true, force: true }); } catch {} } }
|
|
5298
|
+
} catch (e) { wired = false; } finally { for (const d of [proj, cwd]) { if (d) { try { fs.rmSync(d, { recursive: true, force: true }); } catch {} } } }
|
|
5298
5299
|
return wired && u;
|
|
5299
5300
|
} },
|
|
5300
5301
|
{ name: '클린룸 (UR-0184): feature add/show/link/impact positional-path 와이어 + 미초기화 게이트 + _taskPositionalPath 값-플래그 skip (1.36.2)', run: () => {
|
|
@@ -7090,18 +7091,24 @@ function _selfTestCases() {
|
|
|
7090
7091
|
} },
|
|
7091
7092
|
{ name: 'parent detect (1.30.2 #157): 상위 leerness 부모 탐지(행위) + 독립 null + assetCount', run: () => {
|
|
7092
7093
|
const osx = require('os'); const fsx = require('fs');
|
|
7093
|
-
|
|
7094
|
-
const alone = fsx.mkdtempSync(path.join(osx.tmpdir(), 'leer-alone-st-'));
|
|
7094
|
+
let base = null, alone = null;
|
|
7095
7095
|
try {
|
|
7096
|
+
base = fsx.mkdtempSync(path.join(osx.tmpdir(), 'leer-parent-st-'));
|
|
7097
|
+
alone = fsx.mkdtempSync(path.join(osx.tmpdir(), 'leer-alone-st-'));
|
|
7096
7098
|
fsx.mkdirSync(path.join(base, '.leerness'), { recursive: true });
|
|
7097
7099
|
fsx.writeFileSync(path.join(base, '.leerness', 'design-system.md'), '# ds');
|
|
7098
7100
|
const sub = path.join(base, 'sub'); fsx.mkdirSync(sub, { recursive: true });
|
|
7099
|
-
|
|
7100
|
-
|
|
7101
|
+
// The fixture owns only the immediate parent. Do not let an unrelated
|
|
7102
|
+
// workspace above os.tmpdir() change this selftest's standalone control.
|
|
7103
|
+
const found = _findParentWorkspace(sub, { maxDepth: 1 });
|
|
7104
|
+
const standalone = _findParentWorkspace(alone, { maxDepth: 1 });
|
|
7101
7105
|
// 부모 탐지: 워크스페이스 .leerness + assetCount≥1(design-system) · 독립: null · read-only(소스에 파일쓰기 없음 — adopt 미구현)
|
|
7102
7106
|
const readOnly = /이 명령은 아무 파일도 쓰지 않는다/.test(read(__filename));
|
|
7103
7107
|
return !!found && found.workspaceDir === '.leerness' && found.assetCount >= 1 && standalone === null && readOnly;
|
|
7104
|
-
} finally {
|
|
7108
|
+
} finally {
|
|
7109
|
+
if (base) { try { fsx.rmSync(base, { recursive: true, force: true }); } catch {} }
|
|
7110
|
+
if (alone) { try { fsx.rmSync(alone, { recursive: true, force: true }); } catch {} }
|
|
7111
|
+
}
|
|
7105
7112
|
} },
|
|
7106
7113
|
{ name: 'VERSION 형식 (x.y.z)', run: () => /^\d+\.\d+\.\d+$/.test(VERSION) },
|
|
7107
7114
|
{ name: 'VERSION ↔ package.json 일치 (1.30.2: 한쪽만 bump 하던 실수 2초 내 차단)', run: () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leerness",
|
|
3
|
-
"version": "1.36.
|
|
3
|
+
"version": "1.36.179",
|
|
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",
|
|
@@ -48,17 +48,18 @@
|
|
|
48
48
|
"LICENSE"
|
|
49
49
|
],
|
|
50
50
|
"scripts": {
|
|
51
|
-
"test": "node ./scripts/lint.js && node ./scripts/workspace-dir-source-ratchet.js && node ./scripts/workspace-dir-migration-probe.js && node ./scripts/workspace-dir-lock-order-probe.js && node ./scripts/lock-probe.js && node ./scripts/platform-smoke.js && node ./scripts/verify-code-cross-runtime-probe.js && node ./scripts/release-runtime-probe.js && node ./scripts/which-shim-probe.js && node ./bin/leerness.js --version && node ./bin/leerness.js selftest && node ./scripts/e2e-core.js && node ./scripts/mutation-integrity-probe.js && node ./scripts/handoff-readonly-probe.js && node ./scripts/mcp-presence-probe.js && npm run test:commands && npm run test:installed && node ./scripts/e2e.js",
|
|
52
|
-
"test:core": "node ./scripts/lint.js && node ./scripts/workspace-dir-source-ratchet.js && node ./scripts/workspace-dir-migration-probe.js && node ./scripts/workspace-dir-lock-order-probe.js && node ./scripts/lock-probe.js && node ./scripts/platform-smoke.js && node ./scripts/verify-code-cross-runtime-probe.js && node ./scripts/release-runtime-probe.js && node ./scripts/which-shim-probe.js && node ./bin/leerness.js --version && node ./bin/leerness.js selftest && node ./scripts/e2e-core.js && node ./scripts/mutation-integrity-probe.js && node ./scripts/handoff-readonly-probe.js && node ./scripts/mcp-presence-probe.js",
|
|
51
|
+
"test": "node ./scripts/lint.js && node ./scripts/workspace-dir-source-ratchet.js && node ./scripts/workspace-dir-migration-probe.js && node ./scripts/workspace-dir-lock-order-probe.js && node ./scripts/lock-probe.js && node ./scripts/platform-smoke.js && node ./scripts/parent-detect-selftest-probe.js && node ./scripts/verify-code-cross-runtime-probe.js && node ./scripts/release-runtime-probe.js && node ./scripts/which-shim-probe.js && node ./bin/leerness.js --version && node ./bin/leerness.js selftest && node ./scripts/e2e-core.js && node ./scripts/mutation-integrity-probe.js && node ./scripts/handoff-readonly-probe.js && node ./scripts/mcp-presence-probe.js && npm run test:commands && npm run test:installed && node ./scripts/e2e.js",
|
|
52
|
+
"test:core": "node ./scripts/lint.js && node ./scripts/workspace-dir-source-ratchet.js && node ./scripts/workspace-dir-migration-probe.js && node ./scripts/workspace-dir-lock-order-probe.js && node ./scripts/lock-probe.js && node ./scripts/platform-smoke.js && node ./scripts/parent-detect-selftest-probe.js && node ./scripts/verify-code-cross-runtime-probe.js && node ./scripts/release-runtime-probe.js && node ./scripts/which-shim-probe.js && node ./bin/leerness.js --version && node ./bin/leerness.js selftest && node ./scripts/e2e-core.js && node ./scripts/mutation-integrity-probe.js && node ./scripts/handoff-readonly-probe.js && node ./scripts/mcp-presence-probe.js",
|
|
53
53
|
"test:commands": "node ./scripts/command-flags-probe.js && node ./scripts/dead-flags-probe.js && node ./scripts/false-claim-probe.js && node ./scripts/next-action-suggestion-probe.js && node ./scripts/claims-baseline-probe.js && node ./scripts/claims-baseline-concurrency-probe.js && node ./scripts/e2e-command-surface.js",
|
|
54
54
|
"test:next-actions": "node ./scripts/next-action-suggestion-probe.js",
|
|
55
55
|
"test:installed": "node ./scripts/installed-cleanroom-probe.js",
|
|
56
56
|
"test:handoff": "node ./scripts/handoff-readonly-probe.js",
|
|
57
|
-
"test:fast": "node ./scripts/lint.js && node ./scripts/workspace-dir-source-ratchet.js && node ./scripts/workspace-dir-migration-probe.js && node ./scripts/workspace-dir-lock-order-probe.js && node ./scripts/lock-probe.js && node ./scripts/platform-smoke.js && node ./scripts/release-runtime-probe.js && node ./scripts/which-shim-probe.js && node ./scripts/mutation-integrity-probe.js && node ./scripts/mcp-presence-probe.js && node ./scripts/false-claim-probe.js && node ./scripts/smoke.js",
|
|
57
|
+
"test:fast": "node ./scripts/lint.js && node ./scripts/workspace-dir-source-ratchet.js && node ./scripts/workspace-dir-migration-probe.js && node ./scripts/workspace-dir-lock-order-probe.js && node ./scripts/lock-probe.js && node ./scripts/platform-smoke.js && node ./scripts/parent-detect-selftest-probe.js && node ./scripts/release-runtime-probe.js && node ./scripts/which-shim-probe.js && node ./scripts/mutation-integrity-probe.js && node ./scripts/mcp-presence-probe.js && node ./scripts/false-claim-probe.js && node ./scripts/smoke.js",
|
|
58
58
|
"test:false-claims": "node ./scripts/false-claim-probe.js",
|
|
59
59
|
"test:workspace-lock-order": "node ./scripts/workspace-dir-lock-order-probe.js",
|
|
60
60
|
"test:mcp-presence": "node ./scripts/mcp-presence-probe.js",
|
|
61
61
|
"test:smoke": "node ./scripts/smoke.js",
|
|
62
|
+
"test:parent-detect": "node ./scripts/parent-detect-selftest-probe.js",
|
|
62
63
|
"lint": "node ./scripts/lint.js",
|
|
63
64
|
"prepack": "node ./bin/leerness.js readme sync . && node ./bin/leerness.js --version"
|
|
64
65
|
},
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// T-0091: the parent-detect selftest must be independent of a Leerness
|
|
5
|
+
// workspace above the OS temp directory, and partial setup must be cleaned if
|
|
6
|
+
// the second mkdtempSync call fails.
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
const cliPath = process.env.LEERNESS_PARENT_PROBE_CLI
|
|
13
|
+
? path.resolve(process.env.LEERNESS_PARENT_PROBE_CLI)
|
|
14
|
+
: path.resolve(__dirname, '..', 'bin', 'leerness.js');
|
|
15
|
+
const cli = require(cliPath);
|
|
16
|
+
const target = cli._selfTestCases().find(test => test.name.includes('parent detect (1.30.2 #157)'));
|
|
17
|
+
const sibling = cli._selfTestCases().find(test => test.name.includes('rule add flag/경로 break'));
|
|
18
|
+
|
|
19
|
+
if (!target || !sibling) {
|
|
20
|
+
process.stderr.write('parent-detect selftest probe failed: target or sibling case not found\n');
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const originalTempEnv = {};
|
|
25
|
+
for (const key of ['TMPDIR', 'TEMP', 'TMP']) originalTempEnv[key] = process.env[key];
|
|
26
|
+
|
|
27
|
+
const sandbox = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-parent-selftest-probe-'));
|
|
28
|
+
const setupDirs = [];
|
|
29
|
+
|
|
30
|
+
function restoreTempEnv() {
|
|
31
|
+
for (const [key, value] of Object.entries(originalTempEnv)) {
|
|
32
|
+
if (value === undefined) delete process.env[key];
|
|
33
|
+
else process.env[key] = value;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function probeSecondMkdtempCleanup(test) {
|
|
38
|
+
const originalMkdtempSync = fs.mkdtempSync;
|
|
39
|
+
let firstSetupDir = null;
|
|
40
|
+
let setupError = null;
|
|
41
|
+
let setupResult = null;
|
|
42
|
+
let setupCalls = 0;
|
|
43
|
+
fs.mkdtempSync = function injectedMkdtempSync(prefix, options) {
|
|
44
|
+
setupCalls++;
|
|
45
|
+
if (setupCalls === 2) {
|
|
46
|
+
const error = new Error('injected second mkdtempSync failure');
|
|
47
|
+
error.code = 'EACCES';
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
firstSetupDir = originalMkdtempSync.call(fs, prefix, options);
|
|
51
|
+
setupDirs.push(firstSetupDir);
|
|
52
|
+
return firstSetupDir;
|
|
53
|
+
};
|
|
54
|
+
try {
|
|
55
|
+
setupResult = test.run();
|
|
56
|
+
} catch (error) {
|
|
57
|
+
setupError = error;
|
|
58
|
+
} finally {
|
|
59
|
+
fs.mkdtempSync = originalMkdtempSync;
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
cleaned: !!firstSetupDir && !fs.existsSync(firstSetupDir),
|
|
63
|
+
observable: setupCalls === 2
|
|
64
|
+
&& ((setupError && setupError.code === 'EACCES') || setupResult === false),
|
|
65
|
+
calls: setupCalls,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const baselinePass = target.run() === true;
|
|
71
|
+
|
|
72
|
+
const contaminatedTemp = path.join(sandbox, 'tmp');
|
|
73
|
+
fs.mkdirSync(path.join(sandbox, '.leerness'), { recursive: true });
|
|
74
|
+
fs.mkdirSync(contaminatedTemp, { recursive: true });
|
|
75
|
+
process.env.TMPDIR = contaminatedTemp;
|
|
76
|
+
process.env.TEMP = contaminatedTemp;
|
|
77
|
+
process.env.TMP = contaminatedTemp;
|
|
78
|
+
const contaminatedAncestorPass = target.run() === true;
|
|
79
|
+
restoreTempEnv();
|
|
80
|
+
|
|
81
|
+
const targetSetup = probeSecondMkdtempCleanup(target);
|
|
82
|
+
const siblingSetup = probeSecondMkdtempCleanup(sibling);
|
|
83
|
+
const report = {
|
|
84
|
+
ok: baselinePass && contaminatedAncestorPass
|
|
85
|
+
&& targetSetup.cleaned && targetSetup.observable
|
|
86
|
+
&& siblingSetup.cleaned && siblingSetup.observable,
|
|
87
|
+
baselinePass,
|
|
88
|
+
contaminatedAncestorPass,
|
|
89
|
+
partialSetupCleaned: targetSetup.cleaned,
|
|
90
|
+
setupFailureObservable: targetSetup.observable,
|
|
91
|
+
setupCalls: targetSetup.calls,
|
|
92
|
+
siblingPartialSetupCleaned: siblingSetup.cleaned,
|
|
93
|
+
siblingSetupFailureObservable: siblingSetup.observable,
|
|
94
|
+
siblingSetupCalls: siblingSetup.calls,
|
|
95
|
+
};
|
|
96
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
97
|
+
if (!report.ok) process.exitCode = 1;
|
|
98
|
+
} finally {
|
|
99
|
+
restoreTempEnv();
|
|
100
|
+
for (const dir of setupDirs) {
|
|
101
|
+
try { fs.rmSync(dir, { recursive: true, force: true }); } catch {}
|
|
102
|
+
}
|
|
103
|
+
try { fs.rmSync(sandbox, { recursive: true, force: true }); } catch {}
|
|
104
|
+
}
|