superbee 0.3.0-pre.2 → 0.3.0-pre.5

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
@@ -37,6 +37,11 @@ and write through a small command-line tool.
37
37
  needs a network. The document schemas, called kinds, live inside the bundle, so it describes
38
38
  its own structure.
39
39
 
40
+ Bees build comb one cell at a time. The comb holds what the colony gathers, and its shape shows
41
+ the next bee where to build and what belongs where. Superbee works the same way: people and
42
+ agents record what they learn in a structure that fits their domain, and that structure guides
43
+ whoever works next. Each session builds on the last instead of starting over.
44
+
40
45
  The npm package is one self-contained executable with zero runtime dependencies, plus an Agent
41
46
  Skill, an instruction file your agent loads, that teaches it how to use the tool.
42
47
 
package/SKILL.md CHANGED
@@ -88,8 +88,12 @@ When the tone fits, a single 🐝 may mark a successful Superbee outcome.
88
88
 
89
89
  ## Hosted checkouts
90
90
 
91
- - In a folder made by `superbee checkout` the host is the authority. On `AUTH_REQUIRED` (exit 4), relay `details.sign_in_url` to the person, then re-run the same command; `superbee setup hosted` signs in and picks the default workspace in one step.
92
- - Run `superbee sync` once at the end of a batch of edits. Any concurrent change to one document, even to different frontmatter keys, is a conflict: read `$REFS/hosted-checkout.md`, then `sync --inspect --doc <id>` and `--resolve keep|take|revise --doc <id>`. `--resolve` records a decision and never sends: run `superbee sync` after keep or revise. Deleted files sync as deletes; when sync reports `deletions_held`, never accept it yourself: name the documents and ask the person to run its `--accept-deletes` command in their own terminal (it asks them to type the count, and refuses your shell), else run `--restore-deletes`. A refusal that says to do something in the Superbee app is for the person: tell them, and never work around it.
91
+ - In a folder made by `superbee checkout` the host is the authority. On `AUTH_REQUIRED` (exit 4), relay `details.sign_in_url` to the person, then re-run the same command; `superbee setup hosted` signs in and picks the default workspace in one step, and `superbee catalog list --hosted` lists the bundle ids you can check out. A folder reported with `copy_of_checkout` was moved, copied or restored and is not bound: read `$REFS/hosted-checkout.md` before `superbee checkout --adopt`, and let the person confirm the host. `superbee publish --to hosted` moves a local bundle or Git board to hosted: preview it, and add `--yes` only when the person asks.
92
+ - Run `superbee sync` once at the end of a batch of edits; resolve conflicts as below. Deleted files sync as deletes; when sync reports `deletions_held`, never accept it yourself: name the documents and ask the person to run its `--accept-deletes` command in their own terminal (it asks them to type the count, and refuses your shell), else run `--restore-deletes`. A refusal that says to do something in the Superbee app is for the person: tell them, and never work around it. Take a bundle out of hosted only when the person asks: `superbee export` (read `$REFS/hosted-checkout.md` first).
93
+
94
+ ## Sync conflicts
95
+
96
+ - A Git board and a hosted checkout share one playbook. When `superbee sync` exits 5 with conflict rows, run `superbee sync --inspect --doc <id>` to see your version and theirs, then `superbee sync --resolve keep|take|revise --doc <id>`: keep writes yours over theirs, take keeps theirs, revise keeps the document as you edited it to the merged result. `--resolve` never sends: run `superbee sync` after keep or revise. A Git board has already kept the teammate's version and saved yours aside; a hosted checkout keeps your file, treats any concurrent change to one document (even different frontmatter keys) as a conflict, and is explained in `$REFS/hosted-checkout.md`.
93
97
 
94
98
  ## Host setup
95
99
 
@@ -3575,6 +3575,12 @@ function processExists(pid) {
3575
3575
  return err.code !== "ESRCH";
3576
3576
  }
3577
3577
  }
3578
+ var HELD_TOKENS = Symbol.for("superbee.filesystem-lock.held-tokens");
3579
+ var heldTokens = globalThis[HELD_TOKENS] ??= /* @__PURE__ */ new Set();
3580
+ function ownerIsGone(owner) {
3581
+ if (owner.pid !== process.pid) return !processExists(owner.pid);
3582
+ return !heldTokens.has(owner.token) && owner.created_at_ms < Date.now() - process.uptime() * 1e3;
3583
+ }
3578
3584
  function staleLockQuarantinePath(lockPath, owner) {
3579
3585
  const tokenHash = createHash("sha256").update(owner.token).digest("hex");
3580
3586
  return `${lockPath}.stale-${tokenHash}`;
@@ -3589,7 +3595,9 @@ async function pathExists(candidate) {
3589
3595
  }
3590
3596
  }
3591
3597
  async function quarantineStaleLock(lockPath, owner, policy) {
3592
- if (owner.hostname !== hostname() || processExists(owner.pid)) return false;
3598
+ if (owner.hostname !== hostname() || !ownerIsGone(owner)) return false;
3599
+ const current = await readOwner(lockPath);
3600
+ if (current === null || current.token !== owner.token) return false;
3593
3601
  const quarantinePath = staleLockQuarantinePath(lockPath, owner);
3594
3602
  try {
3595
3603
  await fs.rename(lockPath, quarantinePath);
@@ -3639,17 +3647,26 @@ async function ensurePrivateLockRoot(root, policy) {
3639
3647
  );
3640
3648
  }
3641
3649
  }
3642
- function timeoutError(lockPath, owner, guarded) {
3650
+ async function timeoutError(lockPath, snapshot, guarded) {
3651
+ let diagnosed = snapshot;
3652
+ let stale = false;
3653
+ if (snapshot !== null && snapshot.hostname === hostname() && ownerIsGone(snapshot)) {
3654
+ const current = await readOwner(lockPath);
3655
+ if (current?.token === snapshot.token) stale = true;
3656
+ else diagnosed = current;
3657
+ }
3658
+ const owner = diagnosed;
3643
3659
  const malformed = owner === null;
3644
3660
  const sameHost = owner?.hostname === hostname();
3645
- const stale = owner !== null && sameHost && !processExists(owner.pid);
3646
3661
  let message;
3647
3662
  if (malformed) {
3648
3663
  message = `timed out waiting for filesystem mutation lock '${lockPath}'; its owner metadata is missing or malformed. Inspect and remove the lock only after confirming no process is mutating the target, then retry.`;
3649
3664
  } else if (stale) {
3650
3665
  message = `stale filesystem mutation lock '${lockPath}' belongs to absent PID ${owner.pid} on ${owner.hostname}. Inspect and remove the lock, then retry.`;
3666
+ } else if (!sameHost) {
3667
+ message = `filesystem mutation lock '${lockPath}' is held by PID ${owner.pid} on ${owner.hostname}, which is not this host (${hostname()}); whether its holder is alive cannot be checked from here, so the lock is never reclaimed automatically. A person must check it: once no process on ${owner.hostname} is mutating the target (this host may have been renamed since the lock was taken), remove the lock, then retry.`;
3651
3668
  } else {
3652
- message = `timed out waiting for filesystem mutation lock '${lockPath}' held by PID ${owner.pid} on ${owner.hostname}; retry the mutation.`;
3669
+ message = `timed out waiting for filesystem mutation lock '${lockPath}' held by PID ${owner.pid} on ${owner.hostname}; retry the mutation. If it stays held, PID ${owner.pid} may no longer be the process that claimed it, because a process id is reused after its process exits: remove the lock only after confirming no process is mutating the target.`;
3653
3670
  }
3654
3671
  const heldFor = owner !== null && owner.target !== guarded ? ` Its holder recorded '${owner.target}' for the same lock key.` : "";
3655
3672
  return new FilesystemMutationLockError(`${message} The lock guards '${guarded}'.${heldFor}`, { lockPath, owner, stale, malformed });
@@ -3697,11 +3714,12 @@ async function claimLockPath(lockPath, owner, waitMs, pollMs, policy) {
3697
3714
  if (await quarantineStaleLock(lockPath, existingOwner, policy)) continue;
3698
3715
  existingOwner = await readOwner(lockPath);
3699
3716
  }
3700
- if (Date.now() - started >= waitMs) throw timeoutError(lockPath, existingOwner, owner.target);
3717
+ if (Date.now() - started >= waitMs) throw await timeoutError(lockPath, existingOwner, owner.target);
3701
3718
  await delay(pollMs);
3702
3719
  continue;
3703
3720
  }
3704
3721
  const claimed = await fs.lstat(lockPath, { bigint: true }).catch(() => null);
3722
+ heldTokens.add(owner.token);
3705
3723
  try {
3706
3724
  await fs.writeFile(path.join(lockPath, OWNER_FILE), `${JSON.stringify(owner)}
3707
3725
  `, {
@@ -3711,9 +3729,13 @@ async function claimLockPath(lockPath, owner, waitMs, pollMs, policy) {
3711
3729
  });
3712
3730
  } catch (err) {
3713
3731
  const code = err.code;
3714
- if (code === "EEXIST") continue;
3732
+ if (code === "EEXIST") {
3733
+ heldTokens.delete(owner.token);
3734
+ continue;
3735
+ }
3715
3736
  await rollBackOwnClaim(lockPath, owner, claimed, waitMs, pollMs, policy).catch(() => {
3716
3737
  });
3738
+ heldTokens.delete(owner.token);
3717
3739
  if (code === "ENOENT" || code === "ENOTDIR") continue;
3718
3740
  throw err;
3719
3741
  }
@@ -3728,6 +3750,7 @@ async function claimLockPath(lockPath, owner, waitMs, pollMs, policy) {
3728
3750
  throw changedOwnerRefusal(lockPath, current.state === "record" ? current.owner : null);
3729
3751
  }
3730
3752
  await removeReleasedLock(lockPath, owner, started2, waitMs, pollMs, policy);
3753
+ heldTokens.delete(owner.token);
3731
3754
  })();
3732
3755
  return inFlight.finally(() => {
3733
3756
  inFlight = void 0;
@@ -31529,6 +31529,12 @@ function processExists(pid) {
31529
31529
  return err.code !== "ESRCH";
31530
31530
  }
31531
31531
  }
31532
+ var HELD_TOKENS = Symbol.for("superbee.filesystem-lock.held-tokens");
31533
+ var heldTokens = globalThis[HELD_TOKENS] ??= /* @__PURE__ */ new Set();
31534
+ function ownerIsGone(owner) {
31535
+ if (owner.pid !== process.pid) return !processExists(owner.pid);
31536
+ return !heldTokens.has(owner.token) && owner.created_at_ms < Date.now() - process.uptime() * 1e3;
31537
+ }
31532
31538
  function staleLockQuarantinePath(lockPath, owner) {
31533
31539
  const tokenHash = createHash("sha256").update(owner.token).digest("hex");
31534
31540
  return `${lockPath}.stale-${tokenHash}`;
@@ -31543,7 +31549,9 @@ async function pathExists(candidate) {
31543
31549
  }
31544
31550
  }
31545
31551
  async function quarantineStaleLock(lockPath, owner, policy) {
31546
- if (owner.hostname !== hostname() || processExists(owner.pid)) return false;
31552
+ if (owner.hostname !== hostname() || !ownerIsGone(owner)) return false;
31553
+ const current = await readOwner(lockPath);
31554
+ if (current === null || current.token !== owner.token) return false;
31547
31555
  const quarantinePath = staleLockQuarantinePath(lockPath, owner);
31548
31556
  try {
31549
31557
  await fs.rename(lockPath, quarantinePath);
@@ -31593,17 +31601,26 @@ async function ensurePrivateLockRoot(root, policy) {
31593
31601
  );
31594
31602
  }
31595
31603
  }
31596
- function timeoutError(lockPath, owner, guarded) {
31604
+ async function timeoutError(lockPath, snapshot, guarded) {
31605
+ let diagnosed = snapshot;
31606
+ let stale = false;
31607
+ if (snapshot !== null && snapshot.hostname === hostname() && ownerIsGone(snapshot)) {
31608
+ const current = await readOwner(lockPath);
31609
+ if (current?.token === snapshot.token) stale = true;
31610
+ else diagnosed = current;
31611
+ }
31612
+ const owner = diagnosed;
31597
31613
  const malformed = owner === null;
31598
31614
  const sameHost = owner?.hostname === hostname();
31599
- const stale = owner !== null && sameHost && !processExists(owner.pid);
31600
31615
  let message;
31601
31616
  if (malformed) {
31602
31617
  message = `timed out waiting for filesystem mutation lock '${lockPath}'; its owner metadata is missing or malformed. Inspect and remove the lock only after confirming no process is mutating the target, then retry.`;
31603
31618
  } else if (stale) {
31604
31619
  message = `stale filesystem mutation lock '${lockPath}' belongs to absent PID ${owner.pid} on ${owner.hostname}. Inspect and remove the lock, then retry.`;
31620
+ } else if (!sameHost) {
31621
+ message = `filesystem mutation lock '${lockPath}' is held by PID ${owner.pid} on ${owner.hostname}, which is not this host (${hostname()}); whether its holder is alive cannot be checked from here, so the lock is never reclaimed automatically. A person must check it: once no process on ${owner.hostname} is mutating the target (this host may have been renamed since the lock was taken), remove the lock, then retry.`;
31605
31622
  } else {
31606
- message = `timed out waiting for filesystem mutation lock '${lockPath}' held by PID ${owner.pid} on ${owner.hostname}; retry the mutation.`;
31623
+ message = `timed out waiting for filesystem mutation lock '${lockPath}' held by PID ${owner.pid} on ${owner.hostname}; retry the mutation. If it stays held, PID ${owner.pid} may no longer be the process that claimed it, because a process id is reused after its process exits: remove the lock only after confirming no process is mutating the target.`;
31607
31624
  }
31608
31625
  const heldFor = owner !== null && owner.target !== guarded ? ` Its holder recorded '${owner.target}' for the same lock key.` : "";
31609
31626
  return new FilesystemMutationLockError(`${message} The lock guards '${guarded}'.${heldFor}`, { lockPath, owner, stale, malformed });
@@ -31651,11 +31668,12 @@ async function claimLockPath(lockPath, owner, waitMs, pollMs, policy) {
31651
31668
  if (await quarantineStaleLock(lockPath, existingOwner, policy)) continue;
31652
31669
  existingOwner = await readOwner(lockPath);
31653
31670
  }
31654
- if (Date.now() - started >= waitMs) throw timeoutError(lockPath, existingOwner, owner.target);
31671
+ if (Date.now() - started >= waitMs) throw await timeoutError(lockPath, existingOwner, owner.target);
31655
31672
  await delay(pollMs);
31656
31673
  continue;
31657
31674
  }
31658
31675
  const claimed = await fs.lstat(lockPath, { bigint: true }).catch(() => null);
31676
+ heldTokens.add(owner.token);
31659
31677
  try {
31660
31678
  await fs.writeFile(path.join(lockPath, OWNER_FILE), `${JSON.stringify(owner)}
31661
31679
  `, {
@@ -31665,9 +31683,13 @@ async function claimLockPath(lockPath, owner, waitMs, pollMs, policy) {
31665
31683
  });
31666
31684
  } catch (err) {
31667
31685
  const code2 = err.code;
31668
- if (code2 === "EEXIST") continue;
31686
+ if (code2 === "EEXIST") {
31687
+ heldTokens.delete(owner.token);
31688
+ continue;
31689
+ }
31669
31690
  await rollBackOwnClaim(lockPath, owner, claimed, waitMs, pollMs, policy).catch(() => {
31670
31691
  });
31692
+ heldTokens.delete(owner.token);
31671
31693
  if (code2 === "ENOENT" || code2 === "ENOTDIR") continue;
31672
31694
  throw err;
31673
31695
  }
@@ -31682,6 +31704,7 @@ async function claimLockPath(lockPath, owner, waitMs, pollMs, policy) {
31682
31704
  throw changedOwnerRefusal(lockPath, current.state === "record" ? current.owner : null);
31683
31705
  }
31684
31706
  await removeReleasedLock(lockPath, owner, started2, waitMs, pollMs, policy);
31707
+ heldTokens.delete(owner.token);
31685
31708
  })();
31686
31709
  return inFlight.finally(() => {
31687
31710
  inFlight = void 0;