toolroll 0.9.102 → 0.9.104

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +10 -8
  3. package/dist/assignment.d.ts +32 -0
  4. package/dist/assignment.js +140 -4
  5. package/dist/browser/workspace.js +25 -25
  6. package/dist/browser-workspace.d.ts +21 -5
  7. package/dist/contracts/console-api.d.ts +3 -2
  8. package/dist/contracts/console-api.js +4 -2
  9. package/dist/desktop-update.d.ts +2 -2
  10. package/dist/flow-engine.d.ts +3 -0
  11. package/dist/flow-engine.js +4 -1
  12. package/dist/flows-ui.d.ts +31 -1
  13. package/dist/flows-ui.js +40 -12
  14. package/dist/keymap.d.ts +3 -1
  15. package/dist/keymap.js +3 -0
  16. package/dist/local-owner-acts.d.ts +83 -0
  17. package/dist/local-owner-acts.js +221 -0
  18. package/dist/local-owner.d.ts +12 -1
  19. package/dist/local-owner.js +26 -0
  20. package/dist/operate-remote-arguments.js +4 -2
  21. package/dist/operate.js +76 -1
  22. package/dist/plan-auto.js +5 -0
  23. package/dist/project-knowledge.d.ts +6 -6
  24. package/dist/public-access.d.ts +9 -0
  25. package/dist/public-access.js +15 -0
  26. package/dist/pull-request-flow.js +9 -0
  27. package/dist/release-approval.d.ts +9 -20
  28. package/dist/release-approval.js +14 -53
  29. package/dist/release-card.d.ts +15 -1
  30. package/dist/release-card.js +35 -12
  31. package/dist/release-refusal.d.ts +32 -0
  32. package/dist/release-refusal.js +73 -0
  33. package/dist/result-completion.d.ts +6 -2
  34. package/dist/result-completion.js +13 -7
  35. package/dist/scope.d.ts +32 -1
  36. package/dist/scope.js +116 -25
  37. package/dist/serve.js +13 -14
  38. package/dist/server/guards.d.ts +1 -0
  39. package/dist/server/guards.js +11 -1
  40. package/dist/server/home-asks.d.ts +45 -0
  41. package/dist/server/home-asks.js +108 -0
  42. package/dist/server/home.d.ts +15 -5
  43. package/dist/server/home.js +53 -18
  44. package/dist/server/pages.js +15 -3
  45. package/dist/server/remote.d.ts +1 -0
  46. package/dist/server/render-results.d.ts +4 -0
  47. package/dist/server/render-tasks.d.ts +9 -0
  48. package/dist/server/render-tasks.js +4 -4
  49. package/dist/server/route-table.js +4 -0
  50. package/dist/server/runtime.d.ts +1 -0
  51. package/dist/server/task-views.js +3 -0
  52. package/dist/server/tasks.d.ts +25 -0
  53. package/dist/server/tasks.js +46 -6
  54. package/dist/surface.js +12 -2
  55. package/dist/task-outcome-cli.d.ts +18 -0
  56. package/dist/task-outcome-cli.js +60 -2
  57. package/dist/tui-contract.d.ts +61 -3
  58. package/dist/tui-source.js +91 -20
  59. package/dist/tui.js +54 -54
  60. package/dist/work-index.d.ts +10 -5
  61. package/dist/work-index.js +23 -13
  62. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.104 — 2026-11-12
4
+
5
+ - **Accept, plan and retry from the terminal, without a password.** On your own computer (laptop mode), pressing `c` on a Ready result opens "Accept this result?": the summary, the changed files, each requirement and the real check status. You answer "Did you check it yourself?", give a reason for an exception, or just say yes. The answer is tied to the result you read; if it changed meanwhile, you're shown the new one. A bigger first task now asks for its plan straight away, and `p` asks for one; you still approve the plan before anything is built. After a crash or a model error, `r` retries your own approved build, up to 3 times per approval, with the same plan, agent and limits. Merging, releasing, hands-off modes and settings still need your password; API tokens, the lead and chat bots can't do any of this.
6
+ - **Password-free mode is tighter.** The password-free session now has to come from this computer on every request, not only when it starts. It can no longer accept release, protected or coordinator results, or open pull requests; those need your password.
7
+ - **The release page says why it couldn't read what changed.** "What changed couldn't be read" now gives the reason, and a slow machine no longer drops the list after 15 seconds.
8
+ - **No security warning on first start.** discord.js is updated to 14.28.1, so `npx toolroll` no longer opens with npm's deprecation warning.
9
+ - **Docs match the app.** The getting-started guide, CONTRIBUTING and the README describe the terminal's first run as it is (`b` sends back, `r` retries), and Ready is described as showing the check status.
10
+
11
+ ## 0.9.103 — 2026-11-11
12
+
13
+ - **Home shows releases, flow decisions and setup.** Home's list gains three groups. **Approve a release** lists releases waiting for your yes and releases whose publishing is held for you; the button opens the release page, where the approval happens. **Flow decisions** lists flow cards that wait on you (to decide, choose, answer a question, allow the browser, or move on after no reply); the button opens the card on its flow page. **Finish setting up** lists first-run steps you have not done yet, each with its own button. The top line now reads like "3 tasks and 2 other things need you"; the Tasks count in the menu stays a task count.
14
+ - **A release can only be approved under the release rules.** Before, approving a release from the plain task page, from Home or with `toolroll task approve` skipped some of the release page's checks. Now every approval road refuses a release for the same reasons the release page does: CI failed, the release check failed on this commit, the terms changed after an earlier approval, or the task was put away.
15
+
3
16
  ## 0.9.102 — 2026-11-11
4
17
 
5
18
  - **Pages change in place.** Moving between Home, Tasks, a task page, Projects, Agents, Search and Settings no longer reloads the whole page. The menu stays put and the page changes with a short cross-fade: going deeper slides in, going back slides out (off when your system asks for reduced motion). Resting the pointer on a link reads its page ahead, so the click shows it at once. Back and Forward change the page the same way and return you to the spot you left. The link you pressed is marked, and a thin loading line shows when a page is slow.
package/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
  ### Your agents build. You decide.
9
9
 
10
10
  For Claude Code, Codex and Gemini, on your own machine.<br>
11
- Approve the exact scope once. Get results back Ready with the diff, the passing check and screenshots.
11
+ Approve the exact scope once. Get results back Ready with the diff, the check status and screenshots.
12
12
 
13
13
  [![npm](https://img.shields.io/npm/v/toolroll?color=171717)](https://www.npmjs.com/package/toolroll)
14
14
  [![CI](https://github.com/ap9000/toolroll/actions/workflows/ci.yml/badge.svg)](https://github.com/ap9000/toolroll/actions/workflows/ci.yml)
@@ -44,11 +44,12 @@ toolroll
44
44
  ```
45
45
 
46
46
  Toolroll opens in your terminal and runs while that window is open. No
47
- password and no login file: on your own computer you're already in. Pick an
48
- agent, a project (or the sample one) and a first task; approve the plan, and
49
- the result comes back **Ready**. Press `w` for the browser, or run
50
- `toolroll --web`. To look around first, `toolroll demo` opens a seeded
51
- sandbox with no model calls and no spend.
47
+ password and no login file: on your own computer you're already in. Started
48
+ in a git repository, that repository is your project; press `n` for a first
49
+ task, approve the plan, and the result comes back **Ready**. Elsewhere, add a
50
+ project with `toolroll repos add <folder>`, or open the browser (press `w`, or
51
+ run `toolroll --web`), which also offers a sample project. To look around
52
+ first, `toolroll demo` opens a seeded sandbox with no model calls and no spend.
52
53
 
53
54
  **Letting agents work on their own.** Approving work you start on this
54
55
  computer needs no password. Hands-off modes, merging, releasing and most
@@ -100,8 +101,9 @@ the CLI or MCP. Every act lands in one ledger.
100
101
 
101
102
  - **You approve scope once.** The approval binds to the exact goal, bounds,
102
103
  criteria, agent and model. No model sits in any approval path.
103
- - **Evidence, not trust.** Ready means the check ran on that commit. A failed
104
- check stays failed.
104
+ - **Evidence, not trust.** Ready shows the result and its check status;
105
+ merging needs the project's required check to pass on that exact result. A
106
+ failed check stays failed.
105
107
  - **Your machine, your logins.** Agents use the Claude and Codex subscriptions
106
108
  you already have. An OS-level fence keeps them away from Toolroll's secrets.
107
109
  - **Any agent.** Claude Code, Codex, Gemini and OpenRouter, routed per phase.
@@ -315,4 +315,36 @@ export declare function assignmentUpdates(store: Store, now: Date, access: Assig
315
315
  nextCursor: number;
316
316
  hasMore: boolean;
317
317
  };
318
+ /** What a machine caller hears when the result owes a person's own check. */
319
+ export declare const MACHINE_CANT_CHECK = "Only a person can say they checked this result. Open it in Toolroll and accept it there.";
320
+ /** The terminal's accept, as the ledger keeps it: the exact request (run and receipt as read) and the receipt it made. */
321
+ export declare const LOCAL_ACCEPT_ACTION = "task complete (local owner)";
322
+ /** What the terminal's accept answers: done (or done already, by this same request), or why not. */
323
+ export type LocalAcceptance = {
324
+ ok: true;
325
+ replayed: boolean;
326
+ accepted: "person-check" | "exception" | null;
327
+ assignment: AssignmentSnapshot;
328
+ } | {
329
+ ok: false;
330
+ reason: string;
331
+ message: string;
332
+ };
333
+ /**
334
+ * The terminal's Accept (local-owner plan §3, slice 1): the laptop's local owner, with no password, accepts the exact
335
+ * result its screen read (`runId` and `receiptDigest`, pinned when the screen opened) and completes it, in one
336
+ * transaction. Never a password check: the owner is the one in flight now, acting as exactly `complete-task`, still
337
+ * standing (local mode on at its generation, the account at its generation), and the approver is minted from that,
338
+ * then re-proved by `acceptAndCompleteAsOperator`, which keeps every rule a completion has (exact receipt and run,
339
+ * project access, slice 0's work rule, failed-check and protected-work blockers, rollback of the acceptance if the
340
+ * completion is refused). What is owed decides what the request must carry: a person's own check needs `checked`, an
341
+ * exception needs `reason` (already trimmed and validated by the command). The request and the receipt it produced are
342
+ * recorded; the same request again answers "already" after re-proving the owner, and writes nothing.
343
+ */
344
+ export declare function completeAsLocalOwner(store: Store, taskId: string, input: {
345
+ runId: number;
346
+ receiptDigest: string;
347
+ checked: boolean;
348
+ reason: string | null;
349
+ }, now: Date, root?: string): LocalAcceptance;
318
350
  export {};
@@ -8,12 +8,13 @@ import { replacedWords } from "./task-status.js";
8
8
  import { COMPLETER_ACTOR, COMPLETION_ACTION, COMPLETION_BY_MACHINE, COMPLETION_BY_PERSON, familyChangedFiles, landedReasonOf, personCompletionOwed } from "./result-completion.js";
9
9
  import { homedir } from "node:os";
10
10
  import { readSchemaVersion, refArchived, stopFactOf } from "./store.js";
11
- import { approvalOf, liveReleaseOf, machineActing } from "./scope.js";
11
+ import { approvalOf, liveReleaseOf, localWorkEligible, machineActing } from "./scope.js";
12
+ import { LOCAL_OWNER_LAPSED, currentLocalOwner, localActNow, localOwnerInFlight, localOwnerRefusal, localOwnerStandsNow } from "./local-owner.js";
12
13
  import { openWorkDecisionOf, taskWorkSummaryOf, workDecisionAction } from "./work-summary.js";
13
14
  import { evidenceRoot, readVerifiedArtifact, readVerifiedReport, verifyArtifact } from "./evidence.js";
14
15
  import { sweepIfMoved } from "./sweep-gate.js";
15
16
  import { verificationEvidence } from "./verification-evidence.js";
16
- import { reproveApprover } from "./principal.js";
17
+ import { reproveApprover, verifyApproverStanding } from "./principal.js";
17
18
  import { noteAssignmentStatus } from "./assignment-status.js";
18
19
  import { historicalAssessmentReason } from "./assignment-presentation.js";
19
20
  import { manualReviewOnly, shownVerdictOf } from "./proof.js";
@@ -493,6 +494,11 @@ function acknowledgeCurrent(store, current, receiptDigest, actor, now, root, acc
493
494
  const release = liveReleaseOf(store, current.receipt.taskId, "complete", now);
494
495
  if (release.applies && !release.ok)
495
496
  return { ok: false, reason: "release", message: `This release can't be completed: ${release.why}.` };
497
+ // A laptop's local owner (no password) completes ordinary work only: its exact act, still standing, and the one work
498
+ // rule (scope.ts localWorkEligible), read here in the completion's own transaction, on every road that completes.
499
+ const local = localCompletionRefusal(store, actor, [receipt.taskId, current.rootId], receipt.runId, root);
500
+ if (local !== null)
501
+ return local;
496
502
  // Who is completing, from trusted context (approval policy §1.7a): a person, or a machine credential (the lead's
497
503
  // token, a remote API run) that may complete ordinary work but is never the second person on protected work.
498
504
  const person = actor.startsWith("operator:") && !machineActing() ? actor.slice("operator:".length) : null;
@@ -592,7 +598,9 @@ export function owedAcceptance(receipt, blockers = []) {
592
598
  * request passes its own). */
593
599
  export function acceptAndCompleteAsOperator(store, taskId, input, who, now, root = evidenceRoot(homedir()), finish = digest => checkAssignmentAsOperator(store, taskId, digest, who, now, root)) {
594
600
  try {
595
- return withActor(currentActor() ?? { account: who.name, lead: false }, () => store.transact(() => {
601
+ // A savepoint inside the transaction: a refused completion takes its acceptance back with it even when a caller's
602
+ // own transaction holds this one (the terminal's local accept, a chat tap), not only when this one is outermost.
603
+ return withActor(currentActor() ?? { account: who.name, lead: false }, () => store.transact(() => store.savepoint(() => {
596
604
  if (!reproveApprover(store, who).ok)
597
605
  return { ok: false, reason: "unauthenticated", message: "Sign in again before accepting this result." };
598
606
  const access = { principal: "operator", repos: who.repos };
@@ -603,11 +611,19 @@ export function acceptAndCompleteAsOperator(store, taskId, input, who, now, root
603
611
  if (receipt === null || (input.runId !== null && receipt.runId !== input.runId) || !/^[a-f0-9]{64}$/.test(input.receiptDigest) || receipt.digest !== input.receiptDigest) {
604
612
  return { ok: false, reason: "stale", message: "This result changed. Open the current result before accepting it." };
605
613
  }
614
+ // A local owner's acceptance: the same rule as its completion, before any acceptance is written.
615
+ const local = localCompletionRefusal(store, `operator:${who.name}`, [receipt.taskId, taskId, before.rootId], receipt.runId, root);
616
+ if (local !== null)
617
+ return local;
606
618
  // Already complete, or nothing owed: the plain completion of the receipt as read.
607
619
  const owed = before.state === "ready-to-check" ? owedAcceptance(receipt, before.readiness?.blockers ?? []) : null;
608
620
  // An exception is accepted only with its reason: never a completion that skips it.
609
621
  if (owed === "exception" && input.note === null)
610
622
  return { ok: false, reason: "needs-reason", message: `${ACCEPT_NEEDS_REASON}.` };
623
+ // A person's own check is a person's, whoever holds the approver: never the lead's token or a remote API token in
624
+ // this chain (local-owner plan v2, item 4), whatever the caller decided before calling.
625
+ if (owed === "person-check" && machineActing())
626
+ return { ok: false, reason: "person-required", message: MACHINE_CANT_CHECK };
611
627
  let digest = receipt.digest;
612
628
  if (owed !== null) {
613
629
  store.acceptProof(receipt.runId, who.name, input.note, now);
@@ -621,7 +637,7 @@ export function acceptAndCompleteAsOperator(store, taskId, input, who, now, root
621
637
  if (!done.ok)
622
638
  throw new Undone(done);
623
639
  return done;
624
- }));
640
+ })));
625
641
  }
626
642
  catch (error) {
627
643
  if (error instanceof Undone)
@@ -687,3 +703,123 @@ export function assignmentUpdates(store, now, access, query, root) {
687
703
  }
688
704
  /** The waits of a build Toolroll is still proving stopped by itself (needs-you.ts processNeedOf). */
689
705
  const STOP_WAITS = new Set(["build-finishing", "build-checking", "build-stopping"]);
706
+ /**
707
+ * Local-owner slice 0 (docs/plans/local-owner-acts.md): when a laptop's local owner (no password) is the one completing
708
+ * or accepting, the refusal it hears, or null (anyone else, or an ordinary result it may complete). The owner must be
709
+ * in flight, still standing, acting as exactly `complete-task`, completing as itself; and every task named (the result's
710
+ * own and its family's root) must pass the shared work rule — no release, no protected work (declared or actually
711
+ * changed), nothing a coordinator filed. Read inside the completion's transaction.
712
+ */
713
+ function localCompletionRefusal(store, actor, taskIds, runId, root) {
714
+ const local = currentLocalOwner();
715
+ if (local === null)
716
+ return null;
717
+ if (!localOwnerInFlight(local) || !localOwnerStandsNow(store, local))
718
+ return { ok: false, reason: "local-owner", message: LOCAL_OWNER_LAPSED };
719
+ const instead = localOwnerRefusal(store.accountOf(local.account)?.passwordSet === true, "/");
720
+ const then = instead.action === "Set a password" ? "Set a password first: run `toolroll password set`." : instead.message;
721
+ if (local.act !== "complete-task" || actor !== `operator:${local.account}`)
722
+ return { ok: false, reason: "local-owner", message: `This needs your password. ${then}` };
723
+ // An earlier build in the family whose inventory is missing, and that its record doesn't prove committed nothing, is
724
+ // unknown here, and unknown counts as protected (localWorkEligible).
725
+ const changed = familyChangedFiles(store, taskIds[0], runId, root, { missing: "unknown" });
726
+ for (const taskId of new Set(taskIds)) {
727
+ const eligible = localWorkEligible(store, local.account, taskId, { act: "complete", changed });
728
+ if (!eligible.ok)
729
+ return { ok: false, reason: "local-owner", message: `${eligible.message} ${then}` };
730
+ }
731
+ return null;
732
+ }
733
+ // ---- Local owner, slice 1 (docs/plans/local-owner-acts.md §3): accept a result in the terminal. Kept last.
734
+ /** What a machine caller hears when the result owes a person's own check. */
735
+ export const MACHINE_CANT_CHECK = "Only a person can say they checked this result. Open it in Toolroll and accept it there.";
736
+ /** The terminal's accept, as the ledger keeps it: the exact request (run and receipt as read) and the receipt it made. */
737
+ export const LOCAL_ACCEPT_ACTION = "task complete (local owner)";
738
+ const LOCAL_STALE = { ok: false, reason: "stale", message: "This result changed. Look at the new one before accepting it." };
739
+ /** This owner's earlier terminal accept of exactly this run and receipt, from its ledger binding; null when none. */
740
+ function localAcceptanceOf(store, rootId, account, runId, digest, request) {
741
+ const rows = store.handle.prepare("SELECT outcome, detail FROM action_ledger WHERE action = ? AND task_id = ? AND actor = ? AND run_id = ? ORDER BY id DESC")
742
+ .all(LOCAL_ACCEPT_ACTION, rootId, `operator:${account}`, runId);
743
+ for (const row of rows) {
744
+ let detail;
745
+ try {
746
+ detail = JSON.parse(String(row["detail"] ?? ""));
747
+ }
748
+ catch {
749
+ continue;
750
+ }
751
+ const bound = detail;
752
+ if (bound.v !== 1 || bound.digest !== digest || typeof bound.completed !== "string")
753
+ continue;
754
+ const outcome = String(row["outcome"]);
755
+ // A replay is the whole request again (Codex review 3, finding 2): the same pins, the same "I checked it", the same
756
+ // reason. Anything else about an accepted result is a different request, never reported as saved.
757
+ const same = bound.checked === request.checked && (bound.reason ?? null) === request.reason;
758
+ return { completed: bound.completed, accepted: outcome === "person-check" || outcome === "exception" ? outcome : null, same };
759
+ }
760
+ return null;
761
+ }
762
+ /**
763
+ * The terminal's Accept (local-owner plan §3, slice 1): the laptop's local owner, with no password, accepts the exact
764
+ * result its screen read (`runId` and `receiptDigest`, pinned when the screen opened) and completes it, in one
765
+ * transaction. Never a password check: the owner is the one in flight now, acting as exactly `complete-task`, still
766
+ * standing (local mode on at its generation, the account at its generation), and the approver is minted from that,
767
+ * then re-proved by `acceptAndCompleteAsOperator`, which keeps every rule a completion has (exact receipt and run,
768
+ * project access, slice 0's work rule, failed-check and protected-work blockers, rollback of the acceptance if the
769
+ * completion is refused). What is owed decides what the request must carry: a person's own check needs `checked`, an
770
+ * exception needs `reason` (already trimmed and validated by the command). The request and the receipt it produced are
771
+ * recorded; the same request again answers "already" after re-proving the owner, and writes nothing.
772
+ */
773
+ export function completeAsLocalOwner(store, taskId, input, now, root = evidenceRoot(homedir())) {
774
+ try {
775
+ return store.transact(() => {
776
+ const local = localActNow(store, "complete-task");
777
+ if (local === null)
778
+ return { ok: false, reason: "local-owner", message: LOCAL_OWNER_LAPSED };
779
+ const standing = verifyApproverStanding(store, local.account, local.accountGeneration, store.knownRepos().filter(repo => store.accountCanAccess(local.account, repo)));
780
+ if (!standing.ok)
781
+ return { ok: false, reason: "local-owner", message: LOCAL_OWNER_LAPSED };
782
+ const who = standing.who;
783
+ const access = { principal: "operator", repos: who.repos };
784
+ const before = assignmentOf(store, taskId, now, access, root);
785
+ if (before === null || !store.accountCanAccess(who.name, before.repo))
786
+ return { ok: false, reason: "not-found", message: "That task is unavailable in your projects." };
787
+ // The same request again: recognised from its binding and the receipt it produced, with the owner proved above.
788
+ const bound = localAcceptanceOf(store, before.rootId, who.name, input.runId, input.receiptDigest, { checked: input.checked, reason: input.reason });
789
+ if (bound !== null && !bound.same)
790
+ return { ok: false, reason: "already-complete", message: "This result is already accepted." };
791
+ if (bound !== null) {
792
+ return before.state === "complete" && before.receipt?.runId === input.runId && before.receipt.digest === bound.completed
793
+ ? { ok: true, replayed: true, accepted: bound.accepted, assignment: before } : LOCAL_STALE;
794
+ }
795
+ const receipt = before.receipt;
796
+ if (receipt === null || receipt.runId !== input.runId || receipt.digest !== input.receiptDigest)
797
+ return LOCAL_STALE;
798
+ if (before.state === "complete")
799
+ return { ok: false, reason: "already-complete", message: "This result is already accepted." };
800
+ const owed = before.state === "ready-to-check" ? owedAcceptance(receipt, before.readiness?.blockers ?? []) : null;
801
+ if (owed === "person-check" && !input.checked)
802
+ return { ok: false, reason: "needs-check", message: "This result needs your own check. Look at it, then say you checked it." };
803
+ if (owed === "exception" && input.reason === null)
804
+ return { ok: false, reason: "needs-reason", message: `${ACCEPT_NEEDS_REASON}.` };
805
+ const done = acceptAndCompleteAsOperator(store, taskId, { runId: input.runId, receiptDigest: input.receiptDigest, note: owed === null ? null : input.reason }, who, now, root);
806
+ if (!done.ok)
807
+ throw new Undone(done);
808
+ const after = assignmentOf(store, taskId, now, access, root);
809
+ if (after === null || after.state !== "complete" || after.receipt?.runId !== input.runId)
810
+ throw new Undone(LOCAL_STALE);
811
+ store.recordAction({ at: now.toISOString(), actor: `operator:${who.name}`, repo: after.repo, taskId: after.rootId, runId: input.runId, action: LOCAL_ACCEPT_ACTION,
812
+ outcome: owed ?? "completed", source: "request",
813
+ detail: JSON.stringify({ v: 1, digest: input.receiptDigest, completed: after.receipt.digest, checked: input.checked, reason: input.reason, via: local.transport }) });
814
+ return { ok: true, replayed: false, accepted: owed, assignment: after };
815
+ });
816
+ }
817
+ catch (error) {
818
+ if (error instanceof Undone)
819
+ return error.result;
820
+ // Store.transact's own re-check found the bound owner no longer standing: the same refusal, said, never a crash.
821
+ if (error instanceof Error && error.message === LOCAL_OWNER_LAPSED)
822
+ return { ok: false, reason: "local-owner", message: LOCAL_OWNER_LAPSED };
823
+ throw error;
824
+ }
825
+ }