toolroll 0.9.99 → 0.9.101

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 (103) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/accent-colors.js +1 -1
  3. package/dist/acp-check.js +4 -2
  4. package/dist/activity-line.d.ts +1 -1
  5. package/dist/activity-line.js +1 -1
  6. package/dist/agent-state.js +2 -1
  7. package/dist/agents-board.js +3 -2
  8. package/dist/agents-live.js +3 -2
  9. package/dist/approval-authority.d.ts +2 -1
  10. package/dist/approval-authority.js +2 -1
  11. package/dist/browser/terminal.css +1 -1
  12. package/dist/browser/terminal.js +42 -42
  13. package/dist/browser/workspace.css +2 -2
  14. package/dist/browser/workspace.js +32 -32
  15. package/dist/browser-workspace.d.ts +31 -2
  16. package/dist/chat-ask.js +4 -2
  17. package/dist/chat-core.js +4 -2
  18. package/dist/chat-decide.js +1 -2
  19. package/dist/chat-delivery-state.d.ts +29 -6
  20. package/dist/chat-delivery-state.js +43 -13
  21. package/dist/chat-delivery.js +12 -5
  22. package/dist/chat-flow.js +15 -13
  23. package/dist/chat-live.js +5 -2
  24. package/dist/chat-message-states.d.ts +75 -0
  25. package/dist/chat-message-states.js +74 -0
  26. package/dist/chat-messages.d.ts +10 -4
  27. package/dist/chat-messages.js +12 -7
  28. package/dist/check-failure.js +2 -1
  29. package/dist/check-report.js +1 -1
  30. package/dist/claim.d.ts +40 -2
  31. package/dist/claim.js +92 -19
  32. package/dist/contracts/chat-tables.js +3 -0
  33. package/dist/coordinator.js +3 -3
  34. package/dist/demo.js +7 -2
  35. package/dist/design/kit.css +94 -2
  36. package/dist/design/kit.d.ts +12 -4
  37. package/dist/design/kit.js +8 -2
  38. package/dist/design/tokens.css +5 -5
  39. package/dist/desktop-update.d.ts +1 -1
  40. package/dist/discord-chat.js +2 -3
  41. package/dist/dispatch.js +2 -1
  42. package/dist/dispose.d.ts +4 -1
  43. package/dist/dispose.js +2 -1
  44. package/dist/fallback.js +3 -2
  45. package/dist/fast-start.js +2 -2
  46. package/dist/first-run.d.ts +10 -6
  47. package/dist/first-run.js +19 -18
  48. package/dist/guides.js +1 -1
  49. package/dist/monitoring.js +2 -1
  50. package/dist/operate.js +77 -36
  51. package/dist/plane-review.js +2 -1
  52. package/dist/project-knowledge.d.ts +3 -3
  53. package/dist/release-authority.d.ts +9 -0
  54. package/dist/release-authority.js +13 -0
  55. package/dist/release-auto.js +2 -3
  56. package/dist/release-gate.js +1 -1
  57. package/dist/release-view.js +2 -1
  58. package/dist/release.js +18 -30
  59. package/dist/result-shots.js +1 -1
  60. package/dist/route-results.js +2 -1
  61. package/dist/run-ending.d.ts +21 -0
  62. package/dist/run-ending.js +19 -0
  63. package/dist/scope.d.ts +5 -0
  64. package/dist/scope.js +10 -3
  65. package/dist/server/chrome.d.ts +1 -1
  66. package/dist/server/chrome.js +1 -1
  67. package/dist/server/console.css +46 -17
  68. package/dist/server/kit.d.ts +36 -3
  69. package/dist/server/kit.js +33 -4
  70. package/dist/server/render-chat.d.ts +2 -0
  71. package/dist/server/render-chat.js +1 -0
  72. package/dist/server/render-pages.js +1 -1
  73. package/dist/server/render-results.js +6 -5
  74. package/dist/server/render-tasks.d.ts +10 -0
  75. package/dist/server/render-tasks.js +234 -64
  76. package/dist/server/task-views.js +5 -1
  77. package/dist/settings-groups-ui.js +1 -1
  78. package/dist/setup-guide.d.ts +2 -2
  79. package/dist/slack-chat.js +2 -3
  80. package/dist/store.d.ts +61 -2
  81. package/dist/store.js +141 -80
  82. package/dist/subagent-question.js +8 -6
  83. package/dist/surface.js +2 -2
  84. package/dist/task-archive.d.ts +3 -0
  85. package/dist/task-archive.js +5 -0
  86. package/dist/task-moves.d.ts +90 -0
  87. package/dist/task-moves.js +68 -0
  88. package/dist/task-status.d.ts +2 -2
  89. package/dist/task-status.js +3 -3
  90. package/dist/teams-chat.js +2 -2
  91. package/dist/telegram-flow.d.ts +1 -1
  92. package/dist/telegram-flow.js +1 -1
  93. package/dist/telegram-lead.d.ts +1 -2
  94. package/dist/telegram-lead.js +18 -23
  95. package/dist/telegram-progress.js +2 -1
  96. package/dist/telegram.d.ts +0 -1
  97. package/dist/telegram.js +6 -7
  98. package/dist/tui.js +1 -1
  99. package/dist/vanished-retry.js +3 -2
  100. package/dist/workspace-ui.d.ts +69 -0
  101. package/dist/workspace-ui.js +105 -1
  102. package/dist/worktree.js +3 -2
  103. package/package.json +1 -1
@@ -19,7 +19,7 @@
19
19
  * stays here: status words on their soft wash, mixed toward ink so they read at 4.5:1 or better, and the motion
20
20
  * vocabulary (three durations, one curve; nothing longer, no bounce). */
21
21
  :root {
22
- --so-needs-ink: color-mix(in srgb, var(--so-needs) 78%, var(--so-ink));
22
+ --so-needs-ink: var(--so-ink);
23
23
  --so-live-ink: color-mix(in srgb, var(--so-info) 82%, var(--so-ink));
24
24
  --so-done-ink: color-mix(in srgb, var(--so-success) 80%, var(--so-ink));
25
25
  --so-fail-ink: color-mix(in srgb, var(--so-danger) 85%, var(--so-ink));
@@ -64,7 +64,8 @@
64
64
  /* ---------- Status: five words, one tone each ---------- */
65
65
  .k-chip { display: inline-flex; align-items: center; gap: 6px; height: 20px; padding: 0 7px; border-radius: 6px; font: var(--so-typography-badge); background: var(--so-neutral-soft); color: var(--so-neutral-ink); white-space: nowrap; flex: none; }
66
66
  .k-chip::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: var(--dot, var(--so-muted)); flex: none; }
67
- .k-chip.needs { background: var(--so-needs-soft); color: var(--so-needs-ink); --dot: var(--so-needs); }
67
+ .k-chip.needs { background: var(--so-needs); color: var(--so-on-signal); }
68
+ .k-chip.needs::before { display: none; }
68
69
  .k-chip.ask { background: var(--so-neutral-soft); color: var(--so-ink); --dot: var(--so-needs); }
69
70
  .k-chip.live { background: var(--so-info-soft); color: var(--so-live-ink); --dot: var(--so-info); }
70
71
  .k-chip.done { background: var(--so-success-soft); color: var(--so-done-ink); --dot: var(--so-success); }
@@ -255,6 +256,82 @@
255
256
  .k-step-l { line-height: 1.2; white-space: nowrap; }
256
257
  .k-step-s { margin-top: -4px; min-height: 1.3em; font-size: 11.5px; font-weight: 400; line-height: 1.3; color: var(--so-muted); white-space: nowrap; }
257
258
 
259
+ /* ---------- Step rows: the steps are the page ---------- */
260
+ .k-steprows { display: flex; flex-direction: column; margin: 0; padding: 0; list-style: none; }
261
+ .k-steprow { position: relative; display: grid; grid-template-columns: 20px minmax(0, 1fr); column-gap: 14px; margin: 0; padding: 0; }
262
+ .k-steprow::before { content: ""; position: absolute; left: 9.5px; top: 24px; bottom: 4px; width: 1px; background: var(--so-line); }
263
+ .k-steprow:last-child::before { content: none; }
264
+ .k-steprow-mark { position: relative; z-index: 1; box-sizing: border-box; display: grid; place-items: center; width: 20px; height: 20px; border-radius: 50%; border: 1.5px solid var(--so-field-line); background: var(--so-paper); }
265
+ .k-steprow.done .k-steprow-mark { border-color: var(--so-ink); background: var(--so-ink); }
266
+ .k-steprow.done .k-steprow-mark::after { content: ""; width: 4px; height: 8px; margin-top: -2px; border: solid var(--so-paper); border-width: 0 1.6px 1.6px 0; transform: rotate(45deg); }
267
+ .k-steprow:is(.needs, .live, .wait, .stuck) .k-steprow-mark { border-width: 2px; }
268
+ .k-steprow:is(.needs, .live, .wait, .stuck) .k-steprow-mark::after { content: ""; width: 8px; height: 8px; border-radius: 50%; background: currentColor; }
269
+ .k-steprow.needs .k-steprow-mark { border-color: var(--so-needs); color: var(--so-needs); }
270
+ .k-steprow.live .k-steprow-mark { border-color: var(--so-info); color: var(--so-info); }
271
+ .k-steprow.wait .k-steprow-mark { border-color: var(--so-muted); color: var(--so-muted); }
272
+ .k-steprow.stuck .k-steprow-mark { border-color: var(--so-danger); color: var(--so-danger); }
273
+ .k-steprow.skipped .k-steprow-mark { border-style: dashed; }
274
+ .k-steprow-main { min-width: 0; display: grid; align-content: start; gap: 12px; padding-bottom: 22px; }
275
+ .k-steprow:last-child .k-steprow-main { padding-bottom: 0; }
276
+ .k-steprow:is(.needs, .live, .wait, .stuck) .k-steprow-main { padding-bottom: 26px; }
277
+ .k-steprow-head { display: flex; align-items: baseline; gap: 12px; width: 100%; min-width: 0; min-height: 20px; margin: 0; padding: 0; border: 0; border-radius: 6px; background: none; color: inherit; font: 400 13px/20px var(--so-font-sans); text-align: left; }
278
+ button.k-steprow-head { cursor: pointer; transition: color var(--m-quick) var(--m-ease); }
279
+ button.k-steprow-head::after { content: ""; flex: none; align-self: center; width: 6px; height: 6px; margin: 0 4px 0 2px; border: solid var(--so-muted); border-width: 0 1.5px 1.5px 0; transform: rotate(-45deg); transition: transform var(--m-quick) var(--m-ease); }
280
+ button.k-steprow-head[aria-expanded="true"]::after { transform: rotate(45deg); }
281
+ .k-steprow-label { flex: 0 0 84px; min-width: 0; font: 600 14px/20px var(--so-font-sans); color: var(--so-ink); }
282
+ .k-steprow-say { flex: 1 1 auto; min-width: 0; display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px; color: var(--so-muted); overflow-wrap: anywhere; }
283
+ .k-steprow:is(.needs, .live, .wait, .stuck) .k-steprow-say { color: var(--so-ink); }
284
+ .k-steprow-time { flex: none; color: var(--so-muted); font-size: 12.5px; }
285
+ .k-steprow.todo .k-steprow-label { font-weight: 500; color: var(--so-muted); }
286
+ .k-steprow-body { display: grid; gap: 16px; min-width: 0; }
287
+ .k-steprow-body[hidden] { display: none; }
288
+ /* The one ink outline of a step that waits on the viewer is the row's; a waiting box inside it draws none of its own. */
289
+ .k-steprow.needs > .k-steprow-main > .k-steprow-body { border: 1px solid var(--so-needs); border-radius: var(--so-radius-card); padding: 16px 18px 18px; }
290
+ .k-steprow.needs > .k-steprow-main > .k-steprow-body .k-slots.wait { border: 0; border-radius: 0; padding: 0; }
291
+ .k-steprow-body .k-slot-status h2 { color: var(--so-ink); }
292
+ .k-steprow-body .k-slot-acts:empty { display: none; }
293
+ @media (hover: hover) and (pointer: fine) {
294
+ button.k-steprow-head:hover .k-steprow-say { color: var(--so-ink); }
295
+ }
296
+
297
+ /* ---------- The work: one agent's work as a chat ---------- */
298
+ .k-work { box-sizing: border-box; display: flex; flex-direction: column; min-width: 0; min-height: 0; background: var(--so-paper); color: var(--so-ink); }
299
+ .k-work[hidden] { display: none; }
300
+ .k-work-head { flex: none; display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 12px 10px 12px 18px; border-bottom: 1px solid var(--so-line); }
301
+ .k-work-title { display: grid; gap: 1px; min-width: 0; }
302
+ .k-work-title h2 { margin: 0; font: 600 14px/1.4 var(--so-font-sans); }
303
+ .k-work-who { display: flex; align-items: center; gap: 6px; min-width: 0; margin: 0; font-size: 12px; line-height: 1.4; color: var(--so-muted); }
304
+ .k-work-who > span:last-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
305
+ .k-work-dot { flex: none; width: 6px; height: 6px; border-radius: 50%; background: var(--so-live); }
306
+ .k-work-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; display: grid; align-content: start; gap: 16px; padding: 18px; }
307
+ .k-work-foot { flex: none; display: grid; gap: 8px; padding: 8px 14px 14px; max-height: min(50%, 420px); overflow-y: auto; overscroll-behavior: contain; }
308
+ .k-work-foot:empty { display: none; }
309
+ .k-work-conv { display: grid; gap: var(--so-space-sm); min-width: 0; }
310
+ .k-work-conv-h { display: flex; justify-content: flex-end; }
311
+ .k-work-conv .k-work-tx { max-height: 60vh; overflow-y: auto; overscroll-behavior: contain; }
312
+ .k-work-tx { min-width: 0; display: grid; gap: 12px; }
313
+ .k-work-empty { margin: 0; padding: var(--so-space-md) 0; font-size: 13px; color: var(--so-muted); }
314
+ .k-work-lines { display: grid; gap: 14px; margin: 0; padding: 0; list-style: none; font-size: 14px; line-height: 1.6; }
315
+ .k-work-say { white-space: pre-wrap; overflow-wrap: anywhere; }
316
+ .k-work-mark { font-size: 12px; color: var(--so-muted); }
317
+ .k-work-steps { display: grid; gap: 2px; margin: 0; padding: 0; list-style: none; font-size: 13px; line-height: 1.5; color: var(--so-muted); }
318
+ .k-work-step-l { display: flex; align-items: center; gap: 10px; min-height: 26px; min-width: 0; }
319
+ .k-work-step.running .k-work-step-l { color: var(--so-ink); }
320
+ .k-work-step-t { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; }
321
+ .k-work-step-t .k-mono { font-size: 12px; color: var(--so-ink); }
322
+ .k-work-step-s { flex: none; display: inline-flex; align-items: center; gap: var(--so-space-xs); font-size: 12px; color: var(--so-muted); }
323
+ .k-work-step-s.ok { color: var(--so-done-ink); }
324
+ .k-work-step-s.failed { color: var(--so-fail-ink); }
325
+ .k-work-step-s.running { color: var(--so-live-ink); }
326
+ .k-work-out { max-height: 288px; overflow: auto; margin: 4px 0 6px; padding: var(--so-space-sm) 10px; border-radius: var(--so-radius-md); background: var(--so-code-bg); color: var(--so-ink); font: 12px/1.5 var(--so-font-mono); white-space: pre-wrap; overflow-wrap: anywhere; }
327
+ .k-work-note { display: grid; justify-items: end; gap: 2px; }
328
+ .k-work-bubble { max-width: 82%; margin: 0; padding: 8px 14px; border-radius: 18px; background: var(--so-user-bubble); white-space: pre-wrap; overflow-wrap: anywhere; }
329
+ .k-work-note-s { margin: 0; font-size: 12px; color: var(--so-muted); }
330
+ .k-work-term { display: grid; gap: 2px; margin: 0; padding: 10px var(--so-space-md); list-style: none; border-radius: var(--so-radius-card); background: var(--so-code-bg); color: var(--so-ink); font: 12px/1.45 var(--so-font-mono); white-space: pre-wrap; overflow-wrap: anywhere; }
331
+ .k-work-term-note { color: var(--so-live-ink); }
332
+ .k-work-term-out { display: block; padding-left: var(--so-space-md); }
333
+ .k-work-term-failed { color: var(--so-fail-ink); }
334
+
258
335
  /* ---------- Approval: four fixed slots in every state ---------- */
259
336
  .k-slots { border: 1px solid var(--so-line); border-radius: var(--so-radius-card); padding: 16px 18px; display: grid; gap: 10px; background: var(--so-paper); transition: border-color var(--m-slow) var(--m-ease); }
260
337
  .k-slots.wait { border-color: var(--so-needs); }
@@ -521,6 +598,21 @@ html:not(.k-js) .k-composer:focus-within, html.k-kbd .k-composer:focus-within {
521
598
  .k-card { padding: 12px 16px; }
522
599
  .k-step { font-size: 11px; }
523
600
  .k-step-s { font-size: 10.5px; white-space: normal; }
601
+ .k-steprow-head { flex-wrap: wrap; row-gap: 2px; min-height: 44px; padding: 12px 0; }
602
+ .k-steprow-mark { margin-top: 12px; }
603
+ .k-steprow::before { top: 36px; }
604
+ .k-steprow-main { gap: 8px; padding-bottom: 8px; }
605
+ .k-steprow:is(.needs, .live, .wait, .stuck) .k-steprow-main { padding-bottom: 16px; }
606
+ /* The label and its time on one line, the sentence under them. */
607
+ .k-steprow-label { flex: 1 1 auto; order: 0; }
608
+ .k-steprow-time { order: 1; }
609
+ button.k-steprow-head::after { order: 2; }
610
+ .k-steprow-say { order: 3; flex: 1 1 100%; }
611
+ .k-steprow-say:empty { display: none; }
612
+ .k-steprow.needs > .k-steprow-main > .k-steprow-body { padding: 14px; }
613
+ /* An open body takes the whole width under its head; the rail stops at the head. No z-index: one would trap the body's phone dock under the app's. */
614
+ .k-steprow-body { margin-left: -34px; position: relative; background: var(--so-paper); }
615
+ .k-steprow-body .k-slot-acts > .k-btn, .k-steprow-body .k-slot-acts > form, .k-steprow-body .k-slot-acts > form > .k-btn { flex: 1 1 100%; width: 100%; }
524
616
  .k-doc { padding: 14px 16px 40px; gap: 18px; }
525
617
  .k-dh h1 { font-size: 20px; }
526
618
  .k-backlink { display: none; }
@@ -10,14 +10,14 @@
10
10
  import { type Headline } from "../task-status.js";
11
11
  /** The five status words. */
12
12
  export type StatusKey = "needs" | "running" | "queued" | "finished" | "failed";
13
- /** A chip's tone: "" is neutral; "ask" names a specific ask inside a Needs-you group (ink words, amber dot). */
13
+ /** A chip's tone: "" is neutral; "ask" names a specific ask inside a Needs-you group (ink words, ink dot). */
14
14
  export type ChipTone = "" | "needs" | "ask" | "live" | "done" | "failed";
15
15
  export declare const STATUS: Readonly<Record<StatusKey, {
16
16
  word: string;
17
17
  tone: ChipTone;
18
18
  }>>;
19
19
  /** A task's headline (task-status.ts) as the kit's chip: its own word, in the tone of the status it belongs to. A
20
- * row that waits on a person never shows a failure tone; something that waits on someone else is never amber. */
20
+ * row that waits on a person never shows a failure tone; something that waits on someone else never carries the needs-you mark. */
21
21
  export declare function chipOfHeadline(headline: Headline): {
22
22
  word: string;
23
23
  tone: ChipTone;
@@ -25,7 +25,7 @@ export declare function chipOfHeadline(headline: Headline): {
25
25
  };
26
26
  /** Which of the five a headline is. */
27
27
  export declare function statusOfHeadline(headline: Headline): StatusKey;
28
- /** Resource states (a computer that builds, not work): never amber. */
28
+ /** Resource states (a computer that builds, not work): never the needs-you mark. */
29
29
  export declare const RESOURCE: Readonly<Record<"idle" | "building" | "offline", {
30
30
  word: string;
31
31
  tone: ChipTone;
@@ -77,7 +77,15 @@ export declare function timingBars(rows: readonly TimingRow[], scale?: number):
77
77
  left: number;
78
78
  width: number;
79
79
  } | null>;
80
- /** One step on a progress line: done (a check), now (waits on you: amber), live (running: blue), failed, or to do. */
80
+ /** A step row's marker: done (an ink disc with a check), needs (waits on this person: an ink ring with a dot), live
81
+ * (runs now: blue), wait (current, waiting on nothing this person does), stuck (stopped there: vermilion), skipped (a
82
+ * dashed ring), todo (not reached: a grey ring). */
83
+ export type StepRowMark = "done" | "needs" | "live" | "wait" | "stuck" | "skipped" | "todo";
84
+ /** The words a screen reader hears for a step row's marker. */
85
+ export declare const STEP_ROW_WORDS: Readonly<Record<StepRowMark, string>>;
86
+ /** The id of a step row's body, for its disclosure's aria-controls. */
87
+ export declare const stepRowBodyId: (key: string) => string;
88
+ /** One step on a progress line: done (a check), now (waits on you: ink), live (running: blue), failed, or to do. */
81
89
  export type StepState = "done" | "now" | "live" | "failed" | "todo";
82
90
  export type StepItem = {
83
91
  label: string;
@@ -16,7 +16,7 @@ export const STATUS = {
16
16
  failed: { word: "Failed", tone: "failed" },
17
17
  };
18
18
  /** A task's headline (task-status.ts) as the kit's chip: its own word, in the tone of the status it belongs to. A
19
- * row that waits on a person never shows a failure tone; something that waits on someone else is never amber. */
19
+ * row that waits on a person never shows a failure tone; something that waits on someone else never carries the needs-you mark. */
20
20
  export function chipOfHeadline(headline) {
21
21
  const status = statusOfHeadline(headline);
22
22
  return { word: headline === "Needs you" ? STATUS.needs.word : headline, tone: STATUS[status].tone, status };
@@ -32,7 +32,7 @@ export function statusOfHeadline(headline) {
32
32
  case "neutral": return "queued";
33
33
  }
34
34
  }
35
- /** Resource states (a computer that builds, not work): never amber. */
35
+ /** Resource states (a computer that builds, not work): never the needs-you mark. */
36
36
  export const RESOURCE = {
37
37
  idle: { word: "Idle", tone: "" },
38
38
  building: { word: "Building", tone: "live" },
@@ -100,6 +100,12 @@ export function timingBars(rows, scale) {
100
100
  const sc = scale ?? Math.max(1, ...rows.map(row => row.start + (row.st === "q" ? 0 : row.dur))) * 1.02;
101
101
  return rows.map(row => row.st === "q" ? null : { left: (Math.max(0, row.start) / sc) * 100, width: (Math.max(row.dur, sc / 400) / sc) * 100 });
102
102
  }
103
+ /** The words a screen reader hears for a step row's marker. */
104
+ export const STEP_ROW_WORDS = {
105
+ done: "done", needs: "waiting on you", live: "in progress", wait: "waiting", stuck: "stopped here", skipped: "skipped", todo: "not started",
106
+ };
107
+ /** The id of a step row's body, for its disclosure's aria-controls. */
108
+ export const stepRowBodyId = (key) => `steprow-${key}`;
103
109
  /** The words a screen reader hears for a step's state. */
104
110
  export const STEP_WORDS = { done: "done", now: "waiting on you", live: "in progress", failed: "failed", todo: "not started" };
105
111
  /** How far the line is filled, 0–1: up to the current step (the first that isn't done), or all of it. */
@@ -41,10 +41,10 @@
41
41
  --so-nav-hover: #e4e4e4;
42
42
  /* Nav Ink: idle sidebar rows and their hover fill on the frame. */
43
43
  --so-nav-ink: #525252;
44
- /* Needs You Amber: what waits on this person, and nothing else (the Needs-you chip's dot, a waiting box's outline, the dock's dot). Never a warning, quota, link or decoration. */
45
- --so-needs: var(--so-warning);
46
- /* The wash behind a Needs-you chip. */
47
- --so-needs-soft: var(--so-warning-soft);
44
+ /* Ink accent: the default accent, used for what waits on a person. */
45
+ --so-needs: var(--so-signal);
46
+ /* Ink wash: the fill behind attention badge text and the 3px focus halo around a focused field or composer — one step stronger than the neutral badge fill, so a waiting badge still reads first. */
47
+ --so-needs-soft: var(--so-signal-soft);
48
48
  --so-neutral-ink: #525252;
49
49
  /* Neutral Badge: the badge for anything that is a fact and not a claim on the person, such as queued, running or built today in a project peek, or a count in a fold. */
50
50
  --so-neutral-soft: #f0f0f0;
@@ -156,7 +156,7 @@
156
156
  --so-typography-title-sm-letter-spacing: -0.14px;
157
157
  --so-typography-title-sm-line-height: 1.3;
158
158
  --so-user-bubble: #f2f2f2;
159
- /* Amber: setup trouble and caution. */
159
+ /* The terminal's yellow only. Pages never use it: cautions are neutral ink. */
160
160
  --so-warning: #ab6400;
161
161
  --so-warning-soft: #fff4d5;
162
162
  }
@@ -97,13 +97,13 @@ export declare function sqliteLock(file: string, busyTimeoutMs?: number): Databa
97
97
  export declare function lockHeld(file: string): boolean;
98
98
  export declare const installationLock: (j: UpdateJournal) => DatabaseSync | null;
99
99
  export declare function desktopUpdateStatus(stateDir: string): {
100
- error?: never;
101
100
  active: boolean;
102
101
  phase: string;
103
102
  detail: string;
104
103
  running: boolean;
105
104
  canResume: boolean;
106
105
  canCancel: boolean;
106
+ error?: never;
107
107
  automaticRecovery?: never;
108
108
  recoveryAttempts?: never;
109
109
  recoveryRepairs?: never;
@@ -7,7 +7,7 @@ import { cardWithin } from "./chat-card.js";
7
7
  import { chatFlowButtons } from "./chat-flow.js";
8
8
  import { refuseResultShots, resultShotsPruned, shotWaitsUntil } from "./result-shots.js";
9
9
  import { admitChatEvent } from "./chat-admission.js";
10
- import { ChatState, ChatDeliveryError, chatHash, partContent, } from "./chat-delivery-state.js";
10
+ import { ChatState, ChatDeliveryError, chatHash, chatRetryAt, partContent, } from "./chat-delivery-state.js";
11
11
  import { readChatPart } from "./contracts/chat-content.js";
12
12
  import { readDiscordButtonToken, readDiscordComponentInteraction } from "./contracts/discord-callback.js";
13
13
  import { processChatEvent, planChatNotifications, channelAccess, chatObject as object, planRoomMessages, heldForWatch, } from "./chat-delivery.js";
@@ -418,8 +418,7 @@ export async function deliverDiscordPart(options) {
418
418
  const problem = error instanceof ChatDeliveryError
419
419
  ? error
420
420
  : new DiscordError("Discord delivery is waiting to retry", 15_000, true);
421
- const until = new Date((options.clock?.() ?? new Date()).getTime() +
422
- Math.max(problem.retryMs, [5000, 15000, 60000, 300000][Math.min(row.attempts, 3)])).toISOString();
421
+ const until = chatRetryAt(options.clock?.() ?? new Date(), problem.retryMs, row.attempts);
423
422
  state.partFailed(row.id, { problem: problem.message, until, uncertain: problem.uncertain });
424
423
  state.setProblem(identity.installation, options.owner, problem.code === "ratelimited" ? "Discord asked us to wait. Saved replies will retry." : problem.message);
425
424
  if (problem.code === "ratelimited")
package/dist/dispatch.js CHANGED
@@ -19,6 +19,7 @@ import { failedAttemptSentence, latestFinishedAttempt } from "./needs-you.js";
19
19
  import { buildReviewOf } from "./review-switch.js";
20
20
  import { vanishedStopOf, vanishedStopSentence } from "./vanished-retry.js";
21
21
  import { routeChangeOf } from "./route-change.js";
22
+ import { wasInterrupted } from "./run-ending.js";
22
23
  import { BUILT_IN, refArchived, parseCapabilityKey } from "./store.js";
23
24
  import { shortWhen } from "./when-html.js";
24
25
  export const DEFAULT_MAX_OPEN_DECISIONS = 5;
@@ -157,7 +158,7 @@ function reviewViewOf(sourceRun, retry) {
157
158
  retriesRemaining: retry.retriesRemaining,
158
159
  latestRun: retry.latest?.runId ?? null,
159
160
  latestReason: retry.latest?.reason ?? null,
160
- interrupted: retry.latest !== null && (retry.latest.outcome === "interrupted" || retry.latest.reason === "interrupted"),
161
+ interrupted: retry.latest !== null && wasInterrupted(retry.latest),
161
162
  queued: retry.openRequest === null ? null : { requestedBy: retry.openRequest.requestedBy, origin: retry.openRequest.origin },
162
163
  };
163
164
  }
package/dist/dispose.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { STORAGE_HOLD_PREFIX } from "./needs-you.js";
2
2
  import { type FailureClass } from "./claim.js";
3
3
  import type { BuildResult } from "./builder.js";
4
- import type { Store } from "./store.js";
4
+ import type { ClosedState, Store } from "./store.js";
5
5
  import { type ProofVerdict } from "./proof.js";
6
6
  /**
7
7
  * Which road is disposing. 'tick' = the unattended loop: full task
@@ -59,6 +59,9 @@ export type Disposition = {
59
59
  disposition: string | null;
60
60
  strikes: number | null;
61
61
  sealed: boolean;
62
+ /** The state sealed when strikes ran out on a task a person had already closed (disposition "closed"): it is
63
+ * kept, with no incident, hold or page. Null for every other ending. */
64
+ closedState: ClosedState | null;
62
65
  }
63
66
  /** The standalone road's simple record: outcome written, nothing else. */
64
67
  | {
package/dist/dispose.js CHANGED
@@ -316,7 +316,7 @@ function disposeBuildOutcomeLocked(context, result) {
316
316
  }
317
317
  if (leaseId === undefined) {
318
318
  store.finishRun(runId, { outcome: "failed", reason: result.reason, now: clock() });
319
- return { kind: "failed", failureClass, disposition: null, strikes: null, sealed: false };
319
+ return { kind: "failed", failureClass, disposition: null, strikes: null, sealed: false, closedState: null };
320
320
  }
321
321
  const sealed = finalize(store, leaseId, {
322
322
  kind: "failure",
@@ -333,6 +333,7 @@ function disposeBuildOutcomeLocked(context, result) {
333
333
  disposition: sealed.ok ? sealed.disposition : null,
334
334
  strikes: sealed.ok && "strikes" in sealed ? sealed.strikes : null,
335
335
  sealed: sealed.ok,
336
+ closedState: sealed.ok && sealed.disposition === "closed" ? sealed.state : null,
336
337
  };
337
338
  }
338
339
  if (result.reason === "protected-storage") {
package/dist/fallback.js CHANGED
@@ -22,6 +22,7 @@ import { isProviderId, validateSpec } from "./provider.js";
22
22
  import { providerName } from "./provider-auth.js";
23
23
  import { PLAN_LOGIN_PROVABLE } from "./plan-login.js";
24
24
  import { canonicalChainJson, chainDigestOf, LIVE_CHAIN_VERSION, liveChainFromJson, machineActing, profileDigestOf } from "./scope.js";
25
+ import { moveTask } from "./task-moves.js";
25
26
  export const MAX_FALLBACK_ENTRIES = 3;
26
27
  const tablesSeen = new WeakSet();
27
28
  function hasTables(store) {
@@ -516,7 +517,7 @@ export function blockAfterSignedOut(store, runId, now) {
516
517
  store.handle.prepare("UPDATE run SET reason = 'fallback-signed-out' WHERE id = ? AND outcome = 'refused'").run(runId);
517
518
  const ref = store.refForId(run.taskRef);
518
519
  if (ref !== null)
519
- store.handle.prepare("UPDATE task SET state = 'failed', updated_at = ? WHERE id = ? AND state IN ('queued','running')").run(now.toISOString(), ref.externalId);
520
+ moveTask(store.handle, ref.externalId, "giveUp", now);
520
521
  return true;
521
522
  });
522
523
  }
@@ -568,7 +569,7 @@ export function moveOnIfExhausted(store, runId, taskId, now) {
568
569
  store.handle.prepare("INSERT INTO fallback_move (walk, to_index, from_run, terminal_class, paid, moved_at) VALUES (?, ?, ?, ?, ?, ?)")
569
570
  .run(walk.id, run.chainIndex + 1, run.id, evidence.terminalClass, paid ? 1 : 0, now.toISOString());
570
571
  store.handle.prepare("UPDATE fallback_walk SET cursor = ? WHERE id = ?").run(run.chainIndex + 1, walk.id);
571
- store.handle.prepare("UPDATE task SET state = 'queued', updated_at = ? WHERE id = ? AND state = 'running'").run(now.toISOString(), taskId);
572
+ moveTask(store.handle, taskId, "requeue", now);
572
573
  store.recordAction({ at: now.toISOString(), actor: "toolroll", repo: store.refForId(run.taskRef)?.repo ?? null, taskId, runId: run.id, action: "work moved to fallback agent", outcome: paid ? "paid" : "plan", source: "work", detail: entryWords(to) });
573
574
  tell(store, run, taskId, "moved", to, now);
574
575
  store.bumpWake();
@@ -31,7 +31,7 @@ import { fileTaskProposal } from "./proposal.js";
31
31
  import { releaseCiHoldOf } from "./release-ci-hold.js";
32
32
  import { COMPLETION_ACTION, COMPLETION_BY_PERSON } from "./result-completion.js";
33
33
  import { deployGateScope, machineActing, propose, releaseCheckScope } from "./scope.js";
34
- import { BUILT_IN } from "./store.js";
34
+ import { BUILT_IN, withdrawApproval } from "./store.js";
35
35
  import { classifyTask, filingSizer, heuristicSizing } from "./task-sizing.js";
36
36
  import { issueImported } from "./task-source.js";
37
37
  import { titleWithin } from "./task-text.js";
@@ -295,7 +295,7 @@ export function expireFastStarts(store, now) {
295
295
  store.transact(() => {
296
296
  if (store.currentLiveLease(ref, now) !== null)
297
297
  return;
298
- store.handle.prepare(`UPDATE task_scope SET approved_at = NULL, approved_by = NULL, approved_digest = NULL, approved_profile_json = NULL, approved_route_json = NULL, approval_basis = NULL WHERE task_id = ?`).run(task);
298
+ withdrawApproval(store, { where: "WHERE task_id = ?", args: [task] }, { basisAfter: null, clearRoute: true, clearModeDigest: false });
299
299
  store.handle.prepare("UPDATE task_ref SET fast_start_until = NULL WHERE id = ?").run(ref);
300
300
  store.recordAction({ at: stamp, actor: "system", repo: row["repo"] === null ? null : String(row["repo"]), taskId: task, runId: null, action: "fast start expired", outcome: "seal cleared", source: "work", detail: `no build started within ${FAST_WINDOW_MS / 60_000} minutes` });
301
301
  cleared++;
@@ -76,20 +76,24 @@ export declare const TODO_GREP_ARGS: readonly string[];
76
76
  export declare function findFirstTasks(repo: string, runner?: Runner): Promise<FirstTaskSuggestion[]>;
77
77
  /** "First result in 7 min": how long after the installation began its first Ready result arrived. */
78
78
  export declare function firstResultWords(since: string, at: string): string | null;
79
- /** The first task's five steps, as a person reads them. */
80
- export declare const JOURNEY_STEPS: readonly [readonly ["plan", "Plan"], readonly ["approve", "You approve"], readonly ["build", "Build"], readonly ["checks", "Checks"], readonly ["ready", "Ready"]];
79
+ /** A task's five steps, as a person reads them: the one label set the task page's rows, chat's first-task journey and
80
+ * the progress line all read. */
81
+ export declare const JOURNEY_STEPS: readonly [readonly ["plan", "Plan"], readonly ["approve", "Approval"], readonly ["build", "Build"], readonly ["checks", "Checks"], readonly ["ready", "Ready"]];
81
82
  export type JourneyKey = (typeof JOURNEY_STEPS)[number][0];
82
83
  /** `stuck`: the step it stopped at (Failed, Stopped, or waiting on something other than the approval). */
83
84
  /** `skipped`: a stage the task went past without it happening (its checks didn't run or couldn't be read). `needsYou`:
84
- * the current or stuck step waits on a person, so it is the one step drawn amber. */
85
+ * the current or stuck step waits on a person, so it is the one step drawn in the needs-you ink. */
86
+ /** `say`: what the step's state means when its label alone doesn't say it ("Checks failed", "Checks not run"); the
87
+ * label is always the step's own name from JOURNEY_STEPS. */
85
88
  export type JourneyStep = {
86
89
  key: JourneyKey;
87
90
  label: string;
88
91
  state: "done" | "current" | "stuck" | "skipped" | "todo";
89
92
  needsYou?: true;
93
+ say?: string;
90
94
  };
91
95
  /**
92
- * Where the first task stands on Plan → You approve → Build → Checks → Ready, from the one stage every surface reads
96
+ * Where the first task stands on Plan → Approval → Build → Checks → Ready, from the one stage every surface reads
93
97
  * (task-status.ts). Waiting for a worker before approval is still the plan; after approval it is the build. `built`:
94
98
  * the build finished with a result.
95
99
  */
@@ -101,8 +105,8 @@ export declare function firstTaskJourney(read: {
101
105
  * nothing says (the step is then shown skipped, never done). The road reads this one answer, never a row's words. */
102
106
  export type RoadChecks = "passed" | "running" | "failed" | "not-run" | "off" | "unreadable" | null;
103
107
  /**
104
- * Any task's road (the read-only stepper on its page, after ReUI's Stepper): the first task's five steps, told only
105
- * from recorded facts. You approve is done only when the plan was approved (a rewritten plan waiting on approval
108
+ * Any task's road (the step rows on its page): the first task's five steps, told only
109
+ * from recorded facts. Approval is done only when the plan was approved (a rewritten plan waiting on approval
106
110
  * stands there again); a task that never needed one shows it skipped. Checks is done only when the approved check
107
111
  * passed; a running, missing or Off check says so, and with no checks fact it is skipped. A failed check, or one that
108
112
  * can't be read, stops the road at Checks: Ready isn't done (fail closed), even on a task a person marked complete.
package/dist/first-run.js CHANGED
@@ -133,10 +133,11 @@ export function firstResultWords(since, at) {
133
133
  }
134
134
  return `First result in ${Math.round(minutes / (24 * 60))} days`;
135
135
  }
136
- /** The first task's five steps, as a person reads them. */
137
- export const JOURNEY_STEPS = [["plan", "Plan"], ["approve", "You approve"], ["build", "Build"], ["checks", "Checks"], ["ready", "Ready"]];
136
+ /** A task's five steps, as a person reads them: the one label set the task page's rows, chat's first-task journey and
137
+ * the progress line all read. */
138
+ export const JOURNEY_STEPS = [["plan", "Plan"], ["approve", "Approval"], ["build", "Build"], ["checks", "Checks"], ["ready", "Ready"]];
138
139
  /**
139
- * Where the first task stands on Plan → You approve → Build → Checks → Ready, from the one stage every surface reads
140
+ * Where the first task stands on Plan → Approval → Build → Checks → Ready, from the one stage every surface reads
140
141
  * (task-status.ts). Waiting for a worker before approval is still the plan; after approval it is the build. `built`:
141
142
  * the build finished with a result.
142
143
  */
@@ -155,8 +156,8 @@ export function firstTaskJourney(read, approved, built = false) {
155
156
  state: index < at.index ? "done" : index === at.index ? (at.stuck ? "stuck" : "current") : "todo" }));
156
157
  }
157
158
  /**
158
- * Any task's road (the read-only stepper on its page, after ReUI's Stepper): the first task's five steps, told only
159
- * from recorded facts. You approve is done only when the plan was approved (a rewritten plan waiting on approval
159
+ * Any task's road (the step rows on its page): the first task's five steps, told only
160
+ * from recorded facts. Approval is done only when the plan was approved (a rewritten plan waiting on approval
160
161
  * stands there again); a task that never needed one shows it skipped. Checks is done only when the approved check
161
162
  * passed; a running, missing or Off check says so, and with no checks fact it is skipped. A failed check, or one that
162
163
  * can't be read, stops the road at Checks: Ready isn't done (fail closed), even on a task a person marked complete.
@@ -181,13 +182,13 @@ export function taskRoad(read, approved, built, facts) {
181
182
  return { ...step, state: "done" };
182
183
  case "not-run":
183
184
  waitsAtReady = true;
184
- return { ...step, state: "skipped", label: "Checks not run" };
185
+ return { ...step, state: "skipped", say: "Checks not run" };
185
186
  case "off":
186
187
  waitsAtReady = true;
187
- return { ...step, state: "skipped", label: "Checks at release" };
188
- case "running": return { ...step, state: "current", label: "Checks running" };
189
- case "failed": return { ...step, label: "Checks failed" };
190
- case "unreadable": return { ...step, label: "Checks unreadable" };
188
+ return { ...step, state: "skipped", say: "Checks at release" };
189
+ case "running": return { ...step, state: "current", say: "Checks running" };
190
+ case "failed": return { ...step, say: "Checks failed" };
191
+ case "unreadable": return { ...step, say: "Checks unreadable" };
191
192
  default: return step;
192
193
  }
193
194
  }
@@ -195,16 +196,16 @@ export function taskRoad(read, approved, built, facts) {
195
196
  return step;
196
197
  // At Checks now: only a recorded failure, an unreadable check or a running check changes how it reads.
197
198
  if (step.state === "current")
198
- return facts.checks === "failed" ? { ...step, state: "stuck", label: "Checks failed" }
199
- : facts.checks === "unreadable" ? { ...step, state: "stuck", label: "Checks unreadable" }
200
- : facts.checks === "running" ? { ...step, label: "Checks running" } : step;
199
+ return facts.checks === "failed" ? { ...step, state: "stuck", say: "Checks failed" }
200
+ : facts.checks === "unreadable" ? { ...step, state: "stuck", say: "Checks unreadable" }
201
+ : facts.checks === "running" ? { ...step, say: "Checks running" } : step;
201
202
  switch (facts.checks ?? null) {
202
203
  case "passed": return step;
203
- case "running": return { ...step, state: "current", label: "Checks running" };
204
- case "failed": return { ...step, state: "stuck", label: "Checks failed" };
205
- case "not-run": return { ...step, state: "skipped", label: "Checks not run" };
206
- case "off": return { ...step, state: "skipped", label: "Checks at release" };
207
- case "unreadable": return { ...step, state: "stuck", label: "Checks unreadable", ...(complete ? {} : { needsYou: true }) };
204
+ case "running": return { ...step, state: "current", say: "Checks running" };
205
+ case "failed": return { ...step, state: "stuck", say: "Checks failed" };
206
+ case "not-run": return { ...step, state: "skipped", say: "Checks not run" };
207
+ case "off": return { ...step, state: "skipped", say: "Checks at release" };
208
+ case "unreadable": return { ...step, state: "stuck", say: "Checks unreadable", ...(complete ? {} : { needsYou: true }) };
208
209
  default: return { ...step, state: "skipped" };
209
210
  }
210
211
  });
package/dist/guides.js CHANGED
@@ -335,7 +335,7 @@ ${AUTHORITY_LINE}
335
335
  - The rail is four rows — inbox · board · builds · projects — and a
336
336
  **more** group: portfolio, task list, fleet, system,
337
337
  requirements, people, operating mode, settings. On a phone the same
338
- five sit in the tab bar and more is \`/menu\`. Amber appears only on
338
+ five sit in the tab bar and more is \`/menu\`. The needs-you mark appears only on
339
339
  what needs a person: the inbox count and the one act that resolves a
340
340
  screen (approve, answer, retry).
341
341
 
@@ -19,6 +19,7 @@ import { createHash, createHmac, randomUUID } from "node:crypto";
19
19
  import { closeSync, constants, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, writeSync } from "node:fs";
20
20
  import { hostname } from "node:os";
21
21
  import { basename, join } from "node:path";
22
+ import { wasInterrupted } from "./run-ending.js";
22
23
  import { COMPLETION_ACTION } from "./result-completion.js";
23
24
  export const SIGNATURE_HEADER = "x-standing-orders-signature";
24
25
  const BATCH = 200;
@@ -164,7 +165,7 @@ export function spansFor(store, entries) {
164
165
  continue;
165
166
  const root = rootOf(taskId);
166
167
  const ref = store.refForId(run.taskRef);
167
- const failed = run.outcome === "failed" || run.outcome === "refused" || run.outcome === "interrupted";
168
+ const failed = run.outcome === "failed" || run.outcome === "refused" || wasInterrupted(run);
168
169
  spans.push({
169
170
  traceId: hex(`standing-orders/trace/${root}`, 32), spanId: hex(`standing-orders/run/${run.id}`, 16), parentSpanId: hex(`standing-orders/task/${root}`, 16),
170
171
  name: `${run.role} · ${run.provider}`, kind: 1, startTimeUnixNano: nanos(run.startedAt), endTimeUnixNano: nanos(run.finishedAt),