toolroll 0.9.103 → 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.
- package/CHANGELOG.md +8 -0
- package/README.md +10 -8
- package/dist/assignment.d.ts +32 -0
- package/dist/assignment.js +140 -4
- package/dist/browser/workspace.js +25 -25
- package/dist/browser-workspace.d.ts +13 -0
- package/dist/contracts/console-api.d.ts +3 -2
- package/dist/contracts/console-api.js +4 -2
- package/dist/desktop-update.d.ts +1 -1
- package/dist/keymap.d.ts +3 -1
- package/dist/keymap.js +3 -0
- package/dist/local-owner-acts.d.ts +83 -0
- package/dist/local-owner-acts.js +221 -0
- package/dist/local-owner.d.ts +12 -1
- package/dist/local-owner.js +26 -0
- package/dist/operate-remote-arguments.js +4 -2
- package/dist/operate.js +76 -1
- package/dist/plan-auto.js +5 -0
- package/dist/project-knowledge.d.ts +3 -3
- package/dist/public-access.d.ts +9 -0
- package/dist/public-access.js +15 -0
- package/dist/pull-request-flow.js +9 -0
- package/dist/release-card.d.ts +15 -1
- package/dist/release-card.js +35 -12
- package/dist/result-completion.d.ts +6 -2
- package/dist/result-completion.js +13 -7
- package/dist/scope.d.ts +32 -1
- package/dist/scope.js +110 -25
- package/dist/serve.js +13 -14
- package/dist/server/guards.d.ts +1 -0
- package/dist/server/guards.js +11 -1
- package/dist/server/remote.d.ts +1 -0
- package/dist/server/render-results.d.ts +4 -0
- package/dist/server/render-tasks.d.ts +9 -0
- package/dist/server/render-tasks.js +4 -4
- package/dist/server/route-table.js +4 -0
- package/dist/server/runtime.d.ts +1 -0
- package/dist/server/task-views.js +3 -0
- package/dist/server/tasks.d.ts +25 -0
- package/dist/server/tasks.js +46 -6
- package/dist/surface.js +12 -2
- package/dist/task-outcome-cli.d.ts +18 -0
- package/dist/task-outcome-cli.js +60 -2
- package/dist/tui-contract.d.ts +61 -3
- package/dist/tui-source.js +91 -20
- package/dist/tui.js +54 -54
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
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
|
+
|
|
3
11
|
## 0.9.103 — 2026-11-11
|
|
4
12
|
|
|
5
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.
|
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
|
|
11
|
+
Approve the exact scope once. Get results back Ready with the diff, the check status and screenshots.
|
|
12
12
|
|
|
13
13
|
[](https://www.npmjs.com/package/toolroll)
|
|
14
14
|
[](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.
|
|
48
|
-
|
|
49
|
-
the result comes back **Ready**.
|
|
50
|
-
`toolroll
|
|
51
|
-
|
|
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
|
|
104
|
-
check
|
|
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.
|
package/dist/assignment.d.ts
CHANGED
|
@@ -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 {};
|
package/dist/assignment.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|