toolroll 0.9.11 → 0.9.12

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 (44) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/assignment.d.ts +3 -0
  3. package/dist/assignment.js +3 -1
  4. package/dist/browser/workspace.css +2 -2
  5. package/dist/browser/workspace.js +20 -18
  6. package/dist/browser-workspace.d.ts +61 -0
  7. package/dist/chat-delivery.js +17 -0
  8. package/dist/chat-quiet.js +1 -0
  9. package/dist/chat-voice.d.ts +3 -1
  10. package/dist/chat-voice.js +13 -5
  11. package/dist/cli.d.ts +1 -1
  12. package/dist/cli.js +1 -0
  13. package/dist/coding-update.d.ts +13 -0
  14. package/dist/coding-update.js +68 -0
  15. package/dist/desktop-update.d.ts +7 -0
  16. package/dist/desktop-update.js +42 -1
  17. package/dist/flow-triggers.js +17 -1
  18. package/dist/guides.js +9 -0
  19. package/dist/lead-status.d.ts +10 -1
  20. package/dist/lead-status.js +13 -3
  21. package/dist/lead-voice.d.ts +81 -0
  22. package/dist/lead-voice.js +275 -0
  23. package/dist/needs-you.d.ts +7 -0
  24. package/dist/needs-you.js +9 -0
  25. package/dist/operate.js +80 -4
  26. package/dist/serve.d.ts +5 -1
  27. package/dist/serve.js +438 -224
  28. package/dist/setup-guide.d.ts +2 -2
  29. package/dist/store.js +8 -0
  30. package/dist/surface.js +8 -1
  31. package/dist/task-status.d.ts +6 -0
  32. package/dist/task-status.js +19 -2
  33. package/dist/team-cli.js +1 -1
  34. package/dist/telegram-progress.js +7 -0
  35. package/dist/telegram.js +34 -3
  36. package/dist/test-temp.js +1 -1
  37. package/dist/toolroll-update.d.ts +3 -0
  38. package/dist/toolroll-update.js +13 -2
  39. package/dist/work-index.d.ts +9 -0
  40. package/dist/work-index.js +57 -20
  41. package/dist/work-summary.d.ts +4 -1
  42. package/dist/workspace-ui.d.ts +25 -0
  43. package/dist/workspace-ui.js +20 -1
  44. package/package.json +1 -1
package/dist/serve.js CHANGED
@@ -62,6 +62,7 @@ import { teamWorkspaceHtml } from './team-ui.js';
62
62
  import { createTeamRuntime } from './team-runtime.js';
63
63
  import { prepareWorkspaceRevision, WorkspaceValidatorCache } from "./workspace-revision.js";
64
64
  import { workIndexPage, workCountsByProject, WorkIndexCursorError, WORK_INDEX_MAX_LIMIT } from "./work-index.js";
65
+ import { leadActivity } from "./lead-voice.js";
65
66
  import { openWorkDecisionOf } from "./work-summary.js";
66
67
  import { assignmentOf, checkAssignmentAsOperator } from './assignment.js';
67
68
  import { demoChatHtml, demoThreadHtml, DEMO_CHAT_SCRIPT } from "./demo-chat.js";
@@ -73,7 +74,7 @@ import { leadBriefHtml, LEAD_CONTEXT_CSS } from './lead-context.js';
73
74
  import { assignmentCatchUp } from './assignment-brief.js';
74
75
  import { assignmentActionHref, assignmentCardOf, assignmentStatusOf, assignmentSummaryHtml, assignmentWithEvidence, ASSIGNMENT_CSS } from './assignment-ui.js';
75
76
  import { assignmentStageOf, pullRequestFactOf, stageOfCode, stageOfDispatch, statusDetailsHtml, statusIconSvg, statusWhyHtml, taskStatusOf, TASK_STATUS_CSS } from './task-status.js';
76
- import { NEEDS } from './needs-you.js';
77
+ import { ASKS, ASK_LABEL, NEEDS } from './needs-you.js';
77
78
  import { assignmentPresentationOf, historicalAssessmentReason, shortenedMaterialReason } from './assignment-presentation.js';
78
79
  import { ledgerBody } from "./ledger-view.js";
79
80
  import { EVIDENCE_PACK_CSS, evidencePack, evidencePackHtml, exportDay, ledgerExportChunks } from "./evidence-pack.js";
@@ -162,7 +163,7 @@ import { starterRecipes, savedRecipes, findRecipe, importRecipe, exportRecipe, c
162
163
  import { recipeFromForm, recipeLibraryHtml, recipeEditorHtml, workflowPreviewHtml, recipeScript, RECIPE_CSS, recipeDefinitionPreviewHtml, recipeRunHtml, recipeAnswersFromForm } from "./recipe-ui.js";
163
164
  import { EVIDENCE_CAPS, readVerifiedArtifact, readVerifiedReport, readVerifiedProofForRun, storeEvidence, writeEvidenceFile, scanForSecrets } from "./evidence.js";
164
165
  import { GOAL_ASSESSMENT_PENDING, reviewConflict, manualReviewOnly, manualReviewCriterionOf, personCheckWords, plainReasonWords, dispatchStatusToken, passFraction, semanticCoverage, coverageWords, coverageStateWords } from "./proof.js";
165
- import { WORK_VIEWS, REVIEW_TOKENS, parseWorkView, resultStatusOf, resultHeadlineOf, receiptHeadingOf, receiptPublicationWords, reviewFactsOf, workStatusOf, primaryDestinationOf, needsPerson, dispatchActionLabel, } from "./workspace-ui.js";
166
+ import { WORK_VIEWS, REVIEW_TOKENS, RESULT_DECISION_SENTENCE, acceptWordsOf, parseWorkView, resultStatusOf, resultHeadlineOf, receiptHeadingOf, receiptPublicationWords, reviewFactsOf, workStatusOf, primaryDestinationOf, needsPerson, dispatchActionLabel, } from "./workspace-ui.js";
166
167
  import { PRICED_BUILD_MODELS } from "./pricing.js";
167
168
  import { RESULT_TABS, RESULT_REVIEW_SCRIPT, isRevisionFeedback, evidenceResultStatusOf, evidenceShortenedWords, parseResultTab, resultLeadOf, evidenceProblemDetailsOf, evidenceHealthOf, resultFactsAttributes, resultFactsAttributeMap, resultReturnTarget, commentSourceKey, REQUEST_TOKEN, revisionBatchOf, parseRevisionBatch, revisionSourceOf, } from "./result-review.js";
168
169
  import { parseReport } from "./scout-report.js";
@@ -408,6 +409,8 @@ class PersistentSessions extends Map {
408
409
  }
409
410
  hashOf(session) { const id = this.ids.get(session); return id === undefined ? null : PersistentSessions.hash(id); }
410
411
  }
412
+ /** How long a stop waits for teammates and the lead's follow pass before closing anyway. */
413
+ export const SHUTDOWN_WAIT_MS = 5_000;
411
414
  export function createDecisionServer(options) {
412
415
  const { store, evidenceRoot } = options;
413
416
  let coding = options.codingWorkspace ?? null;
@@ -2026,7 +2029,7 @@ export function createDecisionServer(options) {
2026
2029
  needsVerification: store
2027
2030
  .listCompletedWorkScoped(project, 10, admission)
2028
2031
  .filter(one => visible(one.repo) && (one.proofVerdict === "short" || one.proofVerdict === "refuted") && !one.proofAccepted &&
2029
- assignmentOf(store, one.taskId, now, { principal: "operator", repos: admissionList(), includeUnplaced: visible(null) }, evidenceRoot) === null)
2032
+ assignmentOf(store, one.taskId, now, workAccess(), evidenceRoot) === null)
2030
2033
  .map(one => ({
2031
2034
  taskId: one.taskId,
2032
2035
  title: one.title,
@@ -2364,7 +2367,7 @@ export function createDecisionServer(options) {
2364
2367
  // Admission binds BEFORE the SQL limit (the done page's own rule),
2365
2368
  // and every row is re-proved against the ceiling before ranking.
2366
2369
  const resultRow = (row) => {
2367
- const assignment = assignmentOf(store, row.taskId, now, { principal: "operator", repos: admissionList(), includeUnplaced: visible(null) }, evidenceRoot);
2370
+ const assignment = assignmentOf(store, row.taskId, now, workAccess(), evidenceRoot);
2368
2371
  return { ...row, proofReasons: row.runId === null ? [] : store.proofVerdictFor(row.runId)?.reasons ?? [], ciFailing: ciFailingFor(row.runId, row.prNumber),
2369
2372
  assignment: assignment?.activeTaskId === row.taskId && (assignment.receipt?.runId ?? null) === row.runId ? assignment : null };
2370
2373
  };
@@ -2859,7 +2862,7 @@ export function createDecisionServer(options) {
2859
2862
  // v103: a task's evidence pack, as a printable page or JSON (the whole family, sealed ledger entries included).
2860
2863
  const evidence = matchTaskPath(url.pathname, "/evidence$");
2861
2864
  if (evidence !== null) {
2862
- const access = { principal: "operator", repos: admissionList(), includeUnplaced: visible(null) };
2865
+ const access = workAccess();
2863
2866
  const family = store.taskFamilyOf(evidence.taskId, access.repos, access.includeUnplaced);
2864
2867
  if (family === null || !visible(family.root.repo))
2865
2868
  return refuse(response, who, 404, "no such task", "/tasks");
@@ -2883,7 +2886,7 @@ export function createDecisionServer(options) {
2883
2886
  const to = new Date(Date.parse(last) + 86_400_000).toISOString();
2884
2887
  if (to <= from || Date.parse(to) - Date.parse(from) > 366 * 86_400_000)
2885
2888
  return refuse(response, who, 400, "Choose a range of a year or less, ending on or after its start.", "/ledger");
2886
- const access = { principal: "operator", repos: admissionList(), includeUnplaced: visible(null) };
2889
+ const access = workAccess();
2887
2890
  store.recordAction({ at: now.toISOString(), actor: who.name, repo: null, taskId: null, runId: null, action: "ledger exported", outcome: "exported", source: "access",
2888
2891
  detail: `${from.slice(0, 10)} to ${last.slice(0, 10)}` });
2889
2892
  // A piece at a time (a page of entries, or one pack), yielding between them, and stopping if the reader's access
@@ -3597,7 +3600,7 @@ export function createDecisionServer(options) {
3597
3600
  if (repos.length > 0) {
3598
3601
  try {
3599
3602
  fleetSnapshot = store.chatSnapshot(repos, now);
3600
- const summaries = workIndexPage(store, now, { principal: 'operator', repos, includeUnplaced: false }, { limit: 100 }).items;
3603
+ const summaries = workIndexPage(store, now, { principal: 'operator', repos, includeUnplaced: false, viewer: who.name }, { limit: 100 }).items;
3601
3604
  fleetSnapshot.assignmentStates = Object.fromEntries(summaries.flatMap(value => [value.rootId, value.activeTaskId].map(id => [id, { state: value.assignmentState, label: value.status.label, detail: value.status.detail }])));
3602
3605
  fleetSnapshot.attentionCount = needsYouBadge(null);
3603
3606
  }
@@ -4891,8 +4894,6 @@ export function createDecisionServer(options) {
4891
4894
  const pageHtml = s.body;
4892
4895
  const extras = s.workspace ?? {};
4893
4896
  const notices = [...(extras.notices ?? [])];
4894
- if (s.chrome.demo)
4895
- notices.unshift(DEMO_BANNER);
4896
4897
  if (s.chrome.modeBanner)
4897
4898
  notices.push(s.chrome.modeBanner.words);
4898
4899
  if (s.chrome.updateWaiting)
@@ -4907,10 +4908,12 @@ export function createDecisionServer(options) {
4907
4908
  notices.push('Crew updates are unavailable. Open Tasks to inspect saved work.');
4908
4909
  }
4909
4910
  // "Wake me only for these": the navigation carries the count of tasks
4910
- // waiting on a person across everything this person may see.
4911
+ // waiting on a person behind its Tasks link — the open project's, or
4912
+ // everything this person may see — so it matches that page's Needs you tab.
4911
4913
  let needsYou = 0;
4912
4914
  try {
4913
- needsYou = (requestFacts.workCounts ?? workCountsByProject(store, clock(), workAccess())).reduce((sum, one) => sum + one.totals['needs-you'], 0);
4915
+ needsYou = (requestFacts.workCounts ?? workCountsByProject(store, clock(), workAccess()))
4916
+ .filter(one => s.chrome?.project == null || one.repo === s.chrome.project).reduce((sum, one) => sum + one.totals['needs-you'], 0);
4914
4917
  }
4915
4918
  catch {
4916
4919
  needsYou = 0;
@@ -4947,6 +4950,7 @@ export function createDecisionServer(options) {
4947
4950
  projects: browserProjectsOf(s.chrome.projects ?? []), ...crew,
4948
4951
  conversation, ...(extras.team ? { team: extras.team } : {}), focus: extras.focus ?? null, result: extras.result ?? null,
4949
4952
  catchUpHtml: extras.catchUpHtml ?? '', controlsHtml: extras.controlsHtml ?? '', notices, view: extras.view ?? null,
4953
+ ...(s.chrome.demo ? { demo: { text: DEMO_BANNER, short: DEMO_BANNER_SHORT } } : {}),
4950
4954
  pageHtml: extras.pageHtml === undefined ? (conversation === null ? pageHtml : null) : extras.pageHtml,
4951
4955
  navigation: [...browserNavigationOf(currentPath, s.chrome.project, needsYou), { label: 'Workspace tools', href: '/menu', active: path.pathname === '/menu' }],
4952
4956
  chats,
@@ -5013,7 +5017,7 @@ export function createDecisionServer(options) {
5013
5017
  * phase, four counts, plan-window use (never dollars: subscriptions don't
5014
5018
  * bill per run) and Catch up's items, each tagged with its tab. Reads only. */
5015
5019
  function chatHomeOf(who, repos, now) {
5016
- const access = { principal: "operator", repos: admissionList(), includeUnplaced: visible(null) };
5020
+ const access = workAccess();
5017
5021
  const admitted = (repo) => repo === null ? visible(null) : repos.includes(repo);
5018
5022
  const agents = store.liveRuns(now).filter(run => admitted(run.repo)).map(run => {
5019
5023
  const root = familyOf(run.taskId)?.root ?? null;
@@ -5042,8 +5046,13 @@ export function createDecisionServer(options) {
5042
5046
  })(),
5043
5047
  }));
5044
5048
  const windows = who.via === "cookie" && store.isInstanceOperator(who.name) ? limitsView(store.providerLimits(), [], { project: projectName, teammate: id => `Teammate ${id}` }, now) : null;
5049
+ // One line: what this person's lead is doing now and when it last acted.
5050
+ const activity = leadActivity(store, who.name);
5051
+ const leadTask = activity?.taskId == null ? null : store.lookupRef(activity.taskId);
5052
+ const lead = activity === null ? null : { doing: activity.doing, at: activity.at,
5053
+ href: leadTask == null || !admitted(leadTask.repo) ? null : taskHref(familyOf(activity.taskId)?.root.id ?? activity.taskId) };
5045
5054
  return {
5046
- agents, counts, catchUp, allHref: "/work",
5055
+ agents, counts, catchUp, allHref: "/work", ...(lead === null ? {} : { lead }),
5047
5056
  planUse: (windows?.tiles ?? []).map(one => ({ name: one.name, window: one.window, percent: one.percent, detail: one.detail, tone: one.tone })),
5048
5057
  };
5049
5058
  }
@@ -5141,7 +5150,8 @@ export function createDecisionServer(options) {
5141
5150
  };
5142
5151
  }
5143
5152
  function workAccess() {
5144
- return { principal: 'operator', repos: admissionList(), includeUnplaced: visible(null) };
5153
+ // The reader's own lead's claims read "Your lead is on it" (lead-voice.ts); nobody else's.
5154
+ return { principal: 'operator', repos: admissionList(), includeUnplaced: visible(null), viewer: requestContext.getStore()?.actor ?? null };
5145
5155
  }
5146
5156
  // All project counts are read together. No task artifacts or processes are
5147
5157
  // inspected to paint a badge or a project switcher.
@@ -5272,7 +5282,7 @@ export function createDecisionServer(options) {
5272
5282
  if (earlierLive.length > 0 && !status.views.includes("running"))
5273
5283
  status.views = [...status.views, "running"];
5274
5284
  const otherActive = new Set([...(task.family?.otherActive.map(one => one.id) ?? []), ...earlierLive]).size;
5275
- const recordedAssignment = task.family === undefined ? null : assignmentOf(store, task.id, now, { principal: "operator", repos: admissionList(), includeUnplaced: visible(null) }, evidenceRoot);
5285
+ const recordedAssignment = task.family === undefined ? null : assignmentOf(store, task.id, now, workAccess(), evidenceRoot);
5276
5286
  const assignment = freshAssignment(recordedAssignment, result ?? null);
5277
5287
  const assignmentStatus = assignment === null ? status : assignmentStatusOf(assignment, status);
5278
5288
  if (status.views.includes("running") && !assignmentStatus.views.includes("running"))
@@ -5498,7 +5508,7 @@ export function createDecisionServer(options) {
5498
5508
  const planRevisions = ref === null ? null : revisionLedgerOf(ref.id);
5499
5509
  const milestoneProgress = ref === null ? null : progressOf(ref.id, planRevisions?.current?.document ?? null);
5500
5510
  const family = familyOf(taskId);
5501
- const recordedAssignment = family?.current.id !== taskId ? null : assignmentOf(store, taskId, now, { principal: "operator", repos: admissionList(), includeUnplaced: visible(null) }, evidenceRoot);
5511
+ const recordedAssignment = family?.current.id !== taskId ? null : assignmentOf(store, taskId, now, workAccess(), evidenceRoot);
5502
5512
  const assignment = freshAssignment(recordedAssignment, completion?.receipt ?? null);
5503
5513
  const progressRun = runs.find(one => runIsLive(one))?.id ?? completion?.runId ?? null;
5504
5514
  return {
@@ -5799,7 +5809,7 @@ export function createDecisionServer(options) {
5799
5809
  title: family.root.title,
5800
5810
  state: task.state,
5801
5811
  status: view?.status ?? workRowOf({ ...task, repo: ref.repo }, now).status,
5802
- assignment: view?.assignment ?? assignmentOf(store, task.id, now, { principal: "operator", repos: admissionList(), includeUnplaced: visible(null) }, evidenceRoot),
5812
+ assignment: view?.assignment ?? assignmentOf(store, task.id, now, workAccess(), evidenceRoot),
5803
5813
  project: ref.repo === null ? null : projectName(ref.repo),
5804
5814
  now,
5805
5815
  dispatch: diagnoseTaskDispatch(store, taskId, now),
@@ -5827,6 +5837,7 @@ export function createDecisionServer(options) {
5827
5837
  raceTerms: view.raceTerms ?? null,
5828
5838
  revision: view.revision ?? null,
5829
5839
  coordinator: view.coordinator ?? null,
5840
+ repairChain: view.repairChain ?? null,
5830
5841
  },
5831
5842
  decisions: (view?.decisions ?? store.decisionsForTask(ref.id))
5832
5843
  .filter(one => one.state === "open" || one.state === "expired")
@@ -5864,7 +5875,7 @@ export function createDecisionServer(options) {
5864
5875
  }
5865
5876
  if (!publishing.on || who.via !== "cookie" || who.role !== "approver")
5866
5877
  return null;
5867
- const assignment = assignmentOf(store, taskId, clock(), { principal: "operator", repos: admissionList(), includeUnplaced: visible(null) }, evidenceRoot);
5878
+ const assignment = assignmentOf(store, taskId, clock(), workAccess(), evidenceRoot);
5868
5879
  const receipt = assignment?.receipt ?? null;
5869
5880
  if (assignment?.state !== "complete" || receipt === null || store.publicationForRun(receipt.runId) !== null || pullRequestBlocker(store, receipt.runId) !== null)
5870
5881
  return null;
@@ -11478,7 +11489,7 @@ export function createDecisionServer(options) {
11478
11489
  run,
11479
11490
  receipt,
11480
11491
  assignment: (() => {
11481
- const value = assignmentOf(store, taskId, now, { principal: "operator", repos: admissionList(), includeUnplaced: visible(null) }, evidenceRoot);
11492
+ const value = assignmentOf(store, taskId, now, workAccess(), evidenceRoot);
11482
11493
  return value?.receipt?.runId === run.id && value.activeTaskId === taskId ? freshAssignment(value, receipt) : null;
11483
11494
  })(),
11484
11495
  handoff,
@@ -11486,6 +11497,7 @@ export function createDecisionServer(options) {
11486
11497
  terminal,
11487
11498
  publication,
11488
11499
  pullRequestTo: pullRequestTargetOf(run.id),
11500
+ publishing: pullRequestTargetOf(run.id) !== null ? "pull-request" : publishingOf(store, store.refById(run.taskRef)?.repo ?? null).on ? "other" : "off",
11489
11501
  ciFailing: publication !== null && publication.prNumber !== null && store.hasOpenCiEpisode(publication.githubRepo, publication.prNumber),
11490
11502
  files,
11491
11503
  outsideTouches: files.filter(one => one.outsideTouches).map(one => one.path),
@@ -11665,6 +11677,19 @@ export function createDecisionServer(options) {
11665
11677
  });
11666
11678
  });
11667
11679
  // Keep native process custody until shutdown has verified agent/tool exit.
11680
+ // Coding custody does not depend on teammates or the lead's follow pass, so
11681
+ // it closes at once and never waits behind a model turn: a stop must release
11682
+ // the owner record well inside the service's exit window (Oct 2: deploys
11683
+ // over 0.9.11 found it still held by a killed process).
11684
+ let codingClosing = null;
11685
+ const closeCoding = () => {
11686
+ leadClosing = true;
11687
+ return codingClosing ??= Promise.resolve().then(() => coding?.close());
11688
+ };
11689
+ const bounded = (work) => new Promise(done => {
11690
+ const timer = setTimeout(done, SHUTDOWN_WAIT_MS);
11691
+ void Promise.resolve(work).catch(() => { }).finally(() => { clearTimeout(timer); done(); });
11692
+ });
11668
11693
  const closeServer = server.close.bind(server);
11669
11694
  server.close = ((callback) => {
11670
11695
  leadClosing = true;
@@ -11675,7 +11700,7 @@ export function createDecisionServer(options) {
11675
11700
  stream.end();
11676
11701
  chatStreams.clear();
11677
11702
  flowRooms.close();
11678
- void (async () => { await team.close(); await leadMaintenance?.stop(); await coding?.close(); })().then(() => closeServer(callback)).catch(error => {
11703
+ void Promise.all([closeCoding(), bounded(team.close()), bounded(leadMaintenance?.stop())]).then(() => closeServer(callback)).catch(error => {
11679
11704
  if (callback)
11680
11705
  callback(error instanceof Error ? error : Error('Coding session shutdown failed.'));
11681
11706
  else
@@ -11683,7 +11708,7 @@ export function createDecisionServer(options) {
11683
11708
  });
11684
11709
  return server;
11685
11710
  });
11686
- return Object.assign(server, { mintSignInLink });
11711
+ return Object.assign(server, { mintSignInLink, closeCoding });
11687
11712
  }
11688
11713
  // ---- path plumbing ---------------------------------------------------------
11689
11714
  /**
@@ -12103,6 +12128,199 @@ function acceptanceCeremonyHtml(criteria) {
12103
12128
  .join("") +
12104
12129
  `</ul>`);
12105
12130
  }
12131
+ /** "claude" + "opus" → "Claude Opus"; "claude-opus-5-5" → "Claude Opus 5.5"; anything else keeps its exact id. */
12132
+ function agentNameWords(provider, model) {
12133
+ const name = providerName(provider);
12134
+ if (model === null || model === "" || model === "default")
12135
+ return name;
12136
+ const bare = model.startsWith(`${provider}-`) ? model.slice(provider.length + 1) : model;
12137
+ const title = (word) => `${word[0].toUpperCase()}${word.slice(1)}`;
12138
+ if (/^[a-z]+$/.test(bare))
12139
+ return `${name} ${title(bare)}`;
12140
+ const versioned = /^([a-z]+)-(\d+(?:-\d+)*)$/.exec(bare);
12141
+ if (versioned !== null)
12142
+ return `${name} ${title(versioned[1])} ${versioned[2].replace(/-/g, ".")}`;
12143
+ return `${name} ${model}`;
12144
+ }
12145
+ /** The permission a sealed profile grants, in two or three words. */
12146
+ function permissionWordsOf(profile) {
12147
+ if (profile === null || profile === undefined)
12148
+ return null;
12149
+ return profile.provider === "claude"
12150
+ ? profile.permissionArgv === "bypassPermissions" ? "Full access" : "Auto permissions"
12151
+ : profile.provider === "gemini"
12152
+ ? profile.approvalArgv === "yolo" ? "Full access" : "Auto permissions"
12153
+ : profile.sandboxMode === "danger-full-access" ? "Full access" : "Workspace sandbox";
12154
+ }
12155
+ /** One plain sentence for a revision (approval critique, Oct 2):
12156
+ * "Fixes what build #9 missed: <criterion>." The notes, paths and lineage
12157
+ * stay in the Details fold. */
12158
+ function revisionSentence(revision, acceptance) {
12159
+ const build = `build #${revision.sourceRun}`;
12160
+ if (revision.kind === "ci-repair")
12161
+ return `Fixes the checks that failed in ${build}.`;
12162
+ const clean = (text) => oneLineOf(text, 280).replace(/[.\s]+$/, "");
12163
+ if (revision.kind === "criterion-repair") {
12164
+ const ids = revision.comments.flatMap(one => (/Unmet: ([^.]+)\./.exec(one.note)?.[1] ?? "").split(",").map(id => id.trim()).filter(id => id !== ""));
12165
+ const missed = ids.map(id => acceptance.find(one => one.id === id)?.statement ?? null).filter((one) => one !== null);
12166
+ if (missed.length > 0)
12167
+ return `Fixes what ${build} missed: ${missed.map(clean).join("; ")}.`;
12168
+ }
12169
+ const notes = revision.comments.map(one => clean(one.note)).filter(one => one !== "");
12170
+ return notes.length === 0 ? `Revises ${build}.` : `Fixes what ${build} missed: ${notes.join("; ")}.`;
12171
+ }
12172
+ /** A planner's amendment, said plainly and kept in view: approving binds
12173
+ * the amended terms, so this is never folded away. */
12174
+ function approvalAmendmentHtml(view) {
12175
+ if (view === null || view === undefined || "problem" in view || !view.filed && view.revision !== true || view.changes.length === 0)
12176
+ return "";
12177
+ const line = (change) => {
12178
+ switch (change.field) {
12179
+ case "goal": return `Goal was: ${escape(change.before)}`;
12180
+ case "outOfScope": return change.after === null ? "No longer rules anything out" : change.before === null ? `Now rules out: ${escape(change.after)}` : `Won't touch was: ${escape(change.before)}`;
12181
+ case "touches": return `${change.kind === "added" ? "Adds the path" : "Drops the path"} <span class="mono">${escape(change.path)}</span>`;
12182
+ case "acceptance": return change.kind === "added" ? `Adds a check: ${escape(change.after?.statement ?? "")}` : change.kind === "removed" ? `Drops a check: ${escape(change.before?.statement ?? "")}` : `Rewords a check: ${escape(change.after?.statement ?? "")}`;
12183
+ }
12184
+ };
12185
+ const count = `${view.changes.length} change${view.changes.length === 1 ? "" : "s"}`;
12186
+ return (`<div class="approval-amendment" id="contract-amendment">` +
12187
+ `<p><strong>${view.revision === true ? `The plan makes ${count} from your notes` : `The plan makes ${count} to what you filed`}</strong></p>` +
12188
+ `<ul>${view.changes.map(one => `<li>${line(one)}</li>`).join("")}</ul>` +
12189
+ (view.amendment === null ? "" : `<p class="meta">Why: ${escape(view.amendment)}</p>`) +
12190
+ `</div>`);
12191
+ }
12192
+ /**
12193
+ * The approval sheet (approval critique, Oct 2): the plan open, in plain
12194
+ * rows — Goal, Changes, Won't touch, Done when — one line of who builds,
12195
+ * one Approve & start beside the password, and every remaining signed
12196
+ * term (lineage, routing, runtime limits, the seal) in one Details fold.
12197
+ * Presentation only: the same hidden fields, nonce, digest and password
12198
+ * post to the same route, and the task page and chat card share it.
12199
+ */
12200
+ function approvalSheetHtml(input) {
12201
+ const { scope, route, raceTerms } = input;
12202
+ // A machine-drafted repair appends its brief to the signed goal; the row
12203
+ // shows the goal and one sentence says what the repair fixes. The whole
12204
+ // signed goal stays in Details.
12205
+ const repairText = / — (repair exactly the unmet criteria named below; a comment cannot widen the scope\. Unmet: ([^.]+)\.|Collect only the missing observations for ([^.]+)\. .*|Diagnose the saved failed project check and repair within the original scope\. .*)$/s.exec(scope.goal);
12206
+ const goalShown = repairText === null ? scope.goal : scope.goal.slice(0, repairText.index);
12207
+ const statementsOf = (ids) => ids.split(",").map(id => id.trim()).map(id => scope.acceptance.find(one => one.id === id)?.statement ?? id).map(one => one.replace(/[.\s]+$/, "")).join("; ");
12208
+ const repairBuild = input.revision?.sourceRun ?? input.repairChain?.sourceRun ?? null;
12209
+ const builtBy = repairBuild === null ? "the last build" : `build #${repairBuild}`;
12210
+ const sentence = input.revision !== null && (repairText === null || input.revision.comments.length > 0) ? revisionSentence(input.revision, scope.acceptance)
12211
+ : repairText === null ? null
12212
+ : repairText[2] !== undefined ? `Fixes what ${builtBy} missed: ${statementsOf(repairText[2])}.`
12213
+ : repairText[3] !== undefined ? `Collects the evidence ${builtBy} was missing for: ${statementsOf(repairText[3])}. The saved code stays the same.`
12214
+ : `Fixes the checks that failed in ${builtBy}.`;
12215
+ const plan = input.planDocument === null ? null : parseExecutionPlanDocument(input.planDocument);
12216
+ const milestones = plan !== null && plan.ok ? plan.document.milestones : [];
12217
+ const permission = permissionWordsOf(scope.profile);
12218
+ const money = (micro) => `$${(micro / 1_000_000).toFixed(2)}`;
12219
+ const row = (label, body) => `<div class="approval-row"><dt>${label}</dt><dd>${body}</dd></div>`;
12220
+ const changes = (milestones.length > 0 ? `<ol class="approval-steps">${milestones.map(one => `<li>${escape(one)}</li>`).join("")}</ol>` : "") +
12221
+ (scope.touches.length > 0
12222
+ ? `<p>Only in these paths:</p>${approvalPathsHtml(scope.touches)}`
12223
+ : `<p${milestones.length > 0 ? ` class="meta"` : ""}>Any file in the project.</p>`);
12224
+ const doneWhen = scope.acceptance.length === 0
12225
+ ? `<p>You decide when you review the result.</p>`
12226
+ : `<ul class="approval-done">${scope.acceptance.map(one => `<li>${escape(one.statement)}</li>`).join("")}</ul>`;
12227
+ // Who builds, in one line: the builder and planner by name, the cap, and full access when granted.
12228
+ const legs = route?.projection?.legs ?? [];
12229
+ const legOf = (phase) => legs.find(one => one.phase === phase);
12230
+ const who = [];
12231
+ const builder = legOf("build");
12232
+ if (raceTerms === null) {
12233
+ if (builder !== undefined)
12234
+ who.push(`Builder ${agentNameWords(builder.provider, builder.model)}`);
12235
+ else if (route?.legacy != null)
12236
+ who.push(`Builder ${agentNameWords(route.legacy.provider, route.legacy.model)}`);
12237
+ else if (scope.profile != null)
12238
+ who.push(`Builder ${agentNameWords(scope.profile.provider, scope.profile.model)}`);
12239
+ }
12240
+ const planner = legOf("plan");
12241
+ if (planner !== undefined)
12242
+ who.push(`Planner ${agentNameWords(planner.provider, planner.model)}`);
12243
+ if (scope.budgetMicrousd !== null)
12244
+ who.push(`Cap ${money(scope.budgetMicrousd)} per attempt`);
12245
+ if (permission === "Full access")
12246
+ who.push("Full access");
12247
+ const race = raceTerms === null
12248
+ ? ""
12249
+ : `<p class="approval-race">${raceTerms.n} agents build this separately: ${raceTerms.agents.map(one => escape(agentNameWords(one.provider, one.model))).join(" and ")}. ` +
12250
+ (raceTerms.kind === "comparison"
12251
+ ? "No dollar cap; you compare the results and pick one.</p>"
12252
+ : `Each may spend ${money(raceTerms.perAgentBudgetMicrousd)} plus a ${money(raceTerms.overrunReserveMicrousd)} reserve, ${money(raceTerms.totalBudgetMicrousd)} in all; you compare the results and pick one.</p>`);
12253
+ const after = input.deliverable === "report"
12254
+ ? "An agent investigates without changing the repository. You'll hear when its report is ready."
12255
+ : raceTerms !== null
12256
+ ? "Each agent starts in its own branch. You'll hear when the results are ready to compare."
12257
+ : "An agent starts in its own branch. You'll hear when it's ready to review.";
12258
+ const submit = raceTerms === null ? "Approve & start" : raceTerms.kind === "comparison" ? "Approve comparison" : "Approve tournament";
12259
+ // Everything else the digest binds, one tap away.
12260
+ const detail = (title, body, attrs = "") => body === "" ? "" : `<section class="approval-detail"${attrs}><h3>${title}</h3>${body}</section>`;
12261
+ const revisionDetail = input.revision === null ? "" : detail("Earlier build", `<div class="revision-card" data-revision-feedback>` +
12262
+ `<p><a href="${escape(input.revisionSourceHref)}"${input.surface === "chat" ? " data-revision-source" : ""}>${input.surface === "chat" ? `Original result: build #${input.revision.sourceRun} →` : `build #${input.revision.sourceRun}`}</a></p>` +
12263
+ (input.revision.comments.length === 0 ? "" : `<ul>${input.revision.comments.map(one => `<li>${one.path === null ? "" : `<span class="mono">${escape(one.path)}${one.line === null ? "" : `:${one.line}`}</span> · `}${escape(one.note)} <span class="meta">— ${escape(one.author)}</span></li>`).join("")}</ul>`) +
12264
+ revisionLineageHtml(input.revision.lineage) +
12265
+ `</div>`);
12266
+ const contract = input.planContract;
12267
+ const contractDetail = input.planDocument === null || approvalAmendmentHtml(contract) !== "" ? ""
12268
+ : contract === null || contract === undefined ? `<p class="meta">No record compares this plan with what was filed. Check the rows above against your request.</p>`
12269
+ : "problem" in contract ? `<p class="meta">${escape(contract.problem)} · <a href="/r/${contract.run}">run ${contract.run}</a></p>`
12270
+ : !contract.filed && contract.revision !== true ? `<p class="meta">Nothing was filed before planning, so every term is the planner's proposal.</p>`
12271
+ : `<p class="meta">${contract.revision === true ? "Same terms as the previous version." : "The plan keeps exactly what you filed."} <a href="/r/${contract.run}">Planning run ${contract.run}</a></p>`;
12272
+ const criteriaDetail = scope.acceptance.length === 0 ? ""
12273
+ : `<ul class="approval-signed-criteria">${scope.acceptance.map(one => `<li><code>${escape(one.id)}</code> ${escape(one.statement)} <span class="meta">needs ${one.evidence.map(escape).join(", ")}</span></li>`).join("")}</ul>`;
12274
+ const projection = route?.projection ?? null;
12275
+ const agentsDetail = route === null || route === undefined ? ""
12276
+ : projection === null
12277
+ ? route.legacy === null ? "" : `<p>${escape(agentsSummaryWords(route))}</p>`
12278
+ : `<p>${escape(projection.summary)}</p>` +
12279
+ `<p class="meta">${escape(riskTitle(route.riskLevel))}: ${escape(riskConsequence(route.riskLevel))}. Uses ${escape(projection.postureWords)}.</p>` +
12280
+ (projection.demands.length === 0 ? "" : `<ul class="meta">${projection.demands.map(one => `<li>${escape(one)}</li>`).join("")}</ul>`) +
12281
+ `<dl class="approval-roles">${projection.legs.map(leg => `<div><dt>${escape(ROLE_NOUN[leg.phase])}</dt><dd><span class="mono">${escape(leg.provider)} · ${escape(leg.model)}</span> <span class="meta">${escape(chosenWords(leg))}</span>` +
12282
+ `<ul>${leg.reasons.map(reason => `<li>${escape(reason)}</li>`).join("")}${leg.problem === null ? "" : `<li><strong>${escape(leg.problem)}</strong></li>`}</ul></dd></div>`).join("")}</dl>` +
12283
+ `<p class="meta">These exact agents are part of what you approve. Changing any of them asks for a fresh approval.</p>`;
12284
+ const limits = profileWords(scope) +
12285
+ `<p class="meta">Checks level: ${escape(qualityModeTitle(scope.qualityMode ?? "default"))}${permission === null ? "" : ` · ${escape(permission)}`}</p>` +
12286
+ (scope.budgetMicrousd === null ? "" : `<p class="meta">Each build attempt has a ${money(scope.budgetMicrousd)} agent-reported usage cap. On a subscription this limits work; it is not an API charge.</p>`);
12287
+ const details = `<details class="approval-details"><summary>Details</summary>` +
12288
+ revisionDetail +
12289
+ (repairText === null ? "" : detail("Signed goal", `<p>${escape(scope.goal)}</p>`)) +
12290
+ detail("Plan record", contractDetail) +
12291
+ detail("Signed criteria", criteriaDetail) +
12292
+ detail("Why these agents", agentsDetail, ` id="${input.surface === "task" ? "approval-agents" : "chat-approval-agents"}"`) +
12293
+ detail("Runtime limits", limits) +
12294
+ (scope.candidate ? detail("Saved commit", `<p><code class="approval-commit">${escape(scope.candidate)}</code></p>`) : "") +
12295
+ detail("Seal", `<p class="meta">Approval binds to this exact wording. <span class="seal mono">signs ${shortDigest(scope.digest)}</span></p>`) +
12296
+ `</details>`;
12297
+ return (`<form method="post" action="${input.action}" class="approve-form approval-sheet"${input.surface === "task" ? ` id="approve"` : ""}${input.sticky ? " data-sticky" : ""}>` +
12298
+ `<input type="hidden" name="csrf" value="${escape(input.csrf)}">` +
12299
+ `<input type="hidden" name="nonce" value="${escape(input.nonce)}">` +
12300
+ `<input type="hidden" name="digest" value="${escape(input.digest)}">` +
12301
+ (input.returnTo === null ? "" : `<input type="hidden" name="return" value="${escape(input.returnTo)}">`) +
12302
+ `<input type="text" name="username" autocomplete="username" class="visually-hidden" tabindex="-1" aria-hidden="true">` +
12303
+ `<h2 class="approval-sheet-title">${input.deliverable === "report" ? "The investigation" : "The plan"}</h2>` +
12304
+ (sentence === null ? "" : `<p class="approval-revision">${escape(sentence)}</p>`) +
12305
+ (input.coordinator === null ? "" : `<p class="approval-note">An agent filed this: <span class="mono">${escape(input.coordinator.label)}</span>${input.coordinator.filedAgo === null ? "" : `, ${escape(input.coordinator.filedAgo)}`}. Nothing runs until you approve, and approving runs its request.</p>`) +
12306
+ (input.deliverable === "report" ? `<p class="approval-note">Read-only: it reports back and changes nothing in the repository.</p>` : "") +
12307
+ `<dl class="approval-rows">` +
12308
+ row("Goal", `<p class="approval-goal">${escape(goalShown)}</p>`) +
12309
+ row("Changes", changes) +
12310
+ row("Won’t touch", `<p>${scope.outOfScope === null ? "Nothing is ruled out." : escape(scope.outOfScope)}</p>`) +
12311
+ row("Done when", doneWhen) +
12312
+ `</dl>` +
12313
+ approvalAmendmentHtml(contract) +
12314
+ race +
12315
+ (who.length === 0 ? "" : `<p class="approval-who">${who.map(escape).join(" · ")}</p>`) +
12316
+ `<div class="approval-act" id="${input.surface === "task" ? "approval-confirm" : "chat-approval-confirm"}">` +
12317
+ `<label class="approval-password"><span class="visually-hidden">Your password</span><input type="password" name="token" autocomplete="current-password" placeholder="Your password"></label>` +
12318
+ `<button type="submit" data-primary-action>${submit}</button></div>` +
12319
+ `<p class="approval-after">${after}</p>` +
12320
+ `<p class="approval-secondary"><a class="approval-link" href="${escape(input.editHref)}">Edit plan</a><a class="approval-link" href="${escape(input.notNowHref)}">Not now</a></p>` +
12321
+ details +
12322
+ `</form>`);
12323
+ }
12106
12324
  function consentDoorOf(scope, route) {
12107
12325
  if (scope === null)
12108
12326
  return { open: false, title: "There is no scope to approve yet", why: "write the scope first", road: "scope" };
@@ -12374,6 +12592,8 @@ function agentsStripHtml(view, taskId) {
12374
12592
  }
12375
12593
  /** The demo's one-line promise, on every page. */
12376
12594
  const DEMO_BANNER = `Demo: a scripted lead and sample projects. Nothing calls a model, reaches outside or spends. For your own project, run ${START_COMMAND} in its folder.`;
12595
+ /** The demo notice on a phone: one line. */
12596
+ const DEMO_BANNER_SHORT = "Demo: sample projects. Nothing calls a model or spends.";
12377
12597
  /** Every character that could open a tag or an attribute, dead at the sink. */
12378
12598
  function escape(text) {
12379
12599
  return text
@@ -12864,7 +13084,7 @@ ${THEME_DARK}
12864
13084
  /* Thumbnails, never a full-width poster: auto-fill leaves a lone
12865
13085
  screenshot at thumbnail size (UI polish 2026-09-13). */
12866
13086
  .receipt-visuals { display: grid; grid-template-columns: repeat(auto-fill, minmax(8rem, 14rem)); gap: .55rem; margin-top: .75rem; }
12867
- .receipt-shot { display: grid; gap: .35rem; color: var(--muted-foreground); font-size: .7rem; text-decoration: none; }
13087
+ .receipt-shot { display: grid; gap: .35rem; color: var(--muted-foreground); font-size: .75rem; text-decoration: none; }
12868
13088
  .receipt-shot img { display: block; width: 100%; aspect-ratio: 16 / 10; object-fit: cover; border: 1px solid var(--glass-border); border-radius: calc(var(--radius) - 3px); background: var(--muted); }
12869
13089
  @media (hover: hover) and (pointer: fine) { .receipt-shot:hover { color: var(--foreground); } }
12870
13090
  .receipt-caveats, .receipt-coverage { margin-top: .8rem; padding: .7rem .8rem; border-left: 1px solid var(--border); border-radius: 0 calc(var(--radius) - 3px) calc(var(--radius) - 3px) 0; background: color-mix(in srgb, var(--muted) 62%, transparent); font-size: .78rem; }
@@ -12997,7 +13217,7 @@ ${THEME_DARK}
12997
13217
  }
12998
13218
  .diff-file[open] > summary::before { transform: rotate(45deg) translateY(-.1rem); }
12999
13219
  .diff-file-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font: 500 .75rem/1.4 var(--font-mono); }
13000
- .diff-file-counts { display: inline-flex; gap: .45rem; flex: none; margin-left: auto; font: 500 .68rem/1 var(--font-mono); }
13220
+ .diff-file-counts { display: inline-flex; gap: .45rem; flex: none; margin-left: auto; font: 500 .75rem/1 var(--font-mono); }
13001
13221
  .diff-file-counts b { color: var(--success); font-weight: 500; }
13002
13222
  .diff-file-counts i { color: var(--destructive); font-style: normal; }
13003
13223
  .diff-rename { margin: 0; padding: .4rem .85rem; border-top: 1px solid var(--glass-border); }
@@ -13005,12 +13225,12 @@ ${THEME_DARK}
13005
13225
  .diff-hunk-head {
13006
13226
  overflow-x: auto; padding: .42rem .85rem; border-top: 1px solid var(--glass-border); border-bottom: 1px solid var(--glass-border);
13007
13227
  background: color-mix(in srgb, var(--running) 8%, var(--card)); color: color-mix(in srgb, var(--running) 72%, var(--foreground));
13008
- font: 500 .68rem/1.4 var(--font-mono); white-space: pre;
13228
+ font: 500 .75rem/1.4 var(--font-mono); white-space: pre;
13009
13229
  }
13010
13230
  .diff-lines { max-width: 100%; overflow-x: auto; background: color-mix(in srgb, var(--background) 48%, var(--card)); }
13011
13231
  .diff-line {
13012
13232
  display: grid; grid-template-columns: 2rem 3.2rem 3.2rem minmax(max-content, 1fr); align-items: stretch;
13013
- min-width: max-content; min-height: 1.8rem; font: 400 .72rem/1.55 var(--font-mono);
13233
+ min-width: max-content; min-height: 1.8rem; font: 400 .75rem/1.55 var(--font-mono);
13014
13234
  }
13015
13235
  @media (hover: hover) and (pointer: fine) { .diff-line:hover { background: color-mix(in srgb, var(--foreground) 4%, transparent); } }
13016
13236
  .diff-line code { display: flex; min-width: 0; padding: .3rem .75rem .3rem .6rem; color: inherit; white-space: pre; }
@@ -13548,7 +13768,9 @@ ${THEME_DARK}
13548
13768
  .approval-title { display: grid; gap: .15rem; }
13549
13769
  .approval-kicker { color: var(--muted-foreground); font: 500 .66rem/1.3 var(--font-mono); letter-spacing: .06em; text-transform: uppercase; }
13550
13770
  .approval-title strong { font-size: 1.05rem; letter-spacing: -.02em; }
13551
- .approval-label { margin: .9rem 0 .2rem; color: var(--muted-foreground); font: 500 .66rem/1.3 var(--font-mono); letter-spacing: .06em; text-transform: uppercase; }
13771
+ .approval-label { display: block; margin: .9rem 0 .2rem; color: var(--muted-foreground); font: 500 12px/1.4 var(--font-sans); }
13772
+ .approval-label::first-letter, .planner-plan .eyebrow::first-letter { text-transform: uppercase; }
13773
+ .planner-plan .eyebrow { font: 500 12px/1.4 var(--font-sans); letter-spacing: 0; text-transform: none; }
13552
13774
  .approval-goal { margin: 0; font-size: 1rem; line-height: 1.55; white-space: pre-wrap; }
13553
13775
  .approval-boundaries { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .6rem; margin-top: .75rem; }
13554
13776
  .approval-boundary { padding: .7rem .8rem; border: 1px solid var(--glass-border); border-radius: calc(var(--radius) - 3px); background: color-mix(in srgb, var(--muted) 45%, transparent); }
@@ -13634,6 +13856,66 @@ ${THEME_DARK}
13634
13856
  .approval-confirm { scroll-margin-top: 5rem; }
13635
13857
  .approval-paths { margin: 0; padding: 0; list-style: none; display: grid; gap: .15rem; }
13636
13858
  .approval-paths li { color: var(--muted-foreground); font-size: .8rem; overflow-wrap: anywhere; }
13859
+ /* The approval sheet (approval critique, Oct 2): the plan open in plain
13860
+ rows, who builds in one line, one Approve & start beside the password,
13861
+ and every other signed term in one Details fold at the end. Sentence
13862
+ case, Geist, 12px and up; prose capped at 75ch. */
13863
+ .approval-sheet { display: grid; gap: 16px; margin: 0; padding: 20px; border: 1px solid var(--border); border-radius: 10px; background: var(--card); }
13864
+ .approval-sheet.approve-form > :not(input) { margin: 0; }
13865
+ .approval-sheet-title { margin: 0; font-size: 15px; font-weight: 600; line-height: 1.4; letter-spacing: -.01em; }
13866
+ .approval-revision, .approval-note, .approval-race { max-width: 75ch; font-size: 14px; line-height: 1.6; overflow-wrap: anywhere; }
13867
+ .approval-note { color: var(--muted-foreground); }
13868
+ .approval-rows { display: grid; gap: 14px; margin: 0; }
13869
+ .approval-row { display: grid; grid-template-columns: 7.5rem minmax(0, 1fr); gap: 16px; }
13870
+ .approval-row dt { margin: 0; color: var(--muted-foreground); font-size: 13px; font-weight: 500; line-height: 1.6; }
13871
+ .approval-row dd { min-width: 0; max-width: 75ch; margin: 0; font-size: 14px; line-height: 1.6; overflow-wrap: anywhere; }
13872
+ .approval-row dd p, .approval-sheet .approval-goal { margin: 0; font-size: 14px; line-height: 1.6; }
13873
+ .approval-row dd :is(ol, ul) { display: grid; gap: 4px; margin: 0; padding-left: 1.25rem; }
13874
+ .approval-row dd ol + p { margin-top: 6px; }
13875
+ .approval-row dd .approval-paths { margin-top: 2px; padding-left: 0; }
13876
+ .approval-row dd .approval-paths li { font-size: 13px; }
13877
+ .approval-amendment { max-width: 75ch; padding: 12px 14px; border-radius: 8px; background: var(--muted); font-size: 14px; line-height: 1.55; overflow-wrap: anywhere; }
13878
+ .approval-amendment p { margin: 0; }
13879
+ .approval-amendment ul { display: grid; gap: 4px; margin: 6px 0; padding-left: 1.25rem; }
13880
+ .approval-who { max-width: 75ch; color: var(--muted-foreground); font-size: 13px; line-height: 1.5; }
13881
+ .approval-act { display: grid; grid-template-columns: minmax(0, 20rem) auto; justify-content: start; align-items: center; gap: 8px; }
13882
+ .approval-password { margin: 0; }
13883
+ .approval-act input[type=password] { margin: 0; font-family: var(--font-sans); }
13884
+ .approval-act button { margin: 0; min-height: 2.125rem; white-space: nowrap; }
13885
+ .approval-after { max-width: 75ch; color: var(--muted-foreground); font-size: 13px; line-height: 1.5; }
13886
+ .approval-secondary { display: flex; flex-wrap: wrap; gap: 8px; }
13887
+ .approval-link { display: inline-flex; align-items: center; min-height: 32px; padding: 0 12px; border: 1px solid var(--border); border-radius: 8px; background: var(--card); color: var(--foreground); font-size: 13px; font-weight: 500; text-decoration: none; }
13888
+ .approval-link + .approval-link { border-color: transparent; background: none; color: var(--muted-foreground); }
13889
+ @media (hover: hover) and (pointer: fine) { .approval-link:hover { background: var(--muted); color: var(--foreground); } }
13890
+ details.approval-details { margin: 0; padding: 0; border: 0; border-top: 1px solid var(--border); border-radius: 0; background: none; }
13891
+ details.approval-details[open] { padding-bottom: 0; }
13892
+ .approval-details > summary { display: flex; align-items: center; min-height: 40px; padding: 4px 0 0; font-size: 13px; }
13893
+ .approval-detail { max-width: 75ch; margin: 12px 0 0; font-size: 13px; line-height: 1.55; overflow-wrap: anywhere; }
13894
+ .approval-detail h3 { margin: 0 0 4px; font-size: 13px; font-weight: 600; letter-spacing: 0; text-transform: none; }
13895
+ .approval-detail p, .approval-detail ul, .approval-detail dl { margin: 4px 0; }
13896
+ .approval-detail ul { padding-left: 1.25rem; }
13897
+ .approval-detail .revision-card { margin: 0; padding: 0; border: 0; background: none; }
13898
+ .approval-roles { display: grid; gap: 8px; }
13899
+ .approval-roles > div { display: grid; grid-template-columns: 6rem minmax(0, 1fr); gap: 12px; }
13900
+ .approval-roles dt { color: var(--muted-foreground); font-weight: 500; }
13901
+ .approval-roles dd { margin: 0; }
13902
+ .approval-roles dd ul { margin: 2px 0 0; color: var(--muted-foreground); }
13903
+ .chat-plan .approval-sheet { padding: 0; border: 0; background: none; }
13904
+ @media (max-width: 760px) {
13905
+ .approval-sheet { gap: 12px; padding: 14px; }
13906
+ .approval-row { grid-template-columns: 1fr; gap: 2px; }
13907
+ .approval-act { grid-template-columns: 1fr; }
13908
+ .approval-act input[type=password], .approval-act button { min-height: 44px; font-size: 16px; }
13909
+ .approval-act button { width: 100%; }
13910
+ /* The one act stays under the thumb: sticky at the bottom, clear of the home indicator. */
13911
+ .approval-sheet[data-sticky] .approval-act {
13912
+ position: sticky; bottom: 0; z-index: 20; margin: 0 -14px; padding: 10px 14px calc(10px + env(safe-area-inset-bottom, 0px));
13913
+ border-top: 1px solid var(--border); background: var(--card);
13914
+ }
13915
+ .approval-link { min-height: 44px; padding: 0 16px; }
13916
+ .approval-details > summary { min-height: 44px; }
13917
+ .approval-roles > div { grid-template-columns: 1fr; gap: 2px; }
13918
+ }
13637
13919
  .run-facts-details { margin-top: 1.25rem; }
13638
13920
  .run-facts-details > summary { display: flex; align-items: center; justify-content: space-between; gap: .75rem; }
13639
13921
  .run-facts-details > summary .meta { font-size: .75rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@@ -13867,7 +14149,7 @@ ${THEME_DARK}
13867
14149
  .result-pin[open] > summary::before { content: "▾"; }
13868
14150
  .result-pin > summary .meta { font-weight: 400; }
13869
14151
  .result-pin .diff-comment-target { margin: .25rem 0 .5rem; }
13870
- .chat-approval-section, .revision-card, .revision-lineage, .task-history, .diff-comment p { min-width: 0; overflow-wrap: anywhere; }
14152
+ .revision-card, .revision-lineage, .task-history, .diff-comment p { min-width: 0; overflow-wrap: anywhere; }
13871
14153
  .result-feedback-history > summary, .task-history > summary, .task-history a { display: inline-flex; align-items: center; min-height: 44px; padding: .25rem .5rem; }
13872
14154
  .task-history > summary::before { content: "▸"; margin-right: .35rem; }
13873
14155
  .task-history[open] > summary::before { content: "▾"; }
@@ -14332,11 +14614,10 @@ ${THEME_DARK}
14332
14614
  .check-progress[data-final="passed"] { color: var(--success); }
14333
14615
  .check-progress[data-final="failed"] { color: var(--destructive); }
14334
14616
  .check-progress[data-final="unknown"] { color: var(--warning); }
14335
- .receipt-actions [data-primary-action], .task-plan-review .button-link { min-height: 44px; }
14617
+ .receipt-actions [data-primary-action] { min-height: 44px; }
14336
14618
  .task-status-reason { margin-top: .75rem; }
14337
14619
  .task-status-reason summary, .task-status-details > summary { color: var(--muted-foreground); }
14338
14620
  .task-status-details > summary { min-height: 44px; }
14339
- .task-plan-review > summary { min-height: 44px; display: flex; align-items: center; cursor: pointer; font-weight: 650; }
14340
14621
  .task-status-details, #task-control-details { margin: 1rem 0; }
14341
14622
  .task-control-copy { min-width: 0; overflow-wrap: anywhere; }
14342
14623
  [data-primary-action] { max-width: 100%; }
@@ -14351,16 +14632,9 @@ ${THEME_DARK}
14351
14632
  .chat-action-card > summary strong { font-size: .92rem; }
14352
14633
  .chat-action-card > summary small { color: var(--muted-foreground); font-size: .68rem; font-weight: 400; }
14353
14634
  .chat-action-card[open] > summary { border-bottom: 1px solid var(--border); }
14354
- .chat-approval-form { display: grid; gap: .85rem; padding: 1.05rem; }
14355
- .chat-approval-section { display: grid; gap: .3rem; }
14356
- .chat-approval-section > p, .chat-approval-section > pre { margin: 0; }
14357
- .chat-approval-form .approval-boundaries { margin: 0; }
14358
14635
  .chat-run-details { padding: .65rem .75rem; border: 1px solid var(--border); border-radius: calc(var(--radius) - 3px); background: color-mix(in srgb,var(--muted) 45%,transparent); }
14359
14636
  .chat-run-details > summary { color: var(--muted-foreground); cursor: pointer; font-size: .68rem; }
14360
14637
  .chat-run-details > .meta { margin-bottom: 0; }
14361
- .chat-approval-form .approval-confirm { align-items: end; margin-top: .15rem; }
14362
- .chat-approval-form .approval-confirm label { flex: 1 1 18rem; }
14363
- .chat-approval-form .approval-confirm button { min-height: 2.65rem; }
14364
14638
  .chat-decisions { display: grid; gap: .7rem; }
14365
14639
  .chat-section-head { margin: .15rem .15rem 0; }
14366
14640
  .chat-section-head h2 { margin: .2rem 0 0; font-size: 1rem; }
@@ -14557,18 +14831,9 @@ ${THEME_DARK}
14557
14831
  /* The concise plan (package 2): a title, the outcome, one line of
14558
14832
  counts and limits, then one Review plan disclosure over the unchanged
14559
14833
  exact-terms form. */
14560
- .chat-plan-head h2 { margin: 0; font-size: 1rem; letter-spacing: -.025em; }
14561
- .chat-plan-outcome { margin: .65rem 0 .55rem; font-size: .88rem; line-height: 1.5; overflow-wrap: anywhere; }
14562
- .chat-plan-facts { margin: 0; color: var(--muted-foreground); font-size: .74rem; line-height: 1.5; overflow-wrap: anywhere; }
14563
- .chat-plan > details.chat-approval { margin: .85rem 0 0; padding: .85rem 0 0; border: 0; border-top: 1px solid var(--border); border-radius: 0; background: transparent; }
14564
- .chat-plan > details.chat-approval > summary { display: flex; align-items: center; gap: .75rem; cursor: pointer; list-style: none; }
14565
- .chat-plan > details.chat-approval > summary::-webkit-details-marker { display: none; }
14566
- .chat-plan > details.chat-approval > summary .button-link { flex: 0 0 auto; white-space: nowrap; min-height: 44px; }
14567
- .chat-plan > details.chat-approval > .chat-approval-form { padding: .85rem 0 0; }
14568
- .chat-plan .chat-approval-lead { margin: 0 0 .85rem; }
14569
14834
  .chat-approval-stale { margin: .75rem 0 0; }
14570
14835
  .chat-approval-stale a { font-weight: 600; }
14571
- form.approve-form[data-stale="1"] .approval-confirm { opacity: .55; }
14836
+ form.approve-form[data-stale="1"] .approval-confirm, form.approve-form[data-stale="1"] .approval-act { opacity: .55; }
14572
14837
  .proposal-filed { display: flex; align-items: center; flex-wrap: wrap; gap: .6rem; margin: .5rem 0 0; }
14573
14838
  .chat-thinking { display: flex; align-items: center; gap: .75rem; padding: .75rem .85rem; }
14574
14839
  .chat-thinking p { flex: 1; margin: 0; }
@@ -14695,10 +14960,6 @@ ${THEME_DARK}
14695
14960
  .chat-action-card > summary { align-items: flex-start; padding: .85rem; }
14696
14961
  .chat-action-card > summary .button-link { min-height: 2.1rem; padding-inline: .6rem; font-size: .65rem; white-space: nowrap; }
14697
14962
  .chat-action-card > summary small { max-width: 14rem; }
14698
- .chat-approval-form { padding: .85rem; }
14699
- .chat-approval-form .approval-boundaries { grid-template-columns: 1fr; }
14700
- .chat-approval-form .approval-confirm { display: grid; }
14701
- .chat-approval-form .approval-confirm button { width: 100%; }
14702
14963
  .chat-budget { gap: .3rem; margin: .65rem 0 .9rem; }
14703
14964
  .chat-budget > span { padding: .25rem .48rem; }
14704
14965
  .chat-budget > span:first-child { max-width: 100%; overflow: hidden; text-overflow: ellipsis; }
@@ -15012,6 +15273,15 @@ button { min-height: 44px; }
15012
15273
  chips flow after it, never above the name. */
15013
15274
  .queue-card p.row > a:first-of-type { flex: 1 1 12rem; min-width: 0; }
15014
15275
  }
15276
+ /* A phone reads a long changed line wrapped, never scrolled sideways: the
15277
+ line keeps its numbers and marker, the code column takes what is left. */
15278
+ @media (max-width: 760px) {
15279
+ .diff-lines { overflow-x: visible; }
15280
+ .diff-line, .diff-review .diff-line { min-width: 0; grid-template-columns: 2.75rem 2.65rem 2.65rem minmax(0, 1fr); }
15281
+ .diff-review[data-mode="view"] .diff-line { grid-template-columns: 0 2.65rem 2.65rem minmax(0, 1fr); }
15282
+ .diff-line code { white-space: pre-wrap; overflow-wrap: anywhere; }
15283
+ .diff-hunk-head { overflow-x: visible; white-space: pre-wrap; overflow-wrap: anywhere; }
15284
+ }
15015
15285
  @media (max-width: 30rem) {
15016
15286
  .dependency-repair-actions .dependency-repair-replace { grid-template-columns: minmax(0, 1fr); }
15017
15287
  .dependency-repair-actions .dependency-repair-replace > button[type=submit] { width: 100%; }
@@ -15155,6 +15425,7 @@ const INBOX_TABS_CSS = '.inbox-tabs{display:inline-flex;gap:2px;max-width:100%;o
15155
15425
  '.inbox-tabs a[aria-current="page"]{background:var(--so-paper);color:var(--so-ink);box-shadow:var(--so-pill-shadow)}' +
15156
15426
  '.inbox-tab-count{min-width:18px;padding:0 5px;border-radius:9999px;font:500 11px/18px var(--so-mono,ui-monospace,monospace);font-variant-numeric:tabular-nums;text-align:center}' +
15157
15427
  '.inbox-tab-count--needs{background:var(--so-signal);color:var(--so-on-signal)}' +
15428
+ '.inbox-ask{margin:18px 0 0}.inbox-ask>h2{display:flex;align-items:baseline;gap:8px;margin:0 0 4px;font-size:15px;font-weight:600}.inbox-ask>h2 .count{font:500 12px var(--so-mono,ui-monospace,monospace);font-variant-numeric:tabular-nums;color:var(--so-muted)}.inbox-ask h3{font-size:13px;font-weight:600;margin:12px 0 4px}' +
15158
15429
  '.inbox-unread{display:none;position:absolute;top:4px;right:3px;width:6px;height:6px;border-radius:50%;background:var(--so-signal)}' +
15159
15430
  '@media (max-width:760px){.inbox-tabs{display:flex;width:100%}.inbox-tabs a{flex:1;justify-content:center;min-height:44px;padding:0 6px}.inbox-unread{display:block}}';
15160
15431
  const WORKSPACE_STYLE = styleAsset(STYLE + INBOX_TABS_CSS + APPROVAL_RULES_CSS + SPEND_CSS + RETENTION_CSS + STORAGE_CSS + UPDATES_CSS + LIMITS_CSS + MONITORING_CSS + INTEGRATIONS_CSS + BACKUP_CSS + EXPORT_CSS + PROJECT_DELETE_CSS + POLICY_CSS + EVIDENCE_PACK_CSS + THEME_CONTROLS_CSS + CODING_CSS + CODING_SHIPPING_CSS + RECIPE_CSS + SKILLS_CSS + TOOLS_CSS + FLOWS_CSS + TEAMMATE_CSS + KITS_CSS + STARTERS_CSS + GALLERY_CSS + SSO_CSS + CREDENTIALS_CSS + KNOWLEDGE_CSS + MODELS_CSS + CHAT_POLISH_CSS + TRANSITIONS_CSS + WORKSPACE_MOTION_CSS + ASSIGNMENT_CSS + TASK_STATUS_CSS + LEAD_CONTEXT_CSS + PULL_REQUEST_SETTINGS_CSS + CHECK_SETTINGS_CSS + '.learning{min-width:0;overflow-wrap:anywhere}.learning .card{min-width:0}.learning code,.learning blockquote,.learning pre{white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word}.learning button,.learning summary,.learning .button-link{min-height:44px}.learning button{white-space:nowrap}.learning summary{padding:12px 0;cursor:pointer}.learning form{margin:12px 0}.learning select{max-width:100%}.learning blockquote{margin:8px 0}.learning ul{padding-left:20px}');
@@ -15880,13 +16151,17 @@ function inboxPage(chrome, data) {
15880
16151
  const listRows = (rows) => rows.map(one => `<p class="row"><a href="${taskHref(one.taskId)}">${escape(one.title)}</a>${chip(one.repo)} <span class="meta">${escape(one.detail)}</span></p>`).join("\n");
15881
16152
  const ready = (data.ready ?? []).length === 0 ? "" : `<h2>ready to review</h2>` + listRows(data.ready ?? []);
15882
16153
  const running = (data.running ?? []).length === 0 ? "" : `<h2>running now</h2>` + listRows(data.running ?? []);
15883
- const counts = {
15884
- "needs-you": data.decisions.length + data.approvals.length + data.requeueables.length + data.cancelledBlockers.length + data.gaps.length,
16154
+ const asks = { decide: data.decisions.length + data.approvals.length, unblock: data.requeueables.length + data.cancelledBlockers.length + data.gaps.length };
16155
+ const counts = { ...asks,
16156
+ "needs-you": asks.decide + asks.unblock,
15885
16157
  ready: data.needsVerification.length + (data.ready ?? []).length,
15886
16158
  running: (data.running ?? []).length,
15887
16159
  all: 0,
15888
16160
  };
15889
16161
  counts.all = counts["needs-you"] + counts.ready + counts.running;
16162
+ // Each ask is a small heading with its count over its sections (their own headings one step down).
16163
+ const askGroup = (ask, count, sections) => count === 0 ? ""
16164
+ : `<section class="inbox-ask" data-ask="${ask}"><h2>${ASK_LABEL[ask]} <span class="count">${count}</span></h2>${sections.join("").replace(/<(\/?)h2>/g, "<$1h3>")}</section>`;
15890
16165
  const tabs = data.tab === undefined ? "" : `<nav class="inbox-tabs" aria-label="Inbox views">` + INBOX_TABS.map(one => `<a href="/inbox?tab=${one.id}"${one.id === tab ? ` aria-current="page"` : ""} data-inbox-tab="${one.id}">${one.label}` +
15891
16166
  `<span class="inbox-tab-count${one.id === "needs-you" && counts[one.id] > 0 ? " inbox-tab-count--needs" : ""}">${counts[one.id]}</span>` +
15892
16167
  `${one.id !== tab && (data.unread ?? []).includes(one.id) ? `<span class="inbox-unread" aria-label="new"></span>` : ""}</a>`).join("") + `</nav>`;
@@ -15900,14 +16175,11 @@ function inboxPage(chrome, data) {
15900
16175
  wizard,
15901
16176
  empty || !shows("needs-you") ? "" : `<p><a class="new-task" style="display:inline-block" href="/next">clear the queue \u2192 one thing at a time</a></p>`,
15902
16177
  empty && data.wizard === null && shows("needs-you") && counts.all === 0 ? `<div class="card"><p><strong>Nothing needs you.</strong></p><p class="meta">The queue is either working or waiting on its own timers. <a href="/board">Watch the board</a> or <a href="/activity">read the activity report</a>.</p></div>` : tabEmpty,
15903
- shows("needs-you") ? decisions : "",
15904
- shows("needs-you") ? approvals : "",
15905
- shows("needs-you") ? requeueables : "",
15906
- shows("ready") ? needsVerification : "",
15907
- shows("ready") ? ready : "",
16178
+ // What waits on a person, grouped by what it asks: Decide, then Review, then Unblock.
16179
+ shows("needs-you") ? askGroup("decide", counts.decide, [approvals, decisions]) : "",
16180
+ shows("ready") ? askGroup("review", counts.ready, [needsVerification, ready]) : "",
16181
+ shows("needs-you") ? askGroup("unblock", counts.unblock, [requeueables, cancelled, gaps]) : "",
15908
16182
  shows("running") ? running : "",
15909
- shows("needs-you") ? cancelled : "",
15910
- shows("needs-you") ? gaps : "",
15911
16183
  data.rollup
15912
16184
  ? `<p class="meta">requirement gaps are checked one project at a time \u2014 open a project to see and fill its gaps · <a href="/projects">open a project</a></p>`
15913
16185
  : "",
@@ -16530,80 +16802,31 @@ function taskChatApproval(focus, csrf) {
16530
16802
  if (approval.nonce === "") {
16531
16803
  return `<section class="card chat-action-card" id="task-chat-action"><span class="eyebrow">approval needs attention</span><h2>The agent setup isn’t ready yet</h2>${profileWords(scope)}<a class="button-link" href="${taskHref(focus.id)}#scope">Fix the agent setup →</a></section>`;
16532
16804
  }
16533
- const profile = scope.profile ?? null;
16534
- const permission = profile === null
16535
- ? null
16536
- : profile.provider === "claude"
16537
- ? profile.permissionArgv === "bypassPermissions" ? "Full access" : "Auto permissions"
16538
- : profile.provider === "gemini"
16539
- ? profile.approvalArgv === "yolo" ? "Full access" : "Auto permissions"
16540
- : profile.sandboxMode === "danger-full-access" ? "Full access" : "Workspace sandbox";
16541
- const revision = approval.revision === null
16542
- ? ""
16543
- : `<div class="chat-approval-section"><span class="approval-label">${approval.revision.kind === "ci-repair" ? "CI repair" : approval.revision.kind === "criterion-repair" ? "criterion repair" : "revision notes"}</span>` +
16544
- (approval.revision.comments.length === 0 ? "" : `<ul class="recap">${approval.revision.comments.map(one => `<li>${one.path === null ? "" : `<span class="mono">${escape(one.path)}${one.line === null ? "" : `:${one.line}`}</span> · `}${escape(one.note)} <span class="meta">— ${escape(one.author)}</span></li>`).join("")}</ul>`) +
16545
- // The link back (package 3): the original result stays on record
16546
- // and one tap away from the revision it produced, in both directions.
16547
- `<p class="meta"><a href="${escape(chatResultHref(focus.id, approval.revision.sourceRun))}" data-revision-source>Original result: build #${approval.revision.sourceRun} →</a></p>` +
16548
- revisionLineageHtml(approval.revision.lineage) +
16549
- `</div>`;
16550
- const race = approval.raceTerms === null
16551
- ? ""
16552
- : `<div class="chat-approval-section"><span class="approval-label">${approval.raceTerms.kind === "comparison" ? "comparison" : "tournament"}</span>` +
16553
- `<p>${approval.raceTerms.agents.length} agents build independently: ${approval.raceTerms.agents.map(one => `<span class="mono">${escape(one.provider)} · ${escape(one.model)}</span>`).join(" vs ")}.</p>` +
16554
- (approval.raceTerms.kind === "comparison"
16555
- ? `<p class="meta">No dollar caps; every result and its evidence is kept for you to compare.</p>`
16556
- : `<p class="meta">$${(approval.raceTerms.perAgentBudgetMicrousd / 1_000_000).toFixed(2)} per agent plus $${(approval.raceTerms.overrunReserveMicrousd / 1_000_000).toFixed(2)} reserve; $${(approval.raceTerms.totalBudgetMicrousd / 1_000_000).toFixed(2)} total.</p>`) +
16557
- `</div>`;
16558
- // The concise plan (package 2, revised on the operator's screenshot
16559
- // feedback and AGENTS.md): a short title, the outcome in one bounded
16560
- // line, one compact line of counts and limits, and ONE Review plan
16561
- // action — no eyebrow, no second sentence saying the same thing, no
16562
- // footer. Every word is a signed fact the exact terms below restate in
16563
- // full; the password and approval instructions live inside the
16564
- // expanded review, where Approve & start is the only submit.
16565
- const plan = approval.planDocument === null ? null : parseExecutionPlanDocument(approval.planDocument);
16566
- const milestones = plan !== null && plan.ok ? plan.document.milestones : [];
16567
- const count = (n, singular) => `${n} ${singular}${n === 1 ? "" : "s"}`;
16568
- const facts = [
16569
- milestones.length > 0 ? count(milestones.length, "step") : count(scope.touches.length, "path"),
16570
- count(scope.acceptance.length, "check"),
16571
- ];
16572
- if (approval.deliverable === "report")
16573
- facts.push("Report only");
16574
- if (scope.budgetMicrousd !== null)
16575
- facts.push(`$${(scope.budgetMicrousd / 1_000_000).toFixed(2)} attempt cap`);
16576
- if (permission !== null)
16577
- facts.push(permission);
16578
- if (approval.raceTerms !== null)
16579
- facts.push(`${approval.raceTerms.agents.length}-agent ${approval.raceTerms.kind}`);
16805
+ // The same sheet as the task page (approval critique, Oct 2): the plan
16806
+ // open in plain rows, one Approve & start, the rest in Details.
16580
16807
  return (`<section class="card chat-action-card chat-plan" id="task-chat-action" data-approval="${escape(approval.digest)}">` +
16581
- `<p class="chat-plan-outcome">${escape(oneLineOf(scope.goal, 200))}</p>` +
16582
- `<p class="chat-plan-facts">${facts.map(escape).join(" · ")}</p>` +
16583
- `<details class="chat-approval">` +
16584
- `<summary data-primary-action><span class="button-link">Approve plan</span></summary>` +
16585
- `<form method="post" action="${taskHref(focus.executionId)}/approve" class="chat-approval-form approve-form">` +
16586
- `<input type="hidden" name="csrf" value="${escape(csrf)}">` +
16587
- `<input type="hidden" name="nonce" value="${escape(approval.nonce)}">` +
16588
- `<input type="hidden" name="digest" value="${escape(approval.digest)}">` +
16589
- `<input type="hidden" name="return" value="${escape(returnTo)}">` +
16590
- `<input type="text" name="username" autocomplete="username" class="visually-hidden" tabindex="-1" aria-hidden="true">` +
16591
- `<p class="meta chat-approval-lead">These are the exact terms. Nothing builds until your password approves them.${approval.coordinator === null ? "" : ` Filed by <span class="mono">${escape(approval.coordinator.label)}</span>${approval.coordinator.filedAgo === null ? "" : ` · ${escape(approval.coordinator.filedAgo)}`}.`}</p>` +
16592
- (approval.planDocument === null ? "" : `<div class="chat-approval-section"><span class="approval-label">proposed plan</span>${executionPlanHtml(approval.planDocument, true)}${planContractHtml(approval.planContract, "ceremony")}</div>`) +
16593
- (approval.deliverable === "report" ? `<p class="meta"><span class="badge">report only</span> This investigates and reports back without changing the repository.</p>` : "") +
16594
- `<div class="chat-approval-section"><span class="approval-label">goal</span><p class="approval-goal">${escape(scope.goal)}</p></div>` +
16595
- `<div class="approval-boundaries"><div class="approval-boundary"><p class="approval-label">not this</p><p>${scope.outOfScope === null ? "<em>no exclusions</em>" : escape(scope.outOfScope)}</p></div>` +
16596
- `<div class="approval-boundary"><p class="approval-label">may touch</p>${approvalPathsHtml(scope.touches)}</div></div>` +
16597
- acceptanceCeremonyHtml(scope.acceptance) + revision + race +
16598
- `<div class="approval-chips"><span class="approval-chip">quality · <strong>${escape(qualityModeTitle(scope.qualityMode ?? "default"))}</strong></span>` +
16599
- (permission === null ? "" : `<span class="approval-chip">${escape(permission)}</span>`) +
16600
- (scope.budgetMicrousd === null ? "" : `<span class="approval-chip">$${(scope.budgetMicrousd / 1_000_000).toFixed(2)} attempt cap</span>`) +
16601
- (focus.route === null || focus.route.projection === null ? "" : `<span class="approval-chip">${escape(riskTitle(focus.route.riskLevel))} · ${escape(focus.route.projection.postureWords)}</span>`) +
16602
- `</div>` +
16603
- agentsCeremonyHtml(focus.route) +
16604
- runtimeDetailsHtml(scope) +
16605
- `<div class="approval-confirm"><label>Your password <span class="meta">— confirms this exact scope</span><input type="password" name="token" autocomplete="current-password" placeholder="Password"></label>` +
16606
- `<button type="submit">Approve & start</button></div></form></details></section>`);
16808
+ approvalSheetHtml({
16809
+ surface: "chat",
16810
+ action: `${taskHref(focus.executionId)}/approve`,
16811
+ csrf,
16812
+ nonce: approval.nonce,
16813
+ digest: approval.digest,
16814
+ returnTo,
16815
+ scope,
16816
+ planDocument: approval.planDocument,
16817
+ planContract: approval.planDocument === null ? null : approval.planContract,
16818
+ revision: approval.revision,
16819
+ revisionSourceHref: approval.revision === null ? "" : chatResultHref(focus.id, approval.revision.sourceRun),
16820
+ repairChain: approval.repairChain,
16821
+ raceTerms: approval.raceTerms,
16822
+ route: focus.route,
16823
+ coordinator: approval.coordinator,
16824
+ deliverable: approval.deliverable,
16825
+ editHref: `${taskHref(focus.id)}#${approval.planDocument === null ? "scope" : "plan-edit"}`,
16826
+ notNowHref: "/chat",
16827
+ sticky: false,
16828
+ }) +
16829
+ `</section>`);
16607
16830
  }
16608
16831
  /** One live, server-derived journey from request to proof. The fragment is
16609
16832
  * safe to refresh independently, so an in-progress message is never lost. */
@@ -17972,6 +18195,7 @@ function statusLineHtml(status, extra = "") {
17972
18195
  function workDiagnosticsHtml(diagnostics) {
17973
18196
  return (diagnostics ?? []).map(one => `<p class="work-detail" data-work-diagnostic="${escape(one.token)}"><strong>${escape(one.label)}</strong> · ${escape(one.detail)}</p>`).join("");
17974
18197
  }
18198
+ const TASK_GROUP_LABEL = { ...ASK_LABEL, building: 'Building', rest: 'Recent' };
17975
18199
  function workPage(chrome, data) {
17976
18200
  const href = (view, cursor) => {
17977
18201
  const query = new URLSearchParams();
@@ -18014,6 +18238,10 @@ function workPage(chrome, data) {
18014
18238
  const view = {
18015
18239
  kind: 'tasks',
18016
18240
  tabs: WORK_VIEWS.map(one => ({ label: one.label, href: href(one.key), count: data.work.totals[one.key], active: one.key === data.view })),
18241
+ needsYou: data.work.totals['needs-you'],
18242
+ groups: data.view !== 'all' && data.view !== 'needs-you' ? null
18243
+ : [...ASKS, ...(data.view === 'all' ? ['building', 'rest'] : [])]
18244
+ .map(key => ({ key, label: TASK_GROUP_LABEL[key], count: data.work.groups[key] })).filter(one => one.count > 0),
18017
18245
  rows: data.work.items.map(row => {
18018
18246
  const target = row.primaryAction?.target;
18019
18247
  const actionHref = row.primaryAction?.code === 'open-result' && target?.runId != null
@@ -18025,6 +18253,7 @@ function workPage(chrome, data) {
18025
18253
  project: data.multiProject || row.repo === null ? (row.repo === null ? 'Unplaced' : projectName(row.repo)) : null,
18026
18254
  age: relativeAge(row.updatedAt, data.now),
18027
18255
  status: { label: row.status.label, tone: row.status.tone, token: row.status.token },
18256
+ ask: row.ask, group: row.ask ?? (row.status.rank === 1 ? 'building' : 'rest'),
18028
18257
  action: actionHref === null || row.primaryAction === null ? null : { label: row.primaryAction.label, href: actionHref },
18029
18258
  detail: needsYouDetail ? row.status.detail : null,
18030
18259
  problem: row.familyProblem,
@@ -19846,6 +20075,10 @@ function taskBodyParts(data) {
19846
20075
  : approval.approved
19847
20076
  ? "approved"
19848
20077
  : "review before starting";
20078
+ // While the approval sheet is open it states any amendment; the plan
20079
+ // card does not repeat it (one #contract-amendment on the page).
20080
+ const sheetStatesContract = scope !== null && !approval.approved && data.plan === "drafted" && data.dispatch?.action !== "repair-dependency" &&
20081
+ !(data.revision != null && "problem" in data.revision) && scope.profileState !== "unresolved" && consentDoorOf(scope, data.route).open;
19849
20082
  const planCard = data.planDocument === null
19850
20083
  ? data.plan === "requested"
19851
20084
  ? data.revision != null && !("problem" in data.revision)
@@ -19859,10 +20092,10 @@ function taskBodyParts(data) {
19859
20092
  (approval.approved ? `<p class="meta">The agent can adapt this route when evidence changes; your approved outcome stays fixed.</p>` : "") +
19860
20093
  `</div><span class="plan-lock">${planStanding}</span>${approval.approved ? `</summary><div class="planner-plan-body">` : `</div>`}` +
19861
20094
  executionPlanHtml(displayedPlanDocument ?? data.planDocument) +
19862
- (approval.approved ? "" : planContractHtml(data.planContract ?? null, "full")) +
20095
+ (approval.approved || sheetStatesContract ? "" : planContractHtml(data.planContract ?? null, "full")) +
19863
20096
  (data.csrf === "" || data.planSha == null || approval.approved
19864
20097
  ? ""
19865
- : `<details class="plan-editor"><summary>Edit plan</summary><form method="post" action="${taskHref(task.id)}/plan-edit">` +
20098
+ : `<details class="plan-editor" id="plan-edit"><summary>Edit plan</summary><form method="post" action="${taskHref(task.id)}/plan-edit">` +
19866
20099
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}"><input type="hidden" name="saw-plan" value="${escape(data.planSha)}">` +
19867
20100
  `<label>plan details <span class="meta">Keep the five headings. Approval locks this version for the build.</span>` +
19868
20101
  `<textarea name="plan-document" rows="14">${escape(data.planDocument)}</textarea></label>` +
@@ -19889,20 +20122,10 @@ function taskBodyParts(data) {
19889
20122
  // The approval form restates every field the digest binds — an operator
19890
20123
  // approves what is on this form, not what is elsewhere on the page — and
19891
20124
  // requires the token typed again. The session got you here; only the
19892
- // token agrees.
19893
- // The ceremony is the page's first card when a scope waits for its yes
19894
- // (task page pass): it states the wait, restates every term the digest
19895
- // binds, and puts the approve act above the fold — the consent-sheet
19896
- // shape. An unapprovable scope gets the problem and the edit road
19897
- // instead of a password it cannot use.
19898
- const approvalProfile = scope?.profile ?? null;
19899
- const approvalPermission = approvalProfile === null
19900
- ? null
19901
- : approvalProfile.provider === "claude"
19902
- ? approvalProfile.permissionArgv === "bypassPermissions" ? "Full access" : "Auto permissions"
19903
- : approvalProfile.provider === "gemini"
19904
- ? approvalProfile.approvalArgv === "yolo" ? "Full access" : "Auto permissions"
19905
- : approvalProfile.sandboxMode === "danger-full-access" ? "Full access" : "Workspace sandbox";
20125
+ // token agrees. The sheet shows the plan open in plain rows with one
20126
+ // Approve & start; the rest of the signed terms sit in its Details fold.
20127
+ // An unapprovable scope gets the problem and the edit road instead of a
20128
+ // password it cannot use.
19906
20129
  const consentDoor = consentDoorOf(scope, data.route);
19907
20130
  const revisionInApproval = scope !== null && !approval.approved && data.plan !== "requested" &&
19908
20131
  scope.profileState !== "unresolved" && consentDoor.open && data.dispatch?.action !== "repair-dependency" &&
@@ -19917,79 +20140,27 @@ function taskBodyParts(data) {
19917
20140
  `<p class="ceremony-road"><a class="button-link" href="#scope">edit the scope to fix it →</a></p></div>`
19918
20141
  : !consentDoor.open
19919
20142
  ? consentClosedHtml(task.id, consentDoor, "task")
19920
- : [
19921
- `<form method="post" action="${taskHref(task.id)}/approve" class="card approve-form approval-card" id="approve">`,
19922
- `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
19923
- `<input type="hidden" name="nonce" value="${escape(data.nonce)}">`,
19924
- `<input type="hidden" name="digest" value="${escape(data.approvalDigest ?? scope.digest)}">`,
19925
- `<input type="text" name="username" autocomplete="username" class="visually-hidden" tabindex="-1" aria-hidden="true">`,
19926
- `<div class="ceremony-head"><span class="approval-title"><strong>approve exactly this:</strong></span><a href="#scope">Edit details</a></div>`,
19927
- // No duplicate summary card or skip-ahead approval action.
19928
- // Every signed term still follows, before password confirmation.
19929
- `<p class="meta approval-orient" data-approval-orient>Nothing starts until you approve.</p>`,
19930
- `<div class="approval-terms" id="approval-terms">`,
19931
- // The deliverable INSIDE the ceremony (mate arc §10): a yes on a
19932
- // scout task authorizes a read-only session and a report, never
19933
- // a branch — said where the signature is given.
19934
- data.deliverable === "report"
19935
- ? `<p class="meta"><span class="badge">scout</span> approving sends a read-only session to investigate this goal and deliver a report — no branch, nothing changes in the repository</p>`
19936
- : "",
19937
- // The filer INSIDE the ceremony (MCP spec v6): a coordinator's
19938
- // request is signed knowing whose it is — and until this
19939
- // signature, nothing plans, claims, or runs it.
19940
- data.coordinator === null || data.coordinator === undefined
19941
- ? ""
19942
- : `<p class="meta">filed by <span class="mono">${escape(data.coordinator.label)}</span>${data.coordinator.filedAgo === null ? "" : ` \u00b7 ${escape(data.coordinator.filedAgo)}`} — an agent asked for this; nothing plans, claims, or runs until you sign, and your signature runs THEIR request</p>`,
19943
- `<p class="approval-label">goal</p><p class="approval-goal">${escape(scope.goal)}</p>`,
19944
- scope.candidate ? `<p class="approval-label">Saved commit</p><p style="overflow-wrap:anywhere"><code>${escape(scope.candidate)}</code></p>` : "",
19945
- revisionCard,
19946
- `<div class="approval-boundaries">`,
19947
- `<div class="approval-boundary"><p class="approval-label">not this</p><p>${scope.outOfScope === null ? "<em>no exclusions</em>" : escape(scope.outOfScope)}</p></div>`,
19948
- `<div class="approval-boundary"><p class="approval-label">touches</p>${approvalPathsHtml(scope.touches)}</div>`,
19949
- `</div>`,
19950
- acceptanceCeremonyHtml(scope.acceptance),
19951
- // The contract amendment INSIDE the ceremony (contract handoff,
19952
- // task 1): what the yes accepts that the operator did not file.
19953
- data.plan === "drafted" ? planContractHtml(data.planContract ?? null, "ceremony") : "",
19954
- `</div>`,
19955
- `<div class="approval-chips"><span class="approval-chip">quality · <strong>${escape(qualityModeTitle(scope.qualityMode ?? "default"))}</strong></span>` +
19956
- (approvalPermission === null ? "" : `<span class="approval-chip">${escape(approvalPermission)}</span>`) +
19957
- `</div>`,
19958
- // The revision feedback above includes its lineage: the source,
19959
- // carried terms, re-resolved terms, and grants that never inherit.
19960
- // The AGENTS inside the ceremony (v47/v48): who plans, builds,
19961
- // repairs, and reviews, and why — said where the yes is given, in
19962
- // the same block chat and /next show. Availability is volatile and
19963
- // stays outside these terms; the runtime limits the profile seals
19964
- // are one tap away, never in the way.
19965
- agentsCeremonyHtml(data.route),
19966
- runtimeDetailsHtml(scope),
19967
- scope.budgetMicrousd === null
19968
- ? ""
19969
- : `<p class="meta">each build attempt has a $${(scope.budgetMicrousd / 1_000_000).toFixed(2)} agent-reported usage cap — on a subscription this is a work limiter, not an API charge</p>`,
19970
- // One yes covers BOTH documents (finding 31): the race terms are
19971
- // restated on the same card the password signs, or they are not
19972
- // approved at all.
19973
- data.raceTerms === null || data.raceTerms === undefined
19974
- ? ""
19975
- : data.raceTerms.kind === "comparison"
19976
- ? `<p><strong>and this comparison:</strong></p>` +
19977
- `<p class="recap" style="margin-top:0">${data.raceTerms.n} agents build this independently — ` +
19978
- `${data.raceTerms.agents.map(agent => `${escape(agent.provider)} · ${escape(agent.model)}`).join(" vs ")}. ` +
19979
- `No dollar caps exist on a comparison — each agent runs until it finishes or stops making progress; ` +
19980
- `spend lands measured only where the harness reports dollars (` +
19981
- `${data.raceTerms.agents.filter(agent => agent.provider === "claude").length} of ${data.raceTerms.n} lanes here). ` +
19982
- `You will compare the results and pick one.</p>`
19983
- : `<p><strong>and this tournament:</strong></p>` +
19984
- `<p class="recap" style="margin-top:0">${data.raceTerms.n} agents build this independently — ` +
19985
- `${data.raceTerms.agents.map(agent => `${escape(agent.provider)} · ${escape(agent.model)}`).join(" vs ")}. ` +
19986
- `Each may spend $${(data.raceTerms.perAgentBudgetMicrousd / 1_000_000).toFixed(2)} plus a ` +
19987
- `$${(data.raceTerms.overrunReserveMicrousd / 1_000_000).toFixed(2)} overrun reserve; the whole tournament is capped at ` +
19988
- `$${(data.raceTerms.totalBudgetMicrousd / 1_000_000).toFixed(2)}. You will compare the results and pick one.</p>`,
19989
- `<div class="approval-confirm" id="approval-confirm"><label>Your password <span class="meta">— confirms exactly the terms above</span><input type="password" name="token" autocomplete="current-password" placeholder="Password"></label>`,
19990
- `<div class="sticky-actions"><button type="submit">${data.raceTerms === null || data.raceTerms === undefined ? "Approve & start" : data.raceTerms.kind === "comparison" ? "Approve comparison" : "Approve tournament"}</button></div></div>`,
19991
- `</form>`,
19992
- ].join("\n");
20143
+ : approvalSheetHtml({
20144
+ surface: "task",
20145
+ action: `${taskHref(task.id)}/approve`,
20146
+ csrf: data.csrf,
20147
+ nonce: data.nonce,
20148
+ digest: data.approvalDigest ?? scope.digest,
20149
+ returnTo: null,
20150
+ scope,
20151
+ planDocument: data.plan === "drafted" ? data.planDocument : null,
20152
+ planContract: data.plan === "drafted" ? data.planContract ?? null : null,
20153
+ revision: data.revision == null || "problem" in data.revision ? null : data.revision,
20154
+ revisionSourceHref: data.revision == null || "problem" in data.revision ? "" : `/r/${data.revision.sourceRun}`,
20155
+ repairChain: data.repairChain ?? null,
20156
+ raceTerms: data.raceTerms ?? null,
20157
+ route: data.route,
20158
+ coordinator: data.coordinator ?? null,
20159
+ deliverable: data.deliverable ?? "branch",
20160
+ editHref: data.csrf !== "" && data.planSha != null && data.planDocument !== null ? "#plan-edit" : "#scope",
20161
+ notNowHref: "/work",
20162
+ sticky: data.dispatch?.action === "approve-scope",
20163
+ });
19993
20164
  // The attended road (Phase 2E): beside the approval, never replacing it.
19994
20165
  // The mint button leads to the CONFIRM screen where every term renders
19995
20166
  // and the password signs; an open authorization shows its state, its
@@ -20514,7 +20685,7 @@ function taskBodyParts(data) {
20514
20685
  `<p class="meta"><strong>Plan first</strong> drafts it from the repository, or <a href="#scope">write it yourself</a>.</p></div>`
20515
20686
  : "";
20516
20687
  const approvalHtml = dependencyChoiceNeeded || approveForm === "" ? "" : data.dispatch?.action === "approve-scope"
20517
- ? `<details class="task-plan-review"><summary data-primary-action><span class="button-link">Approve plan</span></summary>${approveForm}</details>`
20688
+ ? `<section class="task-plan-review" aria-label="Approve the plan">${approveForm}</section>`
20518
20689
  : `<details class="task-secondary-approval"><summary>Updated approval terms</summary>${approveForm}</details>`;
20519
20690
  const optionsHtml = `${identity}${dispatchStatus}${dependencyChoiceNeeded ? "" : actsBar}`;
20520
20691
  const optionsOpen = data.assignment?.primaryAction?.code === "unhold" || data.assignment?.primaryAction?.code === "retry-task";
@@ -21204,7 +21375,7 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
21204
21375
  taskHref: taskHref(view.taskId), chatHref: taskChatHref(view.taskId),
21205
21376
  status: { label: status.label, tone: status.tone, token: status.token },
21206
21377
  problem: view.detail === null ? view.historyProblem : null,
21207
- next: reviewNextActionOf(view, csrf), complete: null, checks: null, intent: intentView, noRun: null, panel: null, contest: "",
21378
+ next: reviewNextActionOf(view, csrf), complete: null, decision: null, checks: null, intent: intentView, noRun: null, panel: null, contest: "",
21208
21379
  notes: view.notes.map(one => ({ author: one.author, at: one.createdAt, note: one.note })),
21209
21380
  };
21210
21381
  if (run === null || view.detail === null) {
@@ -21245,6 +21416,17 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
21245
21416
  ? { action: `${taskHref(view.taskId)}/complete`, receipt: assignment.receipt.digest, run: run.id } : null;
21246
21417
  if (complete !== null)
21247
21418
  parts.push(completionForm(view.taskId, run.id, complete.receipt, csrf, view.detail?.pullRequestTo ?? null));
21419
+ // The one decision, after the evidence: Accept only when every requirement is met and the checks passed.
21420
+ const acceptsHere = complete !== null || panel.panel.need?.accept != null;
21421
+ const matrix = proof === null || proof.proofProblem !== null ? [] : proof.matrix;
21422
+ const decision = !acceptsHere ? null : { sentence: RESULT_DECISION_SENTENCE, ...acceptWordsOf({
21423
+ checks: checks === undefined ? null : checks.running != null ? "running" : checks.level === "off" && checks.status !== "passed" ? "off" : checks.status,
21424
+ unmet: matrix.filter(row => row.state !== "pass" && row.state !== "manual-review").length,
21425
+ yours: complete === null || accepted ? 0 : panel.panel.youCheck?.lines.length ?? 0,
21426
+ action: complete !== null ? "complete" : "accept",
21427
+ publishing: view.detail?.publishing ?? "other",
21428
+ proof: proof !== null && proof.proof !== null && proof.proofProblem === null,
21429
+ }) };
21248
21430
  if (view.notes.length > 0) {
21249
21431
  parts.push(`<section class="card cockpit-section" data-cockpit-section="notes"><h3>operator notes</h3>` +
21250
21432
  view.notes.map(one => `<p class="row"><span class="meta">${escape(one.author)} · ${whenTime(one.createdAt)}</span> ${escape(one.note)}</p>`).join("\n") +
@@ -21253,7 +21435,7 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
21253
21435
  return {
21254
21436
  html: `<section class="cockpit-detail">${parts.join("\n")}</section>`,
21255
21437
  selected: {
21256
- ...selected, complete, panel: panel.panel, contest,
21438
+ ...selected, complete, decision, panel: panel.panel, contest,
21257
21439
  checks: checks === undefined ? null : { detail: checks.detail, problem: checks.status === "failed" || checks.status === "unavailable", logHref: checks.logArtifactId === null ? null : `/r/${run.id}/evidence/${checks.logArtifactId}` },
21258
21440
  },
21259
21441
  };
@@ -22229,6 +22411,7 @@ function resultPanelParts(detail, o) {
22229
22411
  const acceptable = personChecks.length > 0 && humanReview && o.csrf !== "" && (current != null || detail.assignment == null);
22230
22412
  const youCheck = personChecks.length === 0 ? null : {
22231
22413
  lines: [...new Set(personChecks)],
22414
+ items: personCheckItems(proof, patchOk ? patch.text : null, shown, runId),
22232
22415
  accept: acceptable && need?.accept == null ? { action: `${taskHref(detail.taskId)}/accept-proof`, run: run.id, returnTo: o.returnTo } : null,
22233
22416
  };
22234
22417
  const youCheckHtml = youCheck === null ? "" :
@@ -22537,6 +22720,37 @@ function resultPanelParts(detail, o) {
22537
22720
  };
22538
22721
  return { html, panel };
22539
22722
  }
22723
+ /** Changed lines shown beside one "You check this one" item: at most this many per file, three files. */
22724
+ const CHECK_EXCERPT_LINES = 12;
22725
+ /** Each requirement only a person can confirm, with the evidence to judge
22726
+ * it by, inline: the changed lines in the files it cites (citing none, the
22727
+ * change's first file, labelled as not cited), the screenshots it cites
22728
+ * (none cited: every validated one), and the agent's own note. */
22729
+ function personCheckItems(proof, patchText, shots, runId) {
22730
+ const rows = proof.matrix.filter(row => row.state === "manual-review");
22731
+ if (rows.length === 0)
22732
+ return proof.reasons.some(reason => manualReviewCriterionOf(reason) !== null)
22733
+ ? [{ id: "you-check", statement: "", words: personCheckWords(null), note: null, excerpts: [], shots: [] }] : [];
22734
+ const diff = patchText === null ? null : parseReviewDiff(patchText);
22735
+ const excerptOf = (file, cited) => {
22736
+ const changed = file.hunks.flatMap(hunk => hunk.lines).filter((line) => line.kind === "addition" || line.kind === "deletion");
22737
+ return { path: file.path, cited, lines: changed.slice(0, CHECK_EXCERPT_LINES).map(line => ({ kind: line.kind, line: line.newLine ?? line.oldLine, text: line.text })), more: Math.max(0, changed.length - CHECK_EXCERPT_LINES) };
22738
+ };
22739
+ return rows.map(row => {
22740
+ const refs = row.answered ?? [];
22741
+ const paths = refs.filter(one => one.kind === "changed-path").map(one => one.ref);
22742
+ const files = diff === null ? [] : paths.length > 0 ? diff.files.filter(file => paths.includes(file.path)).map(file => excerptOf(file, true))
22743
+ : diff.files.slice(0, 1).map(file => excerptOf(file, false));
22744
+ const cited = refs.filter(one => one.kind === "screenshot").map(one => one.ref);
22745
+ const pictures = cited.length > 0 ? shots.filter(shot => cited.includes(shot.path)) : shots;
22746
+ const notes = refs.filter(one => one.kind === "manual-review").map(one => one.ref.trim()).filter(one => one !== "");
22747
+ return {
22748
+ id: row.id, statement: row.statement, words: personCheckWords(row.statement), note: notes.length === 0 ? null : notes.join(" "),
22749
+ excerpts: files.filter(one => one.lines.length > 0).slice(0, 3),
22750
+ shots: pictures.slice(0, 4).map(shot => ({ src: `/r/${runId}/evidence/${shot.artifactId}`, href: `/r/${runId}/evidence/${shot.artifactId}`, caption: shot.caption })),
22751
+ };
22752
+ });
22753
+ }
22540
22754
  /** A Needs you result's one action (needs-you.ts): a link to the act that resolves it, or Confirm it
22541
22755
  * stopped behind the password. Null under every other headline. */
22542
22756
  export function needActionOf(assignment, status, csrf, returnTo, result) {