leerness 1.36.303 → 1.36.304

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.
Files changed (177) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/CONTRIBUTING.md +2 -0
  3. package/README.md +4 -4
  4. package/bin/leerness.js +1 -1
  5. package/package.json +1 -1
  6. package/scripts/agents-bench-output-probe.js +1 -0
  7. package/scripts/api-skill-match-probe.js +1 -0
  8. package/scripts/audit-env-text-probe.js +1 -0
  9. package/scripts/audit-readme-text-probe.js +1 -0
  10. package/scripts/audit-text-probe.js +1 -0
  11. package/scripts/authoring-language-probe.js +1 -0
  12. package/scripts/auto-update-hooks-probe.js +1 -0
  13. package/scripts/brief-text-probe.js +1 -0
  14. package/scripts/bugfix-areas-probe.js +1 -0
  15. package/scripts/ci-shard-probe.js +99 -0
  16. package/scripts/ci-shard.js +35 -3
  17. package/scripts/claim-runtime-cache-probe.js +1 -0
  18. package/scripts/claim-sweep-git-probe.js +1 -0
  19. package/scripts/claims-baseline-concurrency-probe.js +1 -0
  20. package/scripts/claims-baseline-probe.js +1 -0
  21. package/scripts/command-flags-probe.js +1 -0
  22. package/scripts/context-current-probe.js +1 -0
  23. package/scripts/context-reading-probe.js +1 -0
  24. package/scripts/copy-tree-probe.js +1 -0
  25. package/scripts/crud-check-probe.js +1 -0
  26. package/scripts/dead-flags-probe.js +1 -0
  27. package/scripts/done-status-probe.js +1 -0
  28. package/scripts/dry-run-refusal-probe.js +1 -0
  29. package/scripts/e2e-child-diagnostics-probe.js +1 -0
  30. package/scripts/e2e-command-surface.js +1 -0
  31. package/scripts/e2e-concurrency.js +1 -0
  32. package/scripts/e2e-core.js +1 -0
  33. package/scripts/e2e-lived-in-harness.js +1 -0
  34. package/scripts/e2e-repo-guard.js +1 -0
  35. package/scripts/e2e-temp-scope-probe.js +1 -0
  36. package/scripts/e2e.js +5 -0
  37. package/scripts/encoding-diagnostics-probe.js +1 -0
  38. package/scripts/encoding-selftest-probe.js +1 -0
  39. package/scripts/env-snapshot-text-probe.js +1 -0
  40. package/scripts/env-sync-text-probe.js +1 -0
  41. package/scripts/execution-ledger-sync-probe.js +1 -0
  42. package/scripts/false-claim-probe.js +1 -0
  43. package/scripts/feature-graph-text-probe.js +1 -0
  44. package/scripts/file-lease-probe.js +1 -0
  45. package/scripts/fix-evidence-language-probe.js +1 -0
  46. package/scripts/free-text-args-probe.js +1 -0
  47. package/scripts/generation-text-probe.js +1 -0
  48. package/scripts/git-resolution-cache-probe.js +1 -0
  49. package/scripts/github-login-probe.js +1 -0
  50. package/scripts/handoff-history-text-probe.js +1 -0
  51. package/scripts/handoff-i18n-probe.js +1 -0
  52. package/scripts/handoff-preservation-probe.js +1 -0
  53. package/scripts/handoff-readonly-probe.js +1 -0
  54. package/scripts/handoff-text-probe.js +1 -0
  55. package/scripts/harness-text-probe.js +1 -0
  56. package/scripts/history-retention-probe.js +1 -0
  57. package/scripts/history-retention-write-probe.js +1 -0
  58. package/scripts/i18n-next-cluster-probe.js +1 -0
  59. package/scripts/i18n-priority-surface-probe.js +1 -0
  60. package/scripts/install-text-probe.js +1 -0
  61. package/scripts/installed-cleanroom-probe.js +1 -0
  62. package/scripts/installed-handoff-lifecycle-probe.js +1 -0
  63. package/scripts/lazy-auto-track-probe.js +1 -0
  64. package/scripts/legacy-runtime-negative-probe.js +1 -0
  65. package/scripts/lock-heartbeat-probe.js +1 -0
  66. package/scripts/lock-probe.js +1 -0
  67. package/scripts/managed-text-probe.js +1 -0
  68. package/scripts/mcp-presence-probe.js +1 -0
  69. package/scripts/memory-archive-check-probe.js +1 -0
  70. package/scripts/memory-archive-retention-probe.js +1 -0
  71. package/scripts/memory-arena-cleanup-probe.js +1 -0
  72. package/scripts/memory-checked-input-probe.js +1 -0
  73. package/scripts/memory-guard-order-probe.js +1 -0
  74. package/scripts/memory-input-preflight-probe.js +1 -0
  75. package/scripts/memory-input-unit-probe.js +1 -0
  76. package/scripts/memory-language-probe.js +1 -0
  77. package/scripts/memory-legacy-retention-probe.js +1 -0
  78. package/scripts/memory-migration-concurrency-probe.js +1 -0
  79. package/scripts/memory-migration-test-support.js +11 -28
  80. package/scripts/memory-migration-transport-probe.js +1 -0
  81. package/scripts/memory-output-parent-race-probe.js +1 -0
  82. package/scripts/memory-output-posix-probe.js +1 -0
  83. package/scripts/memory-output-publication-probe.js +1 -0
  84. package/scripts/memory-output-unit-probe.js +1 -0
  85. package/scripts/memory-projection-retention-probe.js +1 -0
  86. package/scripts/memory-recovery-external-probe.js +1 -0
  87. package/scripts/memory-recovery-receipt-probe.js +1 -0
  88. package/scripts/memory-restore-canonical-probe.js +1 -0
  89. package/scripts/memory-utf8-admission-probe.js +1 -0
  90. package/scripts/memory-write-budget-meter.js +1 -0
  91. package/scripts/migration-compat.js +1 -0
  92. package/scripts/mode-text-probe.js +1 -0
  93. package/scripts/mutation-integrity-probe.js +1 -0
  94. package/scripts/next-action-suggestion-probe.js +1 -0
  95. package/scripts/openai-response-retrieval-probe.js +1 -0
  96. package/scripts/openai-ui-review-dispatch-probe.js +1 -0
  97. package/scripts/openai-ui-review-observation-probe.js +1 -0
  98. package/scripts/openai-ui-review-receipt-probe.js +1 -0
  99. package/scripts/parent-detect-selftest-probe.js +1 -0
  100. package/scripts/plain-language-reporting-probe.js +1 -0
  101. package/scripts/plan-text-probe.js +1 -0
  102. package/scripts/platform-smoke.js +1 -0
  103. package/scripts/provider-capacity-probe.js +1 -0
  104. package/scripts/race-overlap-probe.js +1 -0
  105. package/scripts/release-bump-probe.js +1 -0
  106. package/scripts/release-runtime-probe.js +1 -0
  107. package/scripts/reminder-text-probe.js +1 -0
  108. package/scripts/review-checkpoint-transport-probe.js +1 -0
  109. package/scripts/review-control-probe.js +1 -0
  110. package/scripts/review-feedback-probe.js +1 -0
  111. package/scripts/review-request-labels-probe.js +1 -0
  112. package/scripts/review-request-signals-probe.js +1 -0
  113. package/scripts/review-snapshot-transport-probe.js +1 -0
  114. package/scripts/role-agent-schema-probe.js +1 -0
  115. package/scripts/role-fallback-probe.js +1 -0
  116. package/scripts/role-file-identity-probe.js +1 -0
  117. package/scripts/role-store-loader-probe.js +1 -0
  118. package/scripts/rule-applicability-probe.js +1 -0
  119. package/scripts/rule-text-probe.js +1 -0
  120. package/scripts/rule-verification-probe.js +1 -0
  121. package/scripts/runtime-admission-probe.js +1 -0
  122. package/scripts/runtime-cost-probe.js +1 -0
  123. package/scripts/runtime-descriptor-identity-probe.js +1 -0
  124. package/scripts/runtime-layout-probe.js +1 -0
  125. package/scripts/runtime-repl-probe.js +1 -0
  126. package/scripts/runtime-replacement-probe.js +1 -0
  127. package/scripts/runtime-write-probe.js +1 -0
  128. package/scripts/secret-scan-scope-probe.js +1 -0
  129. package/scripts/selftest-cleanup-probe.js +1 -0
  130. package/scripts/session-close-text-probe.js +1 -0
  131. package/scripts/shared-state-lock-probe.js +1 -0
  132. package/scripts/skill-archive-probe.js +1 -0
  133. package/scripts/smoke.js +1 -0
  134. package/scripts/state-inspect-cli-probe.js +1 -0
  135. package/scripts/state-scopes-probe.js +1 -0
  136. package/scripts/store-diagnostic-identity-probe.js +1 -0
  137. package/scripts/store-diagnostic-probe.js +1 -0
  138. package/scripts/store-values-probe.js +1 -0
  139. package/scripts/task-sync-roundtrip-probe.js +1 -0
  140. package/scripts/teams-text-probe.js +1 -0
  141. package/scripts/temp-cleanup-probe.js +433 -0
  142. package/scripts/temp-cleanup.js +182 -1
  143. package/scripts/test-execution-plan-probe.js +1 -0
  144. package/scripts/test-firestore-probe.js +1 -0
  145. package/scripts/test-http-probe.js +1 -0
  146. package/scripts/test-node-runner-probe.js +1 -0
  147. package/scripts/test-npm-script-probe.js +1 -0
  148. package/scripts/test-npm-script-runner-probe.js +1 -0
  149. package/scripts/test-prerequisite-probe.js +1 -0
  150. package/scripts/test-suite-list-runner-probe.js +1 -0
  151. package/scripts/title-args-probe.js +1 -0
  152. package/scripts/tracker-write-language-probe.js +1 -0
  153. package/scripts/ui-boundary-browser-probe.js +1 -0
  154. package/scripts/ui-boundary-contracts-probe.js +1 -0
  155. package/scripts/ui-boundary-dashboard-probe.js +1 -0
  156. package/scripts/ui-copy-boundary-probe.js +1 -0
  157. package/scripts/ui-review-attempt-probe.js +1 -0
  158. package/scripts/ui-review-authority-floor-probe.js +1 -0
  159. package/scripts/ui-review-authority-host-probe.js +1 -0
  160. package/scripts/ui-review-authorization-probe.js +1 -0
  161. package/scripts/ui-review-ledger-probe.js +1 -0
  162. package/scripts/ui-review-observation-probe.js +1 -0
  163. package/scripts/ui-review-proof-use-probe.js +1 -0
  164. package/scripts/ui-review-publication-probe.js +1 -0
  165. package/scripts/ui-review-recovery-probe.js +1 -0
  166. package/scripts/ui-review-result-probe.js +1 -0
  167. package/scripts/ui-review-run-probe.js +1 -0
  168. package/scripts/ui-review-target-probe.js +1 -0
  169. package/scripts/ui-review-transport-probe.js +1 -0
  170. package/scripts/verify-code-cross-runtime-probe.js +1 -0
  171. package/scripts/which-shim-probe.js +1 -0
  172. package/scripts/workspace-dir-lock-order-probe.js +1 -0
  173. package/scripts/workspace-dir-migration-probe.js +1 -0
  174. package/scripts/workspace-migration-heartbeat-probe.js +1 -0
  175. package/scripts/workspace-selection-probe.js +1 -0
  176. package/scripts/write-path-language-probe.js +1 -0
  177. package/scripts/write-primitive-language-probe.js +1 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.36.304 - 2026-10-16
4
+
5
+ - Fixed (tests): probes and e2e cases left their temp folders behind (T-0357). On 2026-10-14 this PC's temp directory held 8,434 `leerness-*` folders under 332 name prefixes; 5,142 of them came from e2e.js case sandboxes and 1,243 from the memory probes' arena (T-0354 fixed that one). The test runner now removes them: the test process records the folders it makes, and `scripts/ci-shard.js` removes them after the test ended with exit status 0.
6
+ - `scripts/temp-cleanup.js` `trackTemp(module)` records only when the environment carries `LEERNESS_TEMP_RECORD`, a directory the runner makes for each leaf; without it nothing is wrapped and the script behaves as before. It wraps `fs.mkdtempSync` and appends one line per folder to its own file in that directory in the same synchronous step that made the folder. The callback and promise forms are not wrapped: they complete later, when another folder may have been put in place (review round 1); their folders are left, and no script in `scripts/` uses them. It removes nothing and installs no exit, emit or exception hook. Child processes that inherit the variable record into the same directory.
7
+ - What is recorded: only a call whose prefix is an absolute path without `.` or `..` segments; a `Buffer` prefix or result only if it is UTF-8 that decodes and encodes back to the same bytes; a result only if it is the prefix followed by the six characters mkdtemp adds; a folder only if it is strictly below the temp directory of the moment of the call and empty when the call returns (a wrapper installed before the tracker could otherwise put an existing folder in its place; review round 2). Others are left, as before.
8
+ - `runNpm` gives each leaf its own record directory and, after the leaf ended with status 0, no signal and no spawn error (the verdict it uses for pass/fail), removes the recorded folders newest first with `removeRecorded`: each must still be the folder recorded (same file identity and real path, not a link) and strictly below both the temp directory recorded with it and the runner's own; links inside are removed as links and never followed, and each folder must still resolve inside the tree before it is listed and before a link in it is removed (the checks T-0354 wrote for the memory arena, shared as `ownTree` and `removeOwnedTree`). A failed leaf keeps its folders. The runner checks its record root and each leaf's directory right after making them (an empty directory, not a link; a wrapper of `fs.mkdtempSync` or `fs.mkdirSync` installed before the runner could put an existing folder there, review round 3); a leaf whose directory fails gets no variable and records nothing. It removes only the leaf directories it checked, the root only when it is empty, and prints one line with the counts.
9
+ - Each of the 171 scripts in `scripts/` that name `mkdtemp` has the tracker as its first statement, before it loads anything from `lib/` or `bin/`; the tracker records only when given a module whose chain reaches the main module, in the main thread (a script loaded with `--require` into another process, anything it loads, and a call without a module record nothing).
10
+ - Runs that do not go through the runner (`node scripts/x.js`, a plain `npm test`, and CI's e2e and core jobs, which run on throw-away machines) record nothing and leave their folders as before; CONTRIBUTING.md names `node ./scripts/ci-shard.js npm test 1/1` for a local run that cleans up.
11
+ - Not covered: folders made by processes that do not inherit the variable or do not run the tracker (such as the CLI a probe starts); a test that writes into the record directory, replaces the mkdtemp functions again, or patches the module loader; a folder a process renames or replaces after recording it (left).
12
+ - Measured with each tree's own runner (the 30 probes of the 2026-10-14 prefix map, one leaf each, a dedicated temp directory per tree, side by side): 29 `leerness-*` folders left with 1.36.303 and 1 now (a memory recovery bundle that `memory-recovery-external-probe.js` makes while it points the temp directory elsewhere); every leaf passed. The full test run (`ci-shard.js npm test i/3`, three shards in parallel, each with its own new temp directory; Windows, Node v26.3.0; on the round-4 code, before the later changes, which touch only `temp-cleanup-probe.js` and `ci-shard-probe.js`): 68/68, 69/69 and 69/69 leaves passed, e2e 469/469 with 432 folders recorded; the runner removed 37, 116 and 150 folders. Five `leerness-*` folders were left: one that an e2e case replaces after it was recorded (`orph116`); two memory recovery bundles from processes that do not record (the one above, and one made by the CLI that `memory-migration-boundary-probe.js` starts); and two that the record-root test of `ci-shard-probe.js` left, the folder it swaps in and the root it moves aside (that test now removes them; run alone and as a leaf it leaves nothing). The first full run of this version left 10 memory recovery bundles: `lib/runtime-writes.js` intercepts fs writes, and the record went through `fs.appendFileSync`, which calls the intercepted `fs.openSync`. Records are now written, and folders read, with the fs functions of the moment the tracker starts.
13
+ - Why the runner: 1.36.302 and 1.36.303 decided inside the test process at exit. In eight review rounds a later piece of JavaScript (another exit listener, a microtask, an exception handler, an interrupted `process.exit()`) kept changing the verdict after the decision; the owner moved the change out of both releases. The exit status the runner reads cannot change afterwards. The first review round of this version found two problems (an asynchronous mkdtemp whose folder was swapped before its completion was recorded; a call without a module skipped the check against preloads) and the second one more (a wrapper preloaded before the tracker swapped an existing folder in); the third found that the runner's own record root was removed without that check, and that the lossy-`Buffer` test did not distinguish on POSIX; the owner chose one more round, and the fourth found that the encoded-result test did not distinguish its check (a test was added, which fails on that mutant; the owner chose a short confirmation of it, which found that the new test failed when the temp path holds a non-ASCII letter: it is skipped there; a second confirmation found that the skip also gives up a setup where a link of the mangled name leads back into the temp path, and the owner chose to document that and ship). Each code fix has a test that fails on the reviewed code.
14
+ - Tests: `scripts/temp-cleanup-probe.js` has twenty-seven cases (four before) and `scripts/ci-shard-probe.js` three more (seventeen):
15
+ - without `LEERNESS_TEMP_RECORD` the tracker changes nothing (the mkdtemp functions stay the same objects, nothing is recorded or removed); with it, `fs.mkdtempSync` records its folder with path, identity, real path and temp directory in the process's own file, and the process removes nothing; `fs.mkdtemp`, `fs.promises.mkdtemp`, `require('fs/promises').mkdtemp` and `util.promisify(fs.mkdtemp)` stay unwrapped and record nothing, also when the folder is swapped for another before the completion (review round 1); a record is written when the call returns, so a run that then fails, throws or calls `process.exit(3)` keeps it; a record is written and the folder read with the fs functions of the start, also after the test replaced `fs.openSync`, `fs.writeSync`, `fs.appendFileSync`, `fs.lstatSync` and `fs.realpathSync.native`;
16
+ - a folder that is not empty when `fs.mkdtempSync` returns is not recorded: a wrapper preloaded before the tracker that swaps in a folder holding a file gets nothing recorded, and that folder survives the runner's removal (an empty folder swapped in is recorded, as named in the stop rule);
17
+ - not recorded: a `Buffer` prefix that is not UTF-8, also when a wrapper preloaded before the tracker planted the sibling its lossy decoding names (POSIX; Windows refuses such a prefix, so there it is a control); a result in another encoding that is not the prefix plus six characters (`hex`, and `latin1` with a non-ASCII letter, also when a wrapper preloaded before the tracker made the folder the mangled result names; review round 4; skipped when the temp path itself holds a non-ASCII letter, which `latin1` mangles too, also where a link of the mangled name would lead back into it); a relative prefix, a dotted prefix, and a folder outside the temp directory; recorded: a `Buffer` result that is UTF-8 and a `file:` URL prefix; each case has a control that must be recorded in the same process;
18
+ - with recording on, `process.reallyExit`, `process.exit`, `process.emit` and the `exit`, `beforeExit`, `uncaughtExceptionMonitor` and `newListener` listener counts are unchanged;
19
+ - nothing is recorded by `trackTemp()` without a module (in a main script, a `--require` preload, or `node -e`; review round 1), by a script loaded with `--require` (directly, through `NODE_OPTIONS`, a module such a script loads, 60 requires deep, or through `createRequire()`), or in a worker thread started from a script file; the same script, and the same 60-deep chain, run as the main script record, and so does a main script after `temp-cleanup.js` itself was preloaded; one tracker per process;
20
+ - `removeRecorded` removes the recorded folders newest first, nested ones too, and leaves a link's target alone; it leaves a folder replaced by a link (and the link) or by another folder, skips one already gone, leaves a folder outside the runner's temp directory or outside the temp directory of its own record, skips lines it cannot use, and removes nothing for an empty or missing directory;
21
+ - `scripts/ci-shard-probe.js`: `runNpm` removes the folders a passing leaf and its tracked child recorded and keeps those of a failing leaf and of a killed one; each leaf gets its own record directory, the runner leaves no record directory behind, prints the count line, and does not change the caller's environment; a wrapper that swaps an existing folder in for the record root or a leaf's record directory gets nothing removed and the leaf no variable (the test then removes those folders itself; the full run found them left); a folder a leaf moves into the record root is left with the root;
22
+ - every script in `scripts/` that names `mkdtemp` in any form has the tracker as its first statement; the check refuses a tracker in a block comment, a line comment (ended by any JavaScript line terminator: CR, LF, U+2028, U+2029), a string, or after other code.
23
+ - 35 deliberate breaks of `temp-cleanup.js` and `ci-shard.js` (each check removed or weakened in turn, including reverting each review fix) were run against this probe, `ci-shard-probe.js` and `memory-arena-cleanup-probe.js`, with the unchanged files as the control (27/27, 17/17 and 5/5): 27 made a case fail on Windows (one of them, the encoded-result check, only through the round-4 test). Of the eight that pass on Windows, two concern POSIX only (a lossily decoded `Buffer` prefix, which Windows refuses; a leaf ended by a signal, which on Windows ends with exit code 1; CI's Linux jobs run the same tests); one is the identity check when the runner removes a leaf's record directory, which matters only if the leaf replaces that directory (a test writing into the record directory, named in the stop rule); the other five do not change what is removed, because another check stops the same case (a link in place of a folder has another identity; a real path that moved fails the inside check of `removeOwnedTree`; the identity check repeated inside `removeOwnedTree` and the inside check before listing guard only against changes during the removal; an inherited variable of the same name is replaced for the leaf).
24
+
3
25
  ## 1.36.303 - 2026-10-15
4
26
 
5
27
  - Fixed (tests): `scripts/ci-shard.js` passed its own `LEERNESS_CI_SHARD_TIMES` to the leaves it runs, so `scripts/ci-shard-probe.js`, which runs the runner itself, overwrote the outer run's times file in the middle of a local full run (found in a full run for this release; the outer runner rewrote the file at its end, so the stored times were right). Leaves no longer see the variable, the way `e2eShard` already hides its own. CI does not set it. A new probe case fails on the 1.36.302 runner (13/14) and passes now (14/14) (T-0364).
package/CONTRIBUTING.md CHANGED
@@ -37,6 +37,8 @@ When you add a guard, construct the **discriminating case**: an input where the
37
37
 
38
38
  Each top-level case in `scripts/e2e.js` starts with `if (e2eShard.take('<unique key>')) {` and counts itself with `total++;` inside the block. CI runs that file, `test:commands` and `test:fast` in parallel shards (`scripts/ci-shard.js`): every shard computes the same split from `scripts/ci-shard-weights.json`, so the shards of one job run each case and leaf exactly once. A top-level block without the guard, a repeated key, or a guard that is not reached exactly once fails the e2e run. `node ./scripts/ci-shard.js plan e2e 4` shows the split; a run with `LEERNESS_E2E_TIMES=<file>` (e2e) or `LEERNESS_CI_SHARD_TIMES=<file>` (npm leaves) writes measured seconds, and `node ./scripts/ci-shard.js weights e2e <file>` stores them.
39
39
 
40
+ A script in `scripts/` that makes temp folders with `fs.mkdtempSync`, `fs.mkdtemp` or `fs.promises.mkdtemp` starts with `require('./temp-cleanup').trackTemp(module);` as its first statement, before it loads anything from `lib/` or `bin/`. Under the test runner (`node ./scripts/ci-shard.js npm <script> <i>/<k>`, as CI runs `test:commands` and `test:fast`, or `node ./scripts/ci-shard.js npm test 1/1` locally), the script records the folders it makes below the temp directory, and the runner removes them after the script ended with exit status 0 and keeps them after a failure; links inside are removed as links, never followed. Child processes that inherit the runner's variable record too. Run directly (`node scripts/x.js`, a plain `npm test`), nothing is recorded or removed. Only code that the main script loaded records; a script loaded with `--require` into another process does not. `scripts/temp-cleanup-probe.js` fails when a script that names `mkdtemp` lacks the line.
41
+
40
42
  ## Release
41
43
 
42
44
  `npm test` (gate) → bump `VERSION` in `bin/leerness.js` **and** `version` in `package.json` (selftest asserts they match) → CHANGELOG entry → commit → publish → push.
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.303 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
198
+ 이 프로젝트는 Leerness v1.36.304 하네스를 사용합니다. 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.303는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **102개 도구**를 노출:
252
+ Leerness v1.36.304는 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.303는 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.303**. 공개 릴리스 여부는 공개 저장소의 태그와 배포 기록에서 별도로 확인합니다.
273
+ 현재 사용 중인 하네스 버전: **1.36.304**. 공개 릴리스 여부는 공개 저장소의 태그와 배포 기록에서 별도로 확인합니다.
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.303: 2026-09-30
311
+ Last synced by Leerness v1.36.304: 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.303';
118
+ const VERSION = '1.36.304';
119
119
 
120
120
  // MCP lifecycle 주소 표식은 현재 CLI 호출 한 번에만 유효하다. CLI bootstrap에서 즉시 env에서
121
121
  // 떼어 두어 `--no-record`/hook처럼 presence 기록 함수에 도달하지 않는 경로도 후속 child에 유출하지 않는다.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.36.303",
3
+ "version": "1.36.304",
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",
@@ -1,4 +1,5 @@
1
1
  'use strict';
2
+ require('./temp-cleanup').trackTemp(module); // T-0357: records this run's temp folders; the runner that started it removes them after a clean exit
2
3
  const assert = require('assert');
3
4
  const fs = require('fs');
4
5
  const path = require('path');
@@ -1,4 +1,5 @@
1
1
  'use strict';
2
+ require('./temp-cleanup').trackTemp(module); // T-0357: records this run's temp folders; the runner that started it removes them after a clean exit
2
3
  // T-0319: saved API skills (.leerness/api-skills/*.md) are matched against the in-progress task by handoff,
3
4
  // handoff --json, health --json and session close --json. All four read row.title and row.notes, which progress
4
5
  // rows do not have, so nothing ever matched. They now share one task-text helper (request and next action), and
@@ -1,4 +1,5 @@
1
1
  'use strict';
2
+ require('./temp-cleanup').trackTemp(module); // T-0357: records this run's temp folders; the runner that started it removes them after a clean exit
2
3
  const assert = require('assert'), fs = require('fs'), path = require('path'), os = require('os'), cp = require('child_process');
3
4
  const { createHash } = require('crypto');
4
5
  const { removeTree } = require('./temp-cleanup');
@@ -1,4 +1,5 @@
1
1
  'use strict';
2
+ require('./temp-cleanup').trackTemp(module); // T-0357: records this run's temp folders; the runner that started it removes them after a clean exit
2
3
  const assert = require('assert'), fs = require('fs'), path = require('path'), os = require('os'), cp = require('child_process');
3
4
  const { createHash } = require('crypto');
4
5
  const { removeTree } = require('./temp-cleanup');
@@ -1,4 +1,5 @@
1
1
  'use strict';
2
+ require('./temp-cleanup').trackTemp(module); // T-0357: records this run's temp folders; the runner that started it removes them after a clean exit
2
3
  const assert = require('assert'), fs = require('fs'), path = require('path'), os = require('os'), cp = require('child_process');
3
4
  const { createHash } = require('crypto');
4
5
  const { removeTree } = require('./temp-cleanup');
@@ -1,4 +1,5 @@
1
1
  'use strict';
2
+ require('./temp-cleanup').trackTemp(module); // T-0357: records this run's temp folders; the runner that started it removes them after a clean exit
2
3
  const assert = require('assert'), fs = require('fs'), path = require('path'), os = require('os'), cp = require('child_process');
3
4
  const { createHash } = require('crypto');
4
5
  const { removeTree } = require('./temp-cleanup');
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
+ require('./temp-cleanup').trackTemp(module); // T-0357: records this run's temp folders; the runner that started it removes them after a clean exit
3
4
 
4
5
  // Real CLI regressions for the configuration consumed by Codex/Claude hooks.
5
6
  const fs = require('fs'), os = require('os'), path = require('path'), cp = require('child_process');
@@ -1,4 +1,5 @@
1
1
  'use strict';
2
+ require('./temp-cleanup').trackTemp(module); // T-0357: records this run's temp folders; the runner that started it removes them after a clean exit
2
3
  const assert = require('assert'), fs = require('fs'), path = require('path'), os = require('os'), cp = require('child_process');
3
4
  const { createHash } = require('crypto');
4
5
  const { removeTree } = require('./temp-cleanup');
@@ -1,4 +1,5 @@
1
1
  'use strict';
2
+ require('./temp-cleanup').trackTemp(module); // T-0357: records this run's temp folders; the runner that started it removes them after a clean exit
2
3
  // T-0330: past fixes at completion. `bugfix receipt --areas` declares the files or folders a fix is about;
3
4
  // verify-claim then lists the bugfix receipts whose declared areas hold a file the task's evidence names: finished
4
5
  // ones (their task is done, also in retained history) as past fixes with the root cause and the probe to re-run,
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
+ require('./temp-cleanup').trackTemp(module); // T-0357: records this run's temp folders; the runner that started it removes them after a clean exit
3
4
  // T-0359: CI runs e2e.js cases and npm-script leaves in parallel shards (scripts/ci-shard.js). A shard that drops a
4
5
  // unit fails nothing — the unit silently stops running — so this probe checks the parts that decide what runs:
5
6
  // the assignment is a deterministic partition, every leaf of the real sharded scripts parses and lands in exactly
@@ -151,6 +152,104 @@ try {
151
152
  assert.strictEqual(env.LEERNESS_CI_SHARD_TIMES, times, 'the caller\'s environment object was changed');
152
153
  });
153
154
 
155
+ check('runNpm removes the temp folders a passing leaf and its tracked child recorded, and keeps a failing or killed leaf\'s (T-0357)', () => {
156
+ // The leaf only records (scripts/temp-cleanup.js trackTemp); the runner, which sees the exit status, removes.
157
+ const dir = path.join(arena, 'temp-record');
158
+ const leafTemp = path.join(dir, 'temp');
159
+ fs.mkdirSync(leafTemp, { recursive: true });
160
+ const TC = JSON.stringify(path.join(__dirname, 'temp-cleanup.js'));
161
+ const make = (name, end) => fs.writeFileSync(path.join(dir, `${name}.js`), [
162
+ `require(${TC}).trackTemp(module);`,
163
+ "const fs = require('fs'), os = require('os'), path = require('path'), cp = require('child_process');",
164
+ `const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-leaf-${name}-'));`,
165
+ `const c = cp.spawnSync(process.execPath, [${JSON.stringify(path.join(dir, 'child.js'))}], { encoding: 'utf8' }).stdout.trim();`,
166
+ `fs.writeFileSync(${JSON.stringify(path.join(dir, `${name}.out`))}, JSON.stringify({ d, c, rec: process.env.LEERNESS_TEMP_RECORD || null }));`,
167
+ end, ''].join('\n'));
168
+ fs.writeFileSync(path.join(dir, 'child.js'), [`require(${TC}).trackTemp(module);`,
169
+ "const fs = require('fs'), os = require('os'), path = require('path');",
170
+ "process.stdout.write(fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-leaf-child-')));", ''].join('\n'));
171
+ make('pass', 'process.exitCode = 0;');
172
+ make('fail', 'process.exitCode = 1;');
173
+ make('killed', "process.kill(process.pid, 'SIGKILL');");
174
+ const recordRoots = () => fs.readdirSync(os.tmpdir()).filter(n => n.startsWith('leerness-temp-record-')).sort();
175
+ const rootsBefore = recordRoots();
176
+ const lines = [];
177
+ const env = { ...process.env, GITHUB_ACTIONS: '', TEMP: leafTemp, TMP: leafTemp, TMPDIR: leafTemp, LEERNESS_TEMP_RECORD: path.join(dir, 'outer-record') };
178
+ const r = cs.runNpm('suite', { index: 1, count: 1 }, { root: dir, scripts: { suite: 'node ./pass.js && node ./fail.js && node ./killed.js' }, weights: { e2e: {}, npm: {} }, stdio: 'ignore', env, log: l => lines.push(l) });
179
+ assert.deepStrictEqual(r.failed, ['node ./fail.js', 'node ./killed.js']);
180
+ const res = n => JSON.parse(fs.readFileSync(path.join(dir, `${n}.out`), 'utf8'));
181
+ const pass = res('pass'), fail = res('fail');
182
+ assert.ok(pass.d && pass.c && fail.d && fail.c, JSON.stringify({ pass, fail }));
183
+ assert.strictEqual(fs.existsSync(pass.d), false, 'the passing leaf\'s folder was kept');
184
+ assert.strictEqual(fs.existsSync(pass.c), false, 'the passing leaf\'s tracked child\'s folder was kept');
185
+ assert.strictEqual(fs.existsSync(fail.d), true, 'the failing leaf\'s folder was removed');
186
+ assert.strictEqual(fs.existsSync(fail.c), true, 'the failing leaf\'s child\'s folder was removed');
187
+ const killedMade = fs.readdirSync(leafTemp).filter(n => n.startsWith('leerness-leaf-killed-'));
188
+ assert.strictEqual(killedMade.length, 1, 'the killed leaf\'s folder was removed');
189
+ assert.ok(pass.rec && fail.rec && pass.rec !== fail.rec && pass.rec !== env.LEERNESS_TEMP_RECORD, 'each leaf gets its own record directory from the runner');
190
+ assert.strictEqual(fs.existsSync(pass.rec) || fs.existsSync(fail.rec), false, 'a record directory was left');
191
+ assert.deepStrictEqual(recordRoots(), rootsBefore, 'the runner left its record root');
192
+ assert.ok(lines.some(l => /^ci-shard: temp folders: 2 removed after passing leaves/.test(l)), lines.join('\n'));
193
+ assert.strictEqual(env.LEERNESS_TEMP_RECORD, path.join(dir, 'outer-record'), 'the caller\'s environment object was changed');
194
+ });
195
+
196
+ // 1.36.304 review round 3: the record root was removed recursively without checking that it was still the folder the
197
+ // runner made. A wrapper of fs.mkdtempSync (or fs.mkdirSync, for a leaf's directory) installed before the runner
198
+ // can put an existing folder there; the runner now checks each directory right after making it (its own identity,
199
+ // empty), records nothing into one that fails, removes only the leaf directories it checked, and the root only when
200
+ // it is empty.
201
+ check('runNpm removes no folder that a wrapper swapped in for its record root or a leaf\'s record directory (T-0357)', () => {
202
+ const dir = path.join(arena, 'temp-record-swap');
203
+ const leafTemp = path.join(dir, 'temp');
204
+ fs.mkdirSync(leafTemp, { recursive: true });
205
+ fs.writeFileSync(path.join(dir, 'pass.js'), [`require(${JSON.stringify(path.join(__dirname, 'temp-cleanup.js'))}).trackTemp(module);`,
206
+ "const fs = require('fs'), os = require('os'), path = require('path');",
207
+ "fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-leaf-swap-'));", ''].join('\n'));
208
+ for (const target of ['root', 'leaf']) {
209
+ const unrelated = path.join(dir, `unrelated-${target}`);
210
+ fs.mkdirSync(unrelated); fs.writeFileSync(path.join(unrelated, 'keep.txt'), 'keep');
211
+ const saved = { mkdtempSync: fs.mkdtempSync, mkdirSync: fs.mkdirSync };
212
+ let swappedAt = null;
213
+ const swapIn = made => { fs.renameSync(made, made + '-aside'); fs.renameSync(unrelated, made); swappedAt = made; };
214
+ if (target === 'root') fs.mkdtempSync = function (prefix, ...rest) { const d = saved.mkdtempSync.call(fs, prefix, ...rest); if (String(prefix).includes('leerness-temp-record-')) swapIn(d); return d; };
215
+ else fs.mkdirSync = function (p, ...rest) { const r = saved.mkdirSync.call(fs, p, ...rest); if (!swappedAt && /leerness-temp-record-/.test(path.dirname(String(p)))) swapIn(String(p)); return r; };
216
+ let r;
217
+ const lines = [];
218
+ try {
219
+ r = cs.runNpm('suite', { index: 1, count: 1 }, { root: dir, scripts: { suite: 'node ./pass.js' }, weights: { e2e: {}, npm: {} }, stdio: 'ignore',
220
+ env: { ...process.env, GITHUB_ACTIONS: '', TEMP: leafTemp, TMP: leafTemp, TMPDIR: leafTemp }, log: l => lines.push(l) });
221
+ } finally { fs.mkdtempSync = saved.mkdtempSync; fs.mkdirSync = saved.mkdirSync; }
222
+ assert.deepStrictEqual(r.failed, [], target);
223
+ assert.ok(swappedAt, `${target}: the wrapper did not run`);
224
+ assert.strictEqual(fs.readFileSync(path.join(swappedAt, 'keep.txt'), 'utf8'), 'keep', `${target}: the folder swapped in was removed`);
225
+ assert.ok(lines.some(l => /^ci-shard: temp folders: .*1 leaves not recorded/.test(l)), `${target}: the leaf was given a directory that failed the check\n${lines.join('\n')}`);
226
+ // What the runner rightly leaves is this test's own, in the temp directory: for the root, the folder swapped in and
227
+ // the root moved aside; for a leaf's directory, the root that holds them (1.36.304 full run: they were left).
228
+ const own = target === 'root' ? [swappedAt, swappedAt + '-aside'] : [path.dirname(swappedAt)];
229
+ for (const p of own) {
230
+ assert.ok(path.resolve(path.dirname(p)) === path.resolve(os.tmpdir()) && path.basename(p).startsWith('leerness-temp-record-'), `${target}: not the runner's folder: ${p}`);
231
+ fs.rmSync(p, { recursive: true, force: true });
232
+ }
233
+ }
234
+ });
235
+
236
+ check('runNpm removes its record root only when it is empty: a folder a leaf moved into it is left (T-0357)', () => {
237
+ const dir = path.join(arena, 'temp-record-into-root');
238
+ const leafTemp = path.join(dir, 'temp');
239
+ fs.mkdirSync(leafTemp, { recursive: true });
240
+ const moved = path.join(dir, 'moved');
241
+ fs.mkdirSync(moved); fs.writeFileSync(path.join(moved, 'keep.txt'), 'keep');
242
+ fs.writeFileSync(path.join(dir, 'mover.js'), ["const fs = require('fs'), path = require('path');",
243
+ `const into = path.join(path.dirname(process.env.LEERNESS_TEMP_RECORD), 'moved'); fs.renameSync(${JSON.stringify(moved)}, into);`,
244
+ `fs.writeFileSync(${JSON.stringify(path.join(dir, 'into.txt'))}, into);`, ''].join('\n'));
245
+ const r = cs.runNpm('suite', { index: 1, count: 1 }, { root: dir, scripts: { suite: 'node ./mover.js' }, weights: { e2e: {}, npm: {} }, stdio: 'ignore',
246
+ env: { ...process.env, GITHUB_ACTIONS: '', TEMP: leafTemp, TMP: leafTemp, TMPDIR: leafTemp }, log: () => {} });
247
+ assert.deepStrictEqual(r.failed, []);
248
+ const into = fs.readFileSync(path.join(dir, 'into.txt'), 'utf8');
249
+ assert.strictEqual(fs.readFileSync(path.join(into, 'keep.txt'), 'utf8'), 'keep', 'the folder moved into the record root was removed with it');
250
+ fs.rmSync(path.dirname(into), { recursive: true, force: true });
251
+ });
252
+
154
253
  const CREATE = names => `const e2eShard = require('./ci-shard').e2eShard(__filename, [${names.map(n => `'${n}'`).join(', ')}]);`;
155
254
  const START = '// e2e-shard: cases start here';
156
255
 
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
+ require('./temp-cleanup').trackTemp(module); // T-0357: records this run's temp folders; the runner that started it removes them after a clean exit
3
4
  // T-0359: split CI work across parallel jobs. Until 1.36.300 each Windows gate job ran e2e.js (167-226 min) and the
4
5
  // command-surface leaves (43-52 min) one after the other, and the Windows fast job ran its leaves for 79 min, so a
5
6
  // release waited about five hours for CI (run 38193803239). A shard runs part of that work. Shards 1..k of one job
@@ -17,11 +18,17 @@
17
18
  // guessed: running it differently from npm would test something else. Leaves run with node directly, in the
18
19
  // package root, in their order in the script. A shard runs all of its leaves and fails if any failed, so one CI
19
20
  // run reports every failing leaf of the shard (a plain `&&` chain stops at the first).
21
+ // T-0357: each leaf records the temp folders it makes (scripts/temp-cleanup.js trackTemp, into a directory this runner
22
+ // gives it through LEERNESS_TEMP_RECORD), and the runner removes them after the leaf ended with status 0, no signal
23
+ // and no spawn error: the verdict it uses for pass/fail, read from the exit status, which no code in the leaf can
24
+ // change afterwards. A failed leaf's folders are kept for inspection.
20
25
  // e2e.js uses e2eShard() below: each top-level case is `if (e2eShard.take('<key>')) {`, and finish() checks that the
21
26
  // file has no unguarded top-level block and that every guard was reached exactly once.
22
27
  const fs = require('fs');
28
+ const os = require('os');
23
29
  const path = require('path');
24
30
  const cp = require('child_process');
31
+ const temp = require('./temp-cleanup');
25
32
 
26
33
  const ROOT = path.resolve(__dirname, '..');
27
34
  const WEIGHTS_FILE = path.join(__dirname, 'ci-shard-weights.json');
@@ -139,23 +146,48 @@ function runNpm(name, spec, opts = {}) {
139
146
  const grouped = env.GITHUB_ACTIONS === 'true';
140
147
  // Leaves do not see this runner's times file: a leaf that runs runNpm itself (ci-shard-probe.js) would overwrite it.
141
148
  const leafEnv = { ...env };
142
- for (const k of Object.keys(leafEnv)) if (k.toUpperCase() === 'LEERNESS_CI_SHARD_TIMES') delete leafEnv[k];
149
+ for (const k of Object.keys(leafEnv)) if (['LEERNESS_CI_SHARD_TIMES', temp.RECORD_ENV].includes(k.toUpperCase())) delete leafEnv[k];
143
150
  log(`ci-shard: ${name} shard ${spec.index}/${spec.count} runs ${mine.length} of ${leaves.length} leaves`);
151
+ // T-0357: one record directory per leaf, below a root this runner makes. Each is checked right after it is made (an
152
+ // empty directory, not a link, its identity kept): a wrapper of fs.mkdtempSync or fs.mkdirSync installed before
153
+ // the runner could put an existing folder there (1.36.304 review round 3). A leaf whose directory fails the check
154
+ // gets no variable and records nothing; only checked directories are removed, and the root only when it is empty.
155
+ const freshDir = d => {
156
+ try {
157
+ const st = fs.lstatSync(d, { bigint: true });
158
+ return st.isSymbolicLink() || !st.isDirectory() || fs.readdirSync(d).length ? null : temp.ownTree(d);
159
+ } catch { return null; }
160
+ };
161
+ let recordRoot = null;
162
+ try { recordRoot = freshDir(fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-temp-record-'))); } catch { /* nothing recorded */ }
163
+ const tempCount = { removed: 0, left: 0, unrecorded: 0 };
144
164
  const results = [];
145
- for (const leaf of mine) {
165
+ for (const [i, leaf] of mine.entries()) {
146
166
  if (grouped) log(`::group::${leaf.key}`);
167
+ let recordDir = null;
168
+ if (recordRoot) { const d = path.join(recordRoot.dir, String(i)); try { fs.mkdirSync(d); recordDir = freshDir(d); } catch { /* nothing recorded */ } }
169
+ if (!recordDir) tempCount.unrecorded++;
147
170
  const started = Date.now();
148
- const r = cp.spawnSync(process.execPath, [path.resolve(root, leaf.file), ...leaf.args], { cwd: root, stdio: opts.stdio || 'inherit', env: leafEnv });
171
+ const r = cp.spawnSync(process.execPath, [path.resolve(root, leaf.file), ...leaf.args], { cwd: root, stdio: opts.stdio || 'inherit', env: recordDir ? { ...leafEnv, [temp.RECORD_ENV]: recordDir.dir } : leafEnv });
149
172
  const seconds = (Date.now() - started) / 1000;
150
173
  const ok = r.status === 0 && !r.signal && !r.error;
174
+ if (ok && recordDir) { const t = temp.removeRecorded(recordDir.dir); tempCount.removed += t.removed; tempCount.left += t.left; }
175
+ if (recordDir) try { temp.removeOwnedTree(recordDir, { timeoutMs: 2000 }); } catch { /* left behind */ }
151
176
  results.push({ key: leaf.key, ok, seconds, status: r.status, signal: r.signal || null, error: r.error ? String(r.error.code || r.error.message) : null });
152
177
  if (grouped) log('::endgroup::');
153
178
  log(`ci-shard: ${ok ? 'ok ' : 'FAIL'} ${seconds.toFixed(1).padStart(7)} s ${leaf.key}${ok ? '' : ` (exit ${r.status}${r.signal ? `, signal ${r.signal}` : ''}${r.error ? `, ${r.error.code || r.error.message}` : ''})`}`);
154
179
  }
180
+ if (recordRoot) {
181
+ try {
182
+ const st = fs.lstatSync(recordRoot.dir, { bigint: true });
183
+ if (!st.isSymbolicLink() && st.dev === recordRoot.dev && st.ino === recordRoot.ino) fs.rmdirSync(recordRoot.dir);
184
+ } catch { /* not empty or replaced: left behind */ }
185
+ }
155
186
  const failed = results.filter(r => !r.ok);
156
187
  const total = results.reduce((s, r) => s + r.seconds, 0);
157
188
  log(`ci-shard: ${name} shard ${spec.index}/${spec.count}: ${results.length - failed.length}/${results.length} leaves passed · ${total.toFixed(0)} s`);
158
189
  for (const f of failed) log(`ci-shard: failed: ${f.key}`);
190
+ log(`ci-shard: temp folders: ${tempCount.removed} removed after passing leaves${tempCount.left ? `, ${tempCount.left} left (replaced, outside the temp directory, or still held)` : ''}${tempCount.unrecorded ? `, ${tempCount.unrecorded} leaves not recorded (their record directory was not the one made)` : ''}; a failed leaf keeps its own`);
159
191
  if (env.LEERNESS_CI_SHARD_TIMES) {
160
192
  fs.writeFileSync(env.LEERNESS_CI_SHARD_TIMES, JSON.stringify(Object.fromEntries(results.map(r => [r.key, Math.round(r.seconds * 10) / 10])), null, 1) + '\n');
161
193
  }
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
+ require('./temp-cleanup').trackTemp(module); // T-0357: records this run's temp folders; the runner that started it removes them after a clean exit
3
4
 
4
5
  // T-0303: generated runtime cache must not create a strict Git mismatch for
5
6
  // an unrelated, already committed claim when an older project lacks .gitignore.
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
+ require('./temp-cleanup').trackTemp(module); // T-0357: records this run's temp folders; the runner that started it removes them after a clean exit
3
4
 
4
5
  // T-0302: count real Git status requests at the CLI boundary, rather than
5
6
  // matching the implementation's source text.
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
+ require('./temp-cleanup').trackTemp(module); // T-0357: records this run's temp folders; the runner that started it removes them after a clean exit
3
4
 
4
5
  // T-0145 multi-session regression: two first-time creators may both observe
5
6
  // "absent", but the create-only check inside the file lock must let exactly
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
+ require('./temp-cleanup').trackTemp(module); // T-0357: records this run's temp folders; the runner that started it removes them after a clean exit
3
4
 
4
5
  // T-0145 regression probe: a reviewed legacy debt boundary unblocks only exact
5
6
  // historical failures. Raw/per-task audits, changed rows, new failures, invalid
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
+ require('./temp-cleanup').trackTemp(module); // T-0357: records this run's temp folders; the runner that started it removes them after a clean exit
3
4
 
4
5
  const fs = require('fs');
5
6
  const crypto = require('crypto');
@@ -1,4 +1,5 @@
1
1
  'use strict';
2
+ require('./temp-cleanup').trackTemp(module); // T-0357: records this run's temp folders; the runner that started it removes them after a clean exit
2
3
  const assert = require('assert'), fs = require('fs'), path = require('path'), os = require('os'), cp = require('child_process');
3
4
  const { createHash } = require('crypto');
4
5
  const { removeTree } = require('./temp-cleanup');
@@ -1,4 +1,5 @@
1
1
  'use strict';
2
+ require('./temp-cleanup').trackTemp(module); // T-0357: records this run's temp folders; the runner that started it removes them after a clean exit
2
3
  const assert = require('assert'), fs = require('fs'), path = require('path'), os = require('os'), cp = require('child_process');
3
4
  const { removeTree } = require('./temp-cleanup');
4
5
  if (!process.argv.includes('--worker')) {
@@ -1,4 +1,5 @@
1
1
  'use strict';
2
+ require('./temp-cleanup').trackTemp(module); // T-0357: records this run's temp folders; the runner that started it removes them after a clean exit
2
3
  // T-0343: lib/copy-tree.js copyTreeSync, the fs.cpSync replacement used for fixture and selftest copies.
3
4
  // It must copy nested directories and file bytes exactly, copy into an existing directory, never overwrite a file,
4
5
  // and work with non-ASCII paths on every supported Node (fs.cpSync crashes on them in Node 22.23.3 on Windows).
@@ -1,4 +1,5 @@
1
1
  'use strict';
2
+ require('./temp-cleanup').trackTemp(module); // T-0357: records this run's temp folders; the runner that started it removes them after a clean exit
2
3
  // T-0323 (owner request 2026-09-30): data features get the CRUD operations they need, by default and in every
3
4
  // leerness project. The owner chose "guidance plus an automatic check at completion" (advisory, never blocking),
4
5
  // for new features and changes to existing ones, and "the four operations plus quality items" as the standard:
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
+ require('./temp-cleanup').trackTemp(module); // T-0357: records this run's temp folders; the runner that started it removes them after a clean exit
3
4
 
4
5
  // T-0094: advertised options must change observable behavior (or fail closed).
5
6
  // This probe intentionally uses isolated projects and real CLI processes so a
@@ -1,4 +1,5 @@
1
1
  'use strict';
2
+ require('./temp-cleanup').trackTemp(module); // T-0357: records this run's temp folders; the runner that started it removes them after a clean exit
2
3
  // T-0332: which tracker statuses read as done. verify-claim (one task, and --all, which gate --claims runs), the claims
3
4
  // baseline, lazy detection and past-fix advice share one exact predicate: done, completed, verified or 완료, in any
4
5
  // case, with the cell's spaces trimmed, also inside one pair of brackets (the older tracker form, like [진행]).
@@ -1,4 +1,5 @@
1
1
  'use strict';
2
+ require('./temp-cleanup').trackTemp(module); // T-0357: records this run's temp folders; the runner that started it removes them after a clean exit
2
3
  // T-0339: commands whose edits need a state lock refuse --dry-run with the dry-run refusal. Under --dry-run the CLI
3
4
  // takes no state lock (it would leave lock metadata), so lib/managed-text.js refused every edit with "Managed text
4
5
  // requires its existing state lock" (E_TEXT_LOCK) before its first write could meet the dry-run guard (task add, drop
@@ -1,4 +1,5 @@
1
1
  'use strict';
2
+ require('./temp-cleanup').trackTemp(module); // T-0357: records this run's temp folders; the runner that started it removes them after a clean exit
2
3
 
3
4
  // Observation integration only: original E2E predicates, real temp files, no heavyweight CLI.
4
5
  const assert = require('node:assert/strict');
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
+ require('./temp-cleanup').trackTemp(module); // T-0357: records this run's temp folders; the runner that started it removes them after a clean exit
3
4
 
4
5
  // T-0099 / UR-0076: historically uncovered command families.
5
6
  // Local state commands execute end-to-end in a disposable fixture. Commands that
@@ -1,6 +1,7 @@
1
1
  // scripts/e2e-concurrency.js — T-0115의 silent lost-update 회귀 방지.
2
2
  // LOCKPROBE_STALL_*은 scripts/lock-probe.js를 명시 preload한 이 테스트에서만 활성화된다.
3
3
  'use strict';
4
+ require('./temp-cleanup').trackTemp(module); // T-0357: records this run's temp folders; the runner that started it removes them after a clean exit
4
5
 
5
6
  const fs = require('fs');
6
7
  const os = require('os');
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
+ require('./temp-cleanup').trackTemp(module); // T-0357: records this run's temp folders; the runner that started it removes them after a clean exit
3
4
 
4
5
  // 1.35.13 (UR-0014): 3-tier 테스트의 중간 티어 — critical-path 행위(behavioral) 스위트.
5
6
  // test:fast (smoke.js) — 명령이 크래시 없이 도는가만 clean 프로젝트에서 확인, <1분. 개발 루프.
@@ -10,6 +10,7 @@
10
10
  // dated decision, a lesson, an active rule, an open user request, a closed session (a handoff naming the
11
11
  // in-progress task, the current state, a pre-wake report), the manifest, the installed skills and the usage counter.
12
12
  'use strict';
13
+ require('./temp-cleanup').trackTemp(module); // T-0357: records this run's temp folders; the runner that started it removes them after a clean exit
13
14
  const fs = require('fs');
14
15
  const path = require('path');
15
16
  const cp = require('child_process');
@@ -8,6 +8,7 @@
8
8
  // A snapshot records size, mtime (ns) and inode of every file and each directory, gitignored state included.
9
9
  // Content hashes are unnecessary: a write moves mtime, and a rename or hard link changes the inode.
10
10
  'use strict';
11
+ require('./temp-cleanup').trackTemp(module); // T-0357: records this run's temp folders; the runner that started it removes them after a clean exit
11
12
  const fs = require('fs');
12
13
  const os = require('os');
13
14
  const path = require('path');
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
+ require('./temp-cleanup').trackTemp(module); // T-0357: records this run's temp folders; the runner that started it removes them after a clean exit
3
4
 
4
5
  // T-0182: execute the original E2E counter and failure assertion, with real
5
6
  // process-local TMP isolation. No product implementation or fs/os mock.
package/scripts/e2e.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
+ require('./temp-cleanup').trackTemp(module); // T-0357: records this run's temp folders; the runner that started it removes them after a clean exit
3
4
 
4
5
  const fs = require('fs');
5
6
  const os = require('os');
@@ -15957,6 +15958,10 @@ setImmediate(() => {
15957
15958
  failed++;
15958
15959
  console.log(`✗ T-0359 e2e 샤드 가드 이상 ${JSON.stringify({ problems: _shard.problems.slice(0, 10), missing: _shard.missing.slice(0, 10), repeated: _shard.repeated.slice(0, 10), unknown: _shard.unknown.slice(0, 10) })}`);
15959
15960
  }
15961
+ // T-0357: 케이스가 만든 임시 폴더는 이 프로세스가 기록만 하고, 이 실행을 시작한 러너(scripts/ci-shard.js)가 종료 코드 0 을
15962
+ // 보고 지운다(링크는 링크로만). 러너 없이 실행하면 기록하지 않는다.
15963
+ const _temp = require('./temp-cleanup').trackTemp(module);
15964
+ if (_temp.active) console.log(` 임시 폴더 ${_temp.pending()}개 기록 — 러너가 종료 코드 0 을 보면 삭제, 실패면 남김`);
15960
15965
  console.log(`\nE2E result: ${total - failed}/${total} passed${_shard.spec ? ` · shard ${_shard.spec.index}/${_shard.spec.count} (${_shard.ran}/${_shard.total} cases)` : ''} · ${((Date.now() - _e2eStart) / 1000).toFixed(0)}s`);
15961
15966
  if (failed > 0) process.exitCode = 1;
15962
15967
  });
@@ -1,4 +1,5 @@
1
1
  'use strict';
2
+ require('./temp-cleanup').trackTemp(module); // T-0357: records this run's temp folders; the runner that started it removes them after a clean exit
2
3
  const assert = require('assert'), fs = require('fs'), path = require('path'), os = require('os'), cp = require('child_process');
3
4
  const { createHash } = require('crypto');
4
5
  const { LEGACY_WORKSPACE_DIR } = require('../lib/workspace-dir');
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
+ require('./temp-cleanup').trackTemp(module); // T-0357: records this run's temp folders; the runner that started it removes them after a clean exit
3
4
 
4
5
  // T-0181: test the confirmed UR-0113 diagnostic/exit-code defect separately
5
6
  // from the unconfirmed Windows CI failure. The default violates the platform's
@@ -1,4 +1,5 @@
1
1
  'use strict';
2
+ require('./temp-cleanup').trackTemp(module); // T-0357: records this run's temp folders; the runner that started it removes them after a clean exit
2
3
  const assert = require('assert'), fs = require('fs'), path = require('path'), os = require('os'), cp = require('child_process');
3
4
  const { createHash } = require('crypto');
4
5
  const { removeTree } = require('./temp-cleanup');
@@ -1,4 +1,5 @@
1
1
  'use strict';
2
+ require('./temp-cleanup').trackTemp(module); // T-0357: records this run's temp folders; the runner that started it removes them after a clean exit
2
3
  const assert = require('assert'), fs = require('fs'), path = require('path'), os = require('os'), cp = require('child_process');
3
4
  const { removeTree } = require('./temp-cleanup');
4
5
  if (!process.argv.includes('--worker')) {
@@ -1,4 +1,5 @@
1
1
  'use strict';
2
+ require('./temp-cleanup').trackTemp(module); // T-0357: records this run's temp folders; the runner that started it removes them after a clean exit
2
3
 
3
4
  // Real filesystem writes and process termination; injected I/O faults are
4
5
  // explicitly synthetic. These checks do not simulate physical power loss.
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
+ require('./temp-cleanup').trackTemp(module); // T-0357: records this run's temp folders; the runner that started it removes them after a clean exit
3
4
 
4
5
  // T-0095 regression probe: read/status commands must state what they actually
5
6
  // measured. A CLI version is not a project version, an invalid memory verb is