toolroll 0.9.8 → 0.9.10

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 (85) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/assignment-brief.d.ts +2 -0
  3. package/dist/assignment-brief.js +2 -1
  4. package/dist/assignment-presentation.js +3 -1
  5. package/dist/assignment-status.d.ts +1 -1
  6. package/dist/assignment-ui.js +5 -0
  7. package/dist/assignment.d.ts +9 -0
  8. package/dist/assignment.js +42 -8
  9. package/dist/browser/workspace.css +1 -1
  10. package/dist/browser/workspace.js +18 -18
  11. package/dist/browser-crew.js +4 -0
  12. package/dist/browser-workspace.d.ts +40 -0
  13. package/dist/builder.d.ts +2 -0
  14. package/dist/builder.js +114 -8
  15. package/dist/chat-controls.d.ts +1 -1
  16. package/dist/chat-controls.js +1 -1
  17. package/dist/chat-delivery-state.d.ts +5 -0
  18. package/dist/chat-delivery.js +45 -8
  19. package/dist/chat-quiet.d.ts +10 -4
  20. package/dist/chat-quiet.js +87 -22
  21. package/dist/chat-voice.d.ts +68 -0
  22. package/dist/chat-voice.js +175 -0
  23. package/dist/check-progress.js +4 -3
  24. package/dist/checkout-cleanup.d.ts +40 -2
  25. package/dist/checkout-cleanup.js +129 -9
  26. package/dist/desktop-update.d.ts +52 -0
  27. package/dist/desktop-update.js +147 -5
  28. package/dist/discord-chat.js +1 -0
  29. package/dist/evidence.d.ts +6 -0
  30. package/dist/evidence.js +8 -0
  31. package/dist/first-run.d.ts +1 -1
  32. package/dist/first-run.js +7 -5
  33. package/dist/lead-context.d.ts +1 -1
  34. package/dist/lead-context.js +4 -2
  35. package/dist/lead-status.d.ts +10 -1
  36. package/dist/lead-status.js +21 -24
  37. package/dist/needs-you.d.ts +55 -0
  38. package/dist/needs-you.js +83 -0
  39. package/dist/operate.d.ts +2 -0
  40. package/dist/operate.js +122 -15
  41. package/dist/plane-review.js +4 -0
  42. package/dist/process-recovery-eligibility.d.ts +1 -0
  43. package/dist/project-delete.js +1 -0
  44. package/dist/project-learning.d.ts +3 -3
  45. package/dist/project-learning.js +28 -17
  46. package/dist/proof.d.ts +3 -1
  47. package/dist/proof.js +4 -2
  48. package/dist/releases.d.ts +9 -0
  49. package/dist/releases.js +30 -0
  50. package/dist/result-completion.d.ts +4 -0
  51. package/dist/result-completion.js +11 -0
  52. package/dist/review-context.d.ts +3 -0
  53. package/dist/review-context.js +45 -5
  54. package/dist/run-closeout.d.ts +89 -0
  55. package/dist/run-closeout.js +302 -0
  56. package/dist/serve.d.ts +15 -1
  57. package/dist/serve.js +129 -14
  58. package/dist/setup-guide.d.ts +2 -2
  59. package/dist/shared-deps.d.ts +94 -0
  60. package/dist/shared-deps.js +365 -0
  61. package/dist/slack-chat.js +2 -0
  62. package/dist/storage.js +2 -0
  63. package/dist/store.d.ts +60 -0
  64. package/dist/store.js +207 -30
  65. package/dist/task-status.d.ts +18 -4
  66. package/dist/task-status.js +35 -18
  67. package/dist/teams-chat.js +1 -1
  68. package/dist/telegram-progress.d.ts +6 -1
  69. package/dist/telegram-progress.js +13 -5
  70. package/dist/telegram-status.d.ts +2 -1
  71. package/dist/telegram-status.js +6 -5
  72. package/dist/telegram.d.ts +1 -1
  73. package/dist/telegram.js +93 -43
  74. package/dist/test-temp.d.ts +19 -0
  75. package/dist/test-temp.js +98 -0
  76. package/dist/toolroll-update-ui.js +7 -2
  77. package/dist/toolroll-update.d.ts +21 -1
  78. package/dist/toolroll-update.js +135 -5
  79. package/dist/work-index.d.ts +11 -0
  80. package/dist/work-index.js +103 -16
  81. package/dist/work-summary.d.ts +3 -1
  82. package/dist/workspace-ui.js +1 -1
  83. package/dist/worktree.d.ts +16 -0
  84. package/dist/worktree.js +47 -0
  85. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.10 — 2026-10-02
4
+
5
+ - **Chat reads like a teammate.** Short messages, outcome first, with real
6
+ choices: Merge or Mark complete, and Look first. Updates within two minutes
7
+ arrive as one message. You're never told about your own actions, release
8
+ checks and replaced tasks stay quiet, and a release sends one "is live" line.
9
+ The Telegram bot is named Toolroll.
10
+
11
+ ## 0.9.9 — 2026-10-02
12
+
13
+ - **Faster, lighter tests.** The unit suite runs about three times faster
14
+ (the biggest test files are split by area), tests use at most 4 workers,
15
+ and a project's Quick check can be a typecheck plus the tests for the
16
+ changed files.
17
+ - **Toolroll cleans up after itself.** When a run ends, it stops what the run
18
+ left running. Tests delete their temp folders, and `toolroll storage clean`
19
+ removes leftovers. Replaced release checks give up their checkouts, and
20
+ kept checkouts drop their dependencies until they're needed.
21
+ - **Checkouts share dependencies.** One install per lockfile, linked into
22
+ each checkout: about 1,900 files instead of 20,000, set up in under a second.
23
+ - **Every "Needs you" says what it needs, with the button that does it.**
24
+ Anything you can't act on shows as Waiting instead.
25
+ - **Finished tasks read Complete everywhere.** Status, the console and chat
26
+ agree, and old "process exit not recorded" entries clear themselves.
27
+ - **Updates never hang.** The updater settles leftover records itself, stops
28
+ waiting after 2 minutes otherwise, and says the one command that clears it.
29
+ Release checks now test upgrading from the last three releases.
30
+ - **Screenshots stay out of the repository.** Images under `evidence/` are
31
+ left out of commits and show as one line in the review.
32
+
3
33
  ## 0.9.8 — 2026-10-01
4
34
 
5
35
  - **One automatic review per build.** When a build's checks pass (or it
@@ -23,6 +23,8 @@ export type AssignmentCatchUp = {
23
23
  } | null;
24
24
  nextAction: AssignmentSnapshot["primaryAction"];
25
25
  attention: string[];
26
+ /** Where the next action leads in the console (navigation only). */
27
+ nextHref?: string | null;
26
28
  decisions: {
27
29
  id: number;
28
30
  runId: number;
@@ -1,5 +1,6 @@
1
1
  /** A bounded catch-up over saved project/task records, not conversation memory.
2
2
  * Callers authenticate first and pass their current project ceiling. */
3
+ import { browserWorkActionHref } from './browser-crew.js';
3
4
  import { createHash } from "node:crypto";
4
5
  import { workIndexPage } from "./work-index.js";
5
6
  import { publicChatText } from "./chat-display.js";
@@ -72,7 +73,7 @@ export function assignmentCatchUp(store, now, access, query = {}, evidenceRoot)
72
73
  runId: item.liveRunId ?? item.unfinishedRunId ?? item.resultRunId, resultRunId: item.resultRunId,
73
74
  outcome: saved?.["handoff"] == null ? null : text(String(saved["handoff"]), 400),
74
75
  checks: item.resultRunId === null ? null : { status: "not-read", exitCode: null, detail: "Saved checks were not revalidated. Use assignment show to inspect them." },
75
- nextAction: item.primaryAction, attention: attention.slice(0, 3).map(one => text(one, 160)),
76
+ nextAction: item.primaryAction, nextHref: browserWorkActionHref(item), attention: attention.slice(0, 3).map(one => text(one, 160)),
76
77
  decisions: decisions.map(row => ({ id: Number(row["id"]), runId: Number(row["run"]), question: text(String(row["question"]), 240), state: String(row["state"]),
77
78
  overdue: row["state"] !== "answered" && row["deadline"] !== null && String(row["deadline"]) <= now.toISOString(), choice: row["choice"] == null ? null : text(String(row["choice"]), 80) })),
78
79
  };
@@ -35,9 +35,11 @@ export function assignmentPresentationOf(assignment, options = {}) {
35
35
  // rank stay the existing projection's (pages and tests key off them).
36
36
  const shared = { label: taskStatus.headline, tone: workToneOf(taskStatus.headline), detail: taskStatus.sentence, action: null };
37
37
  const needsYou = taskStatus.headline === 'Needs you';
38
+ // Waiting needs no one: never listed under Needs you.
39
+ const waiting = taskStatus.headline === 'Waiting';
38
40
  const status = state === 'working'
39
41
  ? { ...shared, token: `assignment-${state}`,
40
- views: [...new Set([...(work?.views?.filter(view => view !== 'completed') ?? ['all']), ...(needsYou ? ['needs-you'] : [])])], rank: needsYou ? 0 : work?.rank ?? 2 }
42
+ views: [...new Set([...(work?.views?.filter(view => view !== 'completed' && !(waiting && view === 'needs-you')) ?? ['all']), ...(needsYou ? ['needs-you'] : [])])], rank: needsYou ? 0 : waiting ? 2 : work?.rank ?? 2 }
41
43
  : { ...shared, token: `assignment-${state}`,
42
44
  views: state === 'ready-to-check' || state === 'needs-decision' ? ['all', 'needs-you'] : state === 'complete' ? ['all', 'completed']
43
45
  : state === 'checking' && work?.views?.includes('running') ? ['all', 'running'] : ['all'],
@@ -17,7 +17,7 @@ export declare function assignmentStatusBrief(assignment: AssignmentSnapshot): {
17
17
  } | null;
18
18
  runId: number | null;
19
19
  primaryAction: {
20
- code: "inspect-decisions" | "inspect-run" | "inspect-stop" | "inspect-task" | "open-pr" | "reconcile-run" | import("./dispatch.js").DispatchAction;
20
+ code: "confirm-stopped" | "inspect-decisions" | "inspect-run" | "inspect-stop" | "inspect-task" | "open-pr" | "reconcile-run" | "sign-in" | import("./dispatch.js").DispatchAction;
21
21
  label: string;
22
22
  target: import("./work-summary.js").WorkTarget;
23
23
  } | null;
@@ -27,6 +27,11 @@ export function assignmentActionHref(assignment) {
27
27
  return `/d/${decisionId}`;
28
28
  if (action.code === 'inspect-decisions')
29
29
  return '/';
30
+ if (action.code === 'sign-in')
31
+ return '/settings#providers';
32
+ // The confirmation sits on the task page, behind the password.
33
+ if (action.code === 'confirm-stopped')
34
+ return `/t/${encodeURIComponent(assignment.rootId)}#confirm-stopped`;
30
35
  if (action.code === 'open-pr' && /^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/pull\/\d+$/.test(assignment.publication?.prUrl ?? ''))
31
36
  return assignment.publication.prUrl;
32
37
  if (runId !== null && (action.code === 'open-result' || action.code === 'open-pr' || action.code === 'retry-review')) {
@@ -3,6 +3,7 @@ import { type WorkAction, type WorkSummaryAccess } from "./work-summary.js";
3
3
  import { type VerifiedApprover } from "./principal.js";
4
4
  import { type CheckLevel } from "./check-levels.js";
5
5
  import { type BuildReviewView } from "./review-switch.js";
6
+ import { type NeedKey, type WaitKey } from "./needs-you.js";
6
7
  export type AssignmentAccess = WorkSummaryAccess;
7
8
  export type AssignmentOwner = {
8
9
  kind: "coordinator" | "lead";
@@ -99,6 +100,14 @@ export type AssignmentSnapshot = {
99
100
  deployment: {
100
101
  status: "not-recorded";
101
102
  };
103
+ /** What this assignment asks of a person, or waits for when no person can act (needs-you.ts). */
104
+ need?: {
105
+ key: NeedKey;
106
+ build: number | null;
107
+ } | {
108
+ wait: WaitKey;
109
+ build: number | null;
110
+ } | null;
102
111
  };
103
112
  /** Status-first handoff for routine reads. Fetch get_assignment only when
104
113
  * inspecting its exact evidence; do not repeat a full proof in every poll. */
@@ -6,7 +6,7 @@ import { currentActor, withActor } from "./actor.js";
6
6
  import { replacedWords } from "./task-status.js";
7
7
  import { COMPLETION_ACTION, familyChangedFiles } from "./result-completion.js";
8
8
  import { homedir } from "node:os";
9
- import { readSchemaVersion } from "./store.js";
9
+ import { readSchemaVersion, stopFactOf } from "./store.js";
10
10
  import { approvalOf } from "./scope.js";
11
11
  import { openWorkDecisionOf, taskWorkSummaryOf, workDecisionAction } from "./work-summary.js";
12
12
  import { evidenceRoot, readVerifiedArtifact, readVerifiedReport } from "./evidence.js";
@@ -17,6 +17,8 @@ import { historicalAssessmentReason } from "./assignment-presentation.js";
17
17
  import { runCheckLevel } from "./check-levels.js";
18
18
  import { followUpChecksOf, withFollowUps } from "./result-follow-ups.js";
19
19
  import { buildReviewOf, findingWords } from "./review-switch.js";
20
+ import { NEEDS, WAITS, processNeedOf } from "./needs-you.js";
21
+ import { assignmentStageOf } from "./task-status.js";
20
22
  /** Status-first handoff for routine reads. Fetch get_assignment only when
21
23
  * inspecting its exact evidence; do not repeat a full proof in every poll. */
22
24
  export function assignmentBrief(assignment) {
@@ -183,7 +185,9 @@ export function assignmentOf(store, taskId, now, access, root) {
183
185
  let detail = work.status.detail;
184
186
  let primaryAction = work.primaryAction;
185
187
  const live = work.liveRunId !== null || work.status.token === "running";
186
- const processProblem = result === null ? null : store.stopQuiescenceProblem(result.id);
188
+ const processFact = result === null ? null : stopFactOf(store, result.id);
189
+ const processProblem = processFact?.problem ?? null;
190
+ let need = null;
187
191
  // The exact completed result, scope, family and custody fences apply to
188
192
  // every deliverable. Human acceptance remains its own recorded authority;
189
193
  // it never changes a machine verdict or supplies a missing report.
@@ -205,10 +209,23 @@ export function assignmentOf(store, taskId, now, access, root) {
205
209
  primaryAction = successor === null ? { code: "inspect-task", label: "View assignment", target: { taskId: current.id, runId: null, decisionId: null }, access: "read", retry: "read-again" }
206
210
  : { code: "inspect-task", label: `Open ${successor}`, target: { taskId: successor, runId: null, decisionId: null }, access: "read", retry: "read-again" };
207
211
  }
208
- else if (!live && processProblem !== null && result !== null) {
209
- state = "needs-decision";
210
- detail = processProblem;
211
- primaryAction = { code: "inspect-run", label: "Inspect run", target: { taskId: current.id, runId: result.id, decisionId: null }, access: "read", retry: "read-again" };
212
+ else if (!live && processFact !== null && result !== null) {
213
+ // Toolroll can't confirm the build stopped. A person confirms it when
214
+ // nothing of it may still run; otherwise the work waits, needing no one.
215
+ const process = processNeedOf(processFact);
216
+ if ("need" in process) {
217
+ state = "needs-decision";
218
+ detail = NEEDS[process.need].sentence({ build: process.build });
219
+ primaryAction = { code: "confirm-stopped", label: NEEDS[process.need].action.label, target: { taskId: current.id, runId: process.build, decisionId: null },
220
+ access: access.principal === "operator" ? "operator-control" : "operator-handoff", retry: "refresh-before-acting" };
221
+ need = { key: process.need, build: process.build };
222
+ }
223
+ else {
224
+ state = "working";
225
+ detail = WAITS[process.wait]({ build: process.build });
226
+ primaryAction = { code: "inspect-run", label: "View the build", target: { taskId: current.id, runId: process.build, decisionId: null }, access: "read", retry: "read-again" };
227
+ need = { wait: process.wait, build: process.build };
228
+ }
212
229
  }
213
230
  else if (current.state === "done" && questions.length > 0 && work.status.tone !== "problem" &&
214
231
  !["stopping", "stopped", "review-failed", "review-exhausted"].includes(work.status.token) &&
@@ -252,6 +269,14 @@ export function assignmentOf(store, taskId, now, access, root) {
252
269
  ? completionKind === null ? "The research report is missing or incomplete. Inspect the saved report before checking this handoff."
253
270
  : "The saved report is awaiting resolution of its current scope or hold."
254
271
  : "Inspect the saved result and resolve its remaining execution or scope issue.";
272
+ // Built to an earlier plan: the result page can't resolve it (accepting would leave it stuck), building again can.
273
+ if (current.state === "done" && result !== null && scope !== null && scope.termsProblem == null && approvalOf(scope).approved && !!result.scopeDigest && scope.digest !== result.scopeDigest &&
274
+ store.activeHolds(current.refId, now).length === 0 && unfinished === null && store.currentLiveLease(current.refId, now) === null && store.finalResultReason(result.id) === null) {
275
+ detail = NEEDS.rebuild.sentence({ build: result.id });
276
+ primaryAction = { code: "retry-task", label: NEEDS.rebuild.action.label, target: { taskId: current.id, runId: result.id, decisionId: null },
277
+ access: access.principal === "operator" ? "operator-control" : "operator-handoff", retry: "refresh-before-acting" };
278
+ need = { key: "rebuild", build: result.id };
279
+ }
255
280
  }
256
281
  if (earlierActive.length > 0) {
257
282
  attention.push(`${earlierActive.length} earlier task version${earlierActive.length === 1 ? " is" : "s are"} still active.`);
@@ -266,6 +291,13 @@ export function assignmentOf(store, taskId, now, access, root) {
266
291
  }
267
292
  if (state === "needs-decision")
268
293
  attention.push(detail);
294
+ // Every Needs you action wears its need's own words (needs-you.ts), on every surface.
295
+ if (state === "needs-decision" && primaryAction !== null && need === null) {
296
+ const read = assignmentStageOf({ state, primaryAction, review }, { token: work.status.token, views: work.status.views });
297
+ // A result review keeps its own, more specific words (See what is missing, Open the failed check).
298
+ if (read.stage === "needs-you" && read.need !== undefined && read.need !== "other" && read.need !== "review-result")
299
+ primaryAction = { ...primaryAction, label: NEEDS[read.need].action.label };
300
+ }
269
301
  if (receipt !== null) {
270
302
  if (finishedBuild && receipt.checks.status !== "passed")
271
303
  attention.push(receipt.checks.detail);
@@ -307,7 +339,7 @@ export function assignmentOf(store, taskId, now, access, root) {
307
339
  return { version: 1, rootId: family.root.id, activeTaskId: current.id, repo: current.repo, title: family.root.title,
308
340
  state, detail, primaryAction, attention: [...new Set(attention)], attempts, owner, receipt, savedContext, completion, handoff,
309
341
  publication: publication === null ? null : { state: publication.state, prUrl: publication.prUrl, remoteState: publication.remoteState },
310
- review, deployment: { status: "not-recorded" } };
342
+ review, deployment: { status: "not-recorded" }, ...(need === null ? {} : { need }) };
311
343
  }
312
344
  function admittedOwner(store, taskId, owner, now, root) {
313
345
  if (owner.kind !== "coordinator")
@@ -377,8 +409,10 @@ function acknowledgeCurrent(store, current, receiptDigest, actor, now, root, acc
377
409
  store.recordAction({ at: now.toISOString(), actor, repo: current.repo,
378
410
  taskId: current.rootId, runId: receipt.runId, action: CHECK_ACTION, outcome: receiptDigest, source: "work" });
379
411
  const ref = store.lookupRef(current.rootId);
412
+ // Whose act this is, even outside a command that named its person: the operator who marked it.
413
+ const by = currentActor() ?? (actor.startsWith("operator:") ? { account: actor.slice("operator:".length), lead: false } : null);
380
414
  if (ref !== null)
381
- store.noteTaskAct(ref.id, "completed", now);
415
+ store.noteTaskAct(ref.id, "completed", now, undefined, by);
382
416
  store.bumpWake();
383
417
  }
384
418
  const assignment = assignmentOf(store, current.rootId, now, access, root);