superbee 0.4.2-pre.3 → 0.4.2

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/dist/superbee.mjs CHANGED
@@ -44,7 +44,7 @@ var __toESM = (mod, isNodeMode, target3) => (target3 = mod != null ? __create(__
44
44
  var define_SUPERBEE_BUILD_IDENTITY_default;
45
45
  var init_define_SUPERBEE_BUILD_IDENTITY = __esm({
46
46
  "<define:__SUPERBEE_BUILD_IDENTITY__>"() {
47
- define_SUPERBEE_BUILD_IDENTITY_default = { schema: "superbee.build-identity.v1", package: { name: "superbee", version: "0.4.2-pre.3" }, source: { commit: "ab47b44609c0dbf90257f5ffc2d739915818b719", dirty: false }, artifact: { channel: "npm-package" }, compatibility_contracts: { skill: 1, hook: 1, mcp: 1 } };
47
+ define_SUPERBEE_BUILD_IDENTITY_default = { schema: "superbee.build-identity.v1", package: { name: "superbee", version: "0.4.2" }, source: { commit: "069a42092f39d8261ccd4322c0dc7ff5488aa2c8", dirty: false }, artifact: { channel: "npm-package" }, compatibility_contracts: { skill: 1, hook: 1, mcp: 1 } };
48
48
  }
49
49
  });
50
50
 
@@ -12923,8 +12923,7 @@ function isConceptDocPath(relPath) {
12923
12923
  return relPath.endsWith(".md") && !isReservedFile(relPath);
12924
12924
  }
12925
12925
  function nameStatusRows(out) {
12926
- const fields2 = out.split("\0");
12927
- if (fields2.length > 0 && fields2[fields2.length - 1] === "") fields2.pop();
12926
+ const fields2 = nulFields(out);
12928
12927
  const rows = [];
12929
12928
  for (let i = 0; i + 1 < fields2.length; i += 2) {
12930
12929
  const letter = (fields2[i] ?? "").trim().charAt(0);
@@ -13004,13 +13003,8 @@ ${bodyLines2.join("\n")}
13004
13003
  return { committed: true, sha, subject, docs };
13005
13004
  }
13006
13005
  function malformedOutgoingDocuments(boardPath) {
13007
- const listed2 = runGit(boardPath, ["status", "--porcelain=v1", "-z", "--untracked-files=all", "--no-renames"]);
13008
- if (listed2.status !== 0) throw classifyGitError(failureOf(["status"], listed2));
13009
13006
  const held2 = [];
13010
- for (const record6 of listed2.stdout.split("\0")) {
13011
- if (record6.length < 4) continue;
13012
- const code3 = record6.slice(0, 2);
13013
- const relPath = record6.slice(3);
13007
+ for (const { status: code3, path: relPath } of readStatusRows(boardPath, { allUntracked: true })) {
13014
13008
  if (code3.includes("D") || !isConceptDocPath(relPath)) continue;
13015
13009
  const file2 = path7.join(boardPath, relPath);
13016
13010
  const content3 = readRegularFile(file2);
@@ -13239,7 +13233,6 @@ function fetchRebaseResolving(boardPath, exportDir) {
13239
13233
  const r2 = runGit(boardPath, [...idFlags, "rebase", boardRef], { rebase: true, timeoutMs: NETWORK_TIMEOUT_MS });
13240
13234
  if (r2.status === 0) return { status: "clean" };
13241
13235
  if (!detectStaleRebase(boardPath)) throw classifyGitError(failureOf(["rebase", boardRef], r2));
13242
- const listConflicted = () => mustGit(boardPath, ["diff", "--name-only", "-z", "--diff-filter=U"]).split("\0").filter((l) => l.length > 0);
13243
13236
  const byPath = /* @__PURE__ */ new Map();
13244
13237
  try {
13245
13238
  let stops = 0;
@@ -13250,7 +13243,7 @@ function fetchRebaseResolving(boardPath, exportDir) {
13250
13243
  `sync's converging rebase did not terminate after ${MAX_REBASE_STOPS} stops \u2014 aborting to leave the board unchanged`
13251
13244
  );
13252
13245
  }
13253
- const conflicted = listConflicted();
13246
+ const conflicted = conflictedPaths(boardPath);
13254
13247
  if (conflicted.length === 0) {
13255
13248
  runGit(boardPath, [...idFlags, "rebase", "--skip"], { rebase: true });
13256
13249
  continue;
@@ -13306,13 +13299,8 @@ function fetchRebaseResolving(boardPath, exportDir) {
13306
13299
  return { status: "resolved", conflicts: [...byPath.values()] };
13307
13300
  }
13308
13301
  function privateStateInOutgoingWorktree(boardPath) {
13309
- const listed2 = runGit(boardPath, ["status", "--porcelain=v1", "-z", "--untracked-files=all", "--no-renames"]);
13310
- if (listed2.status !== 0) throw classifyGitError(failureOf(["status"], listed2));
13311
13302
  const found = [];
13312
- for (const record6 of listed2.stdout.split("\0")) {
13313
- if (record6.length < 4) continue;
13314
- const code3 = record6.slice(0, 2);
13315
- const relPath = record6.slice(3);
13303
+ for (const { status: code3, path: relPath } of readStatusRows(boardPath, { allUntracked: true })) {
13316
13304
  if (code3.includes("D")) continue;
13317
13305
  const finding = privateStateFinding(relPath, readPrivateStateCandidate(path7.join(boardPath, relPath)));
13318
13306
  if (finding) found.push(finding);
@@ -13532,10 +13520,42 @@ function currentBranch(top) {
13532
13520
  const r2 = runGit(top, ["rev-parse", "--abbrev-ref", "HEAD"]);
13533
13521
  return r2.status === 0 ? r2.stdout.trim() : "HEAD";
13534
13522
  }
13523
+ function nulFields(out) {
13524
+ const fields2 = out.split("\0");
13525
+ if (fields2.at(-1) === "") fields2.pop();
13526
+ return fields2;
13527
+ }
13528
+ function readStatusRows(dir, opts = {}) {
13529
+ const args = [
13530
+ "status",
13531
+ "--porcelain=v1",
13532
+ "-z",
13533
+ ...opts.renamePolicy === "configured" ? [] : ["--no-renames"],
13534
+ ...opts.allUntracked ? ["--untracked-files=all"] : [],
13535
+ ...opts.prefix ? ["--", opts.prefix] : []
13536
+ ];
13537
+ const r2 = runGit(dir, args);
13538
+ if (r2.status !== 0) {
13539
+ if (opts.tolerateFailure) return [];
13540
+ throw classifyGitError(failureOf(args, r2));
13541
+ }
13542
+ const fields2 = nulFields(r2.stdout);
13543
+ const rows = [];
13544
+ for (let i = 0; i < fields2.length; i += 1) {
13545
+ const record6 = fields2[i];
13546
+ if (record6.length < 4) continue;
13547
+ const status2 = record6.slice(0, 2).trim();
13548
+ const row4 = { status: status2, path: record6.slice(3) };
13549
+ if (status2.includes("R") || status2.includes("C")) row4.originalPath = fields2[++i];
13550
+ rows.push(row4);
13551
+ }
13552
+ return rows;
13553
+ }
13554
+ function conflictedPaths(dir) {
13555
+ return nulFields(mustGit(dir, ["diff", "--name-only", "--no-renames", "-z", "--diff-filter=U"]));
13556
+ }
13535
13557
  function statusRows(dir, prefix) {
13536
- const r2 = runGit(dir, ["status", "--porcelain", ...prefix ? ["--", prefix] : []]);
13537
- if (r2.status !== 0) return [];
13538
- return r2.stdout.split("\n").map((l) => l.trimEnd()).filter((l) => l.length > 0).map((l) => ({ status: l.slice(0, 2).trim(), path: l.slice(3) }));
13558
+ return readStatusRows(dir, { prefix, tolerateFailure: true, renamePolicy: "configured" });
13539
13559
  }
13540
13560
  function countUncommitted(boardPath, prefix) {
13541
13561
  return statusRows(boardPath, prefix).length;
@@ -21506,7 +21526,7 @@ var init_nofollow_read = __esm({
21506
21526
  }
21507
21527
  });
21508
21528
 
21509
- // ../cli/src/hosted/export-archive.ts
21529
+ // ../core/src/portable-export.ts
21510
21530
  import { createHash as createHash6 } from "node:crypto";
21511
21531
  function crc32(bytes) {
21512
21532
  let crc = 4294967295;
@@ -21645,7 +21665,7 @@ function verifyExport(archive, bundleId, options = {}) {
21645
21665
  }
21646
21666
  if (!isRecord7(manifest)) throw new ExportArchiveError("manifest_mismatch", "the manifest is not an object");
21647
21667
  if (manifest.format !== EXPORT_FORMAT) {
21648
- throw new ExportArchiveError("unsupported_format", `the export format is ${JSON.stringify(manifest.format)}; this CLI reads ${EXPORT_FORMAT}`);
21668
+ throw new ExportArchiveError("unsupported_format", `the export format is ${JSON.stringify(manifest.format)}; this reader accepts ${EXPORT_FORMAT}`);
21649
21669
  }
21650
21670
  const source = manifest.source;
21651
21671
  const counts = manifest.counts;
@@ -21712,25 +21732,81 @@ function verifyExport(archive, bundleId, options = {}) {
21712
21732
  if (collision) {
21713
21733
  throw new ExportArchiveError("unsafe_path", `the archive holds paths one folder cannot keep apart: ${JSON.stringify(collision[0])} and ${JSON.stringify(collision[1])}`);
21714
21734
  }
21715
- return {
21735
+ return rememberVerified({
21716
21736
  source: { tenantId: source.tenantId, bundleId: source.bundleId, revision: source.revision, okfEdition: source.okfEdition },
21717
21737
  exportedAt: manifest.exportedAt,
21718
21738
  counts: tally,
21719
21739
  entries,
21720
21740
  bytes,
21721
21741
  ...page ? { page } : {}
21742
+ }, [{ sha256: sha256Hex2(manifests[0].data), bytes: manifests[0].data.byteLength }], archive.byteLength);
21743
+ }
21744
+ function inventoryOf(verified2) {
21745
+ return verified2.entries.map((entry) => ({
21746
+ path: entry.path,
21747
+ kind: entry.kind,
21748
+ bytes: entry.bytes.byteLength,
21749
+ sha256: sha256Hex2(entry.bytes)
21750
+ })).sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
21751
+ }
21752
+ function fingerprintOf(verified2) {
21753
+ return JSON.stringify({
21754
+ source: verified2.source,
21755
+ exportedAt: verified2.exportedAt,
21756
+ counts: verified2.counts,
21757
+ bytes: verified2.bytes,
21758
+ page: verified2.page,
21759
+ entries: verified2.entries.map((entry) => ({
21760
+ path: entry.path,
21761
+ kind: entry.kind,
21762
+ bytes: entry.bytes.byteLength,
21763
+ sha256: sha256Hex2(entry.bytes)
21764
+ }))
21765
+ });
21766
+ }
21767
+ function rememberVerified(verified2, manifests, archiveBytes) {
21768
+ const snapshot2 = {
21769
+ source: { ...verified2.source },
21770
+ exportedAt: verified2.exportedAt,
21771
+ counts: { ...verified2.counts },
21772
+ entries: verified2.entries.map((entry) => ({ ...entry, bytes: Uint8Array.from(entry.bytes) })),
21773
+ bytes: verified2.bytes,
21774
+ ...verified2.page ? { page: { ...verified2.page, counts: { ...verified2.page.counts } } } : {}
21775
+ };
21776
+ const state = {
21777
+ snapshot: snapshot2,
21778
+ archiveBytes,
21779
+ fingerprint: fingerprintOf(verified2),
21780
+ provenance: {
21781
+ source: { ...verified2.source },
21782
+ exportedAt: verified2.exportedAt,
21783
+ manifests: manifests.map((manifest) => ({ ...manifest })),
21784
+ inventorySha256: sha256Hex2(new TextEncoder().encode(JSON.stringify(inventoryOf(verified2))))
21785
+ }
21722
21786
  };
21787
+ verifiedStates.set(verified2, state);
21788
+ verifiedStates.set(snapshot2, state);
21789
+ return verified2;
21723
21790
  }
21724
- var EXPORT_MANIFEST, IN_PLACE_STAGING, IN_PLACE_JOURNAL, EXPORT_FORMAT, MAX_EXPORT_ENTRIES, MAX_EXPORT_BYTES, LOCAL, CENTRAL, END, UTF8_NAMES, ExportArchiveError, CRC_TABLE, sha256Hex2, utf8, WINDOWS_DEVICE_NAMES, fold3, ExportPageChain;
21725
- var init_export_archive = __esm({
21726
- "../cli/src/hosted/export-archive.ts"() {
21791
+ function exportProvenance(verified2) {
21792
+ const state = verifiedStates.get(verified2);
21793
+ if (!state || state.fingerprint !== fingerprintOf(verified2)) {
21794
+ throw new ExportArchiveError("manifest_mismatch", "the verified export handle is absent or changed");
21795
+ }
21796
+ return {
21797
+ ...state.provenance,
21798
+ source: { ...state.provenance.source },
21799
+ manifests: state.provenance.manifests.map((manifest) => ({ ...manifest }))
21800
+ };
21801
+ }
21802
+ var EXPORT_MANIFEST, EXPORT_FORMAT, MAX_EXPORT_ENTRIES, MAX_EXPORT_BYTES, LOCAL, CENTRAL, END, UTF8_NAMES, ExportArchiveError, CRC_TABLE, sha256Hex2, utf8, WINDOWS_DEVICE_NAMES, fold3, ExportPageChain, verifiedStates;
21803
+ var init_portable_export = __esm({
21804
+ "../core/src/portable-export.ts"() {
21727
21805
  "use strict";
21728
21806
  init_define_SUPERBEE_BUILD_IDENTITY();
21729
21807
  init_define_SUPERBEE_UPDATE_POLICY();
21730
- init_src();
21808
+ init_paths();
21731
21809
  EXPORT_MANIFEST = "superbee-export.json";
21732
- IN_PLACE_STAGING = ".superbee-export-partial";
21733
- IN_PLACE_JOURNAL = "journal.json";
21734
21810
  EXPORT_FORMAT = "superbee-export/1";
21735
21811
  MAX_EXPORT_ENTRIES = 20001;
21736
21812
  MAX_EXPORT_BYTES = 128 * 1024 * 1024;
@@ -21763,17 +21839,23 @@ var init_export_archive = __esm({
21763
21839
  #pages = [];
21764
21840
  #from = 0;
21765
21841
  #ended = false;
21842
+ #archiveBytes = 0;
21766
21843
  /** Adds the next page; answers its next cursor, or undefined once the export is whole. */
21767
21844
  add(verified2) {
21768
- const page = verified2.page;
21769
- const first = this.#pages[0] ?? verified2;
21845
+ exportProvenance(verified2);
21846
+ const state = verifiedStates.get(verified2);
21847
+ const accepted = state.snapshot;
21848
+ const page = accepted.page;
21849
+ const first = this.#pages[0] ?? accepted;
21770
21850
  if (!page || !first.page || this.#ended) throw new ExportArchiveError("manifest_mismatch", "a paged export answered a page outside its run");
21771
21851
  const counts = first.page.counts;
21772
- if (verified2.source.tenantId !== first.source.tenantId || verified2.source.bundleId !== first.source.bundleId || verified2.source.revision !== first.source.revision || verified2.source.okfEdition !== first.source.okfEdition || page.counts.documents !== counts.documents || page.counts.reserved !== counts.reserved || page.counts.blobs !== counts.blobs || page.total !== first.page.total || page.total !== counts.documents + counts.reserved + counts.blobs || page.total > MAX_EXPORT_ENTRIES - 1) {
21852
+ if (accepted.source.tenantId !== first.source.tenantId || accepted.source.bundleId !== first.source.bundleId || accepted.source.revision !== first.source.revision || accepted.source.okfEdition !== first.source.okfEdition || page.counts.documents !== counts.documents || page.counts.reserved !== counts.reserved || page.counts.blobs !== counts.blobs || page.total !== first.page.total || page.total !== counts.documents + counts.reserved + counts.blobs || page.total > MAX_EXPORT_ENTRIES - 1) {
21773
21853
  throw new ExportArchiveError("manifest_mismatch", "the pages do not describe one export of one revision");
21774
21854
  }
21775
21855
  if (page.from !== this.#from) throw new ExportArchiveError("manifest_mismatch", "the pages leave a gap or overlap");
21776
- this.#pages.push(verified2);
21856
+ if (state.archiveBytes > MAX_EXPORT_BYTES - this.#archiveBytes) throw malformed2("the pages are larger than an export carries");
21857
+ this.#archiveBytes += state.archiveBytes;
21858
+ this.#pages.push(accepted);
21777
21859
  this.#from += page.count;
21778
21860
  if (page.next === void 0) this.#ended = true;
21779
21861
  return page.next;
@@ -21788,7 +21870,7 @@ var init_export_archive = __esm({
21788
21870
  let bytes = 0;
21789
21871
  for (const verified2 of this.#pages) {
21790
21872
  for (const entry of verified2.entries) {
21791
- entries.push(entry);
21873
+ entries.push({ ...entry, bytes: Uint8Array.from(entry.bytes) });
21792
21874
  tally[entry.kind === "document" ? "documents" : entry.kind === "reserved" ? "reserved" : "blobs"] += 1;
21793
21875
  bytes += entry.bytes.byteLength;
21794
21876
  }
@@ -21806,9 +21888,23 @@ var init_export_archive = __esm({
21806
21888
  if (collision) {
21807
21889
  throw new ExportArchiveError("unsafe_path", `the archive holds paths one folder cannot keep apart: ${JSON.stringify(collision[0])} and ${JSON.stringify(collision[1])}`);
21808
21890
  }
21809
- return { source: first.source, exportedAt: first.exportedAt, counts: tally, entries, bytes };
21891
+ return rememberVerified({ source: { ...first.source }, exportedAt: first.exportedAt, counts: tally, entries, bytes }, this.#pages.flatMap((verified2) => exportProvenance(verified2).manifests), this.#archiveBytes);
21810
21892
  }
21811
21893
  };
21894
+ verifiedStates = /* @__PURE__ */ new WeakMap();
21895
+ }
21896
+ });
21897
+
21898
+ // ../cli/src/hosted/export-archive.ts
21899
+ var IN_PLACE_STAGING, IN_PLACE_JOURNAL;
21900
+ var init_export_archive = __esm({
21901
+ "../cli/src/hosted/export-archive.ts"() {
21902
+ "use strict";
21903
+ init_define_SUPERBEE_BUILD_IDENTITY();
21904
+ init_define_SUPERBEE_UPDATE_POLICY();
21905
+ init_portable_export();
21906
+ IN_PLACE_STAGING = ".superbee-export-partial";
21907
+ IN_PLACE_JOURNAL = "journal.json";
21812
21908
  }
21813
21909
  });
21814
21910
 
@@ -26582,10 +26678,12 @@ function chunked(items, size) {
26582
26678
  for (let start = 0; start < items.length; start += size) out.push(items.slice(start, start + size));
26583
26679
  return out;
26584
26680
  }
26585
- function concurrencyOf(options) {
26586
- const value = options.concurrency ?? DEFAULT_CONCURRENCY;
26587
- if (!Number.isInteger(value) || value < 1) throw new InvalidInputError(`concurrency must be an integer of at least 1, got ${String(value)}`);
26588
- return value;
26681
+ function admitFetchOptions(options) {
26682
+ const concurrency = options.concurrency ?? DEFAULT_CONCURRENCY;
26683
+ if (!Number.isInteger(concurrency) || concurrency < 1) throw new InvalidInputError(`concurrency must be an integer of at least 1, got ${String(concurrency)}`);
26684
+ const batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;
26685
+ if (!Number.isSafeInteger(batchSize) || batchSize < 1) throw new InvalidInputError(`batchSize must be a safe integer of at least 1, got ${String(batchSize)}`);
26686
+ return { batchSize, concurrency };
26589
26687
  }
26590
26688
  async function forEachBatch(batches, concurrency, work) {
26591
26689
  let next = 0;
@@ -26705,7 +26803,7 @@ async function wireFor(remote, local, feature, options) {
26705
26803
  return (await capabilitiesOf(local, wire))[feature] ? wire : null;
26706
26804
  }
26707
26805
  async function bootstrap(remote, local, options = {}) {
26708
- const concurrency = concurrencyOf(options);
26806
+ const { batchSize, concurrency } = admitFetchOptions(options);
26709
26807
  const backend = await runtimeBackend(local);
26710
26808
  const bodyMode2 = await admitBodyMode(backendOf(local));
26711
26809
  const validateReadSide = bodyMode2 ? () => assertBodyRemoteEdition(remote, bodyMode2) : void 0;
@@ -26724,7 +26822,6 @@ async function bootstrap(remote, local, options = {}) {
26724
26822
  const rootIndex = await remote.readReserved("", "index.md");
26725
26823
  if (rootIndex) await backend.writeReserved("", "index.md", rootIndex.content);
26726
26824
  }
26727
- const batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;
26728
26825
  const findings = [];
26729
26826
  const held2 = [];
26730
26827
  const written = /* @__PURE__ */ new Map();
@@ -26984,6 +27081,16 @@ async function retireQuietly(backend, mode, target3) {
26984
27081
  function conflictResolutionKey(id) {
26985
27082
  return `conflict-resolution:${id}`;
26986
27083
  }
27084
+ function documentResolutionKey(id) {
27085
+ return `conflict-resolution-document:${id}`;
27086
+ }
27087
+ async function readConflictResolution(local, id) {
27088
+ const backend = backendOf(local);
27089
+ const identity = await backend.readMeta(documentResolutionKey(id));
27090
+ if (typeof identity !== "string") return void 0;
27091
+ const receipt3 = await backend.readMeta(conflictResolutionKey(identity));
27092
+ return receipt3?.id === identity && receipt3.reviewed?.id === id ? receipt3 : void 0;
27093
+ }
26987
27094
  async function readConflictRemote(remote, id) {
26988
27095
  try {
26989
27096
  const read = await remote.read(id);
@@ -27054,7 +27161,7 @@ async function resolveConflict(local, remote, reviewed, choice, options = {}) {
27054
27161
  if (reviewedShared.version !== review.remote.version || reviewedShared.content !== review.remote.content || reviewedShared.tombstone !== review.remote.tombstone) throw new ConflictReviewStaleError();
27055
27162
  if (current.deleting && selected2.kind === "revise") throw new InvalidInputError("A deletion in conflict is resolved by keeping the deletion or taking the shared version.");
27056
27163
  const resolvedAt = options.now?.() ?? (/* @__PURE__ */ new Date()).toISOString();
27057
- const requestId2 = selected2.kind === "take-remote" ? null : mintRequestId();
27164
+ const requestId2 = selected2.kind === "take-remote" || current.deleting && shared.base.version === null ? null : mintRequestId();
27058
27165
  const receipt3 = {
27059
27166
  // The original conflict identity lets a caller recover the receipt after a lost local reply.
27060
27167
  id: current.intents[0].requestId,
@@ -27066,6 +27173,7 @@ async function resolveConflict(local, remote, reviewed, choice, options = {}) {
27066
27173
  const resolveIntents = { expected: current.intents };
27067
27174
  const meta3 = [
27068
27175
  { key: conflictResolutionKey(receipt3.id), value: receipt3 },
27176
+ { key: documentResolutionKey(review.id), value: receipt3.id },
27069
27177
  // Someone else's deletion is never remembered as this working copy's own: a later create of
27070
27178
  // the id acknowledges nothing and meets the conflict again, until an explicit keep.
27071
27179
  { key: baseKey(review.id), value: { version: shared.base.version, content: shared.base.content } }
@@ -27074,7 +27182,7 @@ async function resolveConflict(local, remote, reviewed, choice, options = {}) {
27074
27182
  if (current.deleting && selected2.kind === "keep-local") {
27075
27183
  if (shared.base.version === null) {
27076
27184
  await backend.deleteJournaled(review.id, common);
27077
- return { receipt: { ...receipt3, replacementRequestId: null }, version: null, intent: null };
27185
+ return { receipt: receipt3, version: null, intent: null };
27078
27186
  }
27079
27187
  const deleted = await backend.deleteJournaled(review.id, {
27080
27188
  ...common,
@@ -27538,7 +27646,7 @@ async function lastKnownDigest(backend) {
27538
27646
  return { digest: marker.headsDigest, lastPull: read };
27539
27647
  }
27540
27648
  async function pull(local, remote, options = {}) {
27541
- const concurrency = concurrencyOf(options);
27649
+ const { batchSize, concurrency } = admitFetchOptions(options);
27542
27650
  const backend = await runtimeBackend(local);
27543
27651
  const bodyMode2 = await admitBodyMode(backendOf(local));
27544
27652
  const validateReadSide = bodyMode2 ? () => assertBodyRemoteEdition(remote, bodyMode2) : void 0;
@@ -27638,7 +27746,7 @@ async function pull(local, remote, options = {}) {
27638
27746
  }
27639
27747
  };
27640
27748
  let consistent = true;
27641
- const fetchAndApply = (candidates2, listing) => forEachBatch(chunked(candidates2, options.batchSize ?? DEFAULT_BATCH_SIZE), concurrency, async (batch) => {
27749
+ const fetchAndApply = (candidates2, listing) => forEachBatch(chunked(candidates2, batchSize), concurrency, async (batch) => {
27642
27750
  if (superseded) return;
27643
27751
  const premises2 = bodyMode2 ? await captureBodyRefresh(backendOf(local), bodyMode2, batch) : void 0;
27644
27752
  if (!listing) {
@@ -37502,33 +37610,40 @@ function resolvedRecord(binding, id, choice, fileState, sends, already = false,
37502
37610
  help: sends ? [sync3] : []
37503
37611
  };
37504
37612
  }
37505
- function resolvedKey(id) {
37506
- return `cli-resolved:${id}`;
37613
+ function folderResolutionKey(id) {
37614
+ return `cli-folder-resolved:${id}`;
37507
37615
  }
37508
- async function beginResolution(store, id, choice) {
37616
+ async function beginFolderResolution(store, id, choice) {
37509
37617
  if (choice === "take") return;
37510
37618
  const after = (await store.listIntents()).reduce((max, row4) => Math.max(max, row4.sequence), 0);
37511
- await store.writeMeta(resolvedKey(id), { choice, after });
37619
+ await store.writeMeta(folderResolutionKey(id), { choice, after });
37512
37620
  }
37513
- async function recordResolution(store, id, choice) {
37621
+ async function recordFolderResolution(store, id, choice) {
37514
37622
  if (choice === "take") {
37515
- await store.writeMeta(resolvedKey(id), null);
37623
+ await store.writeMeta(folderResolutionKey(id), null);
37516
37624
  return;
37517
37625
  }
37518
37626
  const requestIds = (await store.readWithJournal(id)).intents.filter((row4) => row4.state !== "acknowledged").map((row4) => row4.requestId);
37519
- await store.writeMeta(resolvedKey(id), { choice, requestIds });
37627
+ await store.writeMeta(folderResolutionKey(id), { choice, requestIds });
37520
37628
  }
37521
37629
  function isResolution(record6, row4) {
37522
- if (!record6?.choice) return false;
37523
- if (Array.isArray(record6.requestIds)) return record6.requestIds.includes(row4.requestId);
37524
- return typeof record6.after === "number" && row4.sequence > record6.after;
37630
+ return !!record6?.choice && Array.isArray(record6.requestIds) && record6.requestIds.includes(row4.requestId);
37525
37631
  }
37526
37632
  async function resolveAgain(session, id, choice, resumeCommand) {
37527
- const { binding, store, projection } = session;
37633
+ const { binding, store, projection, local } = session;
37528
37634
  const file2 = path38.join(binding.path, `${id}.md`);
37529
- const earlier = await store.readMeta(resolvedKey(id));
37530
37635
  const rows = (await store.readWithJournal(id)).intents.filter((row5) => row5.state !== "acknowledged");
37636
+ const receipt3 = await readConflictResolution(local, id);
37637
+ const folder = await store.readMeta(folderResolutionKey(id));
37638
+ const journal = receipt3 ? { choice: receipt3.choice === "keep-local" ? "keep" : receipt3.choice === "revise" ? "revise" : "take", requestIds: receipt3.replacementRequestId === null ? [] : [receipt3.replacementRequestId] } : void 0;
37639
+ const earlier = folder && rows[0] && isResolution(folder, rows[0]) ? folder : journal ?? folder ?? await store.readMeta(`cli-resolved:${id}`);
37531
37640
  const sync3 = syncCommand2(binding);
37641
+ if (earlier?.choice && rows.length > 0 && !Array.isArray(earlier.requestIds) && typeof earlier.after === "number") {
37642
+ throw new CliError("CONFLICT", `'${id}' has an interrupted resolution whose replacement identity is unknown; its pending work is preserved`, {
37643
+ details: { reason: "resolution_identity_unknown", id, earlier: earlier.choice, states: rows.map((row5) => row5.state) },
37644
+ help: `run ${sync3} to send the existing edit; if it reports a conflict on '${id}', inspect and resolve that one`
37645
+ });
37646
+ }
37532
37647
  if (!earlier?.choice || rows.length === 0 || !isResolution(earlier, rows[0])) {
37533
37648
  throw new CliError("CONFLICT", `'${id}' has an unsent change but no conflict to resolve; the next sync sends it`, {
37534
37649
  details: { reason: "unsent_change", id, states: rows.map((row5) => row5.state) },
@@ -37557,11 +37672,14 @@ async function resolveAgain(session, id, choice, resumeCommand) {
37557
37672
  await store.writeJournaled(id, { id, frontmatter: parsed2.frontmatter, body: parsed2.body }, {
37558
37673
  expectedVersion: current.document?.version ?? null,
37559
37674
  supersede: { requestId: row4.requestId, expectedState: "pending", expectedAttempts: 0 },
37560
- meta: [{ key: baseKey(id), value: { version: row4.base, content: row4.baseContent } }]
37675
+ meta: [
37676
+ { key: baseKey(id), value: { version: row4.base, content: row4.baseContent } },
37677
+ { key: folderResolutionKey(id), value: null },
37678
+ { key: `cli-resolved:${id}`, value: null }
37679
+ ]
37561
37680
  });
37562
37681
  if (bytes === null) delete projection.files[id];
37563
37682
  const exported = await exportCheckout(binding.path, store, projection, { only: /* @__PURE__ */ new Set([id]), placeMissing: true });
37564
- await store.writeMeta(resolvedKey(id), null);
37565
37683
  await session.persist();
37566
37684
  return resolvedRecord(binding, id, choice, exported.placed.length > 0 ? bytes === null ? "restored" : "replaced" : "unchanged", false, false, earlier.choice);
37567
37685
  }
@@ -37830,10 +37948,10 @@ async function runResolve2(binding, values, deps, mode) {
37830
37948
  throw error51;
37831
37949
  }
37832
37950
  await assertInspectedCurrent(session, id, remoteVersionOf(conflict), choice);
37833
- await beginResolution(store, id, choice);
37834
37951
  let fileState;
37835
37952
  if (conflict.kind === "folder") {
37836
37953
  if (conflict.deleted && choice !== "take") await assertInspectedTombstone(session, id, null, choice);
37954
+ await beginFolderResolution(store, id, choice);
37837
37955
  fileState = await resolveFolder(session, id, choice, conflict, resumeCommand);
37838
37956
  } else {
37839
37957
  const deleting = conflict.review.local.deleted === true;
@@ -37891,7 +38009,7 @@ async function runResolve2(binding, values, deps, mode) {
37891
38009
  }
37892
38010
  }
37893
38011
  await store.writeMeta(inspectedKey(id), null);
37894
- await recordResolution(store, id, choice);
38012
+ if (conflict.kind === "folder") await recordFolderResolution(store, id, choice);
37895
38013
  await session.persist();
37896
38014
  return resolvedRecord(binding, id, choice, fileState, choice !== "take");
37897
38015
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "superbee",
3
- "version": "0.4.2-pre.3",
3
+ "version": "0.4.2",
4
4
  "type": "module",
5
5
  "description": "Agent-facing Superbee CLI for reading and writing local OKF knowledge bundles: context notes, docs, cross-links, and live bundle Views.",
6
6
  "keywords": [
@@ -37,6 +37,10 @@
37
37
  "./bundle-descriptor": {
38
38
  "types": "./dist/bundle-descriptor/index.d.ts",
39
39
  "default": "./dist/bundle-descriptor.mjs"
40
+ },
41
+ "./portable-export": {
42
+ "types": "./dist/portable-export.d.ts",
43
+ "default": "./dist/portable-export.mjs"
40
44
  }
41
45
  },
42
46
  "files": [
@@ -86,6 +90,6 @@
86
90
  "darwin",
87
91
  "linux"
88
92
  ],
89
- "readme": "# superbee\n\nShared, versioned, conflict-safe knowledge for AI coding agents, stored as plain markdown in\nyour repo.\n\nSuperbee is pre-1.0. Commands and formats may change between releases.\n\n## What is Superbee?\n\nAgents forget everything between sessions, overwrite each other's work, and keep what they know\ninvisible to the humans they work for. Superbee fixes all three with a **knowledge bundle**: a\nfolder of markdown documents, by convention `.superbee/` at your project root, that agents read\nand write through a small command-line tool.\n\n- **Context that persists.** Agents write context notes, decisions, plans, and research into the\n bundle. The next session, or a different agent, picks up where the last one left off. An\n optional `SessionStart` hook for Claude Code, Codex, and OpenCode orients every new session\n automatically.\n- **Safe for many writers.** Each write can carry the actor that made it. A writer can name the\n version it last read; if anyone changed the document since, the write fails with a typed\n conflict error instead of silently overwriting their work.\n- **Visible to humans.** The bundle is plain markdown. Open it in any editor, render it on\n GitHub, diff it in git. `superbee ui` serves it locally as cross-linked pages with backlinks and\n a live activity feed. No bundle content leaves your machine until you run `superbee sync`, which\n shares the bundle with teammates through the board: a copy of the bundle kept on its own git\n branch, separate from your code.\n- **Views on demand.** Ask your agent for a dashboard, a timeline, a filtered task queue, or a\n reading view of one dense document. It builds a self-contained HTML page, stores it in the bundle\n as a View, and `superbee ui` hosts it in a sandboxed frame. A View reads the bundle live and can\n change it only through a write you confirm. Views are bundle content, so they travel with `sync`.\n- **Built for agents.** Output is structured and token-lean, and errors carry a small, stable set\n of exit codes. Agents act on responses without parsing prose or flooding their context window.\n- **Yours, and portable.** Bundles follow the Open Knowledge Format (OKF), a convention of\n markdown with frontmatter, so they outlive the tool: hand the folder to someone else, or read it\n with anything that speaks markdown. New bundles are written as OKF v0.2, and existing v0.1\n bundles keep working as they are. Reading and writing the bundle works offline; only sharing\n needs a network. The document schemas, called kinds, live inside the bundle, so it describes\n its own structure.\n\nBees build comb one cell at a time. The comb holds what the colony gathers, and its shape shows\nthe next bee where to build and what belongs where. Superbee works the same way: people and\nagents record what they learn in a structure that fits their domain, and that structure guides\nwhoever works next. Each session builds on the last instead of starting over.\n\nThe npm package is one self-contained executable with zero runtime dependencies, plus an Agent\nSkill, an instruction file your agent loads, that teaches it how to use the tool.\n\n## Install\n\nRequires a supported Node.js 22, 24, or 26 release (22.14.0 minimum) on macOS or Linux. Native Windows is not supported by this package.\n\n```sh\nnpm install -g superbee\n```\n\nStable releases publish on npm's `latest` tag and prereleases on `next`. To try the prerelease:\n\n```sh\nnpm install -g superbee@next\n```\n\nWindows adapters and the `superbee-windows` executable live in a separate repository and are\nnot included in `superbee`. Most Windows users should run Superbee in WSL2, where npm sees a\nLinux platform and the normal installation applies. An experimental, unsupported native Windows\nbuild is available as open source, with build-from-source instructions: https://github.com/Holaxis-ai/superbee-windows-cli\n\n### Upgrading an existing Windows installation\n\nThe first release containing the Windows extraction removes native Windows support from this\npackage. Its npm `os` metadata permits only `darwin` and `linux`, so a Windows upgrade to an affected\nversion is rejected with `EBADPLATFORM`. This applies to existing prerelease users too. Forcing the\ninstallation does not restore support: the executable refuses commands on unsupported hosts\nbefore running them (the bare `--version` flag can still identify the installed build).\n\nThere is no supported Windows replacement on npm. The alternatives are WSL2 or the experimental\nbuild from source at https://github.com/Holaxis-ai/superbee-windows-cli; neither carries a\nfirst-party support promise for native Windows. An older installed version is not converted or\nremoved by this source change, and existing bundle files are not migrated by it. Review the affected release's notes before changing an existing\nWindows installation. macOS/Linux users can continue using the normal installation and setup flow.\n\nRun `superbee version --check` to compare your install with the current stable release.\n\n## First run: let your agent finish setup\n\n`npm install` gives you the CLI. The integrations (the Agent Skill, the `SessionStart` hook, and\nMCP server registration, where MCP is the Model Context Protocol) are installed by your agent,\nnot by hand. Ask it:\n\n> Run `superbee setup` and follow its instructions.\n\nSetup itself changes nothing. It inspects your configuration and returns one safe next command at\na time, and the agent runs each with your approval. Setup knows Claude Code, Codex, and OpenCode,\nplus Claude Desktop for the MCP registration only.\n\n## Everyday use\n\nYou rarely type Superbee commands yourself. You ask your agent, and the Agent Skill translates the\nrequest into CLI calls:\n\n- \"Set up a Superbee bundle for this project and track our tasks in it.\"\n- \"Write up what we decided about the auth design as a doc, and link it to the task.\"\n- \"What did the last session leave off on? Check the context notes.\"\n- \"Sync the board so my teammate's agent sees this.\"\n- \"Give me a view of the open tasks grouped by owner.\"\n\nBehind those requests the agent uses a small set of commands: `init --dir .superbee` creates the\nbundle, `new` creates a document of a declared kind, `doc write` writes a free-form one,\n`doc update` changes a document, `link add` connects two, `list` and `doc read` query them, and\n`sync` shares the board. `superbee --help` lists the commands, and `superbee <command> --help`\ngives each one's full reference.\n\nThe two commands meant for you are the ones that show you the knowledge:\n\n```sh\nsuperbee ui --open # the whole bundle, rendered in your browser\nsuperbee doc open <id> # one document, by an id from `superbee list`\n```\n\n## Upgrading from aslite\n\nIf you installed the earlier `@holaxis/aslite` package or its marketplace plugin: install\n`superbee` alongside it, have your agent run `superbee setup` to migrate the integrations, then\nrun `npm uninstall -g @holaxis/aslite`. Existing `.agentstate-lite/` bundles and\n`.agentstate.json` bindings keep working with no migration.\n\n## Learn more\n\nThe [repository](https://github.com/Holaxis-ai/superbee) holds the source, the\n[CLI contract](https://github.com/Holaxis-ai/superbee/blob/main/packages/superbee/AXI-CONTRACT.md),\nand the [wire protocol](https://github.com/Holaxis-ai/superbee/blob/main/docs/WIRE-PROTOCOL.md).\n\n## License\n\nApache-2.0 © 2026 Holaxis\n",
93
+ "readme": "# superbee\n\nShared, versioned, conflict-safe knowledge for AI coding agents, stored as plain markdown in\nyour repo.\n\nSuperbee is pre-1.0. Commands and formats may change between releases.\n\n## What is Superbee?\n\nAgents forget everything between sessions, overwrite each other's work, and keep what they know\ninvisible to the humans they work for. Superbee fixes all three with a **knowledge bundle**: a\nfolder of markdown documents, by convention `.superbee/` at your project root, that agents read\nand write through a small command-line tool.\n\n- **Context that persists.** Agents write context notes, decisions, plans, and research into the\n bundle. The next session, or a different agent, picks up where the last one left off. An\n optional `SessionStart` hook for Claude Code, Codex, and OpenCode orients every new session\n automatically.\n- **Safe for many writers.** Each write can carry the actor that made it. A writer can name the\n version it last read; if anyone changed the document since, the write fails with a typed\n conflict error instead of silently overwriting their work.\n- **Visible to humans.** The bundle is plain markdown. Open it in any editor, render it on\n GitHub, diff it in git. `superbee ui` serves it locally as cross-linked pages with backlinks and\n a live activity feed. No bundle content leaves your machine until you run `superbee sync`, which\n shares the bundle with teammates through the board: a copy of the bundle kept on its own git\n branch, separate from your code.\n- **Views on demand.** Ask your agent for a dashboard, a timeline, a filtered task queue, or a\n reading view of one dense document. It builds a self-contained HTML page, stores it in the bundle\n as a View, and `superbee ui` hosts it in a sandboxed frame. A View reads the bundle live and can\n change it only through a write you confirm. Views are bundle content, so they travel with `sync`.\n- **Built for agents.** Output is structured and token-lean, and errors carry a small, stable set\n of exit codes. Agents act on responses without parsing prose or flooding their context window.\n- **Yours, and portable.** Bundles follow the Open Knowledge Format (OKF), a convention of\n markdown with frontmatter, so they outlive the tool: hand the folder to someone else, or read it\n with anything that speaks markdown. New bundles are written as OKF v0.2, and existing v0.1\n bundles keep working as they are. Reading and writing the bundle works offline; only sharing\n needs a network. The document schemas, called kinds, live inside the bundle, so it describes\n its own structure.\n\nBees build comb one cell at a time. The comb holds what the colony gathers, and its shape shows\nthe next bee where to build and what belongs where. Superbee works the same way: people and\nagents record what they learn in a structure that fits their domain, and that structure guides\nwhoever works next. Each session builds on the last instead of starting over.\n\nThe npm package is one self-contained executable with zero runtime dependencies, plus an Agent\nSkill, an instruction file your agent loads, that teaches it how to use the tool.\n\n## Install\n\nRequires a supported Node.js 22, 24, or 26 release (22.14.0 minimum) on macOS or Linux. Native Windows is not supported by this package.\n\n```sh\nnpm install -g superbee\n```\n\nStable releases publish on npm's `latest` tag and prereleases on `next`.\n\nWindows adapters and the `superbee-windows` executable live in a separate repository and are\nnot included in `superbee`. Most Windows users should run Superbee in WSL2, where npm sees a\nLinux platform and the normal installation applies. An experimental, unsupported native Windows\nbuild is available as open source, with build-from-source instructions: https://github.com/Holaxis-ai/superbee-windows-cli\n\n### Upgrading an existing Windows installation\n\nThe first release containing the Windows extraction removes native Windows support from this\npackage. Its npm `os` metadata permits only `darwin` and `linux`, so a Windows upgrade to an affected\nversion is rejected with `EBADPLATFORM`. This applies to existing prerelease users too. Forcing the\ninstallation does not restore support: the executable refuses commands on unsupported hosts\nbefore running them (the bare `--version` flag can still identify the installed build).\n\nThere is no supported Windows replacement on npm. The alternatives are WSL2 or the experimental\nbuild from source at https://github.com/Holaxis-ai/superbee-windows-cli; neither carries a\nfirst-party support promise for native Windows. An older installed version is not converted or\nremoved by this source change, and existing bundle files are not migrated by it. Review the affected release's notes before changing an existing\nWindows installation. macOS/Linux users can continue using the normal installation and setup flow.\n\nRun `superbee version --check` to compare your install with the current stable release.\n\n## First run: let your agent finish setup\n\n`npm install` gives you the CLI. The integrations (the Agent Skill, the `SessionStart` hook, and\nMCP server registration, where MCP is the Model Context Protocol) are installed by your agent,\nnot by hand. Ask it:\n\n> Run `superbee setup` and follow its instructions.\n\nSetup itself changes nothing. It inspects your configuration and returns one safe next command at\na time, and the agent runs each with your approval. Setup knows Claude Code, Codex, and OpenCode,\nplus Claude Desktop for the MCP registration only.\n\n## Everyday use\n\nYou rarely type Superbee commands yourself. You ask your agent, and the Agent Skill translates the\nrequest into CLI calls:\n\n- \"Set up a Superbee bundle for this project and track our tasks in it.\"\n- \"Write up what we decided about the auth design as a doc, and link it to the task.\"\n- \"What did the last session leave off on? Check the context notes.\"\n- \"Sync the board so my teammate's agent sees this.\"\n- \"Give me a view of the open tasks grouped by owner.\"\n\nBehind those requests the agent uses a small set of commands: `init --dir .superbee` creates the\nbundle, `new` creates a document of a declared kind, `doc write` writes a free-form one,\n`doc update` changes a document, `link add` connects two, `list` and `doc read` query them, and\n`sync` shares the board. `superbee --help` lists the commands, and `superbee <command> --help`\ngives each one's full reference.\n\nThe two commands meant for you are the ones that show you the knowledge:\n\n```sh\nsuperbee ui --open # the whole bundle, rendered in your browser\nsuperbee doc open <id> # one document, by an id from `superbee list`\n```\n\n## Upgrading from aslite\n\nIf you installed the earlier `@holaxis/aslite` package or its marketplace plugin: install\n`superbee` alongside it, have your agent run `superbee setup` to migrate the integrations, then\nrun `npm uninstall -g @holaxis/aslite`. Existing `.agentstate-lite/` bundles and\n`.agentstate.json` bindings keep working with no migration.\n\n## Learn more\n\nThe [repository](https://github.com/Holaxis-ai/superbee) holds the source, the\n[CLI contract](https://github.com/Holaxis-ai/superbee/blob/main/packages/superbee/AXI-CONTRACT.md),\nand the [wire protocol](https://github.com/Holaxis-ai/superbee/blob/main/docs/WIRE-PROTOCOL.md).\n\n## License\n\nApache-2.0 © 2026 Holaxis\n",
90
94
  "readmeFilename": "README.md"
91
95
  }