leerness 1.36.297 → 1.36.299
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 +59 -0
- package/README.md +4 -4
- package/bin/leerness.js +50 -14
- package/lib/copy-tree.js +5 -1
- package/lib/memory-footprint-capture.js +9 -2
- package/lib/runtime-writes.js +24 -4
- package/package.json +8 -5
- package/scripts/bugfix-areas-probe.js +1 -1
- package/scripts/copy-tree-probe.js +17 -0
- package/scripts/done-status-probe.js +1 -1
- package/scripts/e2e-lived-in-harness.js +2 -2
- package/scripts/fix-evidence-language-probe.js +1 -1
- package/scripts/i18n-next-cluster-probe.js +1 -1
- package/scripts/memory-archive-check-probe.js +1 -1
- package/scripts/memory-archive-retention-probe.js +2 -2
- package/scripts/memory-arena-cleanup-probe.js +106 -0
- package/scripts/memory-checked-input-probe.js +1 -1
- package/scripts/memory-guard-order-probe.js +1 -1
- package/scripts/memory-input-preflight-probe.js +1 -1
- package/scripts/memory-legacy-retention-probe.js +2 -2
- package/scripts/memory-migration-test-support.js +40 -2
- package/scripts/memory-output-publication-probe.js +1 -1
- package/scripts/memory-policy-root-spelling-probe.js +110 -0
- package/scripts/memory-projection-retention-probe.js +1 -1
- package/scripts/memory-recovery-external-probe.js +68 -1
- package/scripts/memory-recovery-receipt-probe.js +1 -1
- package/scripts/memory-restore-canonical-probe.js +1 -1
- package/scripts/memory-utf8-admission-probe.js +1 -1
- package/scripts/path-setup-powershell-probe.js +260 -0
- package/scripts/release-bump-probe.js +1 -1
- package/scripts/review-request-labels-probe.js +1 -1
- package/scripts/review-request-signals-probe.js +1 -1
- package/scripts/skill-archive-probe.js +71 -0
- package/scripts/store-values-probe.js +9 -4
- package/scripts/task-sync-roundtrip-probe.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,64 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.36.299 - 2026-10-12
|
|
4
|
+
|
|
5
|
+
- Fixed: on Windows, `leerness skill publish` made a `.zip` instead of the `.tgz` when Git for Windows' GNU tar came first on PATH, as it does in Git Bash and in agent shells built on it (T-0352).
|
|
6
|
+
- Cause: the archive step gave `tar -czf` the absolute output path, and GNU tar reads the drive letter `C:` as a remote host ("Cannot connect to C:"). The step failed, and the Compress-Archive fallback made a `.zip`.
|
|
7
|
+
- The archive is now named relative to the `.leerness` folder, with forward slashes (`skills-publish-tarball/leerness-skills-<version>.tgz`). Windows' own tar (bsdtar) accepts that form too.
|
|
8
|
+
- Fixed: on Windows, `leerness path-setup --apply` could take a different folder for the npm bin folder and not add it (T-0353).
|
|
9
|
+
- Cause: it compared Path entries with PowerShell's `-contains`. That comparison uses the culture and ignores characters such as a soft hyphen (U+00AD), so an entry that differed from the bin folder only by such a character counted as present. Measured in Windows PowerShell 5.1 and PowerShell 7.
|
|
10
|
+
- Entries are now compared by ordinal value, letter case aside.
|
|
11
|
+
- Fixed: the recovery bundle that a memory write saves to the temp folder after a runtime-layout failure could be written inside the project (T-0349). The check that keeps it out compared the project and the temp folder as spelled.
|
|
12
|
+
- When the temp folder was named by an 8.3 short name (GitHub's Windows runners use `C:\Users\RUNNER~1\...`) or reached through a link, it could lie inside the project while its spelling did not.
|
|
13
|
+
- The check also took a folder inside the project whose name starts with two dots, such as `..temp`, for a folder outside it.
|
|
14
|
+
- The check now also compares their real paths, and only a first path part of exactly `..` counts as outside. A temp folder that does not resolve is refused, and so is a project path that fails to resolve for any reason other than being gone. A project path that no longer exists, because the project was renamed or removed during the operation, is compared as spelled only, as before, so its recovery is still saved.
|
|
15
|
+
- The bundle is now written below the temp folder's real path, the one that was checked, and the recovery folder it reports is named by that path. A temp link retargeted into the project after the check can no longer carry the bundle there.
|
|
16
|
+
- Tests (T-0347, T-0349, T-0352, T-0353, T-0354, T-0355):
|
|
17
|
+
- `scripts/skill-archive-probe.js` (`npm run test:skill-archive`, in `test`, `test:core` and `test:fast`) asks the archive step for the `.tgz` with each tar the machine has first on PATH, and checks that tar made it, with the expected entries. Here that is Windows' bsdtar and Git for Windows' GNU tar. With the old absolute path it passes 2 of 6.
|
|
18
|
+
- `scripts/memory-recovery-external-probe.js` (21 tests) adds nine. The old check fails eight of them:
|
|
19
|
+
- A temp folder inside the project, named through a link outside the project or by its 8.3 short name, must be refused before any recovery work. So must a folder inside the project whose name starts with two dots, named directly or through a link.
|
|
20
|
+
- A temp folder that does not resolve must be refused, also when the project path is gone. So must a project path that fails to resolve with an error other than "not found" (simulated). With the old check, an unresolvable temp folder failed the recovery later, with ENOENT.
|
|
21
|
+
- A temp link retargeted into the project while the check runs (simulated) must not carry the bundle into the project.
|
|
22
|
+
- A project renamed away during the operation must still get its recovery outside it. The old check passes this one.
|
|
23
|
+
- `scripts/path-setup-powershell-probe.js` (19 checks):
|
|
24
|
+
- An entry that differs from the bin folder by a soft hyphen must not count as present. The old script fails it.
|
|
25
|
+
- Every registry run now requires the parser check first. With a command injected into the script, no registry run happened. With that requirement removed, the injected command ran.
|
|
26
|
+
- `scripts/memory-policy-root-spelling-probe.js`: the MCP checks and the resolve check use the first non-canonical spelling the machine has. Before, they always used the lowercase drive letter, which a UNC path does not have. They print SKIP when there is none.
|
|
27
|
+
- The shared support of the memory probes (`scripts/memory-migration-test-support.js`) now removes its temp arena when every test passed, and keeps it when one failed. Links inside the arena are removed as links first, so their targets are left alone. An arena, or a folder in it, that was replaced with a link is neither walked nor removed. This holds also when the replacement happens while `finish()` runs: each folder must still resolve inside the arena just before it is listed and before a link in it is removed. A process that kept replacing folders during `finish()` could still race those checks; `finish()` runs after the tests.
|
|
28
|
+
- Before, every run left one arena (1,206 were in this machine's temp folder on 2026-10-11).
|
|
29
|
+
- `scripts/memory-arena-cleanup-probe.js` (`npm run test:memory-arena-cleanup`, in `test`, `test:core` and `test:fast`) checks five cases with links to folders outside the arena, three of them with a replacement before or during `finish()`. On the old support all five fail: the removal case because nothing is removed, the failing-run and replace-before cases only on the note that `finish()` now prints, and the two replacement-during cases because the old `finish()` never looks at the arena, so the replacement never happens.
|
|
30
|
+
- The 24 `fs.cpSync` copies in 21 test files now use `lib/copy-tree.js` (T-0347). On Windows with Node 22.23.3, `fs.cpSync` ends the process with an access violation when a path has non-ASCII letters, as the temp folder does for a Windows user name in Korean.
|
|
31
|
+
- Measured with the temp folder set to `...\AppData\Local\Temp\임시한글` on Node 22.23.3: on the old code all 21 files crashed (Segmentation fault, exit 139 in Git Bash). With the change, 20 passed.
|
|
32
|
+
- The 21st, `scripts/store-values-probe.js`, failed one language check, which read the Hangul in the temp path as Korean text. Its language checks now mask the test folder's path first, and it passes.
|
|
33
|
+
- On Windows, `lib/copy-tree.js` now resolves a relative directory link against the copy's own folder, so the copied link names its sibling in the copy. Before, Node resolved the junction target against the process's working folder. On POSIX the relative link is copied as written and names the same sibling. `fs.cpSync` instead points such a link back into the source tree.
|
|
34
|
+
- `scripts/copy-tree-probe.js` checks this where links can be made. A Windows machine without the symlink privilege skips it.
|
|
35
|
+
|
|
36
|
+
## 1.36.298 - 2026-10-11
|
|
37
|
+
|
|
38
|
+
- Fixed: on Windows, `leerness path-setup --apply` never registered the npm bin folder in the user PATH (T-0351). This was so from 1.9.254.
|
|
39
|
+
- Cause: the PowerShell command joined its statements with spaces only, so PowerShell refused it as a whole (UnexpectedToken), and the command reported a failure.
|
|
40
|
+
- The command now edits `HKCU\Environment\Path` itself. It keeps the value's raw text and registry type, so REG_EXPAND_SZ entries such as `%USERPROFILE%\...\WindowsApps` stay unexpanded, and it refuses other types. It also sends the settings-change broadcast that new terminals rely on.
|
|
41
|
+
- The folder counts as present only when it is written in the Path as such, letter case aside. If the Path names it only through a variable, a second, literal entry is added.
|
|
42
|
+
- It invokes no PowerShell module cmdlet. Write-Output and Where-Object are gone; on a GitHub Windows runner without PSModulePath, the first such cmdlet cost 21.5-52 s per launch (T-0343).
|
|
43
|
+
- A bin path with `;` or `%` is not registered automatically. The command says so and names the manual step.
|
|
44
|
+
- If a user variable named `LEERNESS_PATH_SETUP_NOTIFY` exists, the command stops before changing anything. A failed write ends with exit 1 and is reported as a failure.
|
|
45
|
+
- The dry-run hint of `path-setup` describes the new method.
|
|
46
|
+
- Fixed: a decisions or lessons restore with an explicit write policy (`--write-policy-json`, or `writePolicyJson` in MCP) was refused with "snapshot does not belong to this file" when the project path was spelled differently from its real path (T-0350, listed under "Not fixed here" in 1.36.297).
|
|
47
|
+
- Examples are a lowercase drive letter, a junction or symlink, or an 8.3 short name. A restore without a write policy was not affected.
|
|
48
|
+
- Cause: the memory footprint capture named its files under the resolved (realpath) workspace, and the write admission named the same files under the path as given.
|
|
49
|
+
- The capture now uses the given spelling, after checking that it resolves to the same workspace. Its recheck still compares the resolved forms.
|
|
50
|
+
- Tests (T-0351, T-0350):
|
|
51
|
+
- `scripts/path-setup-powershell-probe.js` (18 checks, Windows only) is `npm run test:path-setup`, part of `test`, `test:core` and `test:fast`.
|
|
52
|
+
- It parses the command with PowerShell's own parser and expects 0 commands and 0 parse errors. The old command has 3 commands and 1 parse error.
|
|
53
|
+
- It runs the command through `_registerPath` without PSModulePath, against a throwaway registry key, and checks that the real user Path is unchanged.
|
|
54
|
+
- On the old code it passes 2 of 18: the parser control and one of its own guard tests.
|
|
55
|
+
- `scripts/memory-policy-root-spelling-probe.js` is part of `npm run test:memory-command-policy`.
|
|
56
|
+
- By the CLI it restores through each spelling the machine has: as created, and a lowercase drive letter, a junction and the 8.3 short name on Windows, or a symlink on POSIX. Each restore must leave the same files as the restore through the real path.
|
|
57
|
+
- By MCP it restores once per surface through a lowercase drive letter on Windows, or a symlink on POSIX, and checks the restored entries.
|
|
58
|
+
- A workspace that resolves to another folder must be refused before any file is read.
|
|
59
|
+
- On the old code it passes only the two "as created" cases on Windows and on Linux.
|
|
60
|
+
- selftest: the path-setup pin checks the registry form.
|
|
61
|
+
|
|
3
62
|
## 1.36.297 - 2026-10-11
|
|
4
63
|
|
|
5
64
|
- Fixed: on Windows, `decision`, `lesson` and `memory` writes that replace an existing file could take 20 s or more per file, or fail with "Windows metadata-preserving replacement failed: spawnSync ... powershell.exe ETIMEDOUT" (T-0343). This happened when Leerness ran in an environment with no PSModulePath.
|
package/README.md
CHANGED
|
@@ -195,7 +195,7 @@ MIT
|
|
|
195
195
|
<!-- leerness:project-readme:start -->
|
|
196
196
|
## Leerness Project Harness
|
|
197
197
|
|
|
198
|
-
이 프로젝트는 Leerness v1.36.
|
|
198
|
+
이 프로젝트는 Leerness v1.36.299 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
|
|
199
199
|
|
|
200
200
|
### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
|
|
201
201
|
|
|
@@ -249,7 +249,7 @@ leerness memory restore decision <date|title>
|
|
|
249
249
|
|
|
250
250
|
### MCP server (외부 AI 통합)
|
|
251
251
|
|
|
252
|
-
Leerness v1.36.
|
|
252
|
+
Leerness v1.36.299는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **102개 도구**를 노출:
|
|
253
253
|
|
|
254
254
|
```jsonc
|
|
255
255
|
// 카테고리별
|
|
@@ -270,7 +270,7 @@ Leerness v1.36.297는 stdio JSON-RPC MCP server를 내장합니다 — Claude Co
|
|
|
270
270
|
`<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
|
|
271
271
|
1) 다음 라운드 후보 선정 → 2) 코드 변경 → 3) 회귀 테스트 갱신 → 4) 전체 e2e 스위트 통과 → 5) npm publish + git tag → 6) main push → 7) session close → 8) 다음 라운드 예약.
|
|
272
272
|
|
|
273
|
-
현재 사용 중인 하네스 버전: **1.36.
|
|
273
|
+
현재 사용 중인 하네스 버전: **1.36.299**. 공개 릴리스 여부는 공개 저장소의 태그와 배포 기록에서 별도로 확인합니다.
|
|
274
274
|
|
|
275
275
|
### 성능 가이드
|
|
276
276
|
|
|
@@ -308,7 +308,7 @@ leerness release pack --close --auto-main-push
|
|
|
308
308
|
- `.leerness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
|
|
309
309
|
- `.leerness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
|
|
310
310
|
|
|
311
|
-
Last synced by Leerness v1.36.
|
|
311
|
+
Last synced by Leerness v1.36.299: 2026-09-30
|
|
312
312
|
<!-- leerness:project-readme:end -->
|
|
313
313
|
GitHub 로그인 계정 확인용 서버 어댑터: [설정·연결·승인과의 구분](docs/github-login.md). 실제 앱 등록/브라우저 세션 연결과 대상별 승인은 후속입니다.
|
|
314
314
|
|
package/bin/leerness.js
CHANGED
|
@@ -115,7 +115,7 @@ const {
|
|
|
115
115
|
migrateLegacyWorkspace,
|
|
116
116
|
} = require('../lib/workspace-dir');
|
|
117
117
|
|
|
118
|
-
const VERSION = '1.36.
|
|
118
|
+
const VERSION = '1.36.299';
|
|
119
119
|
|
|
120
120
|
// MCP lifecycle 주소 표식은 현재 CLI 호출 한 번에만 유효하다. CLI bootstrap에서 즉시 env에서
|
|
121
121
|
// 떼어 두어 `--no-record`/hook처럼 presence 기록 함수에 도달하지 않는 경로도 후속 child에 유출하지 않는다.
|
|
@@ -3992,18 +3992,44 @@ function _pathDiagnose() {
|
|
|
3992
3992
|
// 1.9.255 (UR-0019 2단계): Windows User PATH 등록용 PowerShell 스크립트 생성 (순수 함수 — 테스트/디버그 가능)
|
|
3993
3993
|
// setx 의 1024자 truncation 회피 위해 [Environment]::SetEnvironmentVariable User scope 사용.
|
|
3994
3994
|
// 멱등: 현재 User PATH 에 이미 있으면 EXISTS, 없으면 append 후 ADDED. 시스템 PATH 불변 (관리자 권한 불요).
|
|
3995
|
+
// T-0351: from 1.9.254 the lines were joined with spaces only, so `...SetEnvironmentVariable(...) Write-Output`
|
|
3996
|
+
// was one statement and PowerShell refused the whole script (UnexpectedToken): `path-setup --apply` never ran it on
|
|
3997
|
+
// Windows. Each statement now ends with `;`. Running it showed what [Environment]::Get/SetEnvironmentVariable would
|
|
3998
|
+
// do to the user Path (review round 1): the read expands %VAR% entries with this process's values, and the write
|
|
3999
|
+
// stores REG_SZ, so a REG_EXPAND_SZ Path (the Windows default, e.g. %USERPROFILE%\...\WindowsApps) would lose its
|
|
4000
|
+
// references. The script reads the raw value and its registry type and writes both back. An entry counts as present
|
|
4001
|
+
// only as written (ordinal, letter case aside): expanding %VAR% here would use this shell's variables, which new terminals may
|
|
4002
|
+
// not have, and could report EXISTS for a directory they cannot reach (review round 3; the owner chose this). The
|
|
4003
|
+
// cost is a harmless second entry when the Path already names the directory through a variable. Deleting a variable
|
|
4004
|
+
// that does not exist through SetEnvironmentVariable(..., 'User') writes nothing but sends the WM_SETTINGCHANGE that
|
|
4005
|
+
// new terminals rely on, as the old write did; if a user variable of that name exists, the script stops before
|
|
4006
|
+
// changing anything (round 2).
|
|
4007
|
+
// The script invokes no command: Write-Output and Where-Object were the module cmdlets that cost 21.5-52 s per launch
|
|
4008
|
+
// without PSModulePath on a GitHub Windows runner (T-0343). Inside `try`, PowerShell treats a .NET exception or a
|
|
4009
|
+
// throw as terminating, so a failure ends the script with exit 1 before ADDED. A bin path with ';' or '%' is
|
|
4010
|
+
// refused by _registerPath before this runs.
|
|
3995
4011
|
function _winPathPsScript() {
|
|
3996
4012
|
return [
|
|
3997
4013
|
// npm prefix is data. JSON double quotes are not PowerShell escaping: $()/backticks execute.
|
|
3998
|
-
"$b = $env:LEERNESS_PATH_BIN
|
|
3999
|
-
"$
|
|
4000
|
-
"$
|
|
4001
|
-
"
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4014
|
+
"$b = $env:LEERNESS_PATH_BIN",
|
|
4015
|
+
"$k = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey('Environment', $true)",
|
|
4016
|
+
"if ($null -eq $k) { throw 'HKCU\\Environment cannot be opened for writing' }",
|
|
4017
|
+
"try { $names = $k.GetValueNames(); "
|
|
4018
|
+
+ "if ($names -contains 'LEERNESS_PATH_SETUP_NOTIFY') { throw 'the user variable LEERNESS_PATH_SETUP_NOTIFY exists; leerness deletes that name to announce the change, so the Path was left as it is' }; "
|
|
4019
|
+
+ "$raw = [string]$k.GetValue('Path', '', [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames); "
|
|
4020
|
+
+ "$kind = [Microsoft.Win32.RegistryValueKind]::ExpandString; "
|
|
4021
|
+
+ "if ($names -contains 'Path') { $kind = $k.GetValueKind('Path') }; "
|
|
4022
|
+
+ "if ($kind -ne [Microsoft.Win32.RegistryValueKind]::String -and $kind -ne [Microsoft.Win32.RegistryValueKind]::ExpandString) { throw ('the user Path has registry type ' + $kind) }; "
|
|
4023
|
+
// T-0353: ordinal, case-insensitive. -contains compares with the culture, which ignores characters such as a
|
|
4024
|
+
// soft hyphen (U+00AD), so 'C:\np<U+00AD>m' counted as 'C:\npm' (measured in Windows PowerShell 5.1 and 7).
|
|
4025
|
+
+ "$found = $false; foreach ($p in $raw.Split(';')) { if ([string]::Equals($p, $b, [StringComparison]::OrdinalIgnoreCase)) { $found = $true } }; "
|
|
4026
|
+
+ "if ($found) { [Console]::Out.WriteLine('EXISTS') } "
|
|
4027
|
+
+ "else { $nu = if ($raw -eq '') { $b } else { $raw.TrimEnd(';') + ';' + $b }; "
|
|
4028
|
+
+ "$k.SetValue('Path', $nu, $kind); "
|
|
4029
|
+
+ "[Environment]::SetEnvironmentVariable('LEERNESS_PATH_SETUP_NOTIFY', $null, 'User'); "
|
|
4030
|
+
+ "[Console]::Out.WriteLine('ADDED') } } "
|
|
4031
|
+
+ "finally { $k.Close() }",
|
|
4032
|
+
].join('; ');
|
|
4007
4033
|
}
|
|
4008
4034
|
|
|
4009
4035
|
// 1.9.255 (UR-0019 2단계): Unix shell rc 등록 블록 마커 (순수 — 테스트/디버그 가능, 멱등 키)
|
|
@@ -4020,7 +4046,10 @@ function _registerPath(diag) {
|
|
|
4020
4046
|
if (!bin) return { ok: false, method: 'none', detail: 'npm global bin 경로를 찾을 수 없음' };
|
|
4021
4047
|
const platform = diag.platform || process.platform;
|
|
4022
4048
|
if (platform === 'win32') {
|
|
4023
|
-
// Windows: PowerShell
|
|
4049
|
+
// Windows: PowerShell 로 HKCU\Environment 의 Path 원문과 값 종류를 보존해 append (멱등, 관리자 권한 불요)
|
|
4050
|
+
// T-0351: ';' splits a PATH entry and '%' is expanded in a REG_EXPAND_SZ Path, so such a directory cannot be added
|
|
4051
|
+
// as one literal entry. Registration is refused before PowerShell runs, and the manual step is shown instead.
|
|
4052
|
+
if (/[;%]/.test(bin)) return { ok: false, method: 'powershell-user-path', detail: `npm bin 경로에 ';' 또는 '%' 가 있어 자동 등록하지 않음 (수동 등록 필요): ${bin}` };
|
|
4024
4053
|
try {
|
|
4025
4054
|
const ps = _winPathPsScript(bin);
|
|
4026
4055
|
const r = _spawnPortableArgvSync('powershell', ['-NoProfile', '-Command', ps], {
|
|
@@ -4078,7 +4107,7 @@ function pathSetupCmd(root, opts = {}) {
|
|
|
4078
4107
|
log(yl(` ⚠ leerness 가 PATH 에 없습니다 — 'leerness' 명령이 동작하지 않을 수 있음`));
|
|
4079
4108
|
log(` ${dm('적용 시 추가될 경로:')} ${diag.globalBin}`);
|
|
4080
4109
|
if (diag.platform === 'win32') {
|
|
4081
|
-
log(dm(` 방법: PowerShell
|
|
4110
|
+
log(dm(` 방법: PowerShell 로 HKCU\\Environment 의 Path 끝에 추가 — 원문(%VAR%)과 값 종류 유지 (관리자 권한 불요)`));
|
|
4082
4111
|
} else {
|
|
4083
4112
|
log(dm(` 방법: ${diag.shellRc} 에 export PATH 블록 append (멱등)`));
|
|
4084
4113
|
}
|
|
@@ -4191,7 +4220,7 @@ function _selfTestCases() {
|
|
|
4191
4220
|
{ name: '_classifyCJK: 중국어/일본어 구분', run: () => { const z = Buffer.from('中', 'utf8'), j = Buffer.from('ひ', 'utf8'); return _classifyCJK(z, z.length).chinese > 0 && _classifyCJK(j, j.length).japanese > 0; } },
|
|
4192
4221
|
{ name: '_riskLabel: korean→CP949 / chinese→CP936', run: () => /CP949/.test(_riskLabel({ korean: 3, japanese: 0, chinese: 0, other: 0 }).risk) && /CP936/.test(_riskLabel({ korean: 0, japanese: 0, chinese: 3, other: 0 }).risk) },
|
|
4193
4222
|
{ name: '_dirInPath: 정규화 (trailing slash)', run: () => { const sep = process.platform === 'win32' ? ';' : ':'; const old = process.env.PATH; process.env.PATH = '/usr/bin' + sep + '/opt/x/bin'; const r = _dirInPath('/opt/x/bin/') === true && _dirInPath('/no/such') === false; process.env.PATH = old; return r; } },
|
|
4194
|
-
{ name: '_winPathPsScript:
|
|
4223
|
+
{ name: '_winPathPsScript: HKCU Path 원문·값 종류 보존 + setx 미사용 (T-0351)', run: () => { const ps = _winPathPsScript('C:\\npm'); return ps.includes("OpenSubKey('Environment', $true)") && ps.includes('DoNotExpandEnvironmentNames') && ps.includes("$k.SetValue('Path', $nu, $kind)") && !/SetEnvironmentVariable\('PATH'/i.test(ps) && !/setx/i.test(ps); } },
|
|
4195
4224
|
{ name: '_unixPathBlock: 멱등 마커 + export', run: () => { const b = _unixPathBlock('/x/bin'); return b.includes('managed') && /export PATH="\$PATH:\/x\/bin"/.test(b); } },
|
|
4196
4225
|
{ name: '_shellGuardAnalyze: PS5.1 && → ps5-chain error', run: () => { const r = _shellGuardAnalyze('a && b', { shell: 'powershell', psVersion: '5' }); return r.issues.some(i => i.rule === 'ps5-chain' && i.severity === 'error'); } },
|
|
4197
4226
|
{ name: '_shellGuardAnalyze: bash && → 문제 없음', run: () => _shellGuardAnalyze('a && b', { shell: 'bash' }).issues.length === 0 },
|
|
@@ -26467,7 +26496,12 @@ function _createArchive(cwd, sourceDir, outPath) {
|
|
|
26467
26496
|
// 1) tar.gz (POSIX 환경에서 가장 안정)
|
|
26468
26497
|
if (/\.(tgz|tar\.gz)$/i.test(outPath)) {
|
|
26469
26498
|
tried.push('tar');
|
|
26470
|
-
|
|
26499
|
+
// T-0352: the archive is named relative to cwd, with forward slashes. Git for Windows' GNU tar, first on PATH in
|
|
26500
|
+
// Git Bash, reads the drive letter of an absolute Windows path as a remote host ("Cannot connect to C:") and
|
|
26501
|
+
// failed, so the .zip fallback ran instead. bsdtar accepts either form. A path on another drive stays absolute.
|
|
26502
|
+
const relativeOut = path.relative(cwd, outPath);
|
|
26503
|
+
const tarOut = relativeOut && !path.isAbsolute(relativeOut) ? relativeOut.split(path.sep).join('/') : outPath;
|
|
26504
|
+
const r = _spawnPortableArgvSync('tar', ['-czf', tarOut, sourceDir], {
|
|
26471
26505
|
encoding: 'utf8', timeout: 30000, shell: false, cwd
|
|
26472
26506
|
});
|
|
26473
26507
|
if (r.status === 0 && exists(outPath)) return { ok: true, path: outPath, method: 'tar', tried };
|
|
@@ -34120,6 +34154,8 @@ module.exports = {
|
|
|
34120
34154
|
_cliMutationClass, _mustRemainReadOnly, _runtimeProjectRoot,
|
|
34121
34155
|
_npmGlobalBin, _dirInPath, _leernessResolvable, _pathDiagnose, _registerPath,
|
|
34122
34156
|
_winPathPsScript, _unixPathBlock, pathSetupCmd,
|
|
34157
|
+
// T-0352: the skill publish archive chain (tar, then the zip fallbacks), for scripts/skill-archive-probe.js.
|
|
34158
|
+
_createArchive,
|
|
34123
34159
|
_isSecretKey, compareVer, parseHarnessVersion,
|
|
34124
34160
|
// 1.9.257: CJK 인코딩 분류 (UR-0014 계열) 순수 함수 — 단위 테스트
|
|
34125
34161
|
_classifyCJK, _riskLabel,
|
package/lib/copy-tree.js
CHANGED
|
@@ -12,10 +12,14 @@ const path = require('path');
|
|
|
12
12
|
function copyTreeSync(src, dst) {
|
|
13
13
|
const stat = fs.lstatSync(src);
|
|
14
14
|
if (stat.isSymbolicLink()) {
|
|
15
|
-
|
|
15
|
+
let target = fs.readlinkSync(src);
|
|
16
16
|
let type;
|
|
17
17
|
if (process.platform === 'win32') {
|
|
18
18
|
try { type = fs.statSync(src).isDirectory() ? 'junction' : 'file'; } catch { type = 'file'; }
|
|
19
|
+
// T-0347: a junction's target must be absolute, and Node resolves a relative one against the process cwd.
|
|
20
|
+
// A relative directory link is resolved against the copy's own folder, so it still names the place it named
|
|
21
|
+
// relative to where it now is (fs.cpSync instead points it back into the source tree).
|
|
22
|
+
if (type === 'junction' && !path.isAbsolute(target)) target = path.resolve(path.dirname(dst), target);
|
|
19
23
|
}
|
|
20
24
|
fs.symlinkSync(target, dst, type);
|
|
21
25
|
return;
|
|
@@ -33,7 +33,14 @@ function captureMemoryFootprint(root, surface, limitsJson) {
|
|
|
33
33
|
const resolved = resolveStatePaths(lexicalRoot), runtime = createRuntimeCompatibilityReader(lexicalRoot);
|
|
34
34
|
if (!runtime().compatible) fail('runtime_incompatible');
|
|
35
35
|
if (!['live','substrate'].includes(resolved.workspace.status)) fail('workspace_unavailable');
|
|
36
|
-
|
|
36
|
+
// T-0350: the files are named under the caller's spelling of the root, as admission, preparation and the commands
|
|
37
|
+
// name them. The resolved workspace is the realpath.native form; with a root spelled another way (a lowercase
|
|
38
|
+
// drive letter, a junction or symlink, an 8.3 short name) the snapshots never matched the caller's paths in
|
|
39
|
+
// assertCheckedFileCurrent, and every restore with a write policy was refused. The spelled workspace must resolve
|
|
40
|
+
// to the resolved one before any file is read, and the recheck still compares resolved forms.
|
|
41
|
+
const selectedPath = resolved.workspace.selectedPath;
|
|
42
|
+
const workspace = path.join(lexicalRoot, resolved.workspace.selectedName);
|
|
43
|
+
if (!sameResolvedParent(resolveMemoryParent(workspace), selectedPath)) fail('workspace_unavailable');
|
|
37
44
|
const observed = [], files = {}, snapshots = {};
|
|
38
45
|
let fileBytes = 0;
|
|
39
46
|
for (const [role, suffix] of [['canonical','.json'],['projection','.md'],['archive','.archive.md']]) {
|
|
@@ -50,7 +57,7 @@ function captureMemoryFootprint(root, surface, limitsJson) {
|
|
|
50
57
|
recheck() {
|
|
51
58
|
const now = resolveStatePaths(lexicalRoot);
|
|
52
59
|
if (!sameResolvedParent(originalRoot,resolveMemoryParent(lexicalRoot))
|
|
53
|
-
|| now.projectRoot !== resolved.projectRoot || now.workspace.selectedPath !==
|
|
60
|
+
|| now.projectRoot !== resolved.projectRoot || now.workspace.selectedPath !== selectedPath
|
|
54
61
|
|| now.workspace.status !== resolved.workspace.status) fail('source_changed');
|
|
55
62
|
if (!runtime().compatible || !createRuntimeCompatibilityReader(lexicalRoot)().compatible) fail('runtime_changed');
|
|
56
63
|
for (const record of observed) assertCheckedFileCurrent(record.input,record.file);
|
package/lib/runtime-writes.js
CHANGED
|
@@ -159,13 +159,33 @@ function assertExternalRecoveryAllowed(root) {
|
|
|
159
159
|
}
|
|
160
160
|
const project = path.resolve(root);
|
|
161
161
|
const base = path.resolve(os.tmpdir());
|
|
162
|
-
|
|
163
|
-
|
|
162
|
+
// T-0349: compared as spelled and as real paths. A temp folder reached through a link, or named by an 8.3 short
|
|
163
|
+
// name (GitHub's Windows runners give C:\Users\RUNNER~1\...), can lie inside the project while its spelling does
|
|
164
|
+
// not, and the recovery bundle was then written into the project it must stay out of. A temp folder that cannot
|
|
165
|
+
// be resolved counts as unsafe. A project path that no longer exists (ENOENT or ENOTDIR: renamed or removed during
|
|
166
|
+
// the operation, one way this latch is reached) has nothing at it to lie inside, so only its spelling is compared;
|
|
167
|
+
// any other failure to resolve it counts as unsafe.
|
|
168
|
+
const unsafe = () => {
|
|
164
169
|
const error = new Error(`External recovery temp must stay outside project root: ${base}`);
|
|
165
170
|
error.code = 'E_RECOVERY_TEMP_UNSAFE';
|
|
166
171
|
throw error;
|
|
167
|
-
}
|
|
168
|
-
|
|
172
|
+
};
|
|
173
|
+
// Outside means a first component of exactly '..' (a child folder may be named '..temp', review round 1).
|
|
174
|
+
const inside = (from, to) => {
|
|
175
|
+
const relative = path.relative(from, to);
|
|
176
|
+
return !(relative === '..' || relative.startsWith('..' + path.sep) || path.isAbsolute(relative));
|
|
177
|
+
};
|
|
178
|
+
const real = p => { try { return { path: fs.realpathSync.native(p) }; } catch (error) { return { error }; } };
|
|
179
|
+
const realBase = real(base), realProject = real(project);
|
|
180
|
+
if (!realBase.path) unsafe();
|
|
181
|
+
const missing = !realProject.path && ['ENOENT', 'ENOTDIR'].includes(realProject.error && realProject.error.code);
|
|
182
|
+
if (!realProject.path && !missing) unsafe();
|
|
183
|
+
if (inside(project, base)) unsafe();
|
|
184
|
+
if (realProject.path && inside(realProject.path, realBase.path)) unsafe();
|
|
185
|
+
// The bundle goes below the real path that was checked, not below the alias: a temp link retargeted into the project
|
|
186
|
+
// after this check would otherwise carry the bundle there (review round 2). A real folder replaced while the
|
|
187
|
+
// recovery runs is not covered, as for every path-based write here.
|
|
188
|
+
return realBase.path;
|
|
169
189
|
}
|
|
170
190
|
|
|
171
191
|
// Streaming recovery uses the same latch and external-only destination. The
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leerness",
|
|
3
|
-
"version": "1.36.
|
|
3
|
+
"version": "1.36.299",
|
|
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,15 +48,15 @@
|
|
|
48
48
|
"LICENSE"
|
|
49
49
|
],
|
|
50
50
|
"scripts": {
|
|
51
|
-
"test": "node ./scripts/lint.js && node ./scripts/workspace-dir-source-ratchet.js && npm run test:state-scopes && npm run test:runtime-compatibility && node ./scripts/workspace-dir-migration-probe.js && node ./scripts/workspace-dir-lock-order-probe.js && node ./scripts/lock-probe.js && node ./scripts/e2e-repo-guard.js && node ./scripts/e2e-lived-in-harness.js && npm run test:lock-heartbeat && npm run test:temp-cleanup && 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 ./scripts/i18n-priority-surface-probe.js && npm run test:harness-text && npm run test:test-plan && node ./scripts/i18n-next-cluster-probe.js && node ./scripts/handoff-i18n-probe.js && node ./scripts/provider-capacity-probe.js && npm run test:provider-response && npm run test:github-login && npm run test:provider-dispatch && npm run test:provider-receipt && npm run test:provider-observation && npm run test:ui-review-publication && npm run test:ui-review-target && npm run test:ui-review-authorization && npm run test:ui-review-authority-host && npm run test:ui-review-authority-floor && npm run test:ui-review-proof-use && npm run test:ui-review-recovery && node ./scripts/file-lease-probe.js && node ./scripts/role-agent-schema-probe.js && node ./scripts/role-store-loader-probe.js && node ./scripts/role-fallback-probe.js && npm run test:agent-bench-output && npm run test:agent-chat-stream && npm run test:encoding-diagnostics && npm run test:managed-text && npm run test:plan-text && npm run test:session-close-text && npm run test:brief-text && npm run test:generation-text && npm run test:install-text && npm run test:authoring-language && npm run test:context-reading && npm run test:rule-text && npm run test:mode-text && npm run test:handoff-text && npm run test:audit-text && npm run test:audit-readme-text && npm run test:audit-env-text && npm run test:feature-graph-text && npm run test:env-sync-text && npm run test:teams-text && npm run test:reminder-text && npm run test:handoff-history-text && npm run test:env-snapshot-text && npm run test:context-current && npm run test:review-feedback && npm run test:ledger-sync && npm run test:state-lock && npm run test:history-retention && 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 && node ./scripts/review-checkpoint-probe.js && node ./scripts/review-checkpoint-transport-probe.js && node ./scripts/review-snapshot-probe.js && npm run test:memory-migration && npm run test:memory-items && npm run test:memory-budget && npm run test:memory-footprint && npm run test:memory-write-budget && npm run test:memory-write-preparation && npm run test:memory-write-admission && npm run test:memory-command-policy && npm run test:memory-publication-policy && npm run test:memory-recovery-transfer && npm run test:ui-boundary && node ./scripts/review-control-probe.js && npm run test:ui-review-run && npm run test:rule-verification",
|
|
52
|
-
"test:core": "node ./scripts/lint.js && node ./scripts/workspace-dir-source-ratchet.js && npm run test:state-scopes && npm run test:runtime-compatibility && node ./scripts/workspace-dir-migration-probe.js && node ./scripts/workspace-dir-lock-order-probe.js && node ./scripts/lock-probe.js && node ./scripts/e2e-repo-guard.js && node ./scripts/e2e-lived-in-harness.js && npm run test:lock-heartbeat && npm run test:temp-cleanup && 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 ./scripts/i18n-priority-surface-probe.js && npm run test:harness-text && npm run test:test-plan && node ./scripts/i18n-next-cluster-probe.js && node ./scripts/handoff-i18n-probe.js && node ./scripts/provider-capacity-probe.js && npm run test:provider-response && npm run test:github-login && npm run test:provider-dispatch && npm run test:provider-receipt && npm run test:provider-observation && npm run test:ui-review-publication-smoke && npm run test:ui-review-target-smoke && npm run test:ui-review-authorization-smoke && npm run test:ui-review-authority-host-smoke && npm run test:ui-review-authority-floor-smoke && npm run test:ui-review-proof-use-smoke && npm run test:ui-review-recovery && node ./scripts/file-lease-probe.js && node ./scripts/role-agent-schema-probe.js && node ./scripts/role-store-loader-probe.js && node ./scripts/role-fallback-probe.js && npm run test:agent-bench-output && npm run test:review-feedback && npm run test:ledger-sync && npm run test:state-lock && npm run test:history-retention && node ./bin/leerness.js --version && node ./bin/leerness.js selftest && node ./scripts/e2e-core.js && node ./scripts/mutation-integrity-probe.js && node ./scripts/memory-guard-order-probe.js && node ./scripts/memory-input-preflight-probe.js && node ./scripts/memory-utf8-admission-probe.js && node ./scripts/memory-checked-input-probe.js && node ./scripts/memory-input-unit-probe.js && node ./scripts/memory-restore-canonical-probe.js && node ./scripts/memory-archive-retention-probe.js && node ./scripts/memory-archive-check-probe.js && node ./scripts/memory-projection-retention-probe.js && node ./scripts/memory-language-probe.js && node ./scripts/memory-legacy-retention-probe.js && node ./scripts/memory-recovery-receipt-probe.js && node ./scripts/memory-output-unit-probe.js && node ./scripts/memory-output-posix-probe.js && node ./scripts/memory-output-publication-probe.js && node ./scripts/memory-output-parent-race-probe.js && node ./scripts/handoff-readonly-probe.js && node ./scripts/mcp-presence-probe.js && node ./scripts/rule-applicability-probe.js && node ./scripts/review-checkpoint-probe.js && node ./scripts/review-checkpoint-transport-probe.js && node ./scripts/review-snapshot-probe.js && node ./scripts/review-snapshot-transport-probe.js && npm run test:memory-migration && npm run test:memory-items && npm run test:memory-budget && npm run test:memory-footprint && npm run test:memory-write-budget && npm run test:memory-write-preparation && npm run test:memory-write-admission && npm run test:memory-command-policy && npm run test:memory-publication-policy && npm run test:memory-recovery-transfer && npm run test:ui-boundary && npm run test:ui-review-run && npm run test:rule-verification",
|
|
51
|
+
"test": "node ./scripts/lint.js && node ./scripts/workspace-dir-source-ratchet.js && npm run test:state-scopes && npm run test:runtime-compatibility && node ./scripts/workspace-dir-migration-probe.js && node ./scripts/workspace-dir-lock-order-probe.js && node ./scripts/lock-probe.js && node ./scripts/e2e-repo-guard.js && node ./scripts/e2e-lived-in-harness.js && npm run test:lock-heartbeat && npm run test:temp-cleanup && npm run test:path-setup && npm run test:skill-archive && npm run test:memory-arena-cleanup && 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 ./scripts/i18n-priority-surface-probe.js && npm run test:harness-text && npm run test:test-plan && node ./scripts/i18n-next-cluster-probe.js && node ./scripts/handoff-i18n-probe.js && node ./scripts/provider-capacity-probe.js && npm run test:provider-response && npm run test:github-login && npm run test:provider-dispatch && npm run test:provider-receipt && npm run test:provider-observation && npm run test:ui-review-publication && npm run test:ui-review-target && npm run test:ui-review-authorization && npm run test:ui-review-authority-host && npm run test:ui-review-authority-floor && npm run test:ui-review-proof-use && npm run test:ui-review-recovery && node ./scripts/file-lease-probe.js && node ./scripts/role-agent-schema-probe.js && node ./scripts/role-store-loader-probe.js && node ./scripts/role-fallback-probe.js && npm run test:agent-bench-output && npm run test:agent-chat-stream && npm run test:encoding-diagnostics && npm run test:managed-text && npm run test:plan-text && npm run test:session-close-text && npm run test:brief-text && npm run test:generation-text && npm run test:install-text && npm run test:authoring-language && npm run test:context-reading && npm run test:rule-text && npm run test:mode-text && npm run test:handoff-text && npm run test:audit-text && npm run test:audit-readme-text && npm run test:audit-env-text && npm run test:feature-graph-text && npm run test:env-sync-text && npm run test:teams-text && npm run test:reminder-text && npm run test:handoff-history-text && npm run test:env-snapshot-text && npm run test:context-current && npm run test:review-feedback && npm run test:ledger-sync && npm run test:state-lock && npm run test:history-retention && 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 && node ./scripts/review-checkpoint-probe.js && node ./scripts/review-checkpoint-transport-probe.js && node ./scripts/review-snapshot-probe.js && npm run test:memory-migration && npm run test:memory-items && npm run test:memory-budget && npm run test:memory-footprint && npm run test:memory-write-budget && npm run test:memory-write-preparation && npm run test:memory-write-admission && npm run test:memory-command-policy && npm run test:memory-publication-policy && npm run test:memory-recovery-transfer && npm run test:ui-boundary && node ./scripts/review-control-probe.js && npm run test:ui-review-run && npm run test:rule-verification",
|
|
52
|
+
"test:core": "node ./scripts/lint.js && node ./scripts/workspace-dir-source-ratchet.js && npm run test:state-scopes && npm run test:runtime-compatibility && node ./scripts/workspace-dir-migration-probe.js && node ./scripts/workspace-dir-lock-order-probe.js && node ./scripts/lock-probe.js && node ./scripts/e2e-repo-guard.js && node ./scripts/e2e-lived-in-harness.js && npm run test:lock-heartbeat && npm run test:temp-cleanup && npm run test:path-setup && npm run test:skill-archive && npm run test:memory-arena-cleanup && 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 ./scripts/i18n-priority-surface-probe.js && npm run test:harness-text && npm run test:test-plan && node ./scripts/i18n-next-cluster-probe.js && node ./scripts/handoff-i18n-probe.js && node ./scripts/provider-capacity-probe.js && npm run test:provider-response && npm run test:github-login && npm run test:provider-dispatch && npm run test:provider-receipt && npm run test:provider-observation && npm run test:ui-review-publication-smoke && npm run test:ui-review-target-smoke && npm run test:ui-review-authorization-smoke && npm run test:ui-review-authority-host-smoke && npm run test:ui-review-authority-floor-smoke && npm run test:ui-review-proof-use-smoke && npm run test:ui-review-recovery && node ./scripts/file-lease-probe.js && node ./scripts/role-agent-schema-probe.js && node ./scripts/role-store-loader-probe.js && node ./scripts/role-fallback-probe.js && npm run test:agent-bench-output && npm run test:review-feedback && npm run test:ledger-sync && npm run test:state-lock && npm run test:history-retention && node ./bin/leerness.js --version && node ./bin/leerness.js selftest && node ./scripts/e2e-core.js && node ./scripts/mutation-integrity-probe.js && node ./scripts/memory-guard-order-probe.js && node ./scripts/memory-input-preflight-probe.js && node ./scripts/memory-utf8-admission-probe.js && node ./scripts/memory-checked-input-probe.js && node ./scripts/memory-input-unit-probe.js && node ./scripts/memory-restore-canonical-probe.js && node ./scripts/memory-archive-retention-probe.js && node ./scripts/memory-archive-check-probe.js && node ./scripts/memory-projection-retention-probe.js && node ./scripts/memory-language-probe.js && node ./scripts/memory-legacy-retention-probe.js && node ./scripts/memory-recovery-receipt-probe.js && node ./scripts/memory-output-unit-probe.js && node ./scripts/memory-output-posix-probe.js && node ./scripts/memory-output-publication-probe.js && node ./scripts/memory-output-parent-race-probe.js && node ./scripts/handoff-readonly-probe.js && node ./scripts/mcp-presence-probe.js && node ./scripts/rule-applicability-probe.js && node ./scripts/review-checkpoint-probe.js && node ./scripts/review-checkpoint-transport-probe.js && node ./scripts/review-snapshot-probe.js && node ./scripts/review-snapshot-transport-probe.js && npm run test:memory-migration && npm run test:memory-items && npm run test:memory-budget && npm run test:memory-footprint && npm run test:memory-write-budget && npm run test:memory-write-preparation && npm run test:memory-write-admission && npm run test:memory-command-policy && npm run test:memory-publication-policy && npm run test:memory-recovery-transfer && npm run test:ui-boundary && npm run test:ui-review-run && npm run test:rule-verification",
|
|
53
53
|
"test:commands": "npm run test:hooks && npm run test:e2e-observation && node ./scripts/memory-guard-order-probe.js && node ./scripts/memory-input-preflight-probe.js && node ./scripts/memory-utf8-admission-probe.js && node ./scripts/memory-checked-input-probe.js && node ./scripts/memory-input-unit-probe.js && node ./scripts/memory-restore-canonical-probe.js && node ./scripts/memory-archive-retention-probe.js && node ./scripts/memory-archive-check-probe.js && node ./scripts/memory-projection-retention-probe.js && node ./scripts/memory-language-probe.js && node ./scripts/memory-legacy-retention-probe.js && node ./scripts/memory-recovery-receipt-probe.js && node ./scripts/memory-output-unit-probe.js && node ./scripts/memory-output-posix-probe.js && node ./scripts/memory-output-publication-probe.js && node ./scripts/memory-output-parent-race-probe.js && node ./scripts/command-flags-probe.js && node ./scripts/dead-flags-probe.js && node ./scripts/free-text-args-probe.js && node ./scripts/title-args-probe.js && node ./scripts/false-claim-probe.js && node ./scripts/claim-sweep-git-probe.js && node ./scripts/claim-runtime-cache-probe.js && node ./scripts/runtime-cost-probe.js && node ./scripts/secret-scan-scope-probe.js && node ./scripts/handoff-preservation-probe.js && node ./scripts/next-action-suggestion-probe.js && node ./scripts/api-skill-match-probe.js && node ./scripts/crud-check-probe.js && node ./scripts/claims-baseline-probe.js && node ./scripts/claims-baseline-concurrency-probe.js && node ./scripts/e2e-command-surface.js && node ./scripts/rule-applicability-probe.js && node ./scripts/review-request-labels-probe.js && node ./scripts/review-request-signals-probe.js && node ./scripts/store-values-probe.js && node ./scripts/review-snapshot-transport-probe.js && node ./scripts/bugfix-areas-probe.js && node ./scripts/done-status-probe.js && node ./scripts/fix-evidence-language-probe.js && node ./scripts/decision-fence-offset-probe.js && node ./scripts/task-sync-roundtrip-probe.js && node ./scripts/tracker-write-language-probe.js && node ./scripts/write-path-language-probe.js && node ./scripts/write-primitive-language-probe.js && node ./scripts/dry-run-refusal-probe.js",
|
|
54
54
|
"test:e2e-observation": "node ./scripts/e2e-temp-scope-probe.js && node ./scripts/e2e-child-diagnostics-probe.js",
|
|
55
55
|
"test:next-actions": "node ./scripts/next-action-suggestion-probe.js && node ./scripts/api-skill-match-probe.js",
|
|
56
56
|
"test:installed": "npm run test:installed-lifecycle && node ./scripts/installed-cleanroom-probe.js",
|
|
57
57
|
"test:installed-lifecycle": "node ./scripts/installed-handoff-lifecycle-probe.js",
|
|
58
58
|
"test:handoff": "node ./scripts/handoff-readonly-probe.js",
|
|
59
|
-
"test:fast": "node ./scripts/lint.js && node ./scripts/workspace-dir-source-ratchet.js && npm run test:state-scopes && npm run test:runtime-compatibility && node ./scripts/workspace-dir-migration-probe.js && node ./scripts/workspace-dir-lock-order-probe.js && node ./scripts/lock-probe.js && node ./scripts/e2e-repo-guard.js && npm run test:lock-heartbeat && npm run test:temp-cleanup && 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/i18n-priority-surface-probe.js && npm run test:harness-text && npm run test:test-plan && node ./scripts/i18n-next-cluster-probe.js && node ./scripts/handoff-i18n-probe.js && node ./scripts/mutation-integrity-probe.js && node ./scripts/memory-guard-order-probe.js && node ./scripts/memory-input-preflight-probe.js && node ./scripts/memory-utf8-admission-probe.js && node ./scripts/memory-checked-input-probe.js && node ./scripts/memory-input-unit-probe.js && node ./scripts/memory-restore-canonical-probe.js && node ./scripts/memory-archive-retention-probe.js && node ./scripts/memory-archive-check-probe.js && node ./scripts/memory-projection-retention-probe.js && node ./scripts/memory-language-probe.js && node ./scripts/memory-legacy-retention-probe.js && node ./scripts/memory-recovery-receipt-probe.js && node ./scripts/memory-output-unit-probe.js && node ./scripts/memory-output-posix-probe.js && node ./scripts/memory-output-publication-probe.js && node ./scripts/memory-output-parent-race-probe.js && node ./scripts/mcp-presence-probe.js && node ./scripts/provider-capacity-probe.js && npm run test:provider-response && npm run test:provider-dispatch && npm run test:provider-receipt && npm run test:provider-observation && npm run test:ui-review-recovery && node ./scripts/file-lease-probe.js && node ./scripts/role-agent-schema-probe.js && node ./scripts/role-store-loader-probe.js && node ./scripts/role-fallback-probe.js && npm run test:agent-bench-output && npm run test:ledger-sync && npm run test:state-lock && npm run test:history-retention && node ./scripts/false-claim-probe.js && node ./scripts/smoke.js && node ./scripts/rule-applicability-probe.js && node ./scripts/review-checkpoint-probe.js && node ./scripts/review-checkpoint-transport-probe.js && node ./scripts/review-snapshot-probe.js && node ./scripts/review-snapshot-transport-probe.js && npm run test:memory-migration && npm run test:memory-items && npm run test:memory-budget && npm run test:memory-footprint && npm run test:memory-write-budget && npm run test:memory-write-preparation && npm run test:memory-write-admission && npm run test:memory-command-policy && npm run test:memory-publication-policy && npm run test:memory-recovery-transfer && npm run test:ui-boundary && npm run test:rule-verification",
|
|
59
|
+
"test:fast": "node ./scripts/lint.js && node ./scripts/workspace-dir-source-ratchet.js && npm run test:state-scopes && npm run test:runtime-compatibility && node ./scripts/workspace-dir-migration-probe.js && node ./scripts/workspace-dir-lock-order-probe.js && node ./scripts/lock-probe.js && node ./scripts/e2e-repo-guard.js && npm run test:lock-heartbeat && npm run test:temp-cleanup && npm run test:path-setup && npm run test:skill-archive && npm run test:memory-arena-cleanup && 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/i18n-priority-surface-probe.js && npm run test:harness-text && npm run test:test-plan && node ./scripts/i18n-next-cluster-probe.js && node ./scripts/handoff-i18n-probe.js && node ./scripts/mutation-integrity-probe.js && node ./scripts/memory-guard-order-probe.js && node ./scripts/memory-input-preflight-probe.js && node ./scripts/memory-utf8-admission-probe.js && node ./scripts/memory-checked-input-probe.js && node ./scripts/memory-input-unit-probe.js && node ./scripts/memory-restore-canonical-probe.js && node ./scripts/memory-archive-retention-probe.js && node ./scripts/memory-archive-check-probe.js && node ./scripts/memory-projection-retention-probe.js && node ./scripts/memory-language-probe.js && node ./scripts/memory-legacy-retention-probe.js && node ./scripts/memory-recovery-receipt-probe.js && node ./scripts/memory-output-unit-probe.js && node ./scripts/memory-output-posix-probe.js && node ./scripts/memory-output-publication-probe.js && node ./scripts/memory-output-parent-race-probe.js && node ./scripts/mcp-presence-probe.js && node ./scripts/provider-capacity-probe.js && npm run test:provider-response && npm run test:provider-dispatch && npm run test:provider-receipt && npm run test:provider-observation && npm run test:ui-review-recovery && node ./scripts/file-lease-probe.js && node ./scripts/role-agent-schema-probe.js && node ./scripts/role-store-loader-probe.js && node ./scripts/role-fallback-probe.js && npm run test:agent-bench-output && npm run test:ledger-sync && npm run test:state-lock && npm run test:history-retention && node ./scripts/false-claim-probe.js && node ./scripts/smoke.js && node ./scripts/rule-applicability-probe.js && node ./scripts/review-checkpoint-probe.js && node ./scripts/review-checkpoint-transport-probe.js && node ./scripts/review-snapshot-probe.js && node ./scripts/review-snapshot-transport-probe.js && npm run test:memory-migration && npm run test:memory-items && npm run test:memory-budget && npm run test:memory-footprint && npm run test:memory-write-budget && npm run test:memory-write-preparation && npm run test:memory-write-admission && npm run test:memory-command-policy && npm run test:memory-publication-policy && npm run test:memory-recovery-transfer && npm run test:ui-boundary && npm run test:rule-verification",
|
|
60
60
|
"test:false-claims": "node ./scripts/false-claim-probe.js",
|
|
61
61
|
"test:claim-sweep": "node ./scripts/claim-sweep-git-probe.js",
|
|
62
62
|
"test:claim-runtime": "node ./scripts/claim-runtime-cache-probe.js",
|
|
@@ -119,7 +119,7 @@
|
|
|
119
119
|
"test:memory-write-budget": "node ./scripts/memory-write-budget-probe.js && node ./scripts/memory-write-budget-race-probe.js && node ./scripts/memory-write-budget-cli-probe.js",
|
|
120
120
|
"test:memory-write-preparation": "node ./scripts/memory-write-preparation-probe.js && node ./scripts/memory-write-preparation-cli-probe.js",
|
|
121
121
|
"test:memory-write-admission": "node ./scripts/memory-write-admission-probe.js && node ./scripts/memory-write-dry-run-probe.js",
|
|
122
|
-
"test:memory-command-policy": "node ./scripts/memory-command-size-probe.js && node ./scripts/memory-command-policy-probe.js && node ./scripts/memory-command-policy-mcp-probe.js",
|
|
122
|
+
"test:memory-command-policy": "node ./scripts/memory-command-size-probe.js && node ./scripts/memory-command-policy-probe.js && node ./scripts/memory-command-policy-mcp-probe.js && node ./scripts/memory-policy-root-spelling-probe.js",
|
|
123
123
|
"test:memory-publication-policy": "node ./scripts/memory-publication-policy-probe.js && node ./scripts/memory-publication-cli-probe.js",
|
|
124
124
|
"test:memory-recovery-transfer": "node ./scripts/memory-recovery-transfer-probe.js && node ./scripts/memory-recovery-external-probe.js",
|
|
125
125
|
"test:ui-boundary": "node ./scripts/ui-boundary-probe.js && node ./scripts/ui-boundary-contracts-probe.js && node ./scripts/ui-boundary-scanner-probe.js && node ./scripts/ui-review-probe.js && node ./scripts/ui-review-observation-probe.js && node ./scripts/ui-review-ledger-probe.js && node ./scripts/ui-boundary-browser-probe.js && node ./scripts/ui-boundary-trials-probe.js && npm run test:ui-cli",
|
|
@@ -140,6 +140,9 @@
|
|
|
140
140
|
"test:state-lock": "node ./scripts/shared-state-lock-probe.js",
|
|
141
141
|
"test:lock-heartbeat": "node ./scripts/lock-heartbeat-probe.js",
|
|
142
142
|
"test:temp-cleanup": "node ./scripts/temp-cleanup-probe.js && node ./scripts/copy-tree-probe.js",
|
|
143
|
+
"test:path-setup": "node ./scripts/path-setup-powershell-probe.js",
|
|
144
|
+
"test:skill-archive": "node ./scripts/skill-archive-probe.js",
|
|
145
|
+
"test:memory-arena-cleanup": "node ./scripts/memory-arena-cleanup-probe.js",
|
|
143
146
|
"test:ui-review-publication": "node ./scripts/ui-review-publication-probe.js",
|
|
144
147
|
"test:ui-review-publication-smoke": "node ./scripts/ui-review-publication-probe.js --smoke",
|
|
145
148
|
"test:ui-review-target": "node ./scripts/ui-review-target-probe.js",
|
|
@@ -41,7 +41,7 @@ const seed = lang => {
|
|
|
41
41
|
return (seeds[lang] = dir);
|
|
42
42
|
};
|
|
43
43
|
let copies = 0;
|
|
44
|
-
const fresh = (lang = 'ko') => { const dir = path.join(arena, 'p' + (++copies));
|
|
44
|
+
const fresh = (lang = 'ko') => { const dir = path.join(arena, 'p' + (++copies)); require('../lib/copy-tree').copyTreeSync(seed(lang), dir); return dir; };
|
|
45
45
|
const runIn = (cwd, args, runEnv = env) => cp.spawnSync(process.execPath, [CLI, ...args], { cwd, encoding: 'utf8', timeout: 300000, windowsHide: true, env: runEnv });
|
|
46
46
|
const show = r => `exit ${r.status}\n${String(r.stdout || '').slice(0, 1200)}${String(r.stderr || '').slice(0, 600)}`;
|
|
47
47
|
const json = (r, label) => { const out = String(r.stdout || ''); try { return JSON.parse(out.slice(out.indexOf('{'))); } catch { throw new Error(label + ': not JSON — ' + show(r)); } };
|
|
@@ -50,6 +50,23 @@ try {
|
|
|
50
50
|
assert.throws(() => copyTreeSync(from, to), error => error.code === 'EEXIST');
|
|
51
51
|
assert.strictEqual(fs.readFileSync(path.join(to, 'bytes.bin'), 'utf8'), 'kept');
|
|
52
52
|
});
|
|
53
|
+
// T-0347: a relative directory link must still name its sibling inside the copy. On Windows the copy is a junction,
|
|
54
|
+
// whose target must be absolute; Node resolved the relative one against the process cwd.
|
|
55
|
+
{
|
|
56
|
+
const from = path.join(arena, 'src-relative-link'), to = path.join(arena, 'dst-relative-link');
|
|
57
|
+
fs.mkdirSync(path.join(from, 'sibling'), { recursive: true });
|
|
58
|
+
fs.writeFileSync(path.join(from, 'sibling', 'mark.txt'), 'source');
|
|
59
|
+
let made = true;
|
|
60
|
+
try { fs.symlinkSync('sibling', path.join(from, 'link'), 'dir'); }
|
|
61
|
+
catch (error) { if (error.code !== 'EPERM') throw error; made = false; }
|
|
62
|
+
if (!made) console.log('SKIP a relative directory link (creating one needs the symlink privilege here)');
|
|
63
|
+
else test('a relative directory link names its sibling inside the copy', () => {
|
|
64
|
+
copyTreeSync(from, to);
|
|
65
|
+
fs.writeFileSync(path.join(to, 'sibling', 'mark.txt'), 'copy');
|
|
66
|
+
assert.strictEqual(fs.readFileSync(path.join(to, 'link', 'mark.txt'), 'utf8'), 'copy');
|
|
67
|
+
assert.strictEqual(fs.realpathSync.native(path.join(to, 'link')), fs.realpathSync.native(path.join(to, 'sibling')));
|
|
68
|
+
});
|
|
69
|
+
}
|
|
53
70
|
test('a missing source is an error, not an empty copy', () => {
|
|
54
71
|
assert.throws(() => copyTreeSync(path.join(arena, 'missing'), path.join(arena, 'dst-missing')), error => error.code === 'ENOENT');
|
|
55
72
|
assert.ok(!fs.existsSync(path.join(arena, 'dst-missing')));
|
|
@@ -34,7 +34,7 @@ const seed = () => {
|
|
|
34
34
|
return (seedDir = dir);
|
|
35
35
|
};
|
|
36
36
|
let copies = 0;
|
|
37
|
-
const fresh = () => { const dir = path.join(arena, 'p' + (++copies));
|
|
37
|
+
const fresh = () => { const dir = path.join(arena, 'p' + (++copies)); require('../lib/copy-tree').copyTreeSync(seed(), dir); return dir; };
|
|
38
38
|
const runIn = (cwd, args) => cp.spawnSync(process.execPath, [CLI, ...args], { cwd, encoding: 'utf8', timeout: 300000, windowsHide: true, env });
|
|
39
39
|
const show = r => `exit ${r.status}\n${String(r.stdout || '').slice(0, 1200)}${String(r.stderr || '').slice(0, 600)}`;
|
|
40
40
|
const json = (r, label) => { const out = String(r.stdout || ''); try { return JSON.parse(out.slice(out.indexOf('{'))); } catch { throw new Error(label + ': not JSON — ' + show(r)); } };
|
|
@@ -225,7 +225,7 @@ if (require.main === module) {
|
|
|
225
225
|
]) {
|
|
226
226
|
const copy = path.join(root, 'copy-' + (++n));
|
|
227
227
|
covered.push(what);
|
|
228
|
-
|
|
228
|
+
require('../lib/copy-tree').copyTreeSync(dir, copy);
|
|
229
229
|
t(`fixture: ${what} changed alone is the one reported missing (#${n})`, () => {
|
|
230
230
|
change(copy);
|
|
231
231
|
assert.deepStrictEqual(livedInMissing(copy).map(m => m.split(' (')[0]), [what]);
|
|
@@ -235,7 +235,7 @@ if (require.main === module) {
|
|
|
235
235
|
// Control for the whole-id match: the id in Markdown emphasis still names the task.
|
|
236
236
|
t('fixture: a handoff naming the task as __id__ still counts', () => {
|
|
237
237
|
const copy = path.join(root, 'copy-emphasis');
|
|
238
|
-
|
|
238
|
+
require('../lib/copy-tree').copyTreeSync(dir, copy);
|
|
239
239
|
edit(copy, 'session-handoff.md', built.ids.task1, '__' + built.ids.task1 + '__');
|
|
240
240
|
assert.deepStrictEqual(livedInMissing(copy), []);
|
|
241
241
|
});
|
|
@@ -31,7 +31,7 @@ const seed = lang => {
|
|
|
31
31
|
return (seeds[lang] = dir);
|
|
32
32
|
};
|
|
33
33
|
let copies = 0;
|
|
34
|
-
const fresh = lang => { const dir = path.join(arena, 'p' + (++copies));
|
|
34
|
+
const fresh = lang => { const dir = path.join(arena, 'p' + (++copies)); require('../lib/copy-tree').copyTreeSync(seed(lang), dir); return dir; };
|
|
35
35
|
const runIn = (cwd, args, extraEnv = {}) => cp.spawnSync(process.execPath, [CLI, ...args], { cwd, encoding: 'utf8', timeout: 300000, windowsHide: true, env: { ...env, ...extraEnv } });
|
|
36
36
|
const show = r => `exit ${r.status}\n${String(r.stdout || '').slice(0, 1500)}${String(r.stderr || '').slice(0, 600)}`;
|
|
37
37
|
const addDone = (dir, title, evidence) => {
|
|
@@ -140,7 +140,7 @@ function normalizedJson(label, result) {
|
|
|
140
140
|
function cloneProjectState(target) {
|
|
141
141
|
fs.mkdirSync(target, { recursive: true });
|
|
142
142
|
fs.copyFileSync(path.join(project, 'package.json'), path.join(target, 'package.json'));
|
|
143
|
-
|
|
143
|
+
require('../lib/copy-tree').copyTreeSync(path.join(project, '.leerness'), path.join(target, '.leerness'));
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
const surfaces = [
|
|
@@ -37,7 +37,7 @@ const code = r => { try { return JSON.parse(r.stdout).code; } catch { return '(n
|
|
|
37
37
|
// A project holding `rows` (canonical JSON and its generated Markdown), dropped by `target`: the drop is the product's.
|
|
38
38
|
function dropped(surface, rows, target) {
|
|
39
39
|
const project = path.join(temp, `case-${++sequence}`);
|
|
40
|
-
|
|
40
|
+
require('../lib/copy-tree').copyTreeSync(seed, project);
|
|
41
41
|
const hd = path.join(project, '.leerness');
|
|
42
42
|
const f = { project, surface, json: path.join(hd, `${surface}.json`), md: path.join(hd, `${surface}.md`), archive: path.join(hd, `${surface}.archive.md`) };
|
|
43
43
|
fs.writeFileSync(f.json, JSON.stringify(rows, null, 2) + '\n');
|
|
@@ -40,7 +40,7 @@ function run(project, args) {
|
|
|
40
40
|
function success(r) { assert.strictEqual(r.status, 0, String(r.stdout || r.stderr).slice(-1000)); }
|
|
41
41
|
function fixture(surface, entry) {
|
|
42
42
|
const project = path.join(temp, `case-${++sequence}`);
|
|
43
|
-
|
|
43
|
+
require('../lib/copy-tree').copyTreeSync(seed, project);
|
|
44
44
|
const hd = path.join(project, '.leerness');
|
|
45
45
|
const json = path.join(hd, `${surface}.json`);
|
|
46
46
|
const md = path.join(hd, `${surface}.md`);
|
|
@@ -176,7 +176,7 @@ try {
|
|
|
176
176
|
});
|
|
177
177
|
check('a lesson saved with surrounding spaces through the CLI drops and restores exactly', () => {
|
|
178
178
|
const project = path.join(temp, `case-${++sequence}`);
|
|
179
|
-
|
|
179
|
+
require('../lib/copy-tree').copyTreeSync(seed, project);
|
|
180
180
|
const json = path.join(project, '.leerness', 'lessons.json');
|
|
181
181
|
success(run(project, ['lesson', 'save', ' existing ']));
|
|
182
182
|
const saved = JSON.parse(fs.readFileSync(json, 'utf8'));
|