stackbite 0.5.107 → 0.5.109

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/README.md CHANGED
@@ -251,22 +251,16 @@ provider 완료 증거와 구별합니다. Codex·Claude의 hooks는 시작·작
251
251
  기록합니다. CLI 로그인, workspace 신뢰, hooks 신뢰와 도구 실행 승인은 provider 안에서 처리합니다.
252
252
  Stackbite는 승인·sandbox를 우회하지 않습니다.
253
253
 
254
- Python SDK `AgentStackbite`는 방을 대상으로 합니다. 외부 스크립트는 `room_id`를
255
- 명시하고, 좌석 안의 에이전트는 상속된 `STACKBITE_ROOM_ID`를 사용합니다. 인증된
256
- `TaskRoomClient`로 위임하며 방이 없는 호출은 거절합니다. 직접 방 API를 사용하려면
257
- `TaskRoomClient`를 사용합니다.
254
+ 스크립트와 에이전트는 native CLI 또는 인증된 Room HTTP API를 사용합니다.
255
+ Seat 안에서 현재 Room의 좌석을 조회하려면 다음 명령을 실행합니다.
258
256
 
259
- 기존 방의 좌석을 조회하는 예:
260
-
261
- ```python
262
- from private_pty import AgentStackbite, TaskRoomClient
263
-
264
- sdk = AgentStackbite(room_id="review-room", client=TaskRoomClient())
265
- seats = sdk.list_agents() # 이 방의 정의된 좌석; 정지된 좌석도 포함
257
+ ```sh
258
+ stackbite-room seats --output json
266
259
  ```
267
260
 
268
- 좌석 안에서는 `AgentStackbite()`가 상속된 방을 사용합니다. 생성자는 데몬을 시작하거나
269
- 로컬 에이전트를 생성하지 않습니다.
261
+ 외부 스크립트는 `GET /api/rooms/{room_id}`로 Room과 Seat를 조회할 수 있습니다.
262
+ Python 엔진과 함께 제공되던 소스 SDK(`private_pty.AgentStackbite`, `TaskRoomClient`)는
263
+ native 전환에서 제거했습니다. Room HTTP 경로와 인증 계약은 유지합니다.
270
264
 
271
265
  기본 방 주소는 `http://127.0.0.1:8090`입니다. 명시적 `base_url`,
272
266
  `STACKBITE_ROOM_BRIDGE_URL`, 기본 주소 순서로 선택합니다. 클라이언트는 상태 위치에서
@@ -317,12 +311,11 @@ Work Stack은 작업 정리와 Task 권한을, Stackbite는 로컬 Room/Seat 실
317
311
  유지합니다. 선택한 Task의 workspace·revision을 확인한 뒤 방을 만들며 이 과정은 Task
318
312
  상태를 수정하지 않습니다.
319
313
 
320
- ```powershell
321
- python -m private_pty.workstack_link room --task T-0041 --cwd C:\my-project --isolated --source-repo C:\my-project
322
- ```
314
+ 선택 Task 연결은 native WorkStack 배포 경로와 Rust
315
+ `stackbite_management::public_workstack::link` API에서 처리합니다.
323
316
 
324
317
  handoff의 첫 줄 `WorkStack binding:` JSON marker와 Task ID·workspace·revision 계약은
325
- 유지합니다. `publish_checkpoint()`는 새로 검증한 바인딩과 안정적인 intent ID로 호출자가
318
+ 유지합니다. Rust `Link::publish_checkpoint()`는 새로 검증한 바인딩과 안정적인 intent ID로 호출자가
326
319
  작성한 사실만 전달합니다. commit_unknown은 operation-status로 확인하고 mutation을
327
320
  재전송하지 않습니다. 로그·프롬프트 자동 복사나 Task 자동 완료는 하지 않습니다.
328
321
 
@@ -333,19 +326,28 @@ handoff의 첫 줄 `WorkStack binding:` JSON marker와 Task ID·workspace·revis
333
326
 
334
327
  ## 개발과 성능 검토
335
328
 
336
- ```powershell
337
- python -m pip install -r requirements-build.txt
338
- python stackbite.py
329
+ 제품 실행 경로는 `rust/`의 launcher, management, terminald와 TUI입니다.
330
+ Python 엔진과 Rich TUI는 제거했습니다. Rust package 빌드는
331
+ `scripts/build_rust_terminal.py`를 사용하고, 세 플랫폼 release builder는 그 package를
332
+ native 배포본으로 조립합니다. `scripts/native_bundle.py`가 최종 파일 목록과 Python
333
+ 런타임 부재를 검사합니다.
334
+
335
+ ```sh
336
+ cd rust
337
+ cargo test --workspace
339
338
  ```
340
339
 
341
- 핵심 구현은 `private_pty/`의 PTY·VT 버퍼·세션 registry·Room/Seat·실행 저널·Rich TUI와
342
- `office_backend_bridge.py`의 인증 HTTP API입니다. 런타임 데이터와 개인 설정은 배포에 포함하지
343
- 않습니다. 정적 웹 오피스(`company_office_mockup.html` 등)는 제거되었고 `/agency-agents/`의
344
- manifest identity는 유지합니다.
340
+ 비교 테스트는 고정된 0.5.108 Git 소스를 테스트 캐시에 읽습니다. Git에 해당 커밋의
341
+ 객체와 테스트용 Python이 필요하며 제품 실행에는 필요하지 않습니다. 자세한 조건은
342
+ `rust/test_support/python_oracle_root.rs`에 있습니다. 과거 Python 엔진 자체를 검증하던
343
+ 테스트는 native 제품의 qualification 결과로 집계하지 않습니다.
344
+
345
+ 런타임 데이터와 개인 설정은 배포에 포함하지 않습니다. `/agency-agents/` manifest
346
+ identity와 기존 Room·Seat·기록·인증 계약은 유지합니다.
345
347
  원격 웹 접속의 origin·신뢰 프록시는 `STACKBITE_ALLOWED_ORIGINS`·`STACKBITE_TRUSTED_PROXY_IPS`로
346
348
  명시합니다. `X-Office-Token` 인증은 로컬 요청에도 필요합니다.
347
349
 
348
- ### Python 테스트 스위트의 호스트 선행조건
350
+ ### 개발용 비교 테스트의 호스트 선행조건
349
351
 
350
352
  `tests/host_prerequisites.py`가 스위트가 전제하는 호스트 도구의 단일 표입니다. 빠진 항목은 pytest
351
353
  시작 시 한 번 보고되고, 그 항목이 필요한 테스트만 이유를 밝히며 건너뜁니다(Windows는 건너뛰지 않고
@@ -362,7 +364,7 @@ Linux 검증 명령(4 CPU 기준). 선행조건을 모두 갖춘 호스트에서
362
364
  건너뜀 없이 실행했음을 확인합니다.
363
365
 
364
366
  ```sh
365
- python -m pytest tests -q -p no:randomly -p no:cacheprovider -n 4 --dist loadfile --timeout=600
367
+ python -m pytest tests/test_native_bundle_builder.py tests/test_build_macos_release.py tests/test_linux_release_packaging.py -q
366
368
  ```
367
369
 
368
370
  프로세스 안에서 도는 테스트는 `/etc/machine-id`가 없으면 스위트가 만든 임시 식별자를 쓰지만, 별도
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stackbite",
3
- "version": "0.5.107",
3
+ "version": "0.5.109",
4
4
  "description": "Verified native Stackbite CLI bootstrapper",
5
5
  "license": "UNLICENSED",
6
6
  "engines": {
@@ -505,6 +505,46 @@ function previousCacheHome(home, cache, platform) {
505
505
  return typeof home === 'string' && path.isAbsolute(home) && /^v1-[a-f0-9]{64}$/.test(path.basename(home))
506
506
  && !!cache && spelling(path.dirname(path.resolve(home))) === spelling(cache);
507
507
  }
508
+ // Bounded portable.json of an image; null when absent, linked, oversized or malformed.
509
+ async function portableMetadata(root) {
510
+ try { return JSON.parse((await readSmall(path.join(root, 'portable.json'), 16384)).toString('utf8')); }
511
+ catch { return null; }
512
+ }
513
+ // The release identity Python's former-cache check compares (Windows may omit platform).
514
+ function portableMatches(value, spec) {
515
+ return !!value && typeof value === 'object' && value.product === 'Stackbite' && value.command === 'stackbite'
516
+ && value.version === spec.version && value.commit === spec.commit
517
+ && (Object.hasOwn(value, 'platform') ? value.platform : spec.platform === 'win32' ? spec.host : undefined) === spec.host;
518
+ }
519
+ // An absolute literal path with no linked ancestor and no source repository.
520
+ async function literalPath(home) {
521
+ if (!path.isAbsolute(home) || path.resolve(home) !== home) return false;
522
+ for (let ancestor = home; ; ancestor = path.dirname(ancestor)) {
523
+ let info = null;
524
+ try { info = await fs.lstat(ancestor); } catch (error) { if (error.code !== 'ENOENT') return false; }
525
+ if (info?.isSymbolicLink() || info && await exists(path.join(ancestor, '.git'))) return false;
526
+ if (ancestor === path.dirname(ancestor)) return true;
527
+ }
528
+ }
529
+ // A previous-layout cache image recorded by a host with another cache root:
530
+ // the shape and portable identity Python's _former_cache_image accepts. This
531
+ // classifies the file only; no environment state changes it. It is only read.
532
+ async function formerCache(home, spec) {
533
+ if (typeof home !== 'string' || !/^v1-[a-f0-9]{64}$/.test(path.basename(home))
534
+ || path.basename(path.dirname(home)) !== 'stackbite') return null;
535
+ try {
536
+ if (!await literalPath(home) || !(await fs.lstat(home)).isDirectory()) return null;
537
+ } catch { return null; }
538
+ return portableMatches(await portableMetadata(home), spec) ? home : null;
539
+ }
540
+ // A former cache selection this run may migrate: the engine moves a selection
541
+ // only under its own state, so a per-run STACKBITE_STATE_DIR never does. The
542
+ // engine reads a legacy HELM_STATE_DIR only when STACKBITE_STATE_DIR is absent
543
+ // (even empty, the canonical name wins). It decides again under its locks.
544
+ async function formerSelection(saved, environment, spec) {
545
+ const state = Object.hasOwn(environment, 'STACKBITE_STATE_DIR') ? environment.STACKBITE_STATE_DIR : environment.HELM_STATE_DIR;
546
+ return state && path.resolve(state) !== saved?.state ? null : formerCache(saved?.home, spec);
547
+ }
508
548
  async function publishCache(payload, target, spec, options) {
509
549
  await assertBaseline(options.environment, spec.platform);
510
550
  let migrate = false;
@@ -515,7 +555,8 @@ async function publishCache(payload, target, spec, options) {
515
555
  if (!await exists(locator)) continue;
516
556
  const saved = JSON.parse((await readSmall(locator, 16384)).toString('utf8').replace(/^\uFEFF/, ''));
517
557
  const descriptor = options.environment.STACKBITE_NPM_REGISTRATION;
518
- migrate = previousCacheHome(saved.home, options.cache, spec.platform);
558
+ migrate = previousCacheHome(saved.home, options.cache, spec.platform)
559
+ || !!await formerSelection(saved, options.environment, spec);
519
560
  if (!migrate && typeof saved.home === 'string' && path.isAbsolute(saved.home)
520
561
  && typeof saved.state === 'string' && path.isAbsolute(saved.state) && /^[a-f0-9]{32}$/.test(saved.installation_id)
521
562
  && (!descriptor || JSON.stringify(saved.npm_registration) === JSON.stringify(JSON.parse(descriptor)))) return;
@@ -533,15 +574,73 @@ async function publishCache(payload, target, spec, options) {
533
574
  function runtimeDirectory(environment, platform) {
534
575
  return path.join(installationRoot(environment, platform), 'app');
535
576
  }
536
- // The previous layout's image of this exact pinned identity, when it still
537
- // passes the full pinned validation. It is only read; a live process may use it.
577
+ // This root's first locator, when it fully names an installation: the same
578
+ // shape publishCache keeps without rewriting its home.
579
+ async function establishedLocator(root) {
580
+ for (const name of ['default-install.json', 'install.json']) {
581
+ const locator = path.join(root, name);
582
+ if (!await exists(locator)) continue;
583
+ const saved = JSON.parse((await readSmall(locator, 16384)).toString('utf8').replace(/^\uFEFF/, ''));
584
+ return saved && typeof saved.home === 'string' && path.isAbsolute(saved.home)
585
+ && typeof saved.state === 'string' && path.isAbsolute(saved.state)
586
+ && /^[a-f0-9]{32}$/.test(saved.installation_id) ? saved : null;
587
+ }
588
+ return null;
589
+ }
590
+ async function installationLocator(environment, platform) {
591
+ const root = installationRoot(environment, platform);
592
+ return await selectedDirectory(root, environment, platform) ? establishedLocator(root) : null;
593
+ }
594
+ // The image root when its portable identity is this release; null otherwise.
595
+ async function sameRelease(home, spec) {
596
+ let root;
597
+ try { root = await payloadRoot(home); } catch { return null; }
598
+ return portableMatches(await portableMetadata(root), spec) ? root : null;
599
+ }
600
+ // Another archive of this release (a re-signed build) carries its own valid
601
+ // checksum manifest that pins its portable.json. A damaged image does not.
602
+ async function anotherPin(root) {
603
+ try {
604
+ const hashes = JSON.parse((await readSmall(path.join(root, 'SHA256SUMS.json'))).toString('utf8'));
605
+ expectedMembers(hashes);
606
+ const metadata = await readSmall(path.join(root, 'portable.json'), 16384);
607
+ return hashes['portable.json'] === createHash('sha256').update(metadata).digest('hex');
608
+ } catch { return false; }
609
+ }
610
+ // An image the established installation already selects outside app/: an
611
+ // explicit STACKBITE_HOME, else the locator's home. Another release or pin is
612
+ // skipped; an image of this release must otherwise carry this package's pinned
613
+ // documents, and the caller then validates its whole payload, so a damaged image
614
+ // of this release is refused, not replaced. Publication keeps the locator's
615
+ // home, except that a cache image (this host's or a former host's) is never run
616
+ // in place.
617
+ async function selectedImage(environment, spec, cache) {
618
+ const saved = await installationLocator(environment, spec.platform);
619
+ // A previous-cache selection keeps its existing migration path.
620
+ if (!saved || previousCacheHome(saved.home, cache, spec.platform)) return null;
621
+ const former = await formerCache(saved.home, spec);
622
+ for (const home of [environment.STACKBITE_HOME, saved.home]) {
623
+ if (typeof home !== 'string' || !path.isAbsolute(home) || home === former) continue;
624
+ const root = await sameRelease(home, spec);
625
+ if (!root) continue;
626
+ try { await pinnedDocuments(root, spec); }
627
+ catch (error) { if (await anotherPin(root)) continue; throw error; }
628
+ return path.resolve(home);
629
+ }
630
+ return null;
631
+ }
632
+ // An image of this exact pinned identity, when it passes the full pinned
633
+ // validation. It is only read; a live process may use it.
634
+ async function verifiedImage(home, spec) {
635
+ if (!home) return null;
636
+ try { await validatePayload(home, spec); } catch { return null; }
637
+ return home;
638
+ }
538
639
  async function previousImage(legacy, cache, spec) {
539
- if (!await existingCacheDirectory(cache) || !await exists(legacy)) return null;
540
- try { await validatePayload(legacy, spec); } catch { return null; }
541
- return legacy;
640
+ return await existingCacheDirectory(cache) && await exists(legacy) ? verifiedImage(legacy, spec) : null;
542
641
  }
543
642
  async function ensureBinary(spec, { cache = cacheDirectory(), runtime = null, downloadImpl = download, extractImpl = extractArchive,
544
- checkOnly = false, reportCached = false, includePhone = false, progress = () => {},
643
+ checkOnly = false, reportCached = false, includePhone = false, reuseSelected = false, progress = () => {},
545
644
  environment = process.env, publishImpl = publishCache, registrationSource = null, ...options } = {}) {
546
645
  cache = await resolveCacheDirectory(cache);
547
646
  environment = { ...environment };
@@ -560,18 +659,25 @@ async function ensureBinary(spec, { cache = cacheDirectory(), runtime = null, do
560
659
  if (registrationSource) await registrationSource.validate();
561
660
  if (!checkOnly) await assertBaseline(environment, spec.platform);
562
661
  // Check-only callers (removal, setup --check) never publish; they may still
563
- // use an image of the previous cache layout.
564
- const home = await selectedDirectory(runtime, environment, spec.platform) && await exists(target) ? target
565
- : checkOnly && await existingCacheDirectory(cache) && await exists(legacy) ? legacy : null;
662
+ // use an image of the previous cache layout, this host's or a former host's.
663
+ const published = await selectedDirectory(runtime, environment, spec.platform) && await exists(target);
664
+ const former = !published && reuseSelected
665
+ ? await formerCache((await installationLocator(environment, spec.platform))?.home, spec) : null;
666
+ const home = published ? target
667
+ : reuseSelected && await selectedImage(environment, spec, cache)
668
+ || (checkOnly ? (await existingCacheDirectory(cache) && await exists(legacy) ? legacy : former) : null);
566
669
  if (home) {
567
670
  if (checkOnly || reportCached) progress('validate');
568
671
  const installed = { home, executable: await validatePayload(home, spec, { includePhone }) };
569
- if (!checkOnly) await publishImpl(target, target, spec, { ...options, environment, cache, existing: true });
672
+ // A selected image outside app/ is its own maintenance engine.
673
+ if (!checkOnly) await publishImpl(home, target, spec, { ...options, environment, cache, existing: true });
570
674
  if (checkOnly || reportCached) progress('ready');
571
675
  return installed;
572
676
  }
573
677
  if (checkOnly) throw notCached();
574
- const previous = await previousImage(legacy, cache, spec);
678
+ // A verified previous-layout image is copied once into the durable target;
679
+ // the engine then moves a cache selection there.
680
+ const previous = await previousImage(legacy, cache, spec) || await verifiedImage(former, spec);
575
681
  if (previous) {
576
682
  progress('publish');
577
683
  await admitPublication(environment, spec.platform);
@@ -615,9 +721,11 @@ async function stageRelease(spec, staging, { downloadImpl, extractImpl, progress
615
721
  await fs.mkdir(extraction, { mode: 0o700 });
616
722
  progress('download');
617
723
  await downloadImpl(spec, archive);
618
- // Recheck at the extraction boundary, including for injected test transports.
724
+ // download() itself verifies the exact size and SHA-256 before returning.
725
+ // Recheck any other transport at the extraction boundary.
619
726
  progress('archive');
620
- if ((await fs.stat(archive)).size !== spec.size || await hashFile(archive, LIMITS.download) !== spec.sha256) fail('Pinned archive SHA-256 or size mismatch');
727
+ if (downloadImpl !== download && ((await fs.stat(archive)).size !== spec.size
728
+ || await hashFile(archive, LIMITS.download) !== spec.sha256)) fail('Pinned archive SHA-256 or size mismatch');
621
729
  progress('extract');
622
730
  await extractImpl(archive, extraction, spec);
623
731
  const payload = path.join(extraction, spec.root);
@@ -718,7 +826,7 @@ async function main(argv = process.argv.slice(2), options = {}) {
718
826
  try { installed = await timing.phase(environment, 'launcher.ensure', () => ensureBinary(spec, { ...options,
719
827
  environment, registrationSource,
720
828
  cache: options.cache || cacheDirectory(environment, platform),
721
- checkOnly: removing || setup?.checkOnly || false, reportCached: Boolean(setup),
829
+ checkOnly: removing || setup?.checkOnly || false, reportCached: Boolean(setup), reuseSelected: !removing,
722
830
  includePhone: removing || Boolean(setup) || argv[0] === 'phone',
723
831
  progress: options.progress || stageReporter(options.stderr || process.stderr) })); }
724
832
  catch (error) {
@@ -1,35 +1,35 @@
1
1
  {
2
2
  "schema": 1,
3
- "version": "0.5.107",
4
- "commit": "506288e3f01c20dd709905c95eaa46ad04471b17",
3
+ "version": "0.5.109",
4
+ "commit": "6239b4525c8addd92996c32d14fe4ff987f2106c",
5
5
  "repository": "Shinick-Han/stackbite",
6
6
  "platforms": {
7
7
  "win32-x64": {
8
- "asset": "stackbite-0.5.107-windows-x64-506288e3f01c.zip",
9
- "url": "https://github.com/Shinick-Han/stackbite/releases/download/v0.5.107/stackbite-0.5.107-windows-x64-506288e3f01c.zip",
10
- "sha256": "6da273fbd3805fff41c3dc1346c39eb7290f4969b30e5f14124a9f9948659701",
11
- "size": 96379204,
12
- "root": "stackbite-0.5.107-windows-x64-506288e3f01c",
13
- "manifestSha256": "fb81b39e291be6bd012b2d1e481f38aa506e38681ea463ddb55dd21bfeab309a",
14
- "metadataSha256": "871eed20e507d284102373ce50e586919cf0ed3b5a791b6afa9d8cc7df762604"
8
+ "asset": "stackbite-0.5.109-windows-x64-6239b4525c8a.zip",
9
+ "url": "https://github.com/Shinick-Han/stackbite/releases/download/v0.5.109/stackbite-0.5.109-windows-x64-6239b4525c8a.zip",
10
+ "sha256": "bf854cc3c0ea1c571bf030ee4dcdd3280ebf7fc51842566ae7c84806fb4a2648",
11
+ "size": 75620079,
12
+ "root": "stackbite-0.5.109-windows-x64-6239b4525c8a",
13
+ "manifestSha256": "86d1b0083e8bb24c6d7807323c6dd0367f218df3f2f70b6865853e87498ef0f1",
14
+ "metadataSha256": "729775b4b8f527380f07dca18134bb0337e40630b654d7343b474bc7a351590d"
15
15
  },
16
16
  "linux-x64": {
17
- "asset": "stackbite-0.5.107-linux-x86_64-506288e3f01c.tar.gz",
18
- "url": "https://github.com/Shinick-Han/stackbite/releases/download/v0.5.107/stackbite-0.5.107-linux-x86_64-506288e3f01c.tar.gz",
19
- "sha256": "74e451669bd460b690dc45d4abe9f7bbe6af10d0ab56519c5c4a7c0ff55b2350",
20
- "size": 77586426,
21
- "root": "stackbite-0.5.107-linux-x86_64-506288e3f01c",
22
- "manifestSha256": "a3030ce79fe2b84314064f7a5f5794e744fe8d16cfa223d467a8670cdc957e15",
23
- "metadataSha256": "9df20d2f675d4e4457a060d07b1b821146d9dcfaa92a532960cb8c6b39239d35"
17
+ "asset": "stackbite-0.5.109-linux-x86_64-6239b4525c8a.tar.gz",
18
+ "url": "https://github.com/Shinick-Han/stackbite/releases/download/v0.5.109/stackbite-0.5.109-linux-x86_64-6239b4525c8a.tar.gz",
19
+ "sha256": "c158d9682780799d411de03d669d95f040f1b7eedf243fb2322bf12ebb58eaed",
20
+ "size": 76560731,
21
+ "root": "stackbite-0.5.109-linux-x86_64-6239b4525c8a",
22
+ "manifestSha256": "ceda4e9e9375668273bc06bd8655c5a121ff7e8a8839ed6dab5ac00a0135f6b0",
23
+ "metadataSha256": "ecc12703927e7b16584fc5ef72bd0ba73f8504bacbcd1dc9750517f58578aab1"
24
24
  },
25
25
  "darwin-arm64": {
26
- "asset": "stackbite-0.5.107-macos-arm64-506288e3f01c.tar.gz",
27
- "url": "https://github.com/Shinick-Han/stackbite/releases/download/v0.5.107/stackbite-0.5.107-macos-arm64-506288e3f01c.tar.gz",
28
- "sha256": "ddf4c956c7657df6bb4684aa3f7a9d677144cca2deb4e425e3a6442b88671262",
29
- "size": 33557501,
30
- "root": "stackbite-0.5.107-macos-arm64-506288e3f01c",
31
- "manifestSha256": "4c2ab4d509a2fb1a48696c820b8c3f8cadea9441817e314a595ab8b08f34cef3",
32
- "metadataSha256": "e07aedd09b464458bef021518136bbcfb3d9b521f72e1da991b6d1091cccaed3"
26
+ "asset": "stackbite-0.5.109-macos-arm64-6239b4525c8a.tar.gz",
27
+ "url": "https://github.com/Shinick-Han/stackbite/releases/download/v0.5.109/stackbite-0.5.109-macos-arm64-6239b4525c8a.tar.gz",
28
+ "sha256": "c072020d6f1571cbe6fe2edc9dbc91bbd1e16559629d6cacfa4922a892039932",
29
+ "size": 58348881,
30
+ "root": "stackbite-0.5.109-macos-arm64-6239b4525c8a",
31
+ "manifestSha256": "499e31056ff33004a101f5ea0b8d113b276e33c82520c4fbbcc50108d0af652c",
32
+ "metadataSha256": "ca1457292980aa202810185af7635321b9d35712e954ad29128990312affa474"
33
33
  }
34
34
  }
35
35
  }