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
@@ -127,6 +127,9 @@ get("pages", "projects.page", "/projects", { limited: "unscoped", ...free });
127
127
  get("pages", "home", "/", { limited: "collection", ...free });
128
128
  get("tasks", "inbox", "/inbox", { limited: "collection", ...free });
129
129
  get("tasks", "work", "/work", { limited: "collection", ...free });
130
+ // Put away old tasks (task-tidy.md): the preview reads only; putting away is an approver's step-up in a browser session.
131
+ get("tasks", "work.put-away", "/work/put-away", { limited: "collection", ...free, callers: ["cookie"], ...approver("Only an approver can put tasks away.", "/work") });
132
+ post("tasks", "work.put-away.apply", "/work/put-away", { scope: "step-up", limited: "collection", ...approver("Only an approver can put tasks away.", "/work") });
130
133
  get("tasks", "next", "/next", { limited: "collection" });
131
134
  get("pages", "morning", "/morning");
132
135
  get("pages", "workbench", "/workbench", free);
@@ -270,6 +273,11 @@ for (const one of ["linear-key", "hooks-address", "secrets"])
270
273
  // "Runs on its own": signing raises authority (the owner's password, a step-up); revoking is one click for any approver.
271
274
  post("flows", "flow.unattended", `^\\/flows\\/${FLOW}\\/unattended$`, { scope: "step-up", project: "flow", sample: "/flows/1/unattended", ...EDIT_FLOWS });
272
275
  post("flows", "flow.unattended-off", `^\\/flows\\/${FLOW}\\/unattended-off$`, { project: "flow", sample: "/flows/1/unattended-off", ...EDIT_FLOWS });
276
+ // Browser use in a flow's script zones (flow-browser.ts): allowing it widens the sandbox, so a person's own step-up from
277
+ // their browser, carrying the approval digest they read; turning it off lowers authority, one click. Browser only: a
278
+ // lead's or remote run's token never reaches either (the CLI says the same).
279
+ post("flows", "flow.browser", `^\\/flows\\/${FLOW}\\/browser$`, { scope: "step-up", project: "flow", sample: "/flows/1/browser", ...EDIT_FLOWS });
280
+ post("flows", "flow.browser-off", `^\\/flows\\/${FLOW}\\/browser-off$`, { callers: ["cookie"], project: "flow", limited: "resource", sample: "/flows/1/browser-off", ...EDIT_FLOWS });
273
281
  post("flows", "flow.triggers", `^\\/flows\\/${FLOW}\\/triggers$`, { project: "flow", limited: "resource", sample: "/flows/1/triggers", ...EDIT_FLOWS });
274
282
  for (const one of ["move", "decide", "cancel", "comment", "assign", "watch"])
275
283
  post("flows", `flow.card.${one}`, `^\\/flows\\/${FLOW}\\/cards\\/${FLOW}\\/${one}$`, { project: "flow", limited: "resource", sample: `/flows/1/cards/2/${one}`, ...EDIT_FLOWS });
@@ -336,6 +344,7 @@ for (const one of ["policy", "approval", "request-limits", "sessions", "sign-in"
336
344
  post(one === "sessions" ? "people" : "settings", `settings.${one.replaceAll("/", "-")}-send`, `/settings/${one}`, { ...(["policy", "approval", "request-limits", "sessions", "sign-in", "updates", "updates/seen", "updates/cancel", "retention", "storage", "storage/clean", "storage/discard", "pull-requests", "checks", "backups", "data", "monitoring", "tools/connect", "tools/change"].includes(one) ? { scope: "step-up" } : {}), ...SETTINGS_ACT_ROLES[one] });
337
345
  }
338
346
  post("settings", "settings.flows-on", "/settings/flows/on", { limited: "collection", ...approver("An approver switches starter flows on.", "/settings/flows") });
347
+ post("settings", "settings.integrations-youiyoux", "/settings/integrations/youiyoux", { limited: "collection", ...approver("An approver turns youiyoux requests on or off.", "/settings/integrations") });
339
348
  const MODELS = approver("Sign in as an approver to change models.", "/settings/models");
340
349
  for (const one of ["check", "watch", "update"])
341
350
  post("settings", `settings.models-${one}`, `/settings/models/${one}`, MODELS);
@@ -407,6 +416,9 @@ post("tasks", "task.act.merge", `^\\/t\\/${TASK}\\/merge$`, { project: "task", l
407
416
  post("tasks", "task.act.confirm-stopped", `^\\/t\\/${TASK}\\/confirm-stopped$`, { scope: "step-up", project: "task", limited: "resource", sample: "/t/one/confirm-stopped", ...approver("Only an approver can confirm a build stopped.") });
408
417
  post("tasks", "task.act.stop-carrying", `^\\/t\\/${TASK}\\/stop-carrying$`, { project: "task", limited: "resource", sample: "/t/one/stop-carrying", ...approver("Only an approver can stop carrying a release.") });
409
418
  post("tasks", "task.act.stop", `^\\/t\\/${TASK}\\/stop$`, { project: "task", limited: "resource", sample: "/t/one/stop" });
419
+ // Putting one task away and bringing it back (task-tidy.md): a person's act, so only a browser session.
420
+ post("tasks", "task.act.archive", `^\\/t\\/${TASK}\\/archive$`, { project: "task", limited: "resource", sample: "/t/one/archive", callers: ["cookie"], ...approver("Only an approver can put a task away.") });
421
+ post("tasks", "task.act.restore", `^\\/t\\/${TASK}\\/restore$`, { project: "task", limited: "resource", sample: "/t/one/restore", callers: ["cookie"], ...approver("Only an approver can restore a task.") });
410
422
  post("tasks", "task.act.pause", `^\\/t\\/${TASK}\\/pause$`, { project: "task", limited: "resource", sample: "/t/one/pause" });
411
423
  post("tasks", "task.act.resume-arm", `^\\/t\\/${TASK}\\/resume-arm$`, { project: "task", limited: "resource", sample: "/t/one/resume-arm" });
412
424
  post("tasks", "task.act.resume", `^\\/t\\/${TASK}\\/resume$`, { scope: "step-up", project: "task", limited: "resource", sample: "/t/one/resume" });
@@ -17,7 +17,7 @@ import { grantTool } from "../subagent-tools.js";
17
17
  import { learningHtml } from "../workspace-ui.js";
18
18
  import { adapterPolicy } from "./route-policy.js";
19
19
  import { createHash } from "node:crypto";
20
- import { dirname, join } from "node:path";
20
+ import { basename, dirname, join } from "node:path";
21
21
  import { INSTALLATION_SCOPE } from "../agentconfig.js";
22
22
  import { rulesSummary } from "../approval-rules-ui.js";
23
23
  import { installMethod } from "../install-method.js";
@@ -64,6 +64,7 @@ import { startersHtml } from "../flow-starters-ui.js";
64
64
  import { starterOf, startersFor, switchOnStarter } from "../flow-starters.js";
65
65
  import { disconnectGoogle, googleConnected, googleConsent, readGoogleMail, saveGoogleClient } from "../google-mail.js";
66
66
  import { integrationsHtml } from "../integrations-ui.js";
67
+ import { setYouiyouxIntake } from "../youiyoux-intake.js";
67
68
  import { clearProviderKey, keyStatus, plausibleKey, PROVIDER_KEY_ENV, readAuthMode, readProviderKey, saveProviderKey, setAuthMode, SUBSCRIPTION_CAPABLE, verdictWords, verifyProviderKey } from "../keys.js";
68
69
  import { decisionsHtml, framedSection, knowledgeHtml, MEMORY_INTRO, memorySearchHtml, proposalsHtml } from "../knowledge-ui.js";
69
70
  import { aboutYouOf, checkAboutYou, saveAboutYou } from "../lead-about.js";
@@ -1386,6 +1387,21 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
1386
1387
  const switched = switchOnStarter(store, starter, repo, who.name, clock(), options.configDir ?? null);
1387
1388
  return switched.ok ? back("said", switched.said) : back("problem", switched.said);
1388
1389
  }
1390
+ // Settings → Integrations → youiyoux → Turn off / Turn on: whether a project's change requests become tasks.
1391
+ async function integrationsYouiyoux(ctx) {
1392
+ const { who, response, posted } = ctx;
1393
+ const body = readForm(posted, CONSOLE_FORMS.integrationsYouiyoux);
1394
+ const repo = body.get("repo") ?? "";
1395
+ const back = sayTo(response, "/settings/integrations");
1396
+ const known = [...new Set([...(admissionList() ?? []), ...managedRepos(), ...store.knownRepos()])];
1397
+ if (store.isDemo())
1398
+ return refuse(response, who, 403, "An approver turns youiyoux requests on or off.", "/settings/integrations");
1399
+ if (!known.includes(repo) || !visible(repo))
1400
+ return refuse(response, who, 404, "No such project.", "/settings/integrations");
1401
+ const on = body.get("on") === "1";
1402
+ setYouiyouxIntake(store, repo, on, who.name, clock());
1403
+ return back("said", on ? `youiyoux requests in ${basename(repo)} become tasks again.` : `youiyoux requests in ${basename(repo)} are off.`);
1404
+ }
1389
1405
  // Settings → Integrations → Send test: one harmless read-only check of one integration, now.
1390
1406
  async function integrationsTest(ctx) {
1391
1407
  const { who, response, posted } = ctx;
@@ -2642,6 +2658,7 @@ ${modeWords(terms)
2642
2658
  "settings.backups-send": backupsSave,
2643
2659
  "settings.data-send": dataExport,
2644
2660
  "settings.integrations-test-send": integrationsTest,
2661
+ "settings.integrations-youiyoux": integrationsYouiyoux,
2645
2662
  "settings.monitoring-send": monitoringSave,
2646
2663
  "settings.tools-connect-send": toolsConnect,
2647
2664
  "settings.tools-change-send": toolsChange,
@@ -1,3 +1,5 @@
1
+ import { feedbackImageProblem } from "../result-actions.js";
2
+ import { parseFeedbackImage } from "../result-review.js";
1
3
  import { releaseViewOf } from "../release-view.js";
2
4
  import { releaseCiRunningOf } from "../release-ci-hold.js";
3
5
  import { releaseObligationsOf } from "../release-recheck.js";
@@ -19,6 +21,7 @@ import { newestPullRequestOf, publishingOf, pullRequestBlocker } from '../pull-r
19
21
  import { structuredHandoffView, terminalDiffView } from "../result-evidence-readers.js";
20
22
  import { resultCheckLevel } from '../result-follow-ups.js';
21
23
  import { skillsSnapshotHtml, skillTestFeedbackHtml } from "../skills-ui.js";
24
+ import { archiveOf } from "../store.js";
22
25
  import { runActivityOf } from "../task-activity.js";
23
26
  import { taskFingerprint } from "../task-live.js";
24
27
  import { pullRequestFactOf } from '../task-status.js';
@@ -229,6 +232,7 @@ export function createTaskViews(runtime) {
229
232
  line: one.line === null || one.line === undefined ? null : Number(one.line),
230
233
  note: String(one.note ?? ""),
231
234
  author: String(one.author ?? "?"),
235
+ image: parseFeedbackImage(one.image) || null,
232
236
  })),
233
237
  };
234
238
  }
@@ -789,6 +793,10 @@ export function createTaskViews(runtime) {
789
793
  const presentedData = { ...data, rootId: family?.root.id ?? taskId, rootTitle: family?.root.title ?? data.task.title,
790
794
  ...(family === null || data.assignment != null ? {} : { history: familyHistory(family) }),
791
795
  versionLabel: family !== null && family.current.id !== taskId ? `Viewing ${family.versions.findIndex(one => one.id === taskId) === 0 ? "Original" : `Revision ${family.versions.findIndex(one => one.id === taskId)}`} · ${family.current.state === "running" ? "A newer revision is running" : "A newer revision is current"}` : null };
796
+ // Put away (v145, task-tidy.md): who or what put it away and why, and whether this person can put it away or restore it.
797
+ const archived = archiveOf(runtime.store, taskId);
798
+ presentedData.archived = archived;
799
+ presentedData.canPutAway = who.via === "cookie" && who.role === "approver";
792
800
  if (scopeDraft !== undefined)
793
801
  presentedData.scopeDraft = scopeDraft;
794
802
  if (cancelDraft !== undefined)
@@ -1119,6 +1127,9 @@ export function createTaskViews(runtime) {
1119
1127
  outsideTouches: files.filter(one => one.outsideTouches).map(one => one.path),
1120
1128
  fileAnchors,
1121
1129
  comments: runtime.store.liveDiffComments(run.id).filter(isRevisionFeedback),
1130
+ // The same check that refuses a saved note's screenshot at revision time decides when it can be removed.
1131
+ unusableImageNotes: runtime.store.liveDiffComments(run.id).filter(one => isRevisionFeedback(one) && one.image != null
1132
+ && feedbackImageProblem(runtime.store, runtime.evidenceRoot, run.id, one.image) !== null).map(one => one.id),
1122
1133
  pastComments: runtime.store.allDiffComments(run.id).filter(one => one.reviewerRun === null && (one.consumedBy !== null || one.supersededBy !== null)),
1123
1134
  reviewerFindings: runtime.store.allDiffComments(run.id).filter(one => one.reviewerRun !== null),
1124
1135
  automaticReview: buildReviewOf(runtime.store, run.id),
@@ -2,6 +2,7 @@
2
2
  import { type Html } from "../html.js";
3
3
  import { type ServerResponse } from "node:http";
4
4
  import { type Artifact, type Run, type Store } from "../store.js";
5
+ import type { HandlerContext } from './handler-context.js';
5
6
  import type { ServerRuntime } from './runtime.js';
6
7
  import { type PeekAdmission } from "./render-pages.js";
7
8
  import { type CompletedWorkRow, type RankedReviewRow, type ReviewCockpitView } from "./render-results.js";
@@ -36,3 +37,12 @@ export declare function createTasksHandlers(runtime: ServerRuntime): {
36
37
  };
37
38
  liveRunIds: (rows: readonly (Pick<Run, "id" | "outcome" | "leaseId" | "taskRef">)[]) => Set<number>;
38
39
  };
40
+ /** Put away and Restore one task, and Put away old tasks (docs/plans/task-tidy.md): the preview reads only (GET-quiet);
41
+ * putting away many is an approver's step-up bound to the digest of exactly what the preview showed. Each write
42
+ * re-proves the person's standing inside its own transaction. */
43
+ export declare function createTidyHandlers(runtime: ServerRuntime): {
44
+ "work.put-away": (ctx: HandlerContext) => Promise<void>;
45
+ "work.put-away.apply": (ctx: HandlerContext) => Promise<void>;
46
+ "task.act.archive": (ctx: HandlerContext) => Promise<void>;
47
+ "task.act.restore": (ctx: HandlerContext) => Promise<void>;
48
+ };
@@ -1,4 +1,6 @@
1
- import { releaseAuthorityOfTask, revokeRelease, runLiveCommands } from "../store.js";
1
+ import { parseFeedbackImage, sameFeedbackImage } from "../result-review.js";
2
+ import { archiveTaskFamily, dropFeedbackImage, releaseAuthorityOfTask, restoreTaskFamily, revokeRelease, runLiveCommands } from "../store.js";
3
+ import { applyTidy, approverStandsNow, familyRepoOf, releaseFactsOf, tidyDays, tidyDigest, tidyPreview, TIDY_DEFAULT_DAYS } from "../tidy.js";
2
4
  import { steerCapabilityOf } from "../steer-capabilities.js";
3
5
  import { canReveal, localCheckoutOf, revealInFinder } from "../checkout-proof.js";
4
6
  /** A form's way back to a flow card ("flow:<flow>:<card>"), read strictly: numbers only, never a path. */
@@ -48,7 +50,7 @@ import { fileTaskProposal } from "../proposal.js";
48
50
  import { isProviderId, PROVIDER_IDS, validateSpec } from "../provider.js";
49
51
  import { completeAndOpenPullRequest, mergeAsPerson } from '../pull-request-flow.js';
50
52
  import { isQualityMode } from "../quality.js";
51
- import { createResultRevision, requestResultChanges } from "../result-actions.js";
53
+ import { createResultRevision, requestResultChanges, feedbackImageProblem } from "../result-actions.js";
52
54
  import { structuredHandoffView, terminalDiffView } from "../result-evidence-readers.js";
53
55
  import { fileAddTestsTask, requestFollowUpChecks } from '../result-follow-ups.js';
54
56
  import { commentSourceKey, isRevisionFeedback, parseResultTab, resultReturnTarget } from "../result-review.js";
@@ -66,7 +68,7 @@ import { buildProgressOf, parseWorkView } from "../workspace-ui.js";
66
68
  import { requestContext } from "./request-context.js";
67
69
  import { boardTickScript, oneLineOf, QUEUE_VIEW, refuse, regionScript, screen, transcriptScript, whenTime } from "./chrome.js";
68
70
  import { chatReturnWithSaid, matchTaskPath, redirect, respond, reviewHref, safeChatReturn, safeReturn, SAFETY, taskChatHref, taskHref } from "./http.js";
69
- import { boardBody, boardWorkers, donePage, inboxFingerprints, inboxPage, newTaskPage, nextPage, parseInboxTab, QUEUE_FRONT, queueBody, queueScript, TASK_STATES, workPage } from "./render-pages.js";
71
+ import { putAwayPage, boardBody, boardWorkers, donePage, inboxFingerprints, inboxPage, newTaskPage, nextPage, parseInboxTab, QUEUE_FRONT, queueBody, queueScript, TASK_STATES, workPage } from "./render-pages.js";
70
72
  import { runEfficiencyOf } from "../run-efficiency.js";
71
73
  import { timingFor } from "../latency-timeline.js";
72
74
  import { ACCEPT_ANYWAY_NEEDS_REASON, editorFileHref, proofBundleView, rankReviewQueue, RESULT_REFUSALS, REVIEW_QUEUE_CAP, reviewCockpitDetailParts, reviewCockpitPage, reviewPriorityOf, runFactsFragment, runOutcomeBadge, runPage, RUNS_PAGE, runsPage, withRefusal } from "./render-results.js";
@@ -277,6 +279,7 @@ export function createTasksHandlers(runtime) {
277
279
  multiProject: new Set(work.items.map(task => task.repo ?? "")).size > 1,
278
280
  now,
279
281
  state,
282
+ canPutAway: who.via === "cookie" && who.role === "approver",
280
283
  // v105: subscription windows and monthly budgets, for whoever runs the installation.
281
284
  limits: store.isInstanceOperator(who.name)
282
285
  ? limitsView(store.providerLimits(), store.budgets().length === 0 ? [] : store.monthSpendCached(now).budgets, { project: projectName, subagent: id => subagentNamesOf(store.handle).get(id) ?? `Subagent ${id}` }, now)
@@ -1402,15 +1405,40 @@ export function createTasksHandlers(runtime) {
1402
1405
  const viewParam = returnTo.startsWith("/t/") ? "section" : "tab";
1403
1406
  const back = selectedTab === "summary" ? returnTo
1404
1407
  : `${returnTo.replace(/&section=(?:summary|changes|checks)$/, "")}${returnTo.includes("?") ? "&" : "?"}${viewParam}=${selectedTab}`;
1408
+ if (body.get("intent") === "drop-image") {
1409
+ // A saved note whose screenshot is gone or changed would block every revision that seals it. Its words stay:
1410
+ // the note is superseded by an exact imageless copy, and the original remains in history.
1411
+ // Who: a signed-in browser approver admitted to this run's project (checked above) — the same authority that
1412
+ // seals any saved note into a revision. What: only a screenshot that is unusable right now, re-proved inside
1413
+ // dropFeedbackImage's transaction, and recorded in the ledger under this person.
1414
+ if (who.via !== "cookie" || who.role !== "approver")
1415
+ return refuse(response, who, 403, "Only a signed-in approver can change saved feedback.", back);
1416
+ const comment = Number(body.get("comment") ?? "");
1417
+ if (!Number.isSafeInteger(comment) || comment < 1)
1418
+ return refuse(response, who, 400, "Choose a saved note.", back);
1419
+ const dropped = dropFeedbackImage(store, id, comment, who.name, one => feedbackImageProblem(store, evidenceRoot, id, one), now);
1420
+ if (!dropped.ok && dropped.reason === "not-authorized")
1421
+ return refuse(response, who, 403, "Only a signed-in approver on this project can change saved feedback.", back);
1422
+ if (!dropped.ok)
1423
+ return refuse(response, who, 409, dropped.reason === "image-usable"
1424
+ ? "That screenshot still verifies, so the note keeps it. Reload the result." : "That saved note changed or was already used. Reload the result.", back);
1425
+ return redirect(response, `${back}#request-changes`);
1426
+ }
1427
+ const image = parseFeedbackImage(body.get("image"));
1428
+ if (image === false)
1429
+ return refuse(response, who, 400, "Choose a screenshot from this result again. Your draft is unchanged.", back);
1405
1430
  if (body.get("intent") === "revise") {
1406
1431
  const result = requestResultChanges(store, evidenceRoot, {
1407
1432
  run: id, batch: body.get("batch"), source: body.get("source"), actor: who.name,
1408
1433
  repos: admissionList(), includeUnplaced: visible(null), allowMode: who.via === "cookie",
1409
- note: body.get("note") ?? "", path: body.get("path") ?? "", line: body.get("line") ?? "", request: body.get("request"),
1434
+ note: body.get("note") ?? "", path: body.get("path") ?? "", line: body.get("line") ?? "", request: body.get("request"), image,
1410
1435
  }, now);
1411
1436
  if (!result.ok) {
1412
1437
  const request = body.get("request") ?? "";
1413
- const retryBack = /^[a-f0-9]{32}$/.test(request) ? `${back}${back.includes("?") ? "&" : "?"}conflict=${request}#request-changes` : back;
1438
+ // The queue's link can stop resolving when a newer attempt finishes. The exact
1439
+ // run record still hosts this account/run's recoverable feedback draft.
1440
+ const retryBase = image === null ? back : `/r/${id}?record=1`;
1441
+ const retryBack = /^[a-f0-9]{32}$/.test(request) ? `${retryBase}${retryBase.includes("?") ? "&" : "?"}conflict=${request}#request-changes` : retryBase;
1414
1442
  return refuse(response, who, result.status, result.message, retryBack);
1415
1443
  }
1416
1444
  return redirect(response, revisionDestination(result.id, returnTo));
@@ -1445,7 +1473,17 @@ export function createTasksHandlers(runtime) {
1445
1473
  const request = body.get("request");
1446
1474
  const sourceKey = commentSourceKey(who.name, request);
1447
1475
  const path = rawPath === "" ? null : rawPath;
1448
- const inserted = store.addDiffComment({ artifactId: terminal.id, runId: id, path, line, note: note.note, author: who.name, ...(sourceKey === undefined ? {} : { sourceKey }) }, now);
1476
+ // Freshness, custody and the insert in one write transaction: a newer attempt finishing in between can't leave a
1477
+ // stale screenshot attached (check and write together).
1478
+ const saved = store.transact(() => {
1479
+ const imageProblem = feedbackImageProblem(store, evidenceRoot, id, image);
1480
+ if (imageProblem !== null)
1481
+ return { problem: imageProblem };
1482
+ return { inserted: store.addDiffComment({ artifactId: terminal.id, runId: id, path, line, note: note.note, author: who.name, image, ...(sourceKey === undefined ? {} : { sourceKey }) }, now) };
1483
+ });
1484
+ if ("problem" in saved)
1485
+ return refuse(response, who, 409, saved.problem, `/r/${id}?record=1#request-changes`);
1486
+ const inserted = saved.inserted;
1449
1487
  if (inserted === null && sourceKey !== undefined) {
1450
1488
  // The token was used before (repair 2026-09-14, finding 1). A
1451
1489
  // request identity is IMMUTABLE: it names exactly one note on
@@ -1459,7 +1497,7 @@ export function createTasksHandlers(runtime) {
1459
1497
  if (earlier === null || earlier.run !== id) {
1460
1498
  return refuse(response, who, 409, `this note's request identity was already used on another result (build #${earlier === null ? "?" : earlier.run}) — go back; your words are kept, and the form will carry a new identity`, conflictBack);
1461
1499
  }
1462
- if (earlier.path !== path || earlier.line !== line || earlier.note !== note.note) {
1500
+ if (earlier.path !== path || earlier.line !== line || earlier.note !== note.note || !sameFeedbackImage(earlier.image, image)) {
1463
1501
  return refuse(response, who, 409, `this request identity already recorded a different note on this result (${earlier.path === null ? "no file" : `${earlier.path}${earlier.line === null ? "" : `:${earlier.line}`}`}: "${oneLineOf(earlier.note, 80)}") — go back; your edited words are kept as a new note, and the form will carry a new identity`, conflictBack);
1464
1502
  }
1465
1503
  }
@@ -3021,7 +3059,9 @@ export function createTasksHandlers(runtime) {
3021
3059
  live.add(run.id);
3022
3060
  return live;
3023
3061
  }
3062
+ const tidyHandlers = createTidyHandlers(runtime);
3024
3063
  const registrations = handlersOf("tasks", {
3064
+ ...tidyHandlers,
3025
3065
  "code.page": showCode("workspace"),
3026
3066
  "code.session": showCode("session"),
3027
3067
  "code.state": showCode("state"),
@@ -3118,3 +3158,51 @@ export function createTasksHandlers(runtime) {
3118
3158
  }
3119
3159
  return { registrations, runListPane, peekFragment, taskMutation, queueRegionFor, ciFailingFor, completedRowFor, attemptRowFor, resultRowOf, reviewCockpitViewOf, decisionEvidence, runEvidence, sendArtifact, peekGuards, liveRunIds };
3120
3160
  }
3161
+ /** Put away and Restore one task, and Put away old tasks (docs/plans/task-tidy.md): the preview reads only (GET-quiet);
3162
+ * putting away many is an approver's step-up bound to the digest of exactly what the preview showed. Each write
3163
+ * re-proves the person's standing inside its own transaction. */
3164
+ export function createTidyHandlers(runtime) {
3165
+ const { store, sendScreen, chromeFor, authenticateApprover, workAccess, bustBadge } = runtime;
3166
+ async function showPutAway(ctx) {
3167
+ const { url, who, response, now, project } = ctx;
3168
+ const days = tidyDays(url.searchParams.get("days"));
3169
+ if (days === null)
3170
+ return refuse(response, who, 400, "Choose a number of days from 1 to 365.", "/work/put-away");
3171
+ const releases = releaseFactsOf(store);
3172
+ const items = tidyPreview(store, now, { releases, days, access: workAccess() });
3173
+ return sendScreen(response, 200, putAwayPage(chromeFor(project, "work"), { items, days, digest: tidyDigest(items, days), now,
3174
+ problem: url.searchParams.get("problem"), releasesSkipped: releases.problem }));
3175
+ }
3176
+ async function applyPutAway(ctx) {
3177
+ const { who, response, now, posted } = ctx;
3178
+ const body = readForm(posted, CONSOLE_FORMS.tasksPutAway);
3179
+ const days = tidyDays(body.get("days"));
3180
+ const back = (problem) => redirect(response, `/work/put-away?${new URLSearchParams({ ...(days === null || days === TIDY_DEFAULT_DAYS ? {} : { days: String(days) }), problem })}`);
3181
+ if (days === null)
3182
+ return back("Choose a number of days from 1 to 365.");
3183
+ if (!authenticateApprover(who, body.get("password") ?? "").ok)
3184
+ return back("Enter your Toolroll password to put these away.");
3185
+ const done = applyTidy(store, now, { by: who.name, days, digest: body.get("digest") ?? "", only: body.getAll("task"), releases: releaseFactsOf(store), access: workAccess() });
3186
+ if (!done.ok)
3187
+ return back(done.message);
3188
+ bustBadge();
3189
+ return redirect(response, "/work?view=archived");
3190
+ }
3191
+ const oneTask = (kind) => async (ctx) => {
3192
+ const { url, who, response, now, posted } = ctx;
3193
+ const act = matchTaskPath(url.pathname, `/${kind}$`);
3194
+ if (act === null)
3195
+ return refuse(response, who, 404, "There's no page at this address.", "/work");
3196
+ const body = readForm(posted, kind === "archive" ? CONSOLE_FORMS.taskArchive : CONSOLE_FORMS.taskRestore);
3197
+ const standing = (refs) => approverStandsNow(store, who.name, familyRepoOf(store, refs));
3198
+ const done = kind === "archive"
3199
+ ? archiveTaskFamily(store, act.taskId, { by: who.name, byKind: "person", rule: null, reason: (body.get("reason") ?? "").trim() || "Put away by hand" }, now, { guard: standing })
3200
+ : restoreTaskFamily(store, act.taskId, who.name, now, standing);
3201
+ if (!done.ok)
3202
+ return refuse(response, who, done.reason === "unknown-task" ? 404 : 409, done.message, taskHref(act.taskId));
3203
+ bustBadge();
3204
+ const to = safeReturn(body.get("return"));
3205
+ return redirect(response, to === "/" ? taskHref(act.taskId) : to);
3206
+ };
3207
+ return { "work.put-away": showPutAway, "work.put-away.apply": applyPutAway, "task.act.archive": oneTask("archive"), "task.act.restore": oneTask("restore") };
3208
+ }
@@ -56,6 +56,7 @@ export const PROOF_INVENTORY = [
56
56
  { id: "updates", action: "Update Toolroll", routes: ["/settings/updates", "/settings/updates/seen", "/settings/updates/cancel"], form: "updates", field: "password", proof: "step-up", ...ACCEPT, role: "operator", secrets: [], terms: [] },
57
57
  { id: "storage-cleanup", action: "Choose when checkouts are removed", routes: ["/settings/storage"], form: "storage", field: "password", proof: "step-up", ...ACCEPT, role: "operator", secrets: [], terms: [] },
58
58
  { id: "storage-clean", action: "Clean up checkouts", routes: ["/settings/storage/clean"], form: "storage", field: "password", proof: "step-up", ...ACCEPT, role: "operator", secrets: [], terms: ["preview"], note: "`preview` is the digest of what the page showed; a changed list is refused" },
59
+ { id: "tasks-put-away", action: "Put away old tasks", routes: ["/work/put-away"], form: "tasksPutAway", field: "password", proof: "step-up", ...ACCEPT, role: "approver", secrets: [], terms: ["digest", "days", "task"], note: "`digest` is the preview the page showed; a changed list, or a ticked task outside it, is refused" },
59
60
  { id: "storage-discard", action: "Discard a checkout", routes: ["/settings/storage/discard"], form: "storage", field: "password", proof: "step-up", ...ACCEPT, role: "operator", secrets: [], terms: ["path"] },
60
61
  { id: "project-live-view", action: "Show a project's commands in the live view", routes: ["/settings/project/live-view"], form: "projectLiveView", field: "password", proof: "step-up", ...ACCEPT, role: "any", handlerRole: "an approver of the project", secrets: [], terms: ["repo", "nonce"] },
61
62
  { id: "project-delete", action: "Delete a project", routes: ["/settings/project/delete"], form: "projectDelete", field: "password", proof: "step-up", ...ACCEPT, role: "operator", secrets: [], terms: ["repo", "step"], note: "the project's name typed too" },
package/dist/store.d.ts CHANGED
@@ -1,9 +1,11 @@
1
+ import { type FeedbackImage } from "./result-review.js";
1
2
  import { type WritesMode } from "./writes-mode.js";
2
3
  import { type CheckAccess } from "./check-access.js";
3
4
  import { type ProjectSandbox } from "./project-sandbox.js";
4
5
  import type { FencePolicy } from "./agent-fence.js";
5
6
  import { type NetworkProfile, type ProjectNetwork, type RunNetworkRecord } from "./egress.js";
6
7
  import { type AcpLaunch } from "./acp-attest.js";
8
+ export { refArchived, taskArchived } from "./task-archive.js";
7
9
  import { type ChatBinding } from "./chat-core.js";
8
10
  import { ChatMessages, TELEGRAM_HOLD_REASONS, TELEGRAM_SKIPPED_ELSEWHERE, TELEGRAM_SKIPPED_OTHER_CHAT, TELEGRAM_SKIPPED_QUIET } from "./chat-messages.js";
9
11
  import { isLifecycleNotification, isTelegramProgressNotification, LIFECYCLE_KEY_PREFIX, LIFECYCLE_KINDS, proposalTaskOf, type LifecycleKind } from "./notification-rows.js";
@@ -139,6 +141,18 @@ export declare const V141_ADDED_COLUMNS: Readonly<Record<string, Readonly<Record
139
141
  definition: string;
140
142
  saved: string;
141
143
  }>>>>;
144
+ /** v142 (youiyoux-intake.ts): youiyoux change requests as tasks. `youiyoux_intake` is one row per project the intake
145
+ * has looked at: a person's on/off choice (`enabled` NULL: automatic, on while the project uses youiyoux) and the last
146
+ * pass's answer (whether the project uses youiyoux, how many request files couldn't be read, and the problem, if any).
147
+ * `youiyoux_request` is one row per request taken, keyed by project and request id: the task it became (NULL: left
148
+ * out, `note` says why), never filed again, the status last written beside it, with the console address its links
149
+ * named ('' for none), and where the person pointed (route, component, file:line), cleaned, for the task page. An
150
+ * options request (`kind`) also keeps the options file its agent writes (`proposal`, p-<its hex>), set when filed and
151
+ * never read from the request again: whether it went to the project's main checkout from which result (`proposal_state`,
152
+ * `proposal_run`, and why not in `proposal_note`; `taken`: a file was already there, so it was left and is never read
153
+ * for a decision), the digest of what was copied apart from what a decision changes (`proposal_digest`), and what a
154
+ * person decided on it in youiyoux's hub (`decided`, `decided_note`: what Toolroll did about it). New tables only. */
155
+ export declare const V142_SCHEMA = "\nCREATE TABLE IF NOT EXISTS youiyoux_intake (\n repo TEXT PRIMARY KEY,\n enabled INTEGER CHECK (enabled IS NULL OR enabled IN (0, 1)),\n uses INTEGER NOT NULL DEFAULT 0 CHECK (uses IN (0, 1)),\n changed_by TEXT,\n changed_at TEXT,\n checked_at TEXT,\n invalid INTEGER NOT NULL DEFAULT 0 CHECK (invalid >= 0),\n problem TEXT\n);\nCREATE TABLE IF NOT EXISTS youiyoux_request (\n repo TEXT NOT NULL,\n request TEXT NOT NULL CHECK (length(request) = 12 AND request GLOB 'r-[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]'),\n task_id TEXT,\n note TEXT,\n filed_at TEXT NOT NULL,\n status TEXT CHECK (status IS NULL OR status IN ('queued','needs-you','building','ready','done','failed','stopped')),\n detail TEXT,\n status_at TEXT,\n written_at TEXT,\n written_base TEXT,\n write_problem TEXT,\n route TEXT,\n component TEXT,\n source_at TEXT,\n kind TEXT NOT NULL DEFAULT 'change' CHECK (kind IN ('change','options')),\n proposal TEXT CHECK (proposal IS NULL OR (length(proposal) = 12 AND proposal GLOB 'p-[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]')),\n proposal_state TEXT CHECK (proposal_state IS NULL OR proposal_state IN ('copied','taken','missing','unsafe','retry')),\n proposal_digest TEXT CHECK (proposal_digest IS NULL OR (length(proposal_digest) = 64 AND proposal_digest NOT GLOB '*[^0-9a-f]*')),\n proposal_run INTEGER,\n proposal_note TEXT,\n proposal_at TEXT,\n decided TEXT CHECK (decided IS NULL OR decided IN ('chosen','dismissed')),\n decided_at TEXT,\n decided_note TEXT,\n PRIMARY KEY (repo, request),\n UNIQUE (task_id),\n CHECK ((kind = 'options') = (proposal IS NOT NULL)),\n CHECK (proposal_state IS NULL OR kind = 'options'),\n CHECK (decided IS NULL OR coalesce(proposal_state = 'copied' AND proposal_digest IS NOT NULL, 0))\n);\n";
142
156
  /** A project's "Show commands in the live view" (live-detail.ts): absent means off. */
143
157
  export declare const LIVE_DETAIL_SCHEMA = "\nCREATE TABLE IF NOT EXISTS live_detail_setting (\n repo TEXT PRIMARY KEY,\n commands INTEGER NOT NULL CHECK (commands IN (0,1)),\n updated_by TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\n-- Every run a steering note reached, kept as it happens (live-notes.ts): a\n-- note's own attached/mailed fields move to the next attempt, this does not.\nCREATE TABLE IF NOT EXISTS steer_note_run (\n note INTEGER NOT NULL REFERENCES task_steer(id) ON DELETE CASCADE,\n run INTEGER NOT NULL REFERENCES run(id) ON DELETE CASCADE,\n how TEXT NOT NULL CHECK (how IN ('attached','mailed','read')),\n at TEXT NOT NULL,\n PRIMARY KEY (note, run, how)\n);\nCREATE INDEX IF NOT EXISTS steer_note_run_by_run ON steer_note_run (run, note);\n";
144
158
  export declare const CHAT_APPROVAL_SCHEMA = "\nCREATE TABLE IF NOT EXISTS chat_approval_setting (\n approver TEXT NOT NULL,\n scope TEXT NOT NULL,\n enabled INTEGER NOT NULL CHECK (enabled IN (0,1)),\n full_access INTEGER NOT NULL DEFAULT 0 CHECK (full_access IN (0,1)),\n attempt_cap_microusd INTEGER CHECK (attempt_cap_microusd IS NULL OR attempt_cap_microusd >= 0),\n generation INTEGER NOT NULL,\n digest TEXT NOT NULL,\n updated_by TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (approver, scope)\n);\n";
@@ -219,7 +233,7 @@ export type SealedLedgerEntry = LedgerEntry & {
219
233
  hash: string;
220
234
  } | null;
221
235
  };
222
- export declare const SCHEMA_VERSION = 141;
236
+ export declare const SCHEMA_VERSION = 145;
223
237
  /** v118: the tables merged into `credential`, in the order they move (a person's API tokens keep their ids first). */
224
238
  export declare const V118_MERGED_TABLES: Readonly<Record<string, CredentialKind>>;
225
239
  /**
@@ -376,6 +390,21 @@ export declare const V140_ADDED_COLUMNS: Readonly<Record<string, Readonly<Record
376
390
  definition: string;
377
391
  saved: string;
378
392
  }>>>>;
393
+ /** v143: optional screenshot context on result feedback; existing feedback stays unchanged (saved rows read NULL). */
394
+ export declare const V143_ADDED_COLUMNS: Readonly<Record<string, Readonly<Record<string, {
395
+ definition: string;
396
+ saved: string;
397
+ }>>>>;
398
+ /** v145 (docs/plans/task-tidy.md): `task_archive` is a put-away task version. A row hides it from every list and count
399
+ * and refuses every door that would dispatch, claim or approve it; restore deletes the row. Its history, results,
400
+ * approvals and ledger stay exactly as they were. `merge_in_flight` is a direct merge between its last check and GitHub's
401
+ * answer, owned by its token: a task can't be put away under it (a row older than 15 minutes is a crash, and no
402
+ * longer counts). `deploy_in_flight` is a deployment whose candidate passed its gate (30 minutes, refreshed by every
403
+ * phase that proves it again). `flow_step_task` is the tasks a flow step was admitted for, kept whatever the card does
404
+ * next, so a step still running holds them. `branch_check_run` keeps each completed GitHub workflow run (pushed to
405
+ * the watched branch of the watched repository) the "checks failed" trigger saw, green or red, so tidy can tell a
406
+ * failure notice was fixed by a later commit. */
407
+ export declare const V145_SCHEMA = "\nCREATE TABLE IF NOT EXISTS task_archive (\n task_ref INTEGER PRIMARY KEY REFERENCES task_ref(id),\n by TEXT NOT NULL,\n by_kind TEXT NOT NULL CHECK (by_kind IN ('person','tidy')),\n rule TEXT,\n reason TEXT NOT NULL,\n at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS branch_check_run (\n repo TEXT NOT NULL,\n run_id INTEGER NOT NULL,\n branch TEXT NOT NULL,\n workflow TEXT NOT NULL,\n head_sha TEXT NOT NULL,\n conclusion TEXT NOT NULL CHECK (conclusion IN ('success','failure')),\n run_created_at TEXT NOT NULL,\n observed_at TEXT NOT NULL,\n PRIMARY KEY (repo, run_id)\n);\nCREATE INDEX IF NOT EXISTS branch_check_run_by_workflow ON branch_check_run (repo, branch, workflow, run_created_at);\nCREATE TABLE IF NOT EXISTS merge_in_flight (\n publication INTEGER PRIMARY KEY REFERENCES publication(id),\n task_ref INTEGER NOT NULL REFERENCES task_ref(id),\n token TEXT NOT NULL,\n by TEXT NOT NULL,\n at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS deploy_in_flight (\n task_ref INTEGER PRIMARY KEY REFERENCES task_ref(id),\n run INTEGER NOT NULL,\n at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS flow_step_task (\n card INTEGER NOT NULL,\n entry INTEGER NOT NULL,\n task_id TEXT NOT NULL,\n PRIMARY KEY (card, entry, task_id)\n);\n";
379
408
  /**
380
409
  * v135: one approval per release (docs/plans/release-once.md). `release_authority` is the single source of truth for a
381
410
  * release a person approved: one row per generation, changed only by compare-and-swap on `rev`, with a lease whose
@@ -2406,8 +2435,9 @@ export declare class Store {
2406
2435
  sealScopeApproval(taskId: string, by: string, now: Date, mutation?: Mutation, basis?: {
2407
2436
  kind: "mode" | "chat" | "grant" | "schedule" | "auto-check" | "auto-green";
2408
2437
  modeDigest: string;
2409
- /** A chat seal of a deploy gate: the release card it came from (chat-management.md §0a.5). */
2410
- card?: "release";
2438
+ /** A chat seal of a deploy gate: the release card it came from (chat-management.md §0a.5). A chat seal of a task
2439
+ * filed from outside text: `terms`, and the digest of every word the card showed (chat-terms.ts). */
2440
+ card?: "release" | "terms";
2411
2441
  cardDigest?: string;
2412
2442
  }, approverKind?: ApproverKind): boolean;
2413
2443
  /** Whether this task's scope carries a LIVE seal: approved, and the
@@ -3207,6 +3237,7 @@ export declare class Store {
3207
3237
  author: string;
3208
3238
  /** Ingested comments carry their origin id — the idempotency key (M8.17). */
3209
3239
  sourceKey?: string;
3240
+ image?: FeedbackImage | null;
3210
3241
  }, now: Date): number | null;
3211
3242
  /** Live (unsuperseded, unconsumed) comments on a run's terminal diff. */
3212
3243
  liveDiffComments(runId: number): DiffComment[];
@@ -3405,6 +3436,15 @@ export declare class Store {
3405
3436
  entry: number;
3406
3437
  sourceJson: string;
3407
3438
  } | null;
3439
+ /** v142: where a task filed from a youiyoux change or options request came from, as kept when it was filed
3440
+ * (youiyoux-intake.ts; a revision reads its first version's): the request, its kind, and where the person pointed. Null for every other task. */
3441
+ youiyouxOriginOf(taskId: string): {
3442
+ request: string;
3443
+ kind: "change" | "options";
3444
+ route: string | null;
3445
+ component: string | null;
3446
+ at: string | null;
3447
+ } | null;
3408
3448
  /**
3409
3449
  * Seal a revision in ONE transaction (Codex M5-M8 audit, C-3; contract
3410
3450
  * handoff task 2): the SOURCE BINDING is proven here, against live rows,
@@ -3716,7 +3756,7 @@ export declare class Store {
3716
3756
  rejectedRun: number | null;
3717
3757
  } | {
3718
3758
  ok: false;
3719
- reason: "unknown-task" | "not-stalled" | "claimed" | "accepted-result" | "published" | "moved";
3759
+ reason: "unknown-task" | "not-stalled" | "claimed" | "accepted-result" | "published" | "moved" | "archived";
3720
3760
  };
3721
3761
  /**
3722
3762
  * Cancel, aware of who might be building right now. A live claim wins:
@@ -6863,7 +6903,7 @@ export declare class Store {
6863
6903
  stop: RunStop;
6864
6904
  } | {
6865
6905
  ok: false;
6866
- reason: "no-stop" | "wrong-task" | "stopping" | "already-resumed" | "superseded" | "busy" | "review";
6906
+ reason: "no-stop" | "wrong-task" | "stopping" | "already-resumed" | "superseded" | "busy" | "review" | "archived";
6867
6907
  detail: string;
6868
6908
  };
6869
6909
  /** Every stop recorded on a task, newest request first. */
@@ -7376,6 +7416,7 @@ export type DiffComment = {
7376
7416
  id: number;
7377
7417
  artifact: number;
7378
7418
  artifactSha: string;
7419
+ image?: FeedbackImage | null;
7379
7420
  run: number;
7380
7421
  path: string | null;
7381
7422
  line: number | null;
@@ -8021,3 +8062,73 @@ export declare function sealStoreMoneyMethods(prototype: object): void;
8021
8062
  * when the prototype's value is the very reference recorded at the seal. `prototype` is Store's own unless a test
8022
8063
  * says otherwise; either way nothing but the recorded references is ever installed. */
8023
8064
  export declare function lockStoreMoneyMethods(store: object, prototype?: object): void;
8065
+ /** Takes a saved note's screenshot off without losing the note or its history (v143). Fail closed, in one write
8066
+ * transaction: the note must be live and unconsumed on this run, its screenshot must be genuinely unusable right now
8067
+ * (`unusable` is result-actions.ts feedbackImageProblem, re-read inside the transaction, never the page's say-so), and
8068
+ * the change is recorded in the action ledger under the person who made it. The note is superseded by an exact
8069
+ * imageless copy; the original stays in history. The caller has already proved the person may request changes on
8070
+ * this run (the same authority that seals any saved note into a revision); that standing is re-proved here too. */
8071
+ export declare function dropFeedbackImage(store: Store, runId: number, commentId: number, actor: string, unusable: (image: FeedbackImage) => string | null, now: Date): {
8072
+ ok: true;
8073
+ id: number;
8074
+ } | {
8075
+ ok: false;
8076
+ reason: "not-found" | "image-usable" | "not-authorized";
8077
+ };
8078
+ /** A put-away task version (v145, docs/plans/task-tidy.md). */
8079
+ export type TaskArchive = {
8080
+ taskRef: number;
8081
+ taskId: string;
8082
+ by: string;
8083
+ byKind: "person" | "tidy";
8084
+ rule: string | null;
8085
+ reason: string;
8086
+ at: string;
8087
+ };
8088
+ /** Who put a task away, and why: a person by name, or tidy by its rule. */
8089
+ export type ArchiveBy = {
8090
+ by: string;
8091
+ byKind: "person" | "tidy";
8092
+ rule: string | null;
8093
+ reason: string;
8094
+ };
8095
+ export type ArchiveRefusal = "unknown-task" | "live" | "archived" | "not-archived" | "blocked";
8096
+ /** The put-away record of this task, or null when it is not put away. */
8097
+ export declare function archiveOf(store: Store, taskId: string): TaskArchive | null;
8098
+ /** Every version of a task's family, by the same proven lineage the task page reads (`taskFamilyOf`: same project, each
8099
+ * revision bound to its parent's own run by its brief). A family whose lineage doesn't prove is just this task. */
8100
+ export declare function taskFamilyRefs(store: Store, taskId: string): {
8101
+ id: number;
8102
+ taskId: string;
8103
+ }[];
8104
+ /** The first family version with something still going on, or null: running, a live newest claim, an unfinished run,
8105
+ * a process Toolroll hasn't seen exit, a build whose processes were never recorded, a stop still settling, a follow-up
8106
+ * check asked for and not finished, a flow step running for its card, a release in flight (a live release approval
8107
+ * that names it, or a deploy or merge reserved, running or unknown), or a pull request merging (an intent claimed or
8108
+ * firing, or a person's merge between its last check and GitHub's answer). */
8109
+ export declare function liveFamilyAttempt(store: Store, refs: readonly number[], now: Date): string | null;
8110
+ /** Puts a task away (v145): every version of its family, in one write transaction that re-checks it has no live
8111
+ * attempt, that its versions are exactly `expect` when the caller showed a person a family (a preview), and that
8112
+ * `guard` (the caller's own rule, re-read under the lock) still allows it. Its state, history, results, approvals and
8113
+ * ledger are untouched; its open notifications are resolved; one ledger row records who and why. */
8114
+ export declare function archiveTaskFamily(store: Store, taskId: string, by: ArchiveBy, now: Date, options?: {
8115
+ expect?: readonly number[];
8116
+ guard?: (refs: readonly number[]) => string | null;
8117
+ }): {
8118
+ ok: true;
8119
+ refs: number[];
8120
+ } | {
8121
+ ok: false;
8122
+ reason: ArchiveRefusal;
8123
+ message: string;
8124
+ };
8125
+ /** Brings a put-away task back (v145): every version of its family, with a ledger row. Tidy never puts a restored
8126
+ * task away again (tidy.ts reads the `task restored` row). */
8127
+ export declare function restoreTaskFamily(store: Store, taskId: string, by: string, now: Date, guard?: (refs: readonly number[]) => string | null): {
8128
+ ok: true;
8129
+ refs: number[];
8130
+ } | {
8131
+ ok: false;
8132
+ reason: ArchiveRefusal;
8133
+ message: string;
8134
+ };