toolroll 0.9.84 → 0.9.86

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 (132) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +94 -1193
  3. package/dist/agent-fence.d.ts +9 -5
  4. package/dist/agent-fence.js +6 -5
  5. package/dist/agent-onboard.d.ts +2 -0
  6. package/dist/agent-onboard.js +3 -0
  7. package/dist/assignment.js +4 -3
  8. package/dist/browser/workspace.css +1 -1
  9. package/dist/browser/workspace.js +33 -31
  10. package/dist/browser-workspace.d.ts +45 -0
  11. package/dist/chat-actions.d.ts +11 -1
  12. package/dist/chat-actions.js +53 -1
  13. package/dist/chat-channel.d.ts +10 -1
  14. package/dist/chat-channel.js +36 -5
  15. package/dist/chat-decide.d.ts +16 -4
  16. package/dist/chat-decide.js +239 -31
  17. package/dist/chat-messages.d.ts +5 -0
  18. package/dist/chat-messages.js +1 -1
  19. package/dist/chat-quiet.js +6 -4
  20. package/dist/chat-review.d.ts +4 -0
  21. package/dist/chat-review.js +13 -4
  22. package/dist/chat-terms.d.ts +9 -0
  23. package/dist/chat-terms.js +46 -0
  24. package/dist/claim.d.ts +3 -0
  25. package/dist/claim.js +4 -1
  26. package/dist/contracts/chat-actions.d.ts +9 -0
  27. package/dist/contracts/chat-actions.js +3 -0
  28. package/dist/contracts/console-api.d.ts +6 -2
  29. package/dist/contracts/console-api.js +6 -2
  30. package/dist/contracts/flow-propose.d.ts +4 -1
  31. package/dist/contracts/flow-propose.js +1 -1
  32. package/dist/contracts/flow.d.ts +41 -0
  33. package/dist/contracts/flow.js +4 -2
  34. package/dist/contracts/lead-tools.d.ts +51 -0
  35. package/dist/contracts/lead-tools.js +5 -0
  36. package/dist/contracts/store-json.d.ts +2 -0
  37. package/dist/contracts/trigger-payloads.d.ts +20 -0
  38. package/dist/contracts/trigger-payloads.js +4 -0
  39. package/dist/contracts/youiyoux-request.d.ts +248 -0
  40. package/dist/contracts/youiyoux-request.js +152 -0
  41. package/dist/coordinator.js +1 -0
  42. package/dist/deploy-gate.js +4 -0
  43. package/dist/dispatch.d.ts +3 -0
  44. package/dist/dispatch.js +4 -1
  45. package/dist/flow-browser.d.ts +103 -0
  46. package/dist/flow-browser.js +204 -0
  47. package/dist/flow-cadence.d.ts +8 -2
  48. package/dist/flow-cadence.js +16 -3
  49. package/dist/flow-card-task.d.ts +9 -0
  50. package/dist/flow-card-task.js +15 -6
  51. package/dist/flow-code.d.ts +5 -1
  52. package/dist/flow-code.js +4 -1
  53. package/dist/flow-engine.js +16 -0
  54. package/dist/flow-live.js +9 -1
  55. package/dist/flow-pull-request.js +15 -7
  56. package/dist/flow-share.js +3 -1
  57. package/dist/flow-steps.d.ts +9 -1
  58. package/dist/flow-steps.js +55 -5
  59. package/dist/flow-triggers.js +55 -0
  60. package/dist/flows-cli.d.ts +19 -0
  61. package/dist/flows-cli.js +47 -2
  62. package/dist/flows-ui.js +11 -2
  63. package/dist/flows.js +5 -2
  64. package/dist/integrations-ui.js +9 -4
  65. package/dist/integrations.d.ts +9 -1
  66. package/dist/integrations.js +43 -2
  67. package/dist/landed-cleanup.d.ts +4 -0
  68. package/dist/landed-cleanup.js +3 -2
  69. package/dist/lead-capabilities.js +2 -1
  70. package/dist/lead-cli.js +3 -1
  71. package/dist/lead-tools.d.ts +2 -0
  72. package/dist/lead-tools.js +65 -10
  73. package/dist/lead-voice.js +16 -5
  74. package/dist/lead.d.ts +2 -0
  75. package/dist/lead.js +3 -3
  76. package/dist/operate.d.ts +2 -3
  77. package/dist/operate.js +226 -21
  78. package/dist/project-activity.d.ts +20 -0
  79. package/dist/project-activity.js +23 -0
  80. package/dist/project-cli.d.ts +1 -0
  81. package/dist/project-cli.js +36 -19
  82. package/dist/project-delete.js +7 -3
  83. package/dist/project-knowledge.d.ts +3 -3
  84. package/dist/project-outcomes.d.ts +119 -0
  85. package/dist/project-outcomes.js +79 -0
  86. package/dist/pull-request-flow.js +35 -4
  87. package/dist/result-actions.d.ts +4 -0
  88. package/dist/result-actions.js +41 -9
  89. package/dist/result-follow-ups.js +4 -0
  90. package/dist/result-review.d.ts +9 -0
  91. package/dist/result-review.js +45 -5
  92. package/dist/scope.d.ts +5 -0
  93. package/dist/scope.js +7 -0
  94. package/dist/server/chat.js +4 -2
  95. package/dist/server/console.css +41 -0
  96. package/dist/server/flows.js +15 -0
  97. package/dist/server/pages.js +11 -1
  98. package/dist/server/render-chat.js +3 -2
  99. package/dist/server/render-pages.d.ts +15 -2
  100. package/dist/server/render-pages.js +52 -4
  101. package/dist/server/render-results.d.ts +2 -0
  102. package/dist/server/render-results.js +20 -7
  103. package/dist/server/render-tasks.d.ts +9 -0
  104. package/dist/server/render-tasks.js +32 -11
  105. package/dist/server/route-table.js +12 -0
  106. package/dist/server/settings.js +18 -1
  107. package/dist/server/task-views.js +11 -0
  108. package/dist/server/tasks.d.ts +10 -0
  109. package/dist/server/tasks.js +95 -7
  110. package/dist/settings-proofs.js +1 -0
  111. package/dist/store.d.ts +116 -5
  112. package/dist/store.js +385 -24
  113. package/dist/surface.js +18 -2
  114. package/dist/task-archive.d.ts +10 -0
  115. package/dist/task-archive.js +16 -0
  116. package/dist/task-control.d.ts +1 -1
  117. package/dist/task-source.d.ts +13 -1
  118. package/dist/task-source.js +17 -1
  119. package/dist/telegram-lead.js +7 -5
  120. package/dist/telegram.js +1 -1
  121. package/dist/tidy.d.ts +130 -0
  122. package/dist/tidy.js +503 -0
  123. package/dist/toolroll-update.js +2 -2
  124. package/dist/webhooks.d.ts +6 -0
  125. package/dist/webhooks.js +25 -0
  126. package/dist/work-index.d.ts +28 -0
  127. package/dist/work-index.js +82 -28
  128. package/dist/workspace-ui.d.ts +1 -1
  129. package/dist/workspace-ui.js +3 -2
  130. package/dist/youiyoux-intake.d.ts +234 -0
  131. package/dist/youiyoux-intake.js +908 -0
  132. package/package.json +11 -8
@@ -2730,3 +2730,44 @@ html.so-swap::view-transition-old(*):only-child { animation: so-card-out 160ms e
2730
2730
  html.so-swap::view-transition-group(*), html.so-swap::view-transition-new(*), html.so-swap::view-transition-old(*) { animation: none !important; }
2731
2731
  .worker.flash { animation: none; }
2732
2732
  }
2733
+
2734
+ /* Exact-image feedback shares the existing comment form. */
2735
+ .result-image { margin: 0; min-width: 0; }
2736
+ .result-image > button { margin-top: .5rem; min-height: 44px; white-space: nowrap; }
2737
+ .feedback-image { display: flex; align-items: center; flex-wrap: wrap; gap: .75rem; margin: .5rem 0; min-width: 0; }
2738
+ .feedback-image[hidden], .feedback-image img[hidden] { display: none; }
2739
+ .feedback-image img { width: 5rem; max-height: 5rem; object-fit: contain; border: 1px solid var(--border); border-radius: var(--radius); }
2740
+ .feedback-image figcaption { flex: 1 1 12rem; overflow-wrap: anywhere; font-size: .875rem; }
2741
+ .feedback-image button { min-height: 44px; white-space: nowrap; }
2742
+
2743
+ .result-feedback-actions { display: flex; flex-wrap: wrap; gap: .5rem; }
2744
+ .result-feedback-actions button { min-height: 44px; white-space: nowrap; }
2745
+
2746
+ /* Put away old tasks (docs/plans/task-tidy.md): grouped checklist on the kit's column. */
2747
+ .put-away { max-width: var(--so-settings-width, 760px); }
2748
+ .put-away h1 { margin: 0 0 4px; }
2749
+ .put-away-days { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 0 0 16px; }
2750
+ .put-away-days label { display: flex; align-items: center; gap: 8px; margin: 0; font-size: 14px; }
2751
+ .put-away-days input[type=number] { width: 5rem; margin: 0; }
2752
+ .put-away-days button { width: auto; }
2753
+ .put-away-group { margin: 0 0 20px; padding: 0; border: 0; }
2754
+ .put-away-group legend { display: flex; align-items: center; gap: 8px; padding: 0; margin: 0 0 6px; font-weight: 600; font-size: 15px; }
2755
+ .put-away-group legend .count { font-family: var(--so-mono, ui-monospace, monospace); font-size: 11px; font-weight: 500; color: var(--so-muted); }
2756
+ .put-away-group ul { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--so-line); }
2757
+ .put-away-group li { border-bottom: 1px solid var(--so-line); }
2758
+ .put-away-item { display: grid; grid-template-columns: auto minmax(0, 1fr); column-gap: 10px; align-items: start; margin: 0; padding: 10px 2px; cursor: pointer; }
2759
+ .put-away-item input { margin: 3px 0 0; width: auto; min-height: 0; align-self: start; }
2760
+ .put-away-title { font-size: 13.5px; font-weight: 500; overflow-wrap: anywhere; }
2761
+ .put-away-why { grid-column: 2; font-size: 12.5px; color: var(--so-muted); }
2762
+ .put-away-id { grid-column: 2; font-family: var(--so-mono, ui-monospace, monospace); font-size: 11.5px; color: var(--so-muted); overflow-wrap: anywhere; }
2763
+ .put-away-cli { margin: 12px 0 0; font-size: 13px; color: var(--so-muted); overflow-wrap: anywhere; }
2764
+ .put-away-cli code { font-size: 12px; }
2765
+ .task-archived { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px 12px; margin: 0 0 12px; padding: 10px 12px; border-radius: 8px; background: var(--so-raised); }
2766
+ .task-archived p { margin: 0; font-size: 13.5px; }
2767
+ .task-archived form { margin: 0; }
2768
+ .task-archived button { width: auto; }
2769
+ .work-row-status form.inline { margin: 0; }
2770
+ @media (max-width: 900px) {
2771
+ .put-away-item { min-height: 44px; }
2772
+ .put-away-days button, .task-archived button, .work-row-status form.inline button { min-height: 44px; }
2773
+ }
@@ -21,6 +21,7 @@ import { exportFlow, fetchFlowFile, FlowFileError, importFlow, parseFlowFile, pl
21
21
  import { addFlowTriggerTo, checkFlowTriggerNow, pressFlowButton, removeFlowTrigger, removeLinearKey, setFlowTriggerOn, renewFlowHook, saveHooksBase, saveLinearKey, saveSigningSecret, setUpFlowHook, changeFlowHookSender, setFlowHookScope, shareFlowButton, stopSharingFlowButton, rawTakesDeliveries, rawAdmitsAnyone } from "../flow-triggers.js";
22
22
  import { FLOW_IMPORT_SCRIPT, flowFallbackHtml, flowImportHtml, flowsListHtml, flowView } from "../flows-ui.js";
23
23
  import { FLOW_TEMPLATES, FlowContractError, validateFlowDefinition, withZoneNames } from "../flows.js";
24
+ import { revokeFlowBrowser, signFlowBrowser } from "../flow-browser.js";
24
25
  import { prepareUnattendedCandidates, parseUnattendedAsk, revokeUnattended, signUnattended, untilWords } from "../flow-unattended.js";
25
26
  import { keyStatus } from "../keys.js";
26
27
  import { kitPageHtml, kitsGalleryHtml } from "../kits-ui.js";
@@ -656,6 +657,18 @@ export function createFlowsHandlers(runtime) {
656
657
  if (action === "unattended-off") {
657
658
  return revokeUnattended(store, flow.id, who.name, now) ? settle("It no longer runs on its own. New cards wait for you.") : answer(409, { ok: false, said: "It wasn't running on its own." });
658
659
  }
660
+ if (action === "browser") {
661
+ // A person's own step-up (a lead's or remote run's token never gets here: route table), then signFlowBrowser
662
+ // re-reads the flow, its zones and scripts in its one write and refuses unless they are the digest the panel showed.
663
+ const proved = authenticateApprover(who, body.get("password") ?? "", flow.repo);
664
+ if (!proved.ok)
665
+ return answer(403, { ok: false, said: "That password isn't right. Nothing was approved." });
666
+ const signed = signFlowBrowser(store, { flow: flow.id, signer: who.name, proof: proved.method, sawDigest: body.get("digest") ?? "", console: { generation: who.via === "cookie" ? who.session.generation : -1 } }, now);
667
+ return signed.ok ? settle(`Browser allowed. ${signed.grant.zones.length === 1 ? "Its script" : "Its scripts"} can start Chromium now.`) : answer(409, { ok: false, said: signed.message });
668
+ }
669
+ if (action === "browser-off") {
670
+ return revokeFlowBrowser(store, flow.id, who.name, now) ? settle("Browser turned off. Its browser zones wait until someone allows it again.") : answer(409, { ok: false, said: "Browser use wasn't allowed for this flow." });
671
+ }
659
672
  if (action === "save") {
660
673
  let saved, drawn;
661
674
  try {
@@ -975,6 +988,8 @@ export function createFlowsHandlers(runtime) {
975
988
  "flow.secrets": flowAct("secrets"),
976
989
  "flow.unattended": flowAct("unattended"),
977
990
  "flow.unattended-off": flowAct("unattended-off"),
991
+ "flow.browser": flowAct("browser"),
992
+ "flow.browser-off": flowAct("browser-off"),
978
993
  "flow.triggers": flowAct("triggers"),
979
994
  "flow.card.move": cardAct("move"),
980
995
  "flow.card.decide": cardAct("decide"),
@@ -1,3 +1,4 @@
1
+ import { projectOutcomes } from '../project-outcomes.js';
1
2
  import { steerControlsFor } from "../steer-controls.js";
2
3
  import { canReveal, cdCommandOf, localCheckoutOf } from "../checkout-proof.js";
3
4
  import { instanceHttpAnswer } from "../console-instance.js";
@@ -967,7 +968,16 @@ export function createPagesHandlers(runtime) {
967
968
  peeks[one.path] = peekOf(one.path);
968
969
  for (const path of candidates)
969
970
  peeks[path] = peekOf(path);
970
- return sendScreen(response, status, projectsPage(chromeFor(open, "projects"), recent, [...candidates], open, problem, !restricted() && unscopedMode, !restricted() && (ceiling.roots.length > 0 || unscopedMode), onboardState, peeks, returnTo, who.via === "cookie" ? (path) => publishingOf(store, path).on : undefined, who.via === "cookie" ? (path) => projectCheckLevel(store, path).level : undefined));
971
+ let activity;
972
+ if (open !== null && !returnTo.startsWith("/tasks/new") && visible(open) && store.accountCanAccess(who.name, open)) {
973
+ try {
974
+ activity = { project: projectName(open), report: projectOutcomes(store, open, now, { principal: 'operator', repos: [open] }) };
975
+ }
976
+ catch {
977
+ activity = { project: projectName(open), report: null };
978
+ }
979
+ }
980
+ return sendScreen(response, status, projectsPage(chromeFor(open, "projects"), recent, [...candidates], open, problem, !restricted() && unscopedMode, !restricted() && (ceiling.roots.length > 0 || unscopedMode), onboardState, peeks, returnTo, who.via === "cookie" ? (path) => publishingOf(store, path).on : undefined, who.via === "cookie" ? (path) => projectCheckLevel(store, path).level : undefined, activity));
971
981
  }
972
982
  async function instanceEdge(ctx) {
973
983
  const { request, response, url } = ctx;
@@ -1,3 +1,4 @@
1
+ import { parseFeedbackImage } from "../result-review.js";
1
2
  import { assignmentSummaryHtml } from "../assignment-ui.js";
2
3
  import {} from "../assignment.js";
3
4
  import {} from "../browser-workspace.js";
@@ -610,8 +611,8 @@ export function proposalCardParts(view, inert, decision, returnTo = null) {
610
611
  const snapshot = payload["snapshot"];
611
612
  const ids = Array.isArray(payload["notes"]) ? payload["notes"] : [];
612
613
  const selected = snapshot?.notes.filter(one => ids.includes(one.id)) ?? [];
613
- const notes = [...selected, ...(text("note") === "" ? [] : [{ note: text("note"), path: text("path") || null, line: typeof payload["line"] === "number" ? payload["line"] : null }])];
614
- what = html `<h3>${text("taskTitle") || task}</h3><ul class="proposal-review-notes">${notes.map(one => html `<li>${one.path === null ? "" : html `<span class="mono">${one.path}${one.line === null ? "" : html `:${one.line}`}</span> · `}${one.note}</li>`)}</ul>${text("operation") === "revise" ? html `<p class="meta">Updates this task. Your approval settings apply.</p>` : html `<p class="meta">No work starts.</p>`}`;
614
+ const notes = [...selected, ...(text("note") === "" ? [] : [{ note: text("note"), path: text("path") || null, line: typeof payload["line"] === "number" ? payload["line"] : null, image: parseFeedbackImage(payload["image"]) || null }])];
615
+ what = html `<h3>${text("taskTitle") || task}</h3><ul class="proposal-review-notes">${notes.map(one => html `<li>${one.image == null ? "" : html `<a href="/r/${one.image.run}/evidence/${one.image.artifact}">Screenshot ${one.image.artifact} · result #${one.image.run}</a><br>`}${one.path === null ? "" : html `<span class="mono">${one.path}${one.line === null ? "" : html `:${one.line}`}</span> · `}${one.note}</li>`)}</ul>${text("operation") === "revise" ? html `<p class="meta">Updates this task. Your approval settings apply.</p>` : html `<p class="meta">No work starts.</p>`}`;
615
616
  }
616
617
  else if (view.kind === "next") {
617
618
  what = html `<h3>Move <a href="${taskHref(task)}">${task}</a> to the front</h3>${facts(["current position", html `${String(payload["position"] ?? "?")} of ${String(payload["of"] ?? "?")}`], ["project", mono(repoId)])}`;
@@ -1,9 +1,10 @@
1
1
  /** Everyday console pages: home, inbox, board, work, projects, fleet and the queue. */
2
+ import { type TidyItem } from "../tidy.js";
2
3
  import { type TaskOrigin } from "../task-source.js";
3
4
  import { type Html } from "../html.js";
4
5
  import { type AssignmentSnapshot } from "../assignment.js";
5
6
  import { type BoardCard } from "../board.js";
6
- import { type BrowserLimits } from "../browser-workspace.js";
7
+ import { type BrowserLimits, type BrowserProjectsView } from "../browser-workspace.js";
7
8
  import { type CheckLevel } from "../check-levels.js";
8
9
  import { type DispatchDiagnosis } from "../dispatch.js";
9
10
  import { type FirstRunStep } from "../first-run.js";
@@ -295,6 +296,8 @@ export declare function workPage(chrome: Chrome, data: {
295
296
  now: Date;
296
297
  limits?: BrowserLimits | null;
297
298
  state?: TaskState | null;
299
+ /** An approver in a browser session: rows offer Put away (or Restore, archived), and the page offers Put away old tasks. */
300
+ canPutAway?: boolean;
298
301
  }): Screen;
299
302
  export declare function browsePage(chrome: Chrome, data: {
300
303
  at: string;
@@ -451,7 +454,7 @@ export declare function projectsPage(chrome: Chrome, recent: {
451
454
  path: string;
452
455
  name: string;
453
456
  lastOpenedAt: string;
454
- }[], candidates: string[], open: string | null, problem: string | null, _unscopedMode: boolean, browsable?: boolean, onboard?: OnboardCardState | null, peeks?: Record<string, ProjectPeek | null>, returnTo?: string, pullRequestsOn?: (path: string) => boolean, checkLevelOf?: (path: string) => CheckLevel): Screen;
457
+ }[], candidates: string[], open: string | null, problem: string | null, _unscopedMode: boolean, browsable?: boolean, onboard?: OnboardCardState | null, peeks?: Record<string, ProjectPeek | null>, returnTo?: string, pullRequestsOn?: (path: string) => boolean, checkLevelOf?: (path: string) => CheckLevel, activity?: BrowserProjectsView["activity"]): Screen;
455
458
  /** The no-script "move to the front" sentinel: the form cannot name the
456
459
  * front of a partition, so the handler resolves it (slice 1b, fix 1). */
457
460
  export declare const QUEUE_FRONT = "__TOP__";
@@ -562,3 +565,13 @@ export type PeekAdmission = {
562
565
  epoch: string;
563
566
  entries: ReturnType<typeof parseBaseTreeSnapshot>;
564
567
  };
568
+ /** Put away old tasks (docs/plans/task-tidy.md): exactly what would be put away, grouped, each with why, every one
569
+ * ticked; one step-up puts away the ticked ones, bound to this preview's digest. The CLI twin is named beside it. */
570
+ export declare function putAwayPage(chrome: Chrome, data: {
571
+ items: readonly TidyItem[];
572
+ days: number;
573
+ digest: string;
574
+ now: Date;
575
+ problem: string | null;
576
+ releasesSkipped: string | null;
577
+ }): Screen;
@@ -1,4 +1,7 @@
1
+ import { activityMetrics, activityScope, activityLimit } from '../project-activity.js';
1
2
  /** Everyday console pages: home, inbox, board, work, projects, fleet and the queue. */
3
+ import { archivedWords, tidyGroups, TIDY_DEFAULT_DAYS } from "../tidy.js";
4
+ import { stepUpForm } from "../settings-kit.js";
2
5
  import { originNote } from "../task-source.js";
3
6
  import { html, htmlString, joinHtml, postForm, replaceMarkup, reauth } from "../html.js";
4
7
  import {} from "../assignment.js";
@@ -631,14 +634,25 @@ export function workPage(chrome, data) {
631
634
  return `/work${query.size ? `?${query}` : ''}`;
632
635
  };
633
636
  const tabs = html `<nav class="work-views" aria-label="Task views">${WORK_VIEWS.map(one => html `<a href="${href(one.key)}"${one.key === data.view ? html ` class="active" aria-current="page"` : null}>${one.label}<span class="count">${data.work.totals[one.key]}</span></a>`)}</nav>`;
637
+ // Put away (or Restore, in Archived): one quiet act per row, never on a task with work going on.
638
+ const tidyAct = (row) => data.canPutAway !== true ? null : row.archived !== undefined ? { action: `${taskHref(row.rootId)}/restore`, label: 'Restore' }
639
+ : row.liveRunId !== null || row.unfinishedRunId !== null || row.status.views.includes('running') ? null : { action: `${taskHref(row.rootId)}/archive`, label: 'Put away' };
640
+ const returnTo = href(data.view, undefined);
634
641
  const rowHtml = (row) => {
642
+ if (row.archived !== undefined) {
643
+ const act = tidyAct(row);
644
+ const project = data.multiProject || row.repo === null ? html `<span class="project-label">${row.repo === null ? 'Unplaced' : projectName(row.repo)}</span>` : null;
645
+ return html `<article class="work-row" data-task="${row.rootId}" data-work-status="archived" data-work-views="archived"><div class="work-row-main"><a class="work-title" href="${taskHref(row.rootId)}">${row.title}</a><p class="work-meta">${project}<span>${relativeAge(row.archived.at, data.now)}</span><span class="mono">${row.rootId}</span></p><p class="work-detail" data-archived-by>${archivedWords(row.archived)}</p></div><div class="work-row-status">${act === null ? null : postForm(act.action, html `<button type="submit" class="secondary">${act.label}</button>`, { hidden: { return: returnTo }, attrs: { class: 'inline', 'data-tidy-act': 'restore' } })}</div></article>`;
646
+ }
635
647
  const target = row.primaryAction?.target;
636
648
  const actionHref = row.primaryAction?.code === 'open-result' && target?.runId != null
637
649
  ? `/review?result=${encodeURIComponent(target.taskId)}&run=${target.runId}`
638
650
  : browserWorkActionHref(row);
639
651
  const action = actionHref === null || row.primaryAction === null ? null : html `<a class="work-action" data-primary-action href="${actionHref}">${row.primaryAction.label} →</a>`;
640
652
  const project = data.multiProject || row.repo === null ? html `<span class="project-label">${row.repo === null ? 'Unplaced' : projectName(row.repo)}</span>` : null;
641
- return html `<article class="work-row" data-task="${row.rootId}" data-work-status="${row.status.token}" data-work-views="${row.status.views.join(' ')}"><div class="work-row-main"><a class="work-title" href="${taskHref(row.rootId)}">${row.title}</a><p class="work-meta">${project}<span>${relativeAge(row.updatedAt, data.now)}</span><span class="mono">${row.rootId}</span></p></div><div class="work-row-status">${statusLineHtml(row.status)}${action}${row.familyProblem === null ? null : html `<p class="problem">${row.familyProblem}</p>`}${row.status.views.includes('needs-you') && !['write-scope', 'approve-scope'].includes(row.primaryAction?.code ?? '') && row.status.detail !== row.status.label && row.status.detail !== row.familyProblem ? html `<p class="work-detail">${row.status.detail}</p>` : null}${workDiagnosticsHtml(row.status.diagnostics)}</div></article>`;
653
+ const act = tidyAct(row);
654
+ const putAway = act === null ? null : postForm(act.action, html `<button type="submit" class="quiet">${act.label}</button>`, { hidden: { return: returnTo }, attrs: { class: 'inline', 'data-tidy-act': 'put-away' } });
655
+ return html `<article class="work-row" data-task="${row.rootId}" data-work-status="${row.status.token}" data-work-views="${row.status.views.join(' ')}"><div class="work-row-main"><a class="work-title" href="${taskHref(row.rootId)}">${row.title}</a><p class="work-meta">${project}<span>${relativeAge(row.updatedAt, data.now)}</span><span class="mono">${row.rootId}</span></p></div><div class="work-row-status">${statusLineHtml(row.status)}${action}${putAway}${row.familyProblem === null ? null : html `<p class="problem">${row.familyProblem}</p>`}${row.status.views.includes('needs-you') && !['write-scope', 'approve-scope'].includes(row.primaryAction?.code ?? '') && row.status.detail !== row.status.label && row.status.detail !== row.familyProblem ? html `<p class="work-detail">${row.status.detail}</p>` : null}${workDiagnosticsHtml(row.status.diagnostics)}</div></article>`;
642
656
  };
643
657
  const current = WORK_VIEWS.find(one => one.key === data.view);
644
658
  const filtered = (data.state ?? null) !== null;
@@ -653,6 +667,7 @@ export function workPage(chrome, data) {
653
667
  const pages = !data.previous && data.work.nextCursor === null ? null : html `<nav class="row work-pagination" aria-label="Task pages">${data.previous ? html `<a class="button-link" href="${href(data.view)}">First page</a>` : null}${data.work.nextCursor === null ? null : html `<a class="button-link" rel="next" href="${href(data.view, data.work.nextCursor)}">Next page</a>`}</nav>`;
654
668
  const tools = html `<details class="work-tools"><summary>Work tools${CHEVRON_ICON}</summary><nav class="work-tools-menu">${[['/inbox', 'Inbox'], ['/board', 'Board'], ['/board?view=order', 'Order'], ['/recipes', 'Recipes'], ...(chrome.projectScoped ? [] : [['/workbench', 'Portfolio']]), ['/ledger', 'Action ledger']]
655
669
  .map(([path, label]) => html `<a href="${path}">${label}</a>`)}</nav></details>`;
670
+ const putAwayLink = data.canPutAway === true && data.view !== 'archived' ? { label: 'Put away old tasks', href: '/work/put-away' } : null;
656
671
  const toolLinks = [['/inbox', 'Inbox'], ['/board', 'Board'], ['/board?view=order', 'Order'], ['/recipes', 'Recipes'], ...(chrome.projectScoped ? [] : [['/workbench', 'Portfolio']])];
657
672
  const view = {
658
673
  kind: 'tasks',
@@ -666,6 +681,15 @@ export function workPage(chrome, data) {
666
681
  const actionHref = row.primaryAction?.code === 'open-result' && target?.runId != null
667
682
  ? `/review?result=${encodeURIComponent(target.taskId)}&run=${target.runId}`
668
683
  : browserWorkActionHref(row);
684
+ const act = tidyAct(row);
685
+ const tidy = act === null ? null : { action: act.action, label: act.label, returnTo };
686
+ if (row.archived !== undefined)
687
+ return {
688
+ id: row.rootId, title: row.title, href: taskHref(row.rootId),
689
+ project: data.multiProject || row.repo === null ? (row.repo === null ? 'Unplaced' : projectName(row.repo)) : null,
690
+ age: relativeAge(row.archived.at, data.now), status: { label: 'Archived', tone: 'muted', token: 'archived' },
691
+ ask: null, chip: null, group: 'rest', action: null, detail: archivedWords(row.archived), problem: null, notes: [], tidy,
692
+ };
669
693
  const needsYouDetail = (row.status.views.includes('needs-you') || row.status.label === 'Failed') && !['write-scope', 'approve-scope'].includes(row.primaryAction?.code ?? '') && row.status.label !== 'Ready for review' && row.status.detail !== row.status.label && row.status.detail !== row.familyProblem;
670
694
  return {
671
695
  id: row.rootId, title: row.title, href: taskHref(row.rootId),
@@ -677,6 +701,7 @@ export function workPage(chrome, data) {
677
701
  detail: needsYouDetail ? row.status.detail : row.progress ?? null,
678
702
  problem: row.familyProblem,
679
703
  notes: (row.status.diagnostics ?? []).map(one => `${one.label} · ${one.detail}`),
704
+ tidy,
680
705
  };
681
706
  }),
682
707
  empty: data.work.items.length > 0 ? null : filtered && !data.previous ? { text: `No tasks are ${data.state} in this view.`, action: { label: "Show every state", href: href(data.view, undefined, null) } } : {
@@ -686,11 +711,12 @@ export function workPage(chrome, data) {
686
711
  pages: { first: data.previous ? href(data.view) : null, next: data.work.nextCursor === null ? null : href(data.view, data.work.nextCursor) },
687
712
  tools: toolLinks.map(([path, label]) => ({ label: label, href: path })),
688
713
  newTask: { label: 'New task', href: '/tasks/new' },
714
+ putAway: putAwayLink,
689
715
  limits: data.limits ?? null,
690
716
  states: [{ label: 'Any state', href: href(data.view, undefined, null), active: (data.state ?? null) === null },
691
717
  ...TASK_STATES.map(one => ({ label: one.charAt(0).toUpperCase() + one.slice(1), href: href(data.view, undefined, one), active: data.state === one }))],
692
718
  };
693
- return screen('work', html `<div class="work-head"><h1>Tasks</h1>${tools}</div>${limitsHtml(data.limits ?? null)}${tabs}${stateLinks}${list}${pages}`, { chrome, workspace: { view } });
719
+ return screen('work', html `<div class="work-head"><h1>Tasks</h1>${putAwayLink === null ? null : html `<a class="button-link" href="${putAwayLink.href}" data-put-away-old>${putAwayLink.label}</a>`}${tools}</div>${limitsHtml(data.limits ?? null)}${tabs}${stateLinks}${list}${pages}`, { chrome, workspace: { view } });
694
720
  }
695
721
  export function browsePage(chrome, data) {
696
722
  const crumb = data.at === data.root ? projectName(data.root) : `${projectName(data.root)}${data.at.slice(data.root.length)}`;
@@ -921,7 +947,10 @@ export function githubReposPage(chrome, data) {
921
947
  html `<p class="row" style="margin-top:.6rem"><a class="badge" href="/projects">← back to projects</a></p>`,
922
948
  ], "\n"), { chrome });
923
949
  }
924
- export function projectsPage(chrome, recent, candidates, open, problem, _unscopedMode, browsable = false, onboard = null, peeks = {}, returnTo = "/", pullRequestsOn, checkLevelOf) {
950
+ export function projectsPage(chrome, recent, candidates, open, problem, _unscopedMode, browsable = false, onboard = null, peeks = {}, returnTo = "/", pullRequestsOn, checkLevelOf, activity) {
951
+ const activityHtml = activity === undefined ? null : html `<details><summary>Activity · ${activity.project}</summary>${activity.report === null
952
+ ? html `<p role="alert">Activity is unavailable.</p><a href="/projects">Reload</a>`
953
+ : html `<p>${activityScope(activity.report)}</p>${activity.report.truncated ? html `<p>${activityLimit(activity.report)}</p>` : null}${activityMetrics(activity.report).map(metric => html `<details><summary>${metric.label} · ${metric.value}</summary><p>${metric.detail}</p>${metric.rows.length === 0 ? html `<p>No tasks in this group.</p>` : html `<ul>${metric.rows.map(row => html `<li><a href="${row.href}">${row.title}</a> · Recorded checks: ${row.checks}</li>`)}</ul>`}</details>`)}`}</details>`;
925
954
  // The onboarding card (repo onboarding, findings 1-39): preview first,
926
955
  // then a password-confirmed clone into a configured root. Disabled
927
956
  // states explain themselves in words (finding 28/39).
@@ -1047,11 +1076,12 @@ export function projectsPage(chrome, recent, candidates, open, problem, _unscope
1047
1076
  recentItems.length === 0 && candidateItems.length === 0
1048
1077
  ? html `<div class="card"><p><strong>Nothing to open yet.</strong></p><p class="meta">Add one below — opening it registers it here for next time.</p></div>`
1049
1078
  : null,
1079
+ activityHtml,
1050
1080
  recentItems.length > 0 ? html `<h2>Recent</h2>${cards(recentItems)}` : null,
1051
1081
  candidateItems.length > 0 ? html `<h2>Available</h2>${cards(candidateItems)}` : null,
1052
1082
  addCard,
1053
1083
  ], "\n"), { chrome, workspace: { view: {
1054
- kind: "projects", choosing, problem, returnTo,
1084
+ kind: "projects", choosing, problem, returnTo, ...(activity === undefined ? {} : { activity }),
1055
1085
  recent: recent.map(one => rowOf(one.path, one.name, one.lastOpenedAt)),
1056
1086
  available: candidates.map(path => rowOf(path, projectName(path), null)),
1057
1087
  add: { browse: browsable ? "/projects/browse" : null, github: onboard === null ? null : "/projects/github", html: htmlString(addForms) },
@@ -1315,3 +1345,21 @@ export function nextPage(chrome, data) {
1315
1345
  }
1316
1346
  return screen("next", joinHtml([header, card], "\n"), { chrome });
1317
1347
  }
1348
+ /** Put away old tasks (docs/plans/task-tidy.md): exactly what would be put away, grouped, each with why, every one
1349
+ * ticked; one step-up puts away the ticked ones, bound to this preview's digest. The CLI twin is named beside it. */
1350
+ export function putAwayPage(chrome, data) {
1351
+ const groups = tidyGroups(data.items, data.days);
1352
+ const count = data.items.length;
1353
+ const daysArg = data.days === TIDY_DEFAULT_DAYS ? "" : ` --days ${data.days}`;
1354
+ const crumbs = html `<nav class="kit-crumbs" aria-label="Breadcrumb"><ol><li><a href="/work">Tasks</a></li><li><span aria-current="page">Put away old tasks</span></li></ol></nav>`;
1355
+ const daysForm = html `<form method="get" action="/work/put-away" class="put-away-days"><label>Also finished tasks untouched for <input type="number" name="days" min="1" max="365" value="${String(data.days)}" inputmode="numeric"> days</label><button type="submit" class="secondary">Update list</button></form>`;
1356
+ const list = joinHtml(groups.map(group => html `<fieldset class="put-away-group" data-tidy-group="${group.rule}"><legend>${group.label}<span class="count">${String(group.items.length)}</span></legend><ul>${group.items.map(item => html `<li><label class="put-away-item" data-tidy-task="${item.taskId}"><input type="checkbox" name="task" value="${item.taskId}" checked><span class="put-away-title">${item.title}</span><span class="put-away-why">${item.reason}</span><span class="put-away-id">${item.taskId}${item.repo === null ? "" : ` · ${projectName(item.repo)}`}${item.members.length > 1 ? ` · ${item.members.length} versions` : ""}</span></label></li>`)}</ul></fieldset>`));
1357
+ const cli = html `<p class="put-away-cli" data-tidy-cli>Or run: <code>toolroll task tidy --apply --digest ${data.digest}${daysArg}</code></p>`;
1358
+ const body = count === 0
1359
+ ? html `<p class="kit-state">Nothing to put away right now.</p><p><a href="/work?view=archived">See archived tasks →</a></p>`
1360
+ : stepUpForm({ proof: "tasks-put-away", action: "/work/put-away", id: "put-away", hidden: { digest: data.digest, days: data.days },
1361
+ body: html `${list}${cli}`, save: { label: "Put away selected", idle: `${count} ${count === 1 ? "task" : "tasks"} selected` }, confirm: `Put away the selected tasks. You can restore any of them from Archived.` });
1362
+ return screen("Put away old tasks", html `<div class="put-away">${crumbs}<h1>Put away old tasks</h1>\
1363
+ <p class="kit-state">${count === 0 ? "Old releases, routine runs and fixed CI notices are put away automatically." : `${count} ${count === 1 ? "task" : "tasks"} can be put away. They leave every list and keep their history.`}</p>\
1364
+ ${data.problem ? html `<p class="kit-problem" role="alert">${data.problem}</p>` : ""}${data.releasesSkipped === null ? "" : html `<p class="kit-hint">Release tasks were left out: ${data.releasesSkipped}.</p>`}${daysForm}${body}</div>`, { chrome });
1365
+ }
@@ -614,6 +614,8 @@ export type ResultDetail = {
614
614
  fileAnchors: ReadonlyMap<string, string>;
615
615
  comments: DiffComment[];
616
616
  pastComments?: DiffComment[];
617
+ /** Live saved notes whose screenshot would be refused now (result-actions.ts feedbackImageProblem). */
618
+ unusableImageNotes?: readonly number[];
617
619
  reviewerFindings: DiffComment[];
618
620
  /** The build's one automatic review, when its project had review on. */
619
621
  automaticReview?: BuildReviewView | null;
@@ -1068,7 +1068,7 @@ export function proofBundleView(store, run, artifacts, root) {
1068
1068
  catch {
1069
1069
  problem = "the file could not be read";
1070
1070
  }
1071
- return [{ path, caption: captionFor(path), artifactId: artifact.id, problem }];
1071
+ return [{ path, caption: captionFor(path), artifactId: artifact.id, sha256: artifact.sha256, problem }];
1072
1072
  });
1073
1073
  const stored = new Set(screenshots.map(one => one.path));
1074
1074
  const uncapturedScreenshots = (proof?.screenshots ?? []).map(one => one.path).filter(path => !stored.has(path));
@@ -1598,6 +1598,8 @@ export function resultPanelParts(detail, o) {
1598
1598
  const physicalReasons = receipt.reasons.filter(reason => !assessmentReasons.has(reason));
1599
1599
  const runId = run.id;
1600
1600
  const shots = proof?.screenshots ?? [];
1601
+ const picture = (shot) => html `<figure class="result-image"><a class="receipt-shot" href="/r/${runId}/evidence/${shot.artifactId}"><img src="/r/${runId}/evidence/${shot.artifactId}" alt="${shot.caption}"><span>${shot.caption}</span></a>${detail.canAnnotate && o.csrf !== "" && shot.sha256 !== undefined
1602
+ ? html `<button type="button" class="quiet" data-comment-image="${JSON.stringify({ run: runId, artifact: shot.artifactId, sha256: shot.sha256 })}" data-image-caption="${shot.caption}">Comment</button>` : ""}</figure>`;
1601
1603
  const shown = shots.filter(one => one.problem === null);
1602
1604
  const stat = terminal?.stat ?? null;
1603
1605
  const statOk = stat !== null && !("problem" in stat);
@@ -1694,7 +1696,7 @@ export function resultPanelParts(detail, o) {
1694
1696
  }
1695
1697
  else if (lead === "screenshots") {
1696
1698
  summaryParts.push(html `<div class="receipt-visuals result-visuals" aria-label="validated screenshots">${shown
1697
- .map(shot => html `<a class="receipt-shot" href="/r/${runId}/evidence/${shot.artifactId}"><img src="/r/${runId}/evidence/${shot.artifactId}" alt="${shot.caption}"><span>${shot.caption}</span></a>`)}</div>`);
1699
+ .map(picture)}</div>`);
1698
1700
  }
1699
1701
  const unavailableShots = shots.filter(one => one.problem !== null);
1700
1702
  if (lead === "changes") {
@@ -1848,7 +1850,7 @@ export function resultPanelParts(detail, o) {
1848
1850
  ? directAssessment && !screenshotRequired ? html `` : html `<p class="meta" data-cockpit-source="screenshots">No screenshots${screenshotRequired ? " were captured, although one was required" : " were needed"}.</p>`
1849
1851
  : lead === "screenshots"
1850
1852
  ? html `<p class="meta" data-cockpit-source="screenshots">${shown.length} validated screenshot${shown.length === 1 ? "" : "s"} shown in Summary${unavailableShots.length > 0 ? `; ${unavailableShots.length} unavailable` : ""}.</p>`
1851
- : html `<details class="cockpit-proof-group" data-cockpit-source="screenshots"><summary>Screenshots · ${shown.length}${unavailableShots.length > 0 ? ` (${unavailableShots.length} unavailable)` : ""}</summary><div class="receipt-visuals" aria-label="validated screenshots">${shown.map(shot => html `<a class="receipt-shot" href="/r/${runId}/evidence/${shot.artifactId}"><img src="/r/${runId}/evidence/${shot.artifactId}" alt="${shot.caption}"><span>${shot.caption}</span></a>`)}</div></details>`);
1853
+ : html `<details class="cockpit-proof-group" data-cockpit-source="screenshots"><summary>Screenshots · ${shown.length}${unavailableShots.length > 0 ? ` (${unavailableShots.length} unavailable)` : ""}</summary><div class="receipt-visuals" aria-label="validated screenshots">${shown.map(picture)}</div></details>`);
1852
1854
  checkParts.push(receipt.caveats.length === 0
1853
1855
  ? directAssessment ? html `` : html `<p class="meta" data-cockpit-source="caveats">No caveats were reported.</p>`
1854
1856
  : html `<details class="cockpit-proof-group" data-cockpit-source="caveats"><summary>Agent caveats · ${receipt.caveats.length}</summary><ul>${receipt.caveats.map(one => html `<li>${one}</li>`)}</ul></details>`);
@@ -1863,19 +1865,29 @@ export function resultPanelParts(detail, o) {
1863
1865
  checkParts.push(o.extraChecks);
1864
1866
  // ---- request changes: both feedback styles, one sealed road ------------
1865
1867
  const requestParts = [html `<h3 class="so-sr-only">What should change?</h3>`];
1868
+ const imageNote = (one) => {
1869
+ if (one.image == null)
1870
+ return html ``;
1871
+ const shot = shots.find(shot => shot.artifactId === one.image.artifact && shot.sha256 === one.image.sha256 && one.image.run === runId && shot.problem === null);
1872
+ // A live saved note whose screenshot no longer verifies can be kept without it, so it never blocks a revision.
1873
+ const drop = o.csrf === "" || !(detail.unusableImageNotes ?? []).includes(one.id) ? html ``
1874
+ : postForm(`/r/${runId}/comment`, html `<input type="hidden" name="intent" value="drop-image"><input type="hidden" name="comment" value="${String(one.id)}"><button type="submit" class="quiet">Keep note without screenshot</button>`, { attrs: { class: "feedback-image-drop" }, returnTo: o.returnTo, hidden: { tab: o.tab } });
1875
+ return html `<figure class="feedback-image">${shot === undefined ? html `<figcaption>Screenshot ${one.image.artifact} is unavailable.</figcaption>`
1876
+ : html `<a href="/r/${runId}/evidence/${shot.artifactId}"><img src="/r/${runId}/evidence/${shot.artifactId}" alt="${shot.caption}"></a><figcaption>${shot.caption} · Screenshot ${shot.artifactId}</figcaption>`}${drop}</figure>`;
1877
+ };
1866
1878
  const pathWords = (path, line) => {
1867
1879
  const shownPath = `${path}${line === null ? "" : `:${line}`}`;
1868
1880
  const href = detail.editor === null ? null : editorFileHref(detail.editor.worktree, path, line);
1869
1881
  return href === null ? html `<span class="mono">${shownPath}</span> ` : html `<a class="mono" href="${href}">${shownPath}</a> `;
1870
1882
  };
1871
1883
  if (detail.comments.length > 0) {
1872
- requestParts.push(html `<p class="meta">Saved for later · ${detail.comments.length}</p><div class="diff-comments" data-result-notes="${detail.comments.length}">${detail.comments.map(one => html `<div class="diff-comment"><span class="diff-comment-pin" aria-hidden="true"></span><p>${one.path === null ? "" : pathWords(one.path, one.line)}${one.note}</p><span class="meta">${one.author} · ${whenTime(one.createdAt)}</span></div>`)}</div>`);
1884
+ requestParts.push(html `<p class="meta">Saved for later · ${detail.comments.length}</p><div class="diff-comments" data-result-notes="${detail.comments.length}">${detail.comments.map(one => html `<div class="diff-comment">${imageNote(one)}<span class="diff-comment-pin" aria-hidden="true"></span><p>${one.path === null ? "" : pathWords(one.path, one.line)}${one.note}</p><span class="meta">${one.author} · ${whenTime(one.createdAt)}</span></div>`)}</div>`);
1873
1885
  if (o.csrf !== "" && !detail.canAnnotate) {
1874
1886
  requestParts.push(postForm(`/r/${runId}/revise`, html `${revisionSealFields(detail.comments, detail.sourceDigest)}<div><strong>${detail.comments.length} note${detail.comments.length === 1 ? "" : "s"} ready</strong></div><button type="submit">Request changes</button>`, { attrs: { class: "card revision-from-comments" }, returnTo: o.returnTo }));
1875
1887
  }
1876
1888
  }
1877
1889
  if ((detail.pastComments?.length ?? 0) > 0) {
1878
- requestParts.push(html `<details class="result-feedback-history"><summary>Earlier feedback</summary><div class="diff-comments" data-past-feedback>${detail.pastComments.map(one => html `<div class="diff-comment"><p>${one.path === null ? "" : pathWords(one.path, one.line)}${one.note}</p><span class="meta">${one.author} · ${whenTime(one.createdAt)}</span></div>`)}</div></details>`);
1890
+ requestParts.push(html `<details class="result-feedback-history"><summary>Earlier feedback</summary><div class="diff-comments" data-past-feedback>${detail.pastComments.map(one => html `<div class="diff-comment">${imageNote(one)}<p>${one.path === null ? "" : pathWords(one.path, one.line)}${one.note}</p><span class="meta">${one.author} · ${whenTime(one.createdAt)}</span></div>`)}</div></details>`);
1879
1891
  }
1880
1892
  for (const revision of detail.revisions) {
1881
1893
  // The child's standing is the shared status projection's own words
@@ -1892,6 +1904,7 @@ export function resultPanelParts(detail, o) {
1892
1904
  requestParts.push(html `<details class="result-request-open result-request-form"${detail.comments.length > 0 ? " open" : ""}><summary>What should change?</summary>${postForm(`/r/${runId}/comment`, joinHtml([
1893
1905
  revisionSealFields(detail.comments, detail.sourceDigest),
1894
1906
  html `<input type="hidden" data-recorded-requests value="${[...detail.comments, ...(detail.pastComments ?? [])].filter(one => one.sourceKey?.startsWith(`review:${o.user}:`)).map(one => one.sourceKey.slice(`review:${o.user}:`.length)).join(",")}">`,
1907
+ html `<input type="hidden" name="image" value=""><figure class="feedback-image" data-feedback-image hidden><img alt="Selected screenshot" hidden><figcaption></figcaption><button type="button" class="quiet" data-remove-image>Remove image</button></figure>`,
1895
1908
  html `<textarea name="note" rows="2" maxlength="${LIMITS.note}" placeholder="Describe the change…" aria-label="review comment" aria-describedby="comment-note-limit"${o.noted ? " autofocus" : ""}></textarea>`,
1896
1909
  html `<span class="meta diff-comment-limit" id="comment-note-limit">up to ${LIMITS.note} characters</span>`,
1897
1910
  html `<details class="result-pin"><summary>Attach to a file or line</summary>`,
@@ -1980,7 +1993,7 @@ export function personCheckItems(proof, patchText, shots, runId) {
1980
1993
  return {
1981
1994
  id: row.id, statement: row.statement, words: personCheckWords(row.statement), note: notes.length === 0 ? null : notes.join(" "),
1982
1995
  excerpts: files.filter(one => one.lines.length > 0).slice(0, 3),
1983
- shots: pictures.slice(0, 4).map(shot => ({ src: `/r/${runId}/evidence/${shot.artifactId}`, href: `/r/${runId}/evidence/${shot.artifactId}`, caption: shot.caption })),
1996
+ shots: pictures.slice(0, 4).map(shot => ({ src: `/r/${runId}/evidence/${shot.artifactId}`, href: `/r/${runId}/evidence/${shot.artifactId}`, caption: shot.caption, ...(shot.sha256 === undefined ? {} : { image: { run: runId, artifact: shot.artifactId, sha256: shot.sha256 } }) })),
1984
1997
  };
1985
1998
  });
1986
1999
  }
@@ -2215,7 +2228,7 @@ export function runPage(chrome, run, taskId, artifacts, terminal = null, notes =
2215
2228
  noted,
2216
2229
  requestToken: result.requestToken,
2217
2230
  hrefFor: one => (one === "summary" ? `/r/${run.id}` : `/r/${run.id}?tab=${one}`),
2218
- returnTo: `/r/${run.id}`,
2231
+ returnTo: `/r/${run.id}?record=1`,
2219
2232
  back: null,
2220
2233
  });
2221
2234
  return screen(`build #${run.id}`, joinHtml([
@@ -398,6 +398,7 @@ export type RevisionView = {
398
398
  line: number | null;
399
399
  note: string;
400
400
  author: string;
401
+ image?: import("../result-review.js").FeedbackImage | null;
401
402
  }[];
402
403
  /** The lineage and ACTUAL terms (contract handoff task 2), read from
403
404
  * the child's own rows — the same projection chat and the approval
@@ -437,6 +438,14 @@ export declare function taskBodyParts(data: {
437
438
  /** Markup the caller rendered (the version history). */
438
439
  history?: Html;
439
440
  versionLabel?: string | null;
441
+ /** Put away (v145): by whom and why; and whether the viewer can put it away or restore it (an approver's browser). */
442
+ archived?: {
443
+ by: string;
444
+ byKind: "person" | "tidy";
445
+ reason: string;
446
+ at: string;
447
+ } | null;
448
+ canPutAway?: boolean;
440
449
  status?: DisplayStatus;
441
450
  task: Task;
442
451
  /** Shared read-side lifecycle answer; the atomic claim still re-proves it. */
@@ -1,4 +1,6 @@
1
1
  /** Task pages: approval, agents, plans, controls, decisions and the task body. */
2
+ import { archivedWords } from "../tidy.js";
3
+ import { whenUtc } from "../when-html.js";
2
4
  import { networkWords } from "../egress.js";
3
5
  import { releaseAsksAgainWords, releaseTermsSentence } from "../scope.js";
4
6
  import { RELEASE_CI_RUNNING_WORDS } from "../release-ci-hold.js";
@@ -50,7 +52,7 @@ import { reviewHref, taskChatHref, taskHref } from "./http.js";
50
52
  import { coordinatorProposalsSection } from "./render-chat.js";
51
53
  import { completionReceiptCard, criterionMatrixHtml, evidenceWords, homePhaseWords, incidentWords, needsCheck, pullRequestCardHtml, reasonWords, receiptStatusOf, repairChainHtml, reportItemsHtml, reviewRetryPanel, runNoun, runOutcomeBadge, semanticCoverageHtml, verificationExplanation, verificationRecovered } from "./render-results.js";
52
54
  import {} from "./session.js";
53
- import { linkWords, originNote } from "../task-source.js";
55
+ import { linkWords, originNote, requestName, requestWords } from "../task-source.js";
54
56
  export function contractChangeHtml(change) {
55
57
  const tag = html `<span class="contract-change-kind contract-change-${change.kind}">${change.kind}</span>`;
56
58
  switch (change.field) {
@@ -203,7 +205,8 @@ const fragmentString = (fragment) => fragment === "" ? "" : htmlString(fragment)
203
205
  function originWords(origin) {
204
206
  const flow = origin.flow === null ? null : origin.flow === "hidden" ? "from a flow" : origin.flow === "removed" ? "from a flow (removed)" : `from flow “${origin.flow.name}”`;
205
207
  const source = origin.source === null ? null : `${origin.source.label} (${origin.source.proved})`;
206
- return [flow, source].filter(one => one !== null).join(" · ");
208
+ const request = origin.request == null ? null : `from ${requestName(origin.request)}`;
209
+ return [flow, source, request].filter(one => one !== null).join(" · ");
207
210
  }
208
211
  /** v126: a task's origin as a fact: the flow, linked for those who may open it, and the outside event, linked to where
209
212
  * it lives, with that link's host always shown. */
@@ -219,6 +222,12 @@ function originParts(origin) {
219
222
  if (origin.source.url !== null)
220
223
  parts.push(" · ", { label: linkWords(origin.source.url), href: origin.source.url });
221
224
  }
225
+ if (origin.request != null) {
226
+ if (parts.length > 0)
227
+ parts.push(" · ");
228
+ const where = requestWords(origin.request);
229
+ parts.push(`${requestName(origin.request)}${where === "" ? "" : `: ${where}`}`);
230
+ }
222
231
  return parts;
223
232
  }
224
233
  export function approvalSheetHtml(input) {
@@ -294,7 +303,7 @@ export function approvalSheetHtml(input) {
294
303
  const revisionDetail = input.revision === null ? "" : detail("Earlier build", joinHtml([
295
304
  html `<div class="revision-card" data-revision-feedback>`,
296
305
  html `<p><a href="${input.revisionSourceHref}"${input.surface === "chat" ? html ` data-revision-source` : ""}>${input.surface === "chat" ? html `Original result: build #${input.revision.sourceRun} →` : html `build #${input.revision.sourceRun}`}</a></p>`,
297
- input.revision.comments.length === 0 ? "" : html `<ul>${input.revision.comments.map(one => html `<li>${one.path === null ? "" : html `<span class="mono">${one.path}${one.line === null ? "" : `:${one.line}`}</span> · `}${one.note} <span class="meta">— ${one.author}</span></li>`)}</ul>`,
306
+ input.revision.comments.length === 0 ? "" : html `<ul>${input.revision.comments.map(one => html `<li>${revisionImageLink(one.image)}${one.path === null ? "" : html `<span class="mono">${one.path}${one.line === null ? "" : `:${one.line}`}</span> · `}${one.note} <span class="meta">— ${one.author}</span></li>`)}</ul>`,
298
307
  revisionLineageHtml(input.revision.lineage),
299
308
  html `</div>`,
300
309
  ]));
@@ -767,6 +776,8 @@ export function approveRefusalWords(reason) {
767
776
  return gateWords({ verdict: "refuse", reason });
768
777
  if (reason === "machine-proof")
769
778
  return "not approved: only a person can approve this. Approve it on the task page or from your paired chat";
779
+ if (reason === "archived")
780
+ return "not approved: this task is put away. Restore it first";
770
781
  if (reason === "unrouted")
771
782
  return "not approved: this scope predates agent routing and its old approval no longer stands — edit and re-file the scope so it is routed under today’s agents, then approve it";
772
783
  if (reason === "profile-unresolved")
@@ -1172,6 +1183,9 @@ export function executionPlanHtml(document, compact = false) {
1172
1183
  html `</div>`,
1173
1184
  ]);
1174
1185
  }
1186
+ function revisionImageLink(image) {
1187
+ return image == null ? html `` : html `<a href="/r/${image.run}/evidence/${image.artifact}">Screenshot ${image.artifact} · result #${image.run}</a> · `;
1188
+ }
1175
1189
  /**
1176
1190
  * A revision's lineage and ACTUAL terms, in the same words on the task
1177
1191
  * page, the approval card, and chat (contract handoff task 2): which
@@ -1594,7 +1608,7 @@ export function taskBodyParts(data) {
1594
1608
  : joinHtml([
1595
1609
  html `<div class="revision-card" data-revision-feedback>`,
1596
1610
  html `<p><strong>${data.revision.kind === "ci-repair" ? "CI repair" : data.revision.kind === "criterion-repair" ? "Criterion repair" : "Revision feedback"}</strong> <span class="meta">from <a href="/r/${data.revision.sourceRun}">build #${data.revision.sourceRun}</a></span></p>`,
1597
- ...data.revision.comments.map(one => html `<p class="row"><span class="meta">${one.author}</span> ${one.path === null ? "" : html `<span class="mono">${one.path}${one.line === null ? "" : `:${one.line}`}</span> `}${one.note}</p>`),
1611
+ ...data.revision.comments.map(one => html `<p class="row"><span class="meta">${one.author}</span> ${revisionImageLink(one.image)}${one.path === null ? "" : html `<span class="mono">${one.path}${one.line === null ? "" : `:${one.line}`}</span> `}${one.note}</p>`),
1598
1612
  revisionLineageHtml(data.revision.lineage),
1599
1613
  html `</div>`,
1600
1614
  ], "\n");
@@ -2058,8 +2072,11 @@ export function taskBodyParts(data) {
2058
2072
  // The title is bare; the receipt or shared task status leads once.
2059
2073
  html `<div class="task-title-row"><h1 class="task-main-title">${data.rootTitle ?? task.title}</h1>${data.csrf === "" ? "" : taskViewSwitch(data.rootId ?? task.id, "overview")}</div>`,
2060
2074
  data.versionLabel == null ? "" : html `<p class="meta">${data.versionLabel} · <a href="${taskHref(data.rootId ?? task.id)}">Current work</a></p>`,
2075
+ data.archived == null ? "" : html `<div class="task-archived" data-archived><p>${archivedWords(data.archived)} · ${whenUtc(data.archived.at)}</p>${data.canPutAway === true && data.csrf !== ""
2076
+ ? postForm(`${taskHref(task.id)}/restore`, html `<button type="submit" class="secondary">Restore</button>`, { hidden: { return: taskHref(task.id) }, attrs: { class: "inline" } }) : ""}</div>`,
2061
2077
  (data.history ?? ""),
2062
- statusHtml,
2078
+ // A put-away task's banner is its status: no Needs you card, no act, no approval form (each would be refused).
2079
+ data.archived == null ? statusHtml : "",
2063
2080
  controlHtml,
2064
2081
  previousResult === null ? "" : html `<details class="task-previous-result"><summary>${previousResult.title}</summary>${previousResult.html}</details>`,
2065
2082
  progressCard,
@@ -2068,7 +2085,7 @@ export function taskBodyParts(data) {
2068
2085
  mirrorCard,
2069
2086
  problemHtml,
2070
2087
  needsScopeCard,
2071
- approvalHtml,
2088
+ data.archived == null ? approvalHtml : "",
2072
2089
  html `<details class="task-status-details" id="task-diagnostics"${optionsOpen ? html ` open` : ""}><summary>Task options</summary>${optionsHtml}</details>`,
2073
2090
  // Evidence-first (M5.5): what needs you, then what happened — decisions
2074
2091
  // and incidents above the attempt ledger and spend, the mechanics
@@ -2170,7 +2187,7 @@ export function taskBodyParts(data) {
2170
2187
  const feedback = data.revision;
2171
2188
  thread.push({ key: "revision", at: task.createdAt, kind: "reply", who: "person", author: feedback.comments[0]?.author ?? "You",
2172
2189
  title: feedback.kind === "ci-repair" ? "Asked for a CI repair" : feedback.kind === "criterion-repair" ? "Asked for a repair" : "Asked for changes",
2173
- text: feedback.comments.map(one => `${one.path === null ? "" : `${one.path}${one.line === null ? "" : `:${one.line}`} — `}${one.note.trim()}`).filter(one => one !== "").join("\n") || null,
2190
+ text: feedback.comments.map(one => `${one.image ? `Screenshot ${one.image.artifact} · result #${one.image.run} — ` : ""}${one.path === null ? "" : `${one.path}${one.line === null ? "" : `:${one.line}`} — `}${one.note.trim()}`).filter(one => one !== "").join("\n") || null,
2174
2191
  link: { label: `From build #${feedback.sourceRun}`, href: `/r/${feedback.sourceRun}` }, html: "", more: null });
2175
2192
  }
2176
2193
  for (const decision of data.decisions) {
@@ -2199,7 +2216,8 @@ export function taskBodyParts(data) {
2199
2216
  // The Details panel: the same facts, grouped and said once.
2200
2217
  const lastRunForAgent = liveRun ?? familyRuns.filter(run => run.role !== "reviewer").sort((a, b) => b.id - a.id)[0];
2201
2218
  const work = [];
2202
- work.push({ label: "Status", parts: [data.assignment != null && assignmentOptions !== null ? assignmentCardOf(data.assignment, assignmentOptions).status.headline : status.label] });
2219
+ // A put-away task's one status is Archived (its banner says by whom and why).
2220
+ work.push({ label: "Status", parts: [data.archived != null ? "Archived" : data.assignment != null && assignmentOptions !== null ? assignmentCardOf(data.assignment, assignmentOptions).status.headline : status.label] });
2203
2221
  if (data.repo !== null)
2204
2222
  work.push({ label: "Project", parts: [projectName(data.repo)] });
2205
2223
  if (data.base != null)
@@ -2310,6 +2328,7 @@ export function taskBodyParts(data) {
2310
2328
  ...(sectionIds.has("waits-for") ? [{ group: "work", label: "Dependencies", kind: "goto", tab: "plan", target: "waits-for" }] : []),
2311
2329
  ...(sectionIds.has("holds") ? [{ group: "work", label: "Holds", kind: "goto", tab: "plan", target: "holds" }] : []),
2312
2330
  ...(data.csrf !== "" ? [{ group: "work", label: "Task options", kind: "goto", tab: "plan", target: "task-diagnostics" }] : []),
2331
+ ...(data.canPutAway === true && data.csrf !== "" && data.archived == null && !building ? [{ group: "work", label: "Put away", kind: "post", action: `${taskHref(task.id)}/archive`, fields: { return: taskHref(task.id) } }] : []),
2313
2332
  { group: "share", label: "Copy link", kind: "copy" },
2314
2333
  ...(facts.some(one => one.label === "Audit") ? [{ group: "share", label: "Evidence pack", kind: "link", href: `${taskHref(rootOf)}/evidence` }] : []),
2315
2334
  ...(data.repo !== null && data.scope !== null ? [{ group: "share", label: "Reuse as recipe", kind: "link", href: `/recipes/from-task?task=${encodeURIComponent(task.id)}` }] : []),
@@ -2326,10 +2345,12 @@ export function taskBodyParts(data) {
2326
2345
  { label: "Ask", href: taskChatHref(data.rootId ?? task.id), active: false },
2327
2346
  ],
2328
2347
  version: data.versionLabel == null ? null : { label: data.versionLabel, current: { label: "Current work", href: taskHref(data.rootId ?? task.id) } },
2329
- status: statusCard === null ? null : inPlaceChecks(failedCard ? { ...statusCard, action: null, status: { ...statusCard.status, sentence: data.failure?.line ?? statusCard.status.sentence } }
2348
+ archived: data.archived == null ? null : { words: archivedWords(data.archived), at: data.archived.at,
2349
+ restore: data.canPutAway === true && data.csrf !== "" ? { action: `${taskHref(task.id)}/restore`, returnTo: taskHref(task.id) } : null },
2350
+ status: statusCard === null || data.archived != null ? null : inPlaceChecks(failedCard ? { ...statusCard, action: null, status: { ...statusCard.status, sentence: data.failure?.line ?? statusCard.status.sentence } }
2330
2351
  : progress !== null || record !== null ? { ...statusCard, action: statusCard.action?.href === record?.href ? null : statusCard.action, status: { ...statusCard.status, sentence: progress?.line ?? statusCard.status.sentence } }
2331
2352
  : statusCard.action !== null && rebuild === null && bareSelfLink(statusCard.action.href) ? { ...statusCard, action: null } : statusCard),
2332
- statusHtml: htmlString(statusHtml),
2353
+ statusHtml: data.archived != null ? "" : htmlString(statusHtml),
2333
2354
  failure: failedCard && data.failure != null ? { line: data.failure.line, evidence: data.failure.evidence, suggestion: data.failure.suggestion, link: data.failure.link } : null,
2334
2355
  retry,
2335
2356
  runChecks,
@@ -2342,7 +2363,7 @@ export function taskBodyParts(data) {
2342
2363
  ...(data.csrf !== "" && (data.steerRecent?.length ?? 0) > 0 ? { steerRecent: data.steerRecent } : {}),
2343
2364
  record,
2344
2365
  earlier,
2345
- approval: fragmentString(approvalHtml),
2366
+ approval: data.archived != null ? "" : fragmentString(approvalHtml),
2346
2367
  approvalPlacement: approvalLeads ? "lead" : "plan",
2347
2368
  more,
2348
2369
  confirmStopped: data.csrf !== "" && data.assignment?.primaryAction?.code === "confirm-stopped" && data.assignment.primaryAction.target.runId !== null