toolroll 0.9.13 → 0.9.15

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 (43) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/assignment-ui.d.ts +1 -0
  3. package/dist/assignment-ui.js +8 -5
  4. package/dist/assignment.d.ts +32 -0
  5. package/dist/assignment.js +81 -7
  6. package/dist/browser/workspace.css +1 -1
  7. package/dist/browser/workspace.js +38 -38
  8. package/dist/browser-crew.d.ts +2 -1
  9. package/dist/browser-crew.js +4 -2
  10. package/dist/browser-workspace.d.ts +109 -11
  11. package/dist/chat-actions.d.ts +1 -1
  12. package/dist/chat-actions.js +6 -4
  13. package/dist/chat-delivery-state.d.ts +1 -1
  14. package/dist/chat-voice.d.ts +3 -3
  15. package/dist/chat-voice.js +3 -3
  16. package/dist/demo.js +82 -2
  17. package/dist/disclosure.d.ts +6 -0
  18. package/dist/disclosure.js +13 -0
  19. package/dist/dispatch.js +7 -2
  20. package/dist/lead-status.js +2 -2
  21. package/dist/needs-you.d.ts +72 -0
  22. package/dist/needs-you.js +193 -1
  23. package/dist/result-acts.d.ts +51 -5
  24. package/dist/result-acts.js +36 -3
  25. package/dist/result-review.d.ts +4 -3
  26. package/dist/result-review.js +7 -3
  27. package/dist/serve.d.ts +3 -0
  28. package/dist/serve.js +1235 -711
  29. package/dist/slack-chat.js +1 -1
  30. package/dist/task-status.d.ts +24 -3
  31. package/dist/task-status.js +48 -17
  32. package/dist/telegram-progress.js +2 -2
  33. package/dist/test-temp.js +1 -1
  34. package/dist/transitions-recipes.d.ts +1 -1
  35. package/dist/transitions-recipes.js +5 -9
  36. package/dist/when-html.d.ts +19 -2
  37. package/dist/when-html.js +78 -11
  38. package/dist/work-index.d.ts +3 -1
  39. package/dist/work-index.js +18 -5
  40. package/dist/workspace-motion.js +3 -3
  41. package/dist/workspace-ui.d.ts +86 -6
  42. package/dist/workspace-ui.js +143 -17
  43. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.15 — 2026-10-02
4
+
5
+ - **Accept finishes the task.** One step, Accept and finish, after the
6
+ evidence. Your own checks come first, and the count updates as you answer.
7
+ - **Failures say what failed.** A failed task names the requirement or check
8
+ it missed and suggests what to change, prefilled in Retry. Failed builds get
9
+ a real result page. Builds show "Step 2 of 6" and say when they're stuck.
10
+ - **One set of words.** The same state names in the list, task page, result
11
+ page and Crew. Project checks and PR CI are labelled apart, and every time
12
+ shows in your own time zone.
13
+ - **Approving says what you allow.** A plain "You're allowing" line sits above
14
+ Approve, your password says what it signs, and Edit plan edits in place.
15
+ - **Safer deploys.** An interrupted deploy puts the old service and its coding
16
+ store back.
17
+
18
+ ## 0.9.14 — 2026-10-02
19
+
20
+ - **Results and failures say what went wrong.** A result whose report doesn't
21
+ match its saved changes says so in its headline and lists each mismatch,
22
+ with its requirements shown as unverified; accepting it asks for a reason. A
23
+ failed task shows its latest attempt's reason in one plain line (or that
24
+ none was recorded), linked to the log, with Retry as its one action.
25
+
3
26
  ## 0.9.13 — 2026-10-02
4
27
 
5
28
  - **One page per result.** Every build's result has one page, titled with its
@@ -60,6 +60,7 @@ export type AssignmentCard = {
60
60
  export declare function assignmentCardOf(assignment: AssignmentSnapshot, options?: AssignmentCardOptions): AssignmentCard;
61
61
  export declare function assignmentSummaryHtml(assignment: AssignmentSnapshot, options?: AssignmentCardOptions & {
62
62
  compact?: boolean;
63
+ sentence?: string | undefined;
63
64
  }): string;
64
65
  /** Shared assignment layout; actions remain comfortable on touch screens. */
65
66
  export declare const ASSIGNMENT_CSS = ".assignment-summary{min-width:0;overflow-wrap:anywhere}.assignment-summary .assignment-state{font-weight:600;margin:0}.assignment-summary h2.assignment-state{font-size:1.125rem;color:var(--foreground)}.assignment-summary>.assignment-attempts,.assignment-summary>.assignment-notices{border:0;box-shadow:none;padding:0;margin:.25rem 0 0;background:transparent}.assignment-summary>.button-link{margin-top:.7rem;min-height:44px}.assignment-summary>.work-action{display:inline-flex;margin-inline-start:.6rem;min-height:44px;align-items:center}.assignment-detail{margin:.55rem 0}.assignment-attempts summary,.assignment-notices summary{min-height:44px;display:list-item;align-content:center;cursor:pointer}.assignment-notices summary{font-size:.8125rem;color:var(--foreground)}.assignment-notices p{margin:.25rem 0 .5rem}.assignment-attempts ol{padding-left:1.4rem}.assignment-attempts li{margin:.4rem 0}.assignment-attempts li a{display:inline-flex;align-items:center;min-height:44px;margin-right:.7rem}.assignment-attempts li p{margin:0 0 .4rem}";
@@ -41,7 +41,8 @@ export function assignmentActionHref(assignment) {
41
41
  return `/r/${runId}`;
42
42
  const anchor = action.code === 'approve-scope' ? '#approve'
43
43
  : action.code === 'inspect-stop' || action.code === 'resume-run' ? '#task-control'
44
- : action.code === 'unhold' || action.code === 'retry-task' ? '#task-actions'
44
+ // Retry (and Build again) is the task's status card itself, at the top: no anchor to its options.
45
+ : action.code === 'unhold' ? '#task-actions'
45
46
  : action.code === 'write-scope' || action.code === 'select-agent' ? '#scope'
46
47
  : action.code === 'inspect-hold' ? '#holds'
47
48
  : action.code === 'start-worker' || action.code === 'repair-dependency' ? '#run-status' : '';
@@ -71,10 +72,12 @@ function savedMaterialNotice(detail, assignment) {
71
72
  }
72
73
  export function assignmentCardOf(assignment, options = {}) {
73
74
  const receipt = assignment.receipt;
74
- const links = receipt === null ? {} : { result: options.resultHref ?? chatResultHref(receipt.taskId, receipt.runId), checks: chatResultHref(receipt.taskId, receipt.runId, 'checks'),
75
+ // The checks live on the one result page (a failed build has one too), where Run checks runs in place; never Chat,
76
+ // which would only lead back to this card.
77
+ const checksPage = receipt === null ? null : `/review?result=${encodeURIComponent(receipt.taskId)}&run=${receipt.runId}&tab=checks`;
78
+ const links = receipt === null ? {} : { result: options.resultHref ?? chatResultHref(receipt.taskId, receipt.runId), checks: checksPage,
75
79
  pullRequest: `/t/${encodeURIComponent(assignment.rootId)}#merge`,
76
- // Run checks lives with the result's checks; the row's action leads there.
77
- ...(receipt.completionKind === 'research-report' ? {} : { runChecks: `${chatResultHref(receipt.taskId, receipt.runId, 'checks')}#follow-ups` }) };
80
+ ...(receipt.completionKind === 'research-report' ? {} : { runChecks: `${checksPage}#follow-ups` }) };
78
81
  const presentation = assignmentPresentationOf(assignment, { ...options, links });
79
82
  const { status, diagnostics, taskStatus } = presentation;
80
83
  const href = assignment.primaryAction?.code === 'open-result' && options.resultHref !== undefined ? options.resultHref : assignmentActionHref(assignment);
@@ -99,7 +102,7 @@ export function assignmentSummaryHtml(assignment, options = {}) {
99
102
  const tone = card.status.tone;
100
103
  return `<section class="${options.compact ? 'assignment-summary' : 'card assignment-summary'}" aria-label="assignment progress" data-assignment="${escape(assignment.rootId)}" data-work-status="${card.token}" data-tone="${card.tone}" data-headline="${escape(card.status.headline)}" data-headline-tone="${tone}"${options.compact ? '' : ' data-task-status'}>` +
101
104
  (options.compact ? `<span class="status-line" data-work-status="${card.token}" data-tone="${card.tone}"><i class="status-dot" aria-hidden="true"></i><span class="status-label">${card.label}</span></span>` : `<h2 class="assignment-state status-headline"><i aria-hidden="true"></i>${escape(card.label)}</h2>`) +
102
- `<p class="meta assignment-detail status-sentence">${escape(card.status.sentence)}</p>` +
105
+ `<p class="meta assignment-detail status-sentence">${escape(options.sentence ?? card.status.sentence)}</p>` +
103
106
  (card.action === null ? '' : `<a class="${options.compact ? 'work-action' : 'button-link'}" href="${escape(card.action.href)}"${card.action.openResult ? ' data-open-result' : ''}${options.compact ? '' : ' data-primary-action'}>${escape(card.action.label)}${options.compact ? ' →' : ''}</a>`) +
104
107
  (options.compact ? '' : statusDetailsHtml(card.status)) +
105
108
  statusWhyHtml(card.status, card.reasons, card.diagnostics) +
@@ -111,6 +111,10 @@ export type AssignmentSnapshot = {
111
111
  } | null;
112
112
  /** The person's lead took it on (lead-voice.ts): "Your lead is on it" until done, handed on, or two quiet hours. */
113
113
  lead?: LeadClaim | null;
114
+ /** How many earlier versions of this task are still queued or running; absent when none. */
115
+ earlierActive?: number;
116
+ /** Of those, how many are running rather than only queued; present with earlierActive. */
117
+ earlierRunning?: number;
114
118
  };
115
119
  /** Status-first handoff for routine reads. Fetch get_assignment only when
116
120
  * inspecting its exact evidence; do not repeat a full proof in every poll. */
@@ -180,6 +184,34 @@ export declare function checkAssignment(store: Store, taskId: string, receiptDig
180
184
  /** The signed-in user can mark the same exact receipt handled without taking
181
185
  * lead ownership. This records review, never check success or new authority. */
182
186
  export declare function checkAssignmentAsOperator(store: Store, taskId: string, receiptDigest: string, who: VerifiedApprover, now: Date, root?: string): MutationResult;
187
+ /** The acceptance's own ledger act, as a separate Accept request records it. */
188
+ export declare const ACCEPT_ACTION = "task accept-proof";
189
+ /** Only a person's check stands between this result and done: its proof is short for that alone, and nobody accepted it. */
190
+ export declare function personCheckPending(receipt: AssignmentReceipt | null): boolean;
191
+ type Finished = {
192
+ ok: true;
193
+ } | {
194
+ ok: false;
195
+ reason: string;
196
+ message: string;
197
+ };
198
+ /** An acceptance this receipt owes before it finishes: the person's own check (no reason needed), or an
199
+ * exception the proof needs and the person gave a reason for. Null when Accept and finish is a plain
200
+ * completion: nothing is owed, it was already accepted, or an exception came without its reason. */
201
+ export declare function owedAcceptance(receipt: AssignmentReceipt | null, note: string | null): "person-check" | "exception" | null;
202
+ /** Accept and finish: the person's acceptance of the exact result they read and its completion, in one
203
+ * transaction. Whether an acceptance is owed is decided here, against the receipt as it stands: only a
204
+ * person's own check or a reasoned exception records one (with its ledger act); anything else is the plain
205
+ * completion Mark complete always was. The completion names the receipt the acceptance produced, under the
206
+ * same digest a separate Accept then Mark complete leave. `receiptDigest` is the receipt as read, before
207
+ * accepting (`runId`, when named, must be its run); any other change refuses both, and a refused completion
208
+ * keeps no acceptance. `finish` completes the receipt (Mark complete by default; Complete and open a pull
209
+ * request passes its own). */
210
+ export declare function acceptAndCompleteAsOperator(store: Store, taskId: string, input: {
211
+ runId: number | null;
212
+ receiptDigest: string;
213
+ note: string | null;
214
+ }, who: VerifiedApprover, now: Date, root?: string, finish?: (digest: string) => Finished): Finished;
183
215
  /** The ordinary worker reconciles at most 50 owned roots per pass. A lost
184
216
  * wakeup is recovered after restart; immutable notification keys dedupe it. */
185
217
  export declare function syncAssignmentHandoffs(store: Store, now: Date, repos: readonly string[], root?: string): void;
@@ -14,10 +14,11 @@ import { verificationEvidence } from "./verification-evidence.js";
14
14
  import { reproveApprover } from "./principal.js";
15
15
  import { noteAssignmentStatus } from "./assignment-status.js";
16
16
  import { historicalAssessmentReason } from "./assignment-presentation.js";
17
+ import { manualReviewOnly } from "./proof.js";
17
18
  import { runCheckLevel } from "./check-levels.js";
18
19
  import { followUpChecksOf, withFollowUps } from "./result-follow-ups.js";
19
20
  import { buildReviewOf, findingWords } from "./review-switch.js";
20
- import { NEEDS, WAITS, processNeedOf } from "./needs-you.js";
21
+ import { NEEDS, WAITS, processNeedOf, resultHoldUpSentence } from "./needs-you.js";
21
22
  import { assignmentStageOf } from "./task-status.js";
22
23
  import { leadClaimOf } from "./lead-voice.js";
23
24
  /** Status-first handoff for routine reads. Fetch get_assignment only when
@@ -140,9 +141,11 @@ export function assignmentOf(store, taskId, now, access, root) {
140
141
  return decision === null ? [] : [{ taskId: version.id, decision }];
141
142
  });
142
143
  // A finished task label cannot hide a lease or an unfinished process record.
143
- const earlierActive = family.versions.filter(version => version.id !== current.id &&
144
- (version.state === "queued" || version.state === "running" || store.currentLiveLease(version.refId, now) !== null ||
145
- store.runsFor(version.refId).some(run => run.outcome === null || store.stopQuiescenceProblem(run.id) !== null)));
144
+ const busy = (version) => version.state === "running" || store.currentLiveLease(version.refId, now) !== null ||
145
+ store.runsFor(version.refId).some(run => run.outcome === null || store.stopQuiescenceProblem(run.id) !== null);
146
+ const earlierActive = family.versions.filter(version => version.id !== current.id && (version.state === "queued" || busy(version)));
147
+ // Of those, how many have work under way (the rest only wait in the queue).
148
+ const earlierRunning = earlierActive.filter(busy).length;
146
149
  const unfinished = store.runsFor(current.refId).find(run => run.outcome === null) ?? null;
147
150
  const attempts = family.versions.map(version => {
148
151
  const summary = version.id === current.id ? work : taskWorkSummaryOf(store, version.id, now, access);
@@ -246,7 +249,7 @@ export function assignmentOf(store, taskId, now, access, root) {
246
249
  detail = completionKind === "research-report" ? "The research report is ready for the lead to read."
247
250
  : completionKind === "accepted-exception" ? "An operator accepted this result with its recorded limitations. The lead can inspect that decision; the recorded checks are unchanged."
248
251
  : receipt.checks.detail;
249
- primaryAction = { code: "open-result", label: completionKind === "research-report" ? "Read report" : receipt.checks.status === "failed" ? "Inspect failed check" : completionKind === "accepted-exception" ? "Review acceptance" : "Open result", target: { taskId: current.id, runId: result.id, decisionId: null }, access: "read", retry: "read-again" };
252
+ primaryAction = { code: "open-result", label: completionKind === "research-report" ? "Read report" : "Open result", target: { taskId: current.id, runId: result.id, decisionId: null }, access: "read", retry: "read-again" };
250
253
  const checked = store.handle.prepare("SELECT actor,at FROM action_ledger WHERE task_id = ? AND run_id = ? AND action = ? AND outcome = ? AND source = 'work' ORDER BY id DESC")
251
254
  .all(family.root.id, result.id, CHECK_ACTION, receipt.digest).find(row => /^(operator|coordinator|lead):.+/.test(String(row["actor"])));
252
255
  // This ledger fact was authorized when written. Credential rotation,
@@ -269,7 +272,11 @@ export function assignmentOf(store, taskId, now, access, root) {
269
272
  detail = result?.role === "scout"
270
273
  ? completionKind === null ? "The research report is missing or incomplete. Inspect the saved report before checking this handoff."
271
274
  : "The saved report is awaiting resolution of its current scope or hold."
272
- : "Inspect the saved result and resolve its remaining execution or scope issue.";
275
+ : resultHoldUpSentence({ hold: store.activeHolds(current.refId, now)[0]?.reason ?? null,
276
+ planChanged: result !== null && scope !== null && !!result.scopeDigest && scope.digest !== result.scopeDigest,
277
+ unapproved: scope === null || scope.termsProblem != null || !approvalOf(scope).approved, question: questions.length > 0,
278
+ running: store.currentLiveLease(current.refId, now) !== null, unfinished: unfinished !== null,
279
+ noCommit: result === null || completionKind === null });
273
280
  // Built to an earlier plan: the result page can't resolve it (accepting would leave it stuck), building again can.
274
281
  if (current.state === "done" && result !== null && scope !== null && scope.termsProblem == null && approvalOf(scope).approved && !!result.scopeDigest && scope.digest !== result.scopeDigest &&
275
282
  store.activeHolds(current.refId, now).length === 0 && unfinished === null && store.currentLiveLease(current.refId, now) === null && store.finalResultReason(result.id) === null) {
@@ -341,7 +348,7 @@ export function assignmentOf(store, taskId, now, access, root) {
341
348
  return { version: 1, rootId: family.root.id, activeTaskId: current.id, repo: current.repo, title: family.root.title,
342
349
  state, detail, primaryAction, attention: [...new Set(attention)], attempts, owner, receipt, savedContext, completion, handoff,
343
350
  publication: publication === null ? null : { state: publication.state, prUrl: publication.prUrl, remoteState: publication.remoteState },
344
- review, deployment: { status: "not-recorded" }, ...(need === null ? {} : { need }), ...(lead === null ? {} : { lead }) };
351
+ review, deployment: { status: "not-recorded" }, ...(need === null ? {} : { need }), ...(lead === null ? {} : { lead }), ...(earlierActive.length === 0 ? {} : { earlierActive: earlierActive.length, earlierRunning }) };
345
352
  }
346
353
  function admittedOwner(store, taskId, owner, now, root) {
347
354
  if (owner.kind !== "coordinator")
@@ -445,6 +452,73 @@ export function checkAssignmentAsOperator(store, taskId, receiptDigest, who, now
445
452
  return acknowledgeCurrent(store, current, receiptDigest, `operator:${who.name}`, now, root, access);
446
453
  }));
447
454
  }
455
+ /** The acceptance's own ledger act, as a separate Accept request records it. */
456
+ export const ACCEPT_ACTION = "task accept-proof";
457
+ /** Only a person's check stands between this result and done: its proof is short for that alone, and nobody accepted it. */
458
+ export function personCheckPending(receipt) {
459
+ return receipt !== null && receipt.proofAcceptance === null && manualReviewOnly(receipt.proof === null ? null : { verdict: receipt.proof.verdict, reasons: receipt.proof.reasons, matrix: receipt.proof.matrix });
460
+ }
461
+ class Undone extends Error {
462
+ result;
463
+ constructor(result) {
464
+ super(result.message);
465
+ this.result = result;
466
+ }
467
+ }
468
+ /** An acceptance this receipt owes before it finishes: the person's own check (no reason needed), or an
469
+ * exception the proof needs and the person gave a reason for. Null when Accept and finish is a plain
470
+ * completion: nothing is owed, it was already accepted, or an exception came without its reason. */
471
+ export function owedAcceptance(receipt, note) {
472
+ if (receipt === null || receipt.proofAcceptance !== null)
473
+ return null;
474
+ if (personCheckPending(receipt))
475
+ return "person-check";
476
+ return note !== null && receipt.proof?.verdict !== "verified" && receipt.proof?.verdict !== "attested" ? "exception" : null;
477
+ }
478
+ /** Accept and finish: the person's acceptance of the exact result they read and its completion, in one
479
+ * transaction. Whether an acceptance is owed is decided here, against the receipt as it stands: only a
480
+ * person's own check or a reasoned exception records one (with its ledger act); anything else is the plain
481
+ * completion Mark complete always was. The completion names the receipt the acceptance produced, under the
482
+ * same digest a separate Accept then Mark complete leave. `receiptDigest` is the receipt as read, before
483
+ * accepting (`runId`, when named, must be its run); any other change refuses both, and a refused completion
484
+ * keeps no acceptance. `finish` completes the receipt (Mark complete by default; Complete and open a pull
485
+ * request passes its own). */
486
+ export function acceptAndCompleteAsOperator(store, taskId, input, who, now, root = evidenceRoot(homedir()), finish = digest => checkAssignmentAsOperator(store, taskId, digest, who, now, root)) {
487
+ try {
488
+ return withActor(currentActor() ?? { account: who.name, lead: false }, () => store.transact(() => {
489
+ if (!reproveApprover(store, who).ok)
490
+ return { ok: false, reason: "unauthenticated", message: "Sign in again before accepting this result." };
491
+ const access = { principal: "operator", repos: who.repos };
492
+ const before = assignmentOf(store, taskId, now, access, root);
493
+ if (before === null || !store.accountCanAccess(who.name, before.repo))
494
+ return { ok: false, reason: "not-found", message: "No assignment is available in your projects." };
495
+ const receipt = before.receipt;
496
+ if (receipt === null || (input.runId !== null && receipt.runId !== input.runId) || !/^[a-f0-9]{64}$/.test(input.receiptDigest) || receipt.digest !== input.receiptDigest) {
497
+ return { ok: false, reason: "stale", message: "This result changed. Open the current result before accepting it." };
498
+ }
499
+ // Already complete, or nothing owed: the plain completion of the receipt as read.
500
+ const owed = before.state === "ready-to-check" ? owedAcceptance(receipt, input.note) : null;
501
+ let digest = receipt.digest;
502
+ if (owed !== null) {
503
+ store.acceptProof(receipt.runId, who.name, input.note, now);
504
+ store.recordAction({ at: now.toISOString(), actor: who.name, repo: before.repo, taskId, runId: null, action: ACCEPT_ACTION, outcome: "accepted", source: "request" });
505
+ const accepted = assignmentOf(store, taskId, now, access, root)?.receipt ?? null;
506
+ if (accepted === null || accepted.runId !== receipt.runId)
507
+ throw new Undone({ ok: false, reason: "stale", message: "This result changed. Open the current result before accepting it." });
508
+ digest = accepted.digest;
509
+ }
510
+ const done = finish(digest);
511
+ if (!done.ok)
512
+ throw new Undone(done);
513
+ return done;
514
+ }));
515
+ }
516
+ catch (error) {
517
+ if (error instanceof Undone)
518
+ return error.result;
519
+ throw error;
520
+ }
521
+ }
448
522
  function noteAssignmentHandoff(store, assignment, now) {
449
523
  if (!assignment.owner?.active || assignment.handoff === null || assignment.handoff.acknowledged)
450
524
  return;