leerness 1.36.171 → 1.36.172

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 CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.36.172 — 2026-08-27
4
+
5
+ - GitHub CI에서 재현된 동시 최초 .harness→.leerness 마이그레이션 TOCTOU를 수정했습니다. 라이브 마이그레이션이 두 번째 상태 검사와 충돌 스캔 전에 프로젝트 잠금을 획득하며, 실제 peer EEXIST와 잠금 해제 직전까지의 대기를 검증하는 결정론적 회귀를 fast/core/full 게이트에 연결했습니다.
6
+
3
7
  ## 1.36.171 — 2026-08-27
4
8
 
5
9
  - MCP handoff/session-close가 호출별 sessionKey로 presence를 등록·종료하고, Windows 환경변수 casing·일회성 marker·legacy case variant·손상 closedAt를 안전하게 처리하도록 개선했습니다. 전용 MCP presence 회귀 22/22와 전체 E2E 467/467를 추가·통과했습니다.
package/README.md CHANGED
@@ -138,7 +138,7 @@ MIT
138
138
  <!-- leerness:project-readme:start -->
139
139
  ## Leerness Project Harness
140
140
 
141
- 이 프로젝트는 Leerness v1.36.171 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
141
+ 이 프로젝트는 Leerness v1.36.172 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
142
142
 
143
143
  ### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
144
144
 
@@ -192,7 +192,7 @@ leerness memory restore decision <date|title>
192
192
 
193
193
  ### MCP server (외부 AI 통합)
194
194
 
195
- Leerness v1.36.171는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **89개 도구**를 노출:
195
+ Leerness v1.36.172는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **89개 도구**를 노출:
196
196
 
197
197
  ```jsonc
198
198
  // 카테고리별
@@ -213,7 +213,7 @@ Leerness v1.36.171는 stdio JSON-RPC MCP server를 내장합니다 — Claude Co
213
213
  `<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
214
214
  1) 다음 라운드 후보 선정 → 2) 코드 변경 → 3) 회귀 테스트 갱신 → 4) 전체 e2e 스위트 통과 → 5) npm publish + git tag → 6) main push → 7) session close → 8) 다음 라운드 예약.
215
215
 
216
- 현재 누적: **v1.9.x → 1.36.171 릴리스 태그 이력** (수백 라운드) · _reports/는 비공개 보존.
216
+ 현재 누적: **v1.9.x → 1.36.172 릴리스 태그 이력** (수백 라운드) · _reports/는 비공개 보존.
217
217
 
218
218
  ### 성능 가이드
219
219
 
@@ -251,5 +251,5 @@ leerness release pack --close --auto-main-push
251
251
  - `.leerness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
252
252
  - `.leerness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
253
253
 
254
- Last synced by Leerness v1.36.171: 2026-08-27
254
+ Last synced by Leerness v1.36.172: 2026-08-27
255
255
  <!-- leerness:project-readme:end -->
package/bin/leerness.js CHANGED
@@ -48,7 +48,7 @@ const {
48
48
  migrateLegacyWorkspace,
49
49
  } = require('../lib/workspace-dir');
50
50
 
51
- const VERSION = '1.36.171';
51
+ const VERSION = '1.36.172';
52
52
 
53
53
  // MCP lifecycle 주소 표식은 현재 CLI 호출 한 번에만 유효하다. CLI bootstrap에서 즉시 env에서
54
54
  // 떼어 두어 `--no-record`/hook처럼 presence 기록 함수에 도달하지 않는 경로도 후속 child에 유출하지 않는다.
@@ -376,18 +376,18 @@ function _sameDestinationSnapshot(a, b) {
376
376
  return !!a && !!b && a.kind === b.kind && a.hash === b.hash;
377
377
  }
378
378
 
379
- function migrateLegacyWorkspace(root, opts = {}) {
380
- const state = inspectWorkspace(root);
381
- const dryRun = opts.dryRun === true;
382
- const force = opts.force === true;
383
- const version = String(opts.version || 'unknown');
379
+ const _MIGRATION_LOCK_OWNED = Symbol('leerness.workspace-migration-lock-owned');
380
+
381
+ function _migrationLockWaitMs(opts = {}) {
382
+ const parsed = Number(opts.lockWaitMs);
383
+ return Number.isFinite(parsed) ? Math.max(0, Math.min(60000, parsed)) : 10000;
384
+ }
385
+
386
+ function _newMigrationReport(state, opts = {}) {
384
387
  const at = opts.at || new Date().toISOString();
385
- const parsedLockWait = Number(opts.lockWaitMs);
386
- const lockWaitMs = Number.isFinite(parsedLockWait)
387
- ? Math.max(0, Math.min(60000, parsedLockWait))
388
- : 10000;
389
- const lockFile = path.join(state.root, '.leerness-workspace-migration.lock');
390
- const report = {
388
+ const version = String(opts.version || 'unknown');
389
+ const dryRun = opts.dryRun === true;
390
+ return {
391
391
  at,
392
392
  version,
393
393
  src: state.legacy.abs,
@@ -410,6 +410,74 @@ function migrateLegacyWorkspace(root, opts = {}) {
410
410
  waitedForLock: false,
411
411
  lockWaitedMs: 0,
412
412
  };
413
+ }
414
+
415
+ // Live first migrations serialize before the second workspace inspection or
416
+ // any destination conflict scan. Acquiring later leaves a TOCTOU window where
417
+ // a peer's in-progress canonical copy is mistaken for user-authored conflict.
418
+ // Dry-runs and already-canonical roots stay read-only and skip lock creation.
419
+ function migrateLegacyWorkspace(root, opts = {}) {
420
+ const initial = inspectWorkspace(root);
421
+ if (opts.dryRun === true || opts[_MIGRATION_LOCK_OWNED] === true || !initial.legacy.exists) {
422
+ return _migrateLegacyWorkspace(initial.root, opts);
423
+ }
424
+
425
+ const lockWaitMs = _migrationLockWaitMs(opts);
426
+ const lockFile = path.join(initial.root, '.leerness-workspace-migration.lock');
427
+ const startedAt = Date.now();
428
+ const deadline = startedAt + lockWaitMs;
429
+ let lockFd = null;
430
+ let waited = false;
431
+
432
+ while (lockFd === null) {
433
+ try { lockFd = fs.openSync(lockFile, 'wx'); }
434
+ catch (error) {
435
+ if (!error || (error.code !== 'EEXIST' && !_isTransientLockFsError(error))) {
436
+ const report = _newMigrationReport(initial, opts);
437
+ report.blocked = true;
438
+ report.blockedReason = 'workspace-dir-migration-failed';
439
+ report.errors.push(error && error.message ? error.message : String(error));
440
+ report.waitedForLock = waited;
441
+ report.lockWaitedMs = waited ? Math.max(0, Date.now() - startedAt) : 0;
442
+ return report;
443
+ }
444
+ waited = true;
445
+ if (Date.now() >= deadline) {
446
+ const report = _newMigrationReport(initial, opts);
447
+ report.blocked = true;
448
+ report.blockedReason = 'workspace-dir-migration-locked';
449
+ report.errors.push(`another workspace migration still owns: ${lockFile}`);
450
+ report.waitedForLock = true;
451
+ report.lockWaitedMs = Math.max(0, Date.now() - startedAt);
452
+ return report;
453
+ }
454
+ _sleepSync(Math.min(50, Math.max(1, deadline - Date.now())));
455
+ }
456
+ }
457
+
458
+ try {
459
+ const report = _migrateLegacyWorkspace(initial.root, { ...opts, [_MIGRATION_LOCK_OWNED]: true });
460
+ if (waited) {
461
+ report.waitedForLock = true;
462
+ report.lockWaitedMs = Math.max(report.lockWaitedMs || 0, Date.now() - startedAt);
463
+ }
464
+ return report;
465
+ } finally {
466
+ try { fs.closeSync(lockFd); } catch {}
467
+ _removeOwnedLockFile(lockFile);
468
+ }
469
+ }
470
+
471
+ function _migrateLegacyWorkspace(root, opts = {}) {
472
+ const state = inspectWorkspace(root);
473
+ const dryRun = opts.dryRun === true;
474
+ const migrationLockOwned = opts[_MIGRATION_LOCK_OWNED] === true;
475
+ const force = opts.force === true;
476
+ const version = String(opts.version || 'unknown');
477
+ const at = opts.at || new Date().toISOString();
478
+ const lockWaitMs = _migrationLockWaitMs(opts);
479
+ const lockFile = path.join(state.root, '.leerness-workspace-migration.lock');
480
+ const report = _newMigrationReport(state, opts);
413
481
 
414
482
  const block = (reason, message, extra = {}) => {
415
483
  report.blocked = true;
@@ -423,7 +491,7 @@ function migrateLegacyWorkspace(root, opts = {}) {
423
491
  // look foreign or dual-live while copying. Observe that transaction before
424
492
  // applying any conflict verdict; otherwise simultaneous first handoffs race
425
493
  // against an intermediate state and some fail before reaching the lock.
426
- if (!dryRun && fs.existsSync(lockFile)) {
494
+ if (!dryRun && !migrationLockOwned && fs.existsSync(lockFile)) {
427
495
  const peerStartedAt = Date.now();
428
496
  const peerDeadline = peerStartedAt + lockWaitMs;
429
497
  report.waitedForLock = true;
@@ -559,7 +627,7 @@ function migrateLegacyWorkspace(root, opts = {}) {
559
627
  const stamp = at.replace(/[^A-Za-z0-9_-]/g, '-').slice(0, 96) || String(Date.now());
560
628
  const migrationRoot = path.join(state.canonical.abs, 'archive', `workspace-dir-migration-${stamp}`);
561
629
  const canonicalBackup = path.join(migrationRoot, 'canonical-before-merge');
562
- let lockFd = null;
630
+ let lockFd = migrationLockOwned ? undefined : null;
563
631
  const created = [];
564
632
  const overwritten = [];
565
633
  const canonicalExisted = state.canonical.exists;
@@ -750,7 +818,7 @@ function migrateLegacyWorkspace(root, opts = {}) {
750
818
  }
751
819
  return report;
752
820
  } finally {
753
- if (lockFd !== null) {
821
+ if (!migrationLockOwned && lockFd !== null) {
754
822
  try { fs.closeSync(lockFd); } catch {}
755
823
  _removeOwnedLockFile(lockFile);
756
824
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.36.171",
3
+ "version": "1.36.172",
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,13 +48,14 @@
48
48
  "LICENSE"
49
49
  ],
50
50
  "scripts": {
51
- "test": "node ./scripts/lint.js && node ./scripts/workspace-dir-source-ratchet.js && node ./scripts/workspace-dir-migration-probe.js && node ./scripts/lock-probe.js && node ./scripts/platform-smoke.js && node ./scripts/verify-code-cross-runtime-probe.js && node ./scripts/release-runtime-probe.js && node ./scripts/which-shim-probe.js && node ./bin/leerness.js --version && node ./bin/leerness.js selftest && node ./scripts/e2e-core.js && node ./scripts/handoff-readonly-probe.js && node ./scripts/mcp-presence-probe.js && npm run test:commands && npm run test:installed && node ./scripts/e2e.js",
52
- "test:core": "node ./scripts/lint.js && node ./scripts/workspace-dir-source-ratchet.js && node ./scripts/workspace-dir-migration-probe.js && node ./scripts/lock-probe.js && node ./scripts/platform-smoke.js && node ./scripts/verify-code-cross-runtime-probe.js && node ./scripts/release-runtime-probe.js && node ./scripts/which-shim-probe.js && node ./bin/leerness.js --version && node ./bin/leerness.js selftest && node ./scripts/e2e-core.js && node ./scripts/handoff-readonly-probe.js && node ./scripts/mcp-presence-probe.js",
51
+ "test": "node ./scripts/lint.js && node ./scripts/workspace-dir-source-ratchet.js && node ./scripts/workspace-dir-migration-probe.js && node ./scripts/workspace-dir-lock-order-probe.js && node ./scripts/lock-probe.js && node ./scripts/platform-smoke.js && node ./scripts/verify-code-cross-runtime-probe.js && node ./scripts/release-runtime-probe.js && node ./scripts/which-shim-probe.js && node ./bin/leerness.js --version && node ./bin/leerness.js selftest && node ./scripts/e2e-core.js && node ./scripts/handoff-readonly-probe.js && node ./scripts/mcp-presence-probe.js && npm run test:commands && npm run test:installed && node ./scripts/e2e.js",
52
+ "test:core": "node ./scripts/lint.js && node ./scripts/workspace-dir-source-ratchet.js && node ./scripts/workspace-dir-migration-probe.js && node ./scripts/workspace-dir-lock-order-probe.js && node ./scripts/lock-probe.js && node ./scripts/platform-smoke.js && node ./scripts/verify-code-cross-runtime-probe.js && node ./scripts/release-runtime-probe.js && node ./scripts/which-shim-probe.js && node ./bin/leerness.js --version && node ./bin/leerness.js selftest && node ./scripts/e2e-core.js && node ./scripts/handoff-readonly-probe.js && node ./scripts/mcp-presence-probe.js",
53
53
  "test:commands": "node ./scripts/command-flags-probe.js && node ./scripts/next-action-suggestion-probe.js && node ./scripts/claims-baseline-probe.js && node ./scripts/claims-baseline-concurrency-probe.js && node ./scripts/e2e-command-surface.js",
54
54
  "test:next-actions": "node ./scripts/next-action-suggestion-probe.js",
55
55
  "test:installed": "node ./scripts/installed-cleanroom-probe.js",
56
56
  "test:handoff": "node ./scripts/handoff-readonly-probe.js",
57
- "test:fast": "node ./scripts/lint.js && node ./scripts/workspace-dir-source-ratchet.js && node ./scripts/workspace-dir-migration-probe.js && node ./scripts/lock-probe.js && node ./scripts/platform-smoke.js && node ./scripts/release-runtime-probe.js && node ./scripts/which-shim-probe.js && node ./scripts/mcp-presence-probe.js && node ./scripts/smoke.js",
57
+ "test:fast": "node ./scripts/lint.js && node ./scripts/workspace-dir-source-ratchet.js && node ./scripts/workspace-dir-migration-probe.js && node ./scripts/workspace-dir-lock-order-probe.js && node ./scripts/lock-probe.js && node ./scripts/platform-smoke.js && node ./scripts/release-runtime-probe.js && node ./scripts/which-shim-probe.js && node ./scripts/mcp-presence-probe.js && node ./scripts/smoke.js",
58
+ "test:workspace-lock-order": "node ./scripts/workspace-dir-lock-order-probe.js",
58
59
  "test:mcp-presence": "node ./scripts/mcp-presence-probe.js",
59
60
  "test:smoke": "node ./scripts/smoke.js",
60
61
  "lint": "node ./scripts/lint.js",
@@ -0,0 +1,206 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // A live legacy migration must own its project lock before the second workspace
5
+ // inspection. Otherwise a peer can populate `.leerness/` after validation and
6
+ // make the first caller misclassify the peer's intermediate copy as a user
7
+ // conflict. This probe forces that interleaving without relying on scheduler luck.
8
+
9
+ const fs = require('fs');
10
+ const os = require('os');
11
+ const path = require('path');
12
+ const cp = require('child_process');
13
+ const workspace = require('../lib/workspace-dir');
14
+
15
+ const IS_PEER = process.argv[2] === '--peer';
16
+ const root = IS_PEER
17
+ ? path.resolve(process.argv[3] || '.')
18
+ : fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-lock-order-'));
19
+ const legacy = path.join(root, workspace.LEGACY_WORKSPACE_DIR);
20
+ const canonical = path.join(root, workspace.CANONICAL_WORKSPACE_DIR);
21
+ const lockFile = path.join(root, '.leerness-workspace-migration.lock');
22
+ const peerReadyFile = path.join(root, '.leerness-lock-order-peer-ready');
23
+ const peerContendedFile = path.join(root, '.leerness-lock-order-peer-contended');
24
+ const peerDoneFile = path.join(root, '.leerness-lock-order-peer-done');
25
+
26
+ function write(file, body) {
27
+ fs.mkdirSync(path.dirname(file), { recursive: true });
28
+ fs.writeFileSync(file, body, 'utf8');
29
+ }
30
+
31
+ function sleep(ms) {
32
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
33
+ }
34
+
35
+ function waitForPeerHandshake(timeoutMs) {
36
+ const deadline = Date.now() + timeoutMs;
37
+ while (Date.now() < deadline) {
38
+ const ready = fs.existsSync(peerReadyFile);
39
+ const contended = fs.existsSync(peerContendedFile);
40
+ const done = fs.existsSync(peerDoneFile);
41
+ if (ready && (contended || done)) return { ready, contended, done };
42
+ sleep(2);
43
+ }
44
+ return {
45
+ ready: fs.existsSync(peerReadyFile),
46
+ contended: fs.existsSync(peerContendedFile),
47
+ done: fs.existsSync(peerDoneFile),
48
+ };
49
+ }
50
+
51
+ function runPeer() {
52
+ const originalOpen = fs.openSync;
53
+ fs.openSync = function patchedOpen(target, flags, ...rest) {
54
+ try { return originalOpen.call(fs, target, flags, ...rest); }
55
+ catch (error) {
56
+ if (path.resolve(String(target)) === lockFile && flags === 'wx' && error && error.code === 'EEXIST') {
57
+ write(peerContendedFile, `${process.pid}\n`);
58
+ }
59
+ throw error;
60
+ }
61
+ };
62
+
63
+ write(peerReadyFile, `${process.pid}\n`);
64
+ let report;
65
+ try {
66
+ report = workspace.migrateLegacyWorkspace(root, {
67
+ version: 'lock-order-peer',
68
+ lockWaitMs: 20000,
69
+ at: '2026-08-27T00:00:01.000Z',
70
+ });
71
+ } finally {
72
+ fs.openSync = originalOpen;
73
+ }
74
+ write(peerDoneFile, `${JSON.stringify(report)}\n`);
75
+ if (!report || report.blocked || (!report.migrated && !report.alreadyCanonical)) process.exitCode = 1;
76
+ }
77
+
78
+ async function main() {
79
+ if (IS_PEER) {
80
+ runPeer();
81
+ return;
82
+ }
83
+
84
+ write(path.join(legacy, 'HARNESS_VERSION'), '1.36.161\n');
85
+ write(path.join(legacy, 'progress-tracker.md'), `# Read ${workspace.LEGACY_WORKSPACE_DIR}/progress-tracker.md\n`);
86
+ for (let i = 0; i < 1200; i++) {
87
+ write(path.join(legacy, 'bulk', `f-${String(i).padStart(4, '0')}.md`),
88
+ `Read ${workspace.LEGACY_WORKSPACE_DIR}/bulk/${i}\n${'x'.repeat(2048)}\n`);
89
+ }
90
+
91
+ const originalLstat = fs.lstatSync;
92
+ const originalUnlink = fs.unlinkSync;
93
+ let canonicalInspections = 0;
94
+ let peer = null;
95
+ let peerDone = null;
96
+ let earlyLockSeen = false;
97
+ let peerReadySeen = false;
98
+ let peerContentionSeen = false;
99
+ let peerCompletedBeforeRelease = false;
100
+ let lockReleaseObserved = false;
101
+ let peerBlockedUntilRelease = false;
102
+
103
+ fs.lstatSync = function patchedLstat(target, ...rest) {
104
+ const isCanonicalRoot = path.resolve(String(target)) === canonical;
105
+ let result;
106
+ let thrown;
107
+ try { result = originalLstat.call(fs, target, ...rest); }
108
+ catch (error) { thrown = error; }
109
+
110
+ if (isCanonicalRoot && ++canonicalInspections === 2) {
111
+ earlyLockSeen = fs.existsSync(lockFile);
112
+ peer = cp.spawn(process.execPath, [__filename, '--peer', root], {
113
+ cwd: root,
114
+ env: {
115
+ ...process.env,
116
+ LEERNESS_INTERNAL: '1',
117
+ LEERNESS_NO_BANNER: '1',
118
+ LEERNESS_NO_PROMPT: '1',
119
+ LEERNESS_NO_AUTOCHCP: '1',
120
+ },
121
+ stdio: ['ignore', 'ignore', 'ignore'],
122
+ });
123
+ peerDone = new Promise((resolve) => peer.on('close', (status) => resolve(status)));
124
+
125
+ // The child records an actual failed `open(..., "wx")`, not merely that
126
+ // it was spawned. This proves it reached lock acquisition while the
127
+ // victim is paused inside its second canonical workspace inspection.
128
+ const handshake = waitForPeerHandshake(20000);
129
+ peerReadySeen = handshake.ready;
130
+ peerContentionSeen = handshake.contended;
131
+ peerCompletedBeforeRelease = handshake.done;
132
+ }
133
+
134
+ if (thrown) throw thrown;
135
+ return result;
136
+ };
137
+ fs.unlinkSync = function patchedUnlink(target, ...rest) {
138
+ if (path.resolve(String(target)) === lockFile) {
139
+ lockReleaseObserved = true;
140
+ peerBlockedUntilRelease = peerReadySeen
141
+ && peerContentionSeen
142
+ && !fs.existsSync(peerDoneFile)
143
+ && peer !== null
144
+ && peer.exitCode === null;
145
+ }
146
+ return originalUnlink.call(fs, target, ...rest);
147
+ };
148
+
149
+ let victim;
150
+ try {
151
+ victim = workspace.migrateLegacyWorkspace(root, {
152
+ version: 'lock-order-probe',
153
+ lockWaitMs: 20000,
154
+ at: '2026-08-27T00:00:00.000Z',
155
+ });
156
+ } finally {
157
+ fs.lstatSync = originalLstat;
158
+ fs.unlinkSync = originalUnlink;
159
+ }
160
+
161
+ const peerStatus = peerDone ? await peerDone : null;
162
+ const ok = earlyLockSeen
163
+ && peerReadySeen
164
+ && peerContentionSeen
165
+ && !peerCompletedBeforeRelease
166
+ && lockReleaseObserved
167
+ && peerBlockedUntilRelease
168
+ && peerStatus === 0
169
+ && victim && victim.migrated === true && victim.blocked === false
170
+ && fs.existsSync(path.join(canonical, 'progress-tracker.md'))
171
+ && fs.existsSync(path.join(root, workspace.LEGACY_BACKUP_WORKSPACE_DIR, 'progress-tracker.md'))
172
+ && !fs.existsSync(legacy);
173
+
174
+ if (!ok) {
175
+ process.stderr.write(`WORKSPACE_LOCK_ORDER_RACE ${JSON.stringify({
176
+ earlyLockSeen,
177
+ peerReadySeen,
178
+ peerContentionSeen,
179
+ peerCompletedBeforeRelease,
180
+ lockReleaseObserved,
181
+ peerBlockedUntilRelease,
182
+ peerStatus,
183
+ victim: victim && {
184
+ migrated: victim.migrated,
185
+ alreadyCanonical: victim.alreadyCanonical,
186
+ blocked: victim.blocked,
187
+ blockedReason: victim.blockedReason,
188
+ errors: victim.errors,
189
+ },
190
+ })}\n`);
191
+ process.exitCode = 1;
192
+ } else {
193
+ process.stdout.write('WORKSPACE_LOCK_ORDER_OK\n');
194
+ }
195
+ }
196
+
197
+ main()
198
+ .catch((error) => {
199
+ process.stderr.write(`WORKSPACE_LOCK_ORDER_CRASH ${error && error.stack ? error.stack : error}\n`);
200
+ process.exitCode = 1;
201
+ })
202
+ .finally(() => {
203
+ if (!IS_PEER) {
204
+ try { fs.rmSync(root, { recursive: true, force: true }); } catch {}
205
+ }
206
+ });