toolroll 0.9.84 → 0.9.85

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 (81) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +94 -1193
  3. package/dist/agent-fence.d.ts +3 -3
  4. package/dist/agent-fence.js +3 -3
  5. package/dist/agent-onboard.d.ts +2 -0
  6. package/dist/agent-onboard.js +3 -0
  7. package/dist/browser/workspace.css +1 -1
  8. package/dist/browser/workspace.js +31 -31
  9. package/dist/browser-workspace.d.ts +5 -0
  10. package/dist/chat-channel.d.ts +10 -1
  11. package/dist/chat-channel.js +35 -5
  12. package/dist/chat-decide.d.ts +16 -4
  13. package/dist/chat-decide.js +239 -31
  14. package/dist/chat-messages.d.ts +5 -0
  15. package/dist/chat-messages.js +1 -1
  16. package/dist/chat-quiet.js +3 -2
  17. package/dist/chat-review.d.ts +4 -0
  18. package/dist/chat-review.js +13 -4
  19. package/dist/chat-terms.d.ts +9 -0
  20. package/dist/chat-terms.js +46 -0
  21. package/dist/contracts/console-api.d.ts +2 -1
  22. package/dist/contracts/console-api.js +2 -1
  23. package/dist/contracts/lead-tools.d.ts +30 -0
  24. package/dist/contracts/lead-tools.js +3 -0
  25. package/dist/contracts/youiyoux-request.d.ts +248 -0
  26. package/dist/contracts/youiyoux-request.js +152 -0
  27. package/dist/flow-cadence.d.ts +8 -2
  28. package/dist/flow-cadence.js +16 -3
  29. package/dist/flow-card-task.d.ts +9 -0
  30. package/dist/flow-card-task.js +15 -6
  31. package/dist/integrations-ui.js +9 -4
  32. package/dist/integrations.d.ts +9 -1
  33. package/dist/integrations.js +43 -2
  34. package/dist/landed-cleanup.d.ts +4 -0
  35. package/dist/landed-cleanup.js +3 -2
  36. package/dist/lead-capabilities.js +2 -1
  37. package/dist/lead-cli.js +3 -1
  38. package/dist/lead-tools.d.ts +2 -0
  39. package/dist/lead-tools.js +30 -4
  40. package/dist/lead.d.ts +2 -0
  41. package/dist/lead.js +3 -3
  42. package/dist/operate.d.ts +1 -2
  43. package/dist/operate.js +11 -15
  44. package/dist/project-activity.d.ts +20 -0
  45. package/dist/project-activity.js +23 -0
  46. package/dist/project-cli.d.ts +1 -0
  47. package/dist/project-cli.js +36 -19
  48. package/dist/project-knowledge.d.ts +3 -3
  49. package/dist/project-outcomes.d.ts +119 -0
  50. package/dist/project-outcomes.js +79 -0
  51. package/dist/result-actions.d.ts +4 -0
  52. package/dist/result-actions.js +41 -9
  53. package/dist/result-review.d.ts +9 -0
  54. package/dist/result-review.js +45 -5
  55. package/dist/server/console.css +12 -0
  56. package/dist/server/pages.js +11 -1
  57. package/dist/server/render-chat.js +3 -2
  58. package/dist/server/render-pages.d.ts +2 -2
  59. package/dist/server/render-pages.js +7 -2
  60. package/dist/server/render-results.d.ts +2 -0
  61. package/dist/server/render-results.js +20 -7
  62. package/dist/server/render-tasks.d.ts +1 -0
  63. package/dist/server/render-tasks.js +15 -5
  64. package/dist/server/route-table.js +1 -0
  65. package/dist/server/settings.js +18 -1
  66. package/dist/server/task-views.js +6 -0
  67. package/dist/server/tasks.js +42 -6
  68. package/dist/store.d.ts +46 -3
  69. package/dist/store.js +165 -6
  70. package/dist/task-source.d.ts +13 -1
  71. package/dist/task-source.js +17 -1
  72. package/dist/telegram-lead.js +7 -5
  73. package/dist/telegram.js +1 -1
  74. package/dist/toolroll-update.js +2 -2
  75. package/dist/webhooks.d.ts +6 -0
  76. package/dist/webhooks.js +25 -0
  77. package/dist/work-index.d.ts +4 -0
  78. package/dist/work-index.js +32 -0
  79. package/dist/youiyoux-intake.d.ts +234 -0
  80. package/dist/youiyoux-intake.js +908 -0
  81. package/package.json +11 -8
@@ -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)])}`;
@@ -3,7 +3,7 @@ import { type TaskOrigin } from "../task-source.js";
3
3
  import { type Html } from "../html.js";
4
4
  import { type AssignmentSnapshot } from "../assignment.js";
5
5
  import { type BoardCard } from "../board.js";
6
- import { type BrowserLimits } from "../browser-workspace.js";
6
+ import { type BrowserLimits, type BrowserProjectsView } from "../browser-workspace.js";
7
7
  import { type CheckLevel } from "../check-levels.js";
8
8
  import { type DispatchDiagnosis } from "../dispatch.js";
9
9
  import { type FirstRunStep } from "../first-run.js";
@@ -451,7 +451,7 @@ export declare function projectsPage(chrome: Chrome, recent: {
451
451
  path: string;
452
452
  name: string;
453
453
  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;
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, activity?: BrowserProjectsView["activity"]): Screen;
455
455
  /** The no-script "move to the front" sentinel: the form cannot name the
456
456
  * front of a partition, so the handler resolves it (slice 1b, fix 1). */
457
457
  export declare const QUEUE_FRONT = "__TOP__";
@@ -1,3 +1,4 @@
1
+ import { activityMetrics, activityScope, activityLimit } from '../project-activity.js';
1
2
  /** Everyday console pages: home, inbox, board, work, projects, fleet and the queue. */
2
3
  import { originNote } from "../task-source.js";
3
4
  import { html, htmlString, joinHtml, postForm, replaceMarkup, reauth } from "../html.js";
@@ -921,7 +922,10 @@ export function githubReposPage(chrome, data) {
921
922
  html `<p class="row" style="margin-top:.6rem"><a class="badge" href="/projects">← back to projects</a></p>`,
922
923
  ], "\n"), { chrome });
923
924
  }
924
- export function projectsPage(chrome, recent, candidates, open, problem, _unscopedMode, browsable = false, onboard = null, peeks = {}, returnTo = "/", pullRequestsOn, checkLevelOf) {
925
+ export function projectsPage(chrome, recent, candidates, open, problem, _unscopedMode, browsable = false, onboard = null, peeks = {}, returnTo = "/", pullRequestsOn, checkLevelOf, activity) {
926
+ const activityHtml = activity === undefined ? null : html `<details><summary>Activity · ${activity.project}</summary>${activity.report === null
927
+ ? html `<p role="alert">Activity is unavailable.</p><a href="/projects">Reload</a>`
928
+ : 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
929
  // The onboarding card (repo onboarding, findings 1-39): preview first,
926
930
  // then a password-confirmed clone into a configured root. Disabled
927
931
  // states explain themselves in words (finding 28/39).
@@ -1047,11 +1051,12 @@ export function projectsPage(chrome, recent, candidates, open, problem, _unscope
1047
1051
  recentItems.length === 0 && candidateItems.length === 0
1048
1052
  ? 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
1053
  : null,
1054
+ activityHtml,
1050
1055
  recentItems.length > 0 ? html `<h2>Recent</h2>${cards(recentItems)}` : null,
1051
1056
  candidateItems.length > 0 ? html `<h2>Available</h2>${cards(candidateItems)}` : null,
1052
1057
  addCard,
1053
1058
  ], "\n"), { chrome, workspace: { view: {
1054
- kind: "projects", choosing, problem, returnTo,
1059
+ kind: "projects", choosing, problem, returnTo, ...(activity === undefined ? {} : { activity }),
1055
1060
  recent: recent.map(one => rowOf(one.path, one.name, one.lastOpenedAt)),
1056
1061
  available: candidates.map(path => rowOf(path, projectName(path), null)),
1057
1062
  add: { browse: browsable ? "/projects/browse" : null, github: onboard === null ? null : "/projects/github", html: htmlString(addForms) },
@@ -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
@@ -50,7 +50,7 @@ import { reviewHref, taskChatHref, taskHref } from "./http.js";
50
50
  import { coordinatorProposalsSection } from "./render-chat.js";
51
51
  import { completionReceiptCard, criterionMatrixHtml, evidenceWords, homePhaseWords, incidentWords, needsCheck, pullRequestCardHtml, reasonWords, receiptStatusOf, repairChainHtml, reportItemsHtml, reviewRetryPanel, runNoun, runOutcomeBadge, semanticCoverageHtml, verificationExplanation, verificationRecovered } from "./render-results.js";
52
52
  import {} from "./session.js";
53
- import { linkWords, originNote } from "../task-source.js";
53
+ import { linkWords, originNote, requestName, requestWords } from "../task-source.js";
54
54
  export function contractChangeHtml(change) {
55
55
  const tag = html `<span class="contract-change-kind contract-change-${change.kind}">${change.kind}</span>`;
56
56
  switch (change.field) {
@@ -203,7 +203,8 @@ const fragmentString = (fragment) => fragment === "" ? "" : htmlString(fragment)
203
203
  function originWords(origin) {
204
204
  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
205
  const source = origin.source === null ? null : `${origin.source.label} (${origin.source.proved})`;
206
- return [flow, source].filter(one => one !== null).join(" · ");
206
+ const request = origin.request == null ? null : `from ${requestName(origin.request)}`;
207
+ return [flow, source, request].filter(one => one !== null).join(" · ");
207
208
  }
208
209
  /** 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
210
  * it lives, with that link's host always shown. */
@@ -219,6 +220,12 @@ function originParts(origin) {
219
220
  if (origin.source.url !== null)
220
221
  parts.push(" · ", { label: linkWords(origin.source.url), href: origin.source.url });
221
222
  }
223
+ if (origin.request != null) {
224
+ if (parts.length > 0)
225
+ parts.push(" · ");
226
+ const where = requestWords(origin.request);
227
+ parts.push(`${requestName(origin.request)}${where === "" ? "" : `: ${where}`}`);
228
+ }
222
229
  return parts;
223
230
  }
224
231
  export function approvalSheetHtml(input) {
@@ -294,7 +301,7 @@ export function approvalSheetHtml(input) {
294
301
  const revisionDetail = input.revision === null ? "" : detail("Earlier build", joinHtml([
295
302
  html `<div class="revision-card" data-revision-feedback>`,
296
303
  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>`,
304
+ 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
305
  revisionLineageHtml(input.revision.lineage),
299
306
  html `</div>`,
300
307
  ]));
@@ -1172,6 +1179,9 @@ export function executionPlanHtml(document, compact = false) {
1172
1179
  html `</div>`,
1173
1180
  ]);
1174
1181
  }
1182
+ function revisionImageLink(image) {
1183
+ return image == null ? html `` : html `<a href="/r/${image.run}/evidence/${image.artifact}">Screenshot ${image.artifact} · result #${image.run}</a> · `;
1184
+ }
1175
1185
  /**
1176
1186
  * A revision's lineage and ACTUAL terms, in the same words on the task
1177
1187
  * page, the approval card, and chat (contract handoff task 2): which
@@ -1594,7 +1604,7 @@ export function taskBodyParts(data) {
1594
1604
  : joinHtml([
1595
1605
  html `<div class="revision-card" data-revision-feedback>`,
1596
1606
  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>`),
1607
+ ...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
1608
  revisionLineageHtml(data.revision.lineage),
1599
1609
  html `</div>`,
1600
1610
  ], "\n");
@@ -2170,7 +2180,7 @@ export function taskBodyParts(data) {
2170
2180
  const feedback = data.revision;
2171
2181
  thread.push({ key: "revision", at: task.createdAt, kind: "reply", who: "person", author: feedback.comments[0]?.author ?? "You",
2172
2182
  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,
2183
+ 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
2184
  link: { label: `From build #${feedback.sourceRun}`, href: `/r/${feedback.sourceRun}` }, html: "", more: null });
2175
2185
  }
2176
2186
  for (const decision of data.decisions) {
@@ -336,6 +336,7 @@ for (const one of ["policy", "approval", "request-limits", "sessions", "sign-in"
336
336
  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
337
  }
338
338
  post("settings", "settings.flows-on", "/settings/flows/on", { limited: "collection", ...approver("An approver switches starter flows on.", "/settings/flows") });
339
+ post("settings", "settings.integrations-youiyoux", "/settings/integrations/youiyoux", { limited: "collection", ...approver("An approver turns youiyoux requests on or off.", "/settings/integrations") });
339
340
  const MODELS = approver("Sign in as an approver to change models.", "/settings/models");
340
341
  for (const one of ["check", "watch", "update"])
341
342
  post("settings", `settings.models-${one}`, `/settings/models/${one}`, MODELS);
@@ -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";
@@ -229,6 +231,7 @@ export function createTaskViews(runtime) {
229
231
  line: one.line === null || one.line === undefined ? null : Number(one.line),
230
232
  note: String(one.note ?? ""),
231
233
  author: String(one.author ?? "?"),
234
+ image: parseFeedbackImage(one.image) || null,
232
235
  })),
233
236
  };
234
237
  }
@@ -1119,6 +1122,9 @@ export function createTaskViews(runtime) {
1119
1122
  outsideTouches: files.filter(one => one.outsideTouches).map(one => one.path),
1120
1123
  fileAnchors,
1121
1124
  comments: runtime.store.liveDiffComments(run.id).filter(isRevisionFeedback),
1125
+ // The same check that refuses a saved note's screenshot at revision time decides when it can be removed.
1126
+ unusableImageNotes: runtime.store.liveDiffComments(run.id).filter(one => isRevisionFeedback(one) && one.image != null
1127
+ && feedbackImageProblem(runtime.store, runtime.evidenceRoot, run.id, one.image) !== null).map(one => one.id),
1122
1128
  pastComments: runtime.store.allDiffComments(run.id).filter(one => one.reviewerRun === null && (one.consumedBy !== null || one.supersededBy !== null)),
1123
1129
  reviewerFindings: runtime.store.allDiffComments(run.id).filter(one => one.reviewerRun !== null),
1124
1130
  automaticReview: buildReviewOf(runtime.store, run.id),
@@ -1,4 +1,5 @@
1
- import { releaseAuthorityOfTask, revokeRelease, runLiveCommands } from "../store.js";
1
+ import { parseFeedbackImage, sameFeedbackImage } from "../result-review.js";
2
+ import { dropFeedbackImage, releaseAuthorityOfTask, revokeRelease, runLiveCommands } from "../store.js";
2
3
  import { steerCapabilityOf } from "../steer-capabilities.js";
3
4
  import { canReveal, localCheckoutOf, revealInFinder } from "../checkout-proof.js";
4
5
  /** A form's way back to a flow card ("flow:<flow>:<card>"), read strictly: numbers only, never a path. */
@@ -48,7 +49,7 @@ import { fileTaskProposal } from "../proposal.js";
48
49
  import { isProviderId, PROVIDER_IDS, validateSpec } from "../provider.js";
49
50
  import { completeAndOpenPullRequest, mergeAsPerson } from '../pull-request-flow.js';
50
51
  import { isQualityMode } from "../quality.js";
51
- import { createResultRevision, requestResultChanges } from "../result-actions.js";
52
+ import { createResultRevision, requestResultChanges, feedbackImageProblem } from "../result-actions.js";
52
53
  import { structuredHandoffView, terminalDiffView } from "../result-evidence-readers.js";
53
54
  import { fileAddTestsTask, requestFollowUpChecks } from '../result-follow-ups.js';
54
55
  import { commentSourceKey, isRevisionFeedback, parseResultTab, resultReturnTarget } from "../result-review.js";
@@ -1402,15 +1403,40 @@ export function createTasksHandlers(runtime) {
1402
1403
  const viewParam = returnTo.startsWith("/t/") ? "section" : "tab";
1403
1404
  const back = selectedTab === "summary" ? returnTo
1404
1405
  : `${returnTo.replace(/&section=(?:summary|changes|checks)$/, "")}${returnTo.includes("?") ? "&" : "?"}${viewParam}=${selectedTab}`;
1406
+ if (body.get("intent") === "drop-image") {
1407
+ // A saved note whose screenshot is gone or changed would block every revision that seals it. Its words stay:
1408
+ // the note is superseded by an exact imageless copy, and the original remains in history.
1409
+ // Who: a signed-in browser approver admitted to this run's project (checked above) — the same authority that
1410
+ // seals any saved note into a revision. What: only a screenshot that is unusable right now, re-proved inside
1411
+ // dropFeedbackImage's transaction, and recorded in the ledger under this person.
1412
+ if (who.via !== "cookie" || who.role !== "approver")
1413
+ return refuse(response, who, 403, "Only a signed-in approver can change saved feedback.", back);
1414
+ const comment = Number(body.get("comment") ?? "");
1415
+ if (!Number.isSafeInteger(comment) || comment < 1)
1416
+ return refuse(response, who, 400, "Choose a saved note.", back);
1417
+ const dropped = dropFeedbackImage(store, id, comment, who.name, one => feedbackImageProblem(store, evidenceRoot, id, one), now);
1418
+ if (!dropped.ok && dropped.reason === "not-authorized")
1419
+ return refuse(response, who, 403, "Only a signed-in approver on this project can change saved feedback.", back);
1420
+ if (!dropped.ok)
1421
+ return refuse(response, who, 409, dropped.reason === "image-usable"
1422
+ ? "That screenshot still verifies, so the note keeps it. Reload the result." : "That saved note changed or was already used. Reload the result.", back);
1423
+ return redirect(response, `${back}#request-changes`);
1424
+ }
1425
+ const image = parseFeedbackImage(body.get("image"));
1426
+ if (image === false)
1427
+ return refuse(response, who, 400, "Choose a screenshot from this result again. Your draft is unchanged.", back);
1405
1428
  if (body.get("intent") === "revise") {
1406
1429
  const result = requestResultChanges(store, evidenceRoot, {
1407
1430
  run: id, batch: body.get("batch"), source: body.get("source"), actor: who.name,
1408
1431
  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"),
1432
+ note: body.get("note") ?? "", path: body.get("path") ?? "", line: body.get("line") ?? "", request: body.get("request"), image,
1410
1433
  }, now);
1411
1434
  if (!result.ok) {
1412
1435
  const request = body.get("request") ?? "";
1413
- const retryBack = /^[a-f0-9]{32}$/.test(request) ? `${back}${back.includes("?") ? "&" : "?"}conflict=${request}#request-changes` : back;
1436
+ // The queue's link can stop resolving when a newer attempt finishes. The exact
1437
+ // run record still hosts this account/run's recoverable feedback draft.
1438
+ const retryBase = image === null ? back : `/r/${id}?record=1`;
1439
+ const retryBack = /^[a-f0-9]{32}$/.test(request) ? `${retryBase}${retryBase.includes("?") ? "&" : "?"}conflict=${request}#request-changes` : retryBase;
1414
1440
  return refuse(response, who, result.status, result.message, retryBack);
1415
1441
  }
1416
1442
  return redirect(response, revisionDestination(result.id, returnTo));
@@ -1445,7 +1471,17 @@ export function createTasksHandlers(runtime) {
1445
1471
  const request = body.get("request");
1446
1472
  const sourceKey = commentSourceKey(who.name, request);
1447
1473
  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);
1474
+ // Freshness, custody and the insert in one write transaction: a newer attempt finishing in between can't leave a
1475
+ // stale screenshot attached (check and write together).
1476
+ const saved = store.transact(() => {
1477
+ const imageProblem = feedbackImageProblem(store, evidenceRoot, id, image);
1478
+ if (imageProblem !== null)
1479
+ return { problem: imageProblem };
1480
+ return { inserted: store.addDiffComment({ artifactId: terminal.id, runId: id, path, line, note: note.note, author: who.name, image, ...(sourceKey === undefined ? {} : { sourceKey }) }, now) };
1481
+ });
1482
+ if ("problem" in saved)
1483
+ return refuse(response, who, 409, saved.problem, `/r/${id}?record=1#request-changes`);
1484
+ const inserted = saved.inserted;
1449
1485
  if (inserted === null && sourceKey !== undefined) {
1450
1486
  // The token was used before (repair 2026-09-14, finding 1). A
1451
1487
  // request identity is IMMUTABLE: it names exactly one note on
@@ -1459,7 +1495,7 @@ export function createTasksHandlers(runtime) {
1459
1495
  if (earlier === null || earlier.run !== id) {
1460
1496
  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
1497
  }
1462
- if (earlier.path !== path || earlier.line !== line || earlier.note !== note.note) {
1498
+ if (earlier.path !== path || earlier.line !== line || earlier.note !== note.note || !sameFeedbackImage(earlier.image, image)) {
1463
1499
  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
1500
  }
1465
1501
  }
package/dist/store.d.ts CHANGED
@@ -1,3 +1,4 @@
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";
@@ -139,6 +140,18 @@ export declare const V141_ADDED_COLUMNS: Readonly<Record<string, Readonly<Record
139
140
  definition: string;
140
141
  saved: string;
141
142
  }>>>>;
143
+ /** v142 (youiyoux-intake.ts): youiyoux change requests as tasks. `youiyoux_intake` is one row per project the intake
144
+ * has looked at: a person's on/off choice (`enabled` NULL: automatic, on while the project uses youiyoux) and the last
145
+ * pass's answer (whether the project uses youiyoux, how many request files couldn't be read, and the problem, if any).
146
+ * `youiyoux_request` is one row per request taken, keyed by project and request id: the task it became (NULL: left
147
+ * out, `note` says why), never filed again, the status last written beside it, with the console address its links
148
+ * named ('' for none), and where the person pointed (route, component, file:line), cleaned, for the task page. An
149
+ * options request (`kind`) also keeps the options file its agent writes (`proposal`, p-<its hex>), set when filed and
150
+ * never read from the request again: whether it went to the project's main checkout from which result (`proposal_state`,
151
+ * `proposal_run`, and why not in `proposal_note`; `taken`: a file was already there, so it was left and is never read
152
+ * for a decision), the digest of what was copied apart from what a decision changes (`proposal_digest`), and what a
153
+ * person decided on it in youiyoux's hub (`decided`, `decided_note`: what Toolroll did about it). New tables only. */
154
+ 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
155
  /** A project's "Show commands in the live view" (live-detail.ts): absent means off. */
143
156
  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
157
  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 +232,7 @@ export type SealedLedgerEntry = LedgerEntry & {
219
232
  hash: string;
220
233
  } | null;
221
234
  };
222
- export declare const SCHEMA_VERSION = 141;
235
+ export declare const SCHEMA_VERSION = 143;
223
236
  /** v118: the tables merged into `credential`, in the order they move (a person's API tokens keep their ids first). */
224
237
  export declare const V118_MERGED_TABLES: Readonly<Record<string, CredentialKind>>;
225
238
  /**
@@ -376,6 +389,11 @@ export declare const V140_ADDED_COLUMNS: Readonly<Record<string, Readonly<Record
376
389
  definition: string;
377
390
  saved: string;
378
391
  }>>>>;
392
+ /** v143: optional screenshot context on result feedback; existing feedback stays unchanged (saved rows read NULL). */
393
+ export declare const V143_ADDED_COLUMNS: Readonly<Record<string, Readonly<Record<string, {
394
+ definition: string;
395
+ saved: string;
396
+ }>>>>;
379
397
  /**
380
398
  * v135: one approval per release (docs/plans/release-once.md). `release_authority` is the single source of truth for a
381
399
  * release a person approved: one row per generation, changed only by compare-and-swap on `rev`, with a lease whose
@@ -2406,8 +2424,9 @@ export declare class Store {
2406
2424
  sealScopeApproval(taskId: string, by: string, now: Date, mutation?: Mutation, basis?: {
2407
2425
  kind: "mode" | "chat" | "grant" | "schedule" | "auto-check" | "auto-green";
2408
2426
  modeDigest: string;
2409
- /** A chat seal of a deploy gate: the release card it came from (chat-management.md §0a.5). */
2410
- card?: "release";
2427
+ /** A chat seal of a deploy gate: the release card it came from (chat-management.md §0a.5). A chat seal of a task
2428
+ * filed from outside text: `terms`, and the digest of every word the card showed (chat-terms.ts). */
2429
+ card?: "release" | "terms";
2411
2430
  cardDigest?: string;
2412
2431
  }, approverKind?: ApproverKind): boolean;
2413
2432
  /** Whether this task's scope carries a LIVE seal: approved, and the
@@ -3207,6 +3226,7 @@ export declare class Store {
3207
3226
  author: string;
3208
3227
  /** Ingested comments carry their origin id — the idempotency key (M8.17). */
3209
3228
  sourceKey?: string;
3229
+ image?: FeedbackImage | null;
3210
3230
  }, now: Date): number | null;
3211
3231
  /** Live (unsuperseded, unconsumed) comments on a run's terminal diff. */
3212
3232
  liveDiffComments(runId: number): DiffComment[];
@@ -3405,6 +3425,15 @@ export declare class Store {
3405
3425
  entry: number;
3406
3426
  sourceJson: string;
3407
3427
  } | null;
3428
+ /** v142: where a task filed from a youiyoux change or options request came from, as kept when it was filed
3429
+ * (youiyoux-intake.ts; a revision reads its first version's): the request, its kind, and where the person pointed. Null for every other task. */
3430
+ youiyouxOriginOf(taskId: string): {
3431
+ request: string;
3432
+ kind: "change" | "options";
3433
+ route: string | null;
3434
+ component: string | null;
3435
+ at: string | null;
3436
+ } | null;
3408
3437
  /**
3409
3438
  * Seal a revision in ONE transaction (Codex M5-M8 audit, C-3; contract
3410
3439
  * handoff task 2): the SOURCE BINDING is proven here, against live rows,
@@ -7376,6 +7405,7 @@ export type DiffComment = {
7376
7405
  id: number;
7377
7406
  artifact: number;
7378
7407
  artifactSha: string;
7408
+ image?: FeedbackImage | null;
7379
7409
  run: number;
7380
7410
  path: string | null;
7381
7411
  line: number | null;
@@ -8021,3 +8051,16 @@ export declare function sealStoreMoneyMethods(prototype: object): void;
8021
8051
  * when the prototype's value is the very reference recorded at the seal. `prototype` is Store's own unless a test
8022
8052
  * says otherwise; either way nothing but the recorded references is ever installed. */
8023
8053
  export declare function lockStoreMoneyMethods(store: object, prototype?: object): void;
8054
+ /** Takes a saved note's screenshot off without losing the note or its history (v143). Fail closed, in one write
8055
+ * transaction: the note must be live and unconsumed on this run, its screenshot must be genuinely unusable right now
8056
+ * (`unusable` is result-actions.ts feedbackImageProblem, re-read inside the transaction, never the page's say-so), and
8057
+ * the change is recorded in the action ledger under the person who made it. The note is superseded by an exact
8058
+ * imageless copy; the original stays in history. The caller has already proved the person may request changes on
8059
+ * this run (the same authority that seals any saved note into a revision); that standing is re-proved here too. */
8060
+ export declare function dropFeedbackImage(store: Store, runId: number, commentId: number, actor: string, unusable: (image: FeedbackImage) => string | null, now: Date): {
8061
+ ok: true;
8062
+ id: number;
8063
+ } | {
8064
+ ok: false;
8065
+ reason: "not-found" | "image-usable" | "not-authorized";
8066
+ };