toolroll 0.9.10 → 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 (53) hide show
  1. package/CHANGELOG.md +25 -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-channel.d.ts +9 -7
  8. package/dist/chat-channel.js +23 -17
  9. package/dist/chat-delivery.js +21 -0
  10. package/dist/chat-quiet.js +1 -0
  11. package/dist/chat-voice.d.ts +3 -1
  12. package/dist/chat-voice.js +13 -5
  13. package/dist/cli.d.ts +1 -1
  14. package/dist/cli.js +1 -0
  15. package/dist/coding-update.d.ts +13 -0
  16. package/dist/coding-update.js +68 -0
  17. package/dist/desktop-update.d.ts +7 -0
  18. package/dist/desktop-update.js +42 -1
  19. package/dist/flow-triggers.js +17 -1
  20. package/dist/guides.js +9 -0
  21. package/dist/lead-status.d.ts +10 -1
  22. package/dist/lead-status.js +13 -3
  23. package/dist/lead-voice.d.ts +81 -0
  24. package/dist/lead-voice.js +275 -0
  25. package/dist/needs-you.d.ts +7 -0
  26. package/dist/needs-you.js +9 -0
  27. package/dist/operate.d.ts +2 -0
  28. package/dist/operate.js +186 -38
  29. package/dist/serve.d.ts +5 -1
  30. package/dist/serve.js +447 -226
  31. package/dist/setup-guide.d.ts +2 -2
  32. package/dist/store.d.ts +15 -1
  33. package/dist/store.js +38 -1
  34. package/dist/surface.js +8 -1
  35. package/dist/task-status.d.ts +6 -0
  36. package/dist/task-status.js +19 -2
  37. package/dist/team-cli.js +1 -1
  38. package/dist/telegram-mate.d.ts +7 -0
  39. package/dist/telegram-mate.js +50 -21
  40. package/dist/telegram-progress.js +7 -0
  41. package/dist/telegram-settings.d.ts +3 -0
  42. package/dist/telegram-settings.js +11 -0
  43. package/dist/telegram.d.ts +14 -0
  44. package/dist/telegram.js +105 -12
  45. package/dist/test-temp.js +1 -1
  46. package/dist/toolroll-update.d.ts +3 -0
  47. package/dist/toolroll-update.js +13 -2
  48. package/dist/work-index.d.ts +9 -0
  49. package/dist/work-index.js +57 -20
  50. package/dist/work-summary.d.ts +4 -1
  51. package/dist/workspace-ui.d.ts +25 -0
  52. package/dist/workspace-ui.js +20 -1
  53. 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;
@@ -1674,7 +1677,7 @@ export function createDecisionServer(options) {
1674
1677
  // separately proves cookie, approver standing and password; instance bot
1675
1678
  // settings remain outside this exact allowlist.
1676
1679
  if ((request.method === "GET" && path === "/settings/telegram") ||
1677
- (request.method === "POST" && ["/settings/telegram/pair", "/settings/telegram/unpair"].includes(path)))
1680
+ (request.method === "POST" && ["/settings/telegram/pair", "/settings/telegram/unpair", "/settings/telegram/retry"].includes(path)))
1678
1681
  return true;
1679
1682
  // Coding routes require an instance operator, then prove saved ownership and project access.
1680
1683
  if (path === "/code" || path.startsWith("/code/"))
@@ -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
  }
@@ -4066,7 +4069,7 @@ export function createDecisionServer(options) {
4066
4069
  // The settings view holds no secret (a pairing code only appears in
4067
4070
  // the response to Pair, which stays script-free), so it joins the
4068
4071
  // workspace like every other settings page.
4069
- return sendScreen(response, 200, screen("Telegram", telegramSettingsHtml(store, botId, who.name, who.session.csrf), { chrome: chromeFor(project, "settings") }));
4072
+ return sendScreen(response, 200, screen("Telegram", telegramSettingsHtml(store, botId, who.name, who.session.csrf, { now }), { chrome: chromeFor(project, "settings") }));
4070
4073
  }
4071
4074
  if (url.pathname === "/settings/teams") {
4072
4075
  if (who.via !== "cookie" || who.role !== "approver" || restricted() || !options.configDir)
@@ -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;
@@ -7885,6 +7896,13 @@ export function createDecisionServer(options) {
7885
7896
  }
7886
7897
  return redirect(response, "/settings/slack");
7887
7898
  }
7899
+ if (url.pathname === "/settings/telegram/retry") {
7900
+ // The signed-in person's own unsent replies only: already written and fenced on the pairing; this sends them again now.
7901
+ if (who.via !== "cookie" || who.role !== "approver")
7902
+ return refuse(response, who, 403, "An approver can retry their own replies.", "/settings");
7903
+ store.retryTelegramReplies(who.name, now);
7904
+ return redirect(response, "/settings/telegram");
7905
+ }
7888
7906
  if (url.pathname === "/settings/telegram/pair" || url.pathname === "/settings/telegram/unpair") {
7889
7907
  // The person's own pairing, under their password: a code minted for
7890
7908
  // them alone, or their own chats revoked. Teammates' pairings are
@@ -11471,7 +11489,7 @@ export function createDecisionServer(options) {
11471
11489
  run,
11472
11490
  receipt,
11473
11491
  assignment: (() => {
11474
- const value = assignmentOf(store, taskId, now, { principal: "operator", repos: admissionList(), includeUnplaced: visible(null) }, evidenceRoot);
11492
+ const value = assignmentOf(store, taskId, now, workAccess(), evidenceRoot);
11475
11493
  return value?.receipt?.runId === run.id && value.activeTaskId === taskId ? freshAssignment(value, receipt) : null;
11476
11494
  })(),
11477
11495
  handoff,
@@ -11479,6 +11497,7 @@ export function createDecisionServer(options) {
11479
11497
  terminal,
11480
11498
  publication,
11481
11499
  pullRequestTo: pullRequestTargetOf(run.id),
11500
+ publishing: pullRequestTargetOf(run.id) !== null ? "pull-request" : publishingOf(store, store.refById(run.taskRef)?.repo ?? null).on ? "other" : "off",
11482
11501
  ciFailing: publication !== null && publication.prNumber !== null && store.hasOpenCiEpisode(publication.githubRepo, publication.prNumber),
11483
11502
  files,
11484
11503
  outsideTouches: files.filter(one => one.outsideTouches).map(one => one.path),
@@ -11658,6 +11677,19 @@ export function createDecisionServer(options) {
11658
11677
  });
11659
11678
  });
11660
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
+ });
11661
11693
  const closeServer = server.close.bind(server);
11662
11694
  server.close = ((callback) => {
11663
11695
  leadClosing = true;
@@ -11668,7 +11700,7 @@ export function createDecisionServer(options) {
11668
11700
  stream.end();
11669
11701
  chatStreams.clear();
11670
11702
  flowRooms.close();
11671
- 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 => {
11672
11704
  if (callback)
11673
11705
  callback(error instanceof Error ? error : Error('Coding session shutdown failed.'));
11674
11706
  else
@@ -11676,7 +11708,7 @@ export function createDecisionServer(options) {
11676
11708
  });
11677
11709
  return server;
11678
11710
  });
11679
- return Object.assign(server, { mintSignInLink });
11711
+ return Object.assign(server, { mintSignInLink, closeCoding });
11680
11712
  }
11681
11713
  // ---- path plumbing ---------------------------------------------------------
11682
11714
  /**
@@ -12096,6 +12128,199 @@ function acceptanceCeremonyHtml(criteria) {
12096
12128
  .join("") +
12097
12129
  `</ul>`);
12098
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
+ }
12099
12324
  function consentDoorOf(scope, route) {
12100
12325
  if (scope === null)
12101
12326
  return { open: false, title: "There is no scope to approve yet", why: "write the scope first", road: "scope" };
@@ -12367,6 +12592,8 @@ function agentsStripHtml(view, taskId) {
12367
12592
  }
12368
12593
  /** The demo's one-line promise, on every page. */
12369
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.";
12370
12597
  /** Every character that could open a tag or an attribute, dead at the sink. */
12371
12598
  function escape(text) {
12372
12599
  return text
@@ -12857,7 +13084,7 @@ ${THEME_DARK}
12857
13084
  /* Thumbnails, never a full-width poster: auto-fill leaves a lone
12858
13085
  screenshot at thumbnail size (UI polish 2026-09-13). */
12859
13086
  .receipt-visuals { display: grid; grid-template-columns: repeat(auto-fill, minmax(8rem, 14rem)); gap: .55rem; margin-top: .75rem; }
12860
- .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; }
12861
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); }
12862
13089
  @media (hover: hover) and (pointer: fine) { .receipt-shot:hover { color: var(--foreground); } }
12863
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; }
@@ -12990,7 +13217,7 @@ ${THEME_DARK}
12990
13217
  }
12991
13218
  .diff-file[open] > summary::before { transform: rotate(45deg) translateY(-.1rem); }
12992
13219
  .diff-file-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font: 500 .75rem/1.4 var(--font-mono); }
12993
- .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); }
12994
13221
  .diff-file-counts b { color: var(--success); font-weight: 500; }
12995
13222
  .diff-file-counts i { color: var(--destructive); font-style: normal; }
12996
13223
  .diff-rename { margin: 0; padding: .4rem .85rem; border-top: 1px solid var(--glass-border); }
@@ -12998,12 +13225,12 @@ ${THEME_DARK}
12998
13225
  .diff-hunk-head {
12999
13226
  overflow-x: auto; padding: .42rem .85rem; border-top: 1px solid var(--glass-border); border-bottom: 1px solid var(--glass-border);
13000
13227
  background: color-mix(in srgb, var(--running) 8%, var(--card)); color: color-mix(in srgb, var(--running) 72%, var(--foreground));
13001
- font: 500 .68rem/1.4 var(--font-mono); white-space: pre;
13228
+ font: 500 .75rem/1.4 var(--font-mono); white-space: pre;
13002
13229
  }
13003
13230
  .diff-lines { max-width: 100%; overflow-x: auto; background: color-mix(in srgb, var(--background) 48%, var(--card)); }
13004
13231
  .diff-line {
13005
13232
  display: grid; grid-template-columns: 2rem 3.2rem 3.2rem minmax(max-content, 1fr); align-items: stretch;
13006
- 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);
13007
13234
  }
13008
13235
  @media (hover: hover) and (pointer: fine) { .diff-line:hover { background: color-mix(in srgb, var(--foreground) 4%, transparent); } }
13009
13236
  .diff-line code { display: flex; min-width: 0; padding: .3rem .75rem .3rem .6rem; color: inherit; white-space: pre; }
@@ -13541,7 +13768,9 @@ ${THEME_DARK}
13541
13768
  .approval-title { display: grid; gap: .15rem; }
13542
13769
  .approval-kicker { color: var(--muted-foreground); font: 500 .66rem/1.3 var(--font-mono); letter-spacing: .06em; text-transform: uppercase; }
13543
13770
  .approval-title strong { font-size: 1.05rem; letter-spacing: -.02em; }
13544
- .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; }
13545
13774
  .approval-goal { margin: 0; font-size: 1rem; line-height: 1.55; white-space: pre-wrap; }
13546
13775
  .approval-boundaries { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .6rem; margin-top: .75rem; }
13547
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); }
@@ -13627,6 +13856,66 @@ ${THEME_DARK}
13627
13856
  .approval-confirm { scroll-margin-top: 5rem; }
13628
13857
  .approval-paths { margin: 0; padding: 0; list-style: none; display: grid; gap: .15rem; }
13629
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
+ }
13630
13919
  .run-facts-details { margin-top: 1.25rem; }
13631
13920
  .run-facts-details > summary { display: flex; align-items: center; justify-content: space-between; gap: .75rem; }
13632
13921
  .run-facts-details > summary .meta { font-size: .75rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@@ -13860,7 +14149,7 @@ ${THEME_DARK}
13860
14149
  .result-pin[open] > summary::before { content: "▾"; }
13861
14150
  .result-pin > summary .meta { font-weight: 400; }
13862
14151
  .result-pin .diff-comment-target { margin: .25rem 0 .5rem; }
13863
- .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; }
13864
14153
  .result-feedback-history > summary, .task-history > summary, .task-history a { display: inline-flex; align-items: center; min-height: 44px; padding: .25rem .5rem; }
13865
14154
  .task-history > summary::before { content: "▸"; margin-right: .35rem; }
13866
14155
  .task-history[open] > summary::before { content: "▾"; }
@@ -14325,11 +14614,10 @@ ${THEME_DARK}
14325
14614
  .check-progress[data-final="passed"] { color: var(--success); }
14326
14615
  .check-progress[data-final="failed"] { color: var(--destructive); }
14327
14616
  .check-progress[data-final="unknown"] { color: var(--warning); }
14328
- .receipt-actions [data-primary-action], .task-plan-review .button-link { min-height: 44px; }
14617
+ .receipt-actions [data-primary-action] { min-height: 44px; }
14329
14618
  .task-status-reason { margin-top: .75rem; }
14330
14619
  .task-status-reason summary, .task-status-details > summary { color: var(--muted-foreground); }
14331
14620
  .task-status-details > summary { min-height: 44px; }
14332
- .task-plan-review > summary { min-height: 44px; display: flex; align-items: center; cursor: pointer; font-weight: 650; }
14333
14621
  .task-status-details, #task-control-details { margin: 1rem 0; }
14334
14622
  .task-control-copy { min-width: 0; overflow-wrap: anywhere; }
14335
14623
  [data-primary-action] { max-width: 100%; }
@@ -14344,16 +14632,9 @@ ${THEME_DARK}
14344
14632
  .chat-action-card > summary strong { font-size: .92rem; }
14345
14633
  .chat-action-card > summary small { color: var(--muted-foreground); font-size: .68rem; font-weight: 400; }
14346
14634
  .chat-action-card[open] > summary { border-bottom: 1px solid var(--border); }
14347
- .chat-approval-form { display: grid; gap: .85rem; padding: 1.05rem; }
14348
- .chat-approval-section { display: grid; gap: .3rem; }
14349
- .chat-approval-section > p, .chat-approval-section > pre { margin: 0; }
14350
- .chat-approval-form .approval-boundaries { margin: 0; }
14351
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); }
14352
14636
  .chat-run-details > summary { color: var(--muted-foreground); cursor: pointer; font-size: .68rem; }
14353
14637
  .chat-run-details > .meta { margin-bottom: 0; }
14354
- .chat-approval-form .approval-confirm { align-items: end; margin-top: .15rem; }
14355
- .chat-approval-form .approval-confirm label { flex: 1 1 18rem; }
14356
- .chat-approval-form .approval-confirm button { min-height: 2.65rem; }
14357
14638
  .chat-decisions { display: grid; gap: .7rem; }
14358
14639
  .chat-section-head { margin: .15rem .15rem 0; }
14359
14640
  .chat-section-head h2 { margin: .2rem 0 0; font-size: 1rem; }
@@ -14550,18 +14831,9 @@ ${THEME_DARK}
14550
14831
  /* The concise plan (package 2): a title, the outcome, one line of
14551
14832
  counts and limits, then one Review plan disclosure over the unchanged
14552
14833
  exact-terms form. */
14553
- .chat-plan-head h2 { margin: 0; font-size: 1rem; letter-spacing: -.025em; }
14554
- .chat-plan-outcome { margin: .65rem 0 .55rem; font-size: .88rem; line-height: 1.5; overflow-wrap: anywhere; }
14555
- .chat-plan-facts { margin: 0; color: var(--muted-foreground); font-size: .74rem; line-height: 1.5; overflow-wrap: anywhere; }
14556
- .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; }
14557
- .chat-plan > details.chat-approval > summary { display: flex; align-items: center; gap: .75rem; cursor: pointer; list-style: none; }
14558
- .chat-plan > details.chat-approval > summary::-webkit-details-marker { display: none; }
14559
- .chat-plan > details.chat-approval > summary .button-link { flex: 0 0 auto; white-space: nowrap; min-height: 44px; }
14560
- .chat-plan > details.chat-approval > .chat-approval-form { padding: .85rem 0 0; }
14561
- .chat-plan .chat-approval-lead { margin: 0 0 .85rem; }
14562
14834
  .chat-approval-stale { margin: .75rem 0 0; }
14563
14835
  .chat-approval-stale a { font-weight: 600; }
14564
- 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; }
14565
14837
  .proposal-filed { display: flex; align-items: center; flex-wrap: wrap; gap: .6rem; margin: .5rem 0 0; }
14566
14838
  .chat-thinking { display: flex; align-items: center; gap: .75rem; padding: .75rem .85rem; }
14567
14839
  .chat-thinking p { flex: 1; margin: 0; }
@@ -14688,10 +14960,6 @@ ${THEME_DARK}
14688
14960
  .chat-action-card > summary { align-items: flex-start; padding: .85rem; }
14689
14961
  .chat-action-card > summary .button-link { min-height: 2.1rem; padding-inline: .6rem; font-size: .65rem; white-space: nowrap; }
14690
14962
  .chat-action-card > summary small { max-width: 14rem; }
14691
- .chat-approval-form { padding: .85rem; }
14692
- .chat-approval-form .approval-boundaries { grid-template-columns: 1fr; }
14693
- .chat-approval-form .approval-confirm { display: grid; }
14694
- .chat-approval-form .approval-confirm button { width: 100%; }
14695
14963
  .chat-budget { gap: .3rem; margin: .65rem 0 .9rem; }
14696
14964
  .chat-budget > span { padding: .25rem .48rem; }
14697
14965
  .chat-budget > span:first-child { max-width: 100%; overflow: hidden; text-overflow: ellipsis; }
@@ -15005,6 +15273,15 @@ button { min-height: 44px; }
15005
15273
  chips flow after it, never above the name. */
15006
15274
  .queue-card p.row > a:first-of-type { flex: 1 1 12rem; min-width: 0; }
15007
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
+ }
15008
15285
  @media (max-width: 30rem) {
15009
15286
  .dependency-repair-actions .dependency-repair-replace { grid-template-columns: minmax(0, 1fr); }
15010
15287
  .dependency-repair-actions .dependency-repair-replace > button[type=submit] { width: 100%; }
@@ -15148,6 +15425,7 @@ const INBOX_TABS_CSS = '.inbox-tabs{display:inline-flex;gap:2px;max-width:100%;o
15148
15425
  '.inbox-tabs a[aria-current="page"]{background:var(--so-paper);color:var(--so-ink);box-shadow:var(--so-pill-shadow)}' +
15149
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}' +
15150
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}' +
15151
15429
  '.inbox-unread{display:none;position:absolute;top:4px;right:3px;width:6px;height:6px;border-radius:50%;background:var(--so-signal)}' +
15152
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}}';
15153
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}');
@@ -15873,13 +16151,17 @@ function inboxPage(chrome, data) {
15873
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");
15874
16152
  const ready = (data.ready ?? []).length === 0 ? "" : `<h2>ready to review</h2>` + listRows(data.ready ?? []);
15875
16153
  const running = (data.running ?? []).length === 0 ? "" : `<h2>running now</h2>` + listRows(data.running ?? []);
15876
- const counts = {
15877
- "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,
15878
16157
  ready: data.needsVerification.length + (data.ready ?? []).length,
15879
16158
  running: (data.running ?? []).length,
15880
16159
  all: 0,
15881
16160
  };
15882
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>`;
15883
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}` +
15884
16166
  `<span class="inbox-tab-count${one.id === "needs-you" && counts[one.id] > 0 ? " inbox-tab-count--needs" : ""}">${counts[one.id]}</span>` +
15885
16167
  `${one.id !== tab && (data.unread ?? []).includes(one.id) ? `<span class="inbox-unread" aria-label="new"></span>` : ""}</a>`).join("") + `</nav>`;
@@ -15893,14 +16175,11 @@ function inboxPage(chrome, data) {
15893
16175
  wizard,
15894
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>`,
15895
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,
15896
- shows("needs-you") ? decisions : "",
15897
- shows("needs-you") ? approvals : "",
15898
- shows("needs-you") ? requeueables : "",
15899
- shows("ready") ? needsVerification : "",
15900
- 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]) : "",
15901
16182
  shows("running") ? running : "",
15902
- shows("needs-you") ? cancelled : "",
15903
- shows("needs-you") ? gaps : "",
15904
16183
  data.rollup
15905
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>`
15906
16185
  : "",
@@ -16523,80 +16802,31 @@ function taskChatApproval(focus, csrf) {
16523
16802
  if (approval.nonce === "") {
16524
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>`;
16525
16804
  }
16526
- const profile = scope.profile ?? null;
16527
- const permission = profile === null
16528
- ? null
16529
- : profile.provider === "claude"
16530
- ? profile.permissionArgv === "bypassPermissions" ? "Full access" : "Auto permissions"
16531
- : profile.provider === "gemini"
16532
- ? profile.approvalArgv === "yolo" ? "Full access" : "Auto permissions"
16533
- : profile.sandboxMode === "danger-full-access" ? "Full access" : "Workspace sandbox";
16534
- const revision = approval.revision === null
16535
- ? ""
16536
- : `<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>` +
16537
- (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>`) +
16538
- // The link back (package 3): the original result stays on record
16539
- // and one tap away from the revision it produced, in both directions.
16540
- `<p class="meta"><a href="${escape(chatResultHref(focus.id, approval.revision.sourceRun))}" data-revision-source>Original result: build #${approval.revision.sourceRun} →</a></p>` +
16541
- revisionLineageHtml(approval.revision.lineage) +
16542
- `</div>`;
16543
- const race = approval.raceTerms === null
16544
- ? ""
16545
- : `<div class="chat-approval-section"><span class="approval-label">${approval.raceTerms.kind === "comparison" ? "comparison" : "tournament"}</span>` +
16546
- `<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>` +
16547
- (approval.raceTerms.kind === "comparison"
16548
- ? `<p class="meta">No dollar caps; every result and its evidence is kept for you to compare.</p>`
16549
- : `<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>`) +
16550
- `</div>`;
16551
- // The concise plan (package 2, revised on the operator's screenshot
16552
- // feedback and AGENTS.md): a short title, the outcome in one bounded
16553
- // line, one compact line of counts and limits, and ONE Review plan
16554
- // action — no eyebrow, no second sentence saying the same thing, no
16555
- // footer. Every word is a signed fact the exact terms below restate in
16556
- // full; the password and approval instructions live inside the
16557
- // expanded review, where Approve & start is the only submit.
16558
- const plan = approval.planDocument === null ? null : parseExecutionPlanDocument(approval.planDocument);
16559
- const milestones = plan !== null && plan.ok ? plan.document.milestones : [];
16560
- const count = (n, singular) => `${n} ${singular}${n === 1 ? "" : "s"}`;
16561
- const facts = [
16562
- milestones.length > 0 ? count(milestones.length, "step") : count(scope.touches.length, "path"),
16563
- count(scope.acceptance.length, "check"),
16564
- ];
16565
- if (approval.deliverable === "report")
16566
- facts.push("Report only");
16567
- if (scope.budgetMicrousd !== null)
16568
- facts.push(`$${(scope.budgetMicrousd / 1_000_000).toFixed(2)} attempt cap`);
16569
- if (permission !== null)
16570
- facts.push(permission);
16571
- if (approval.raceTerms !== null)
16572
- 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.
16573
16807
  return (`<section class="card chat-action-card chat-plan" id="task-chat-action" data-approval="${escape(approval.digest)}">` +
16574
- `<p class="chat-plan-outcome">${escape(oneLineOf(scope.goal, 200))}</p>` +
16575
- `<p class="chat-plan-facts">${facts.map(escape).join(" · ")}</p>` +
16576
- `<details class="chat-approval">` +
16577
- `<summary data-primary-action><span class="button-link">Approve plan</span></summary>` +
16578
- `<form method="post" action="${taskHref(focus.executionId)}/approve" class="chat-approval-form approve-form">` +
16579
- `<input type="hidden" name="csrf" value="${escape(csrf)}">` +
16580
- `<input type="hidden" name="nonce" value="${escape(approval.nonce)}">` +
16581
- `<input type="hidden" name="digest" value="${escape(approval.digest)}">` +
16582
- `<input type="hidden" name="return" value="${escape(returnTo)}">` +
16583
- `<input type="text" name="username" autocomplete="username" class="visually-hidden" tabindex="-1" aria-hidden="true">` +
16584
- `<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>` +
16585
- (approval.planDocument === null ? "" : `<div class="chat-approval-section"><span class="approval-label">proposed plan</span>${executionPlanHtml(approval.planDocument, true)}${planContractHtml(approval.planContract, "ceremony")}</div>`) +
16586
- (approval.deliverable === "report" ? `<p class="meta"><span class="badge">report only</span> This investigates and reports back without changing the repository.</p>` : "") +
16587
- `<div class="chat-approval-section"><span class="approval-label">goal</span><p class="approval-goal">${escape(scope.goal)}</p></div>` +
16588
- `<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>` +
16589
- `<div class="approval-boundary"><p class="approval-label">may touch</p>${approvalPathsHtml(scope.touches)}</div></div>` +
16590
- acceptanceCeremonyHtml(scope.acceptance) + revision + race +
16591
- `<div class="approval-chips"><span class="approval-chip">quality · <strong>${escape(qualityModeTitle(scope.qualityMode ?? "default"))}</strong></span>` +
16592
- (permission === null ? "" : `<span class="approval-chip">${escape(permission)}</span>`) +
16593
- (scope.budgetMicrousd === null ? "" : `<span class="approval-chip">$${(scope.budgetMicrousd / 1_000_000).toFixed(2)} attempt cap</span>`) +
16594
- (focus.route === null || focus.route.projection === null ? "" : `<span class="approval-chip">${escape(riskTitle(focus.route.riskLevel))} · ${escape(focus.route.projection.postureWords)}</span>`) +
16595
- `</div>` +
16596
- agentsCeremonyHtml(focus.route) +
16597
- runtimeDetailsHtml(scope) +
16598
- `<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>` +
16599
- `<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>`);
16600
16830
  }
16601
16831
  /** One live, server-derived journey from request to proof. The fragment is
16602
16832
  * safe to refresh independently, so an in-progress message is never lost. */
@@ -17965,6 +18195,7 @@ function statusLineHtml(status, extra = "") {
17965
18195
  function workDiagnosticsHtml(diagnostics) {
17966
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("");
17967
18197
  }
18198
+ const TASK_GROUP_LABEL = { ...ASK_LABEL, building: 'Building', rest: 'Recent' };
17968
18199
  function workPage(chrome, data) {
17969
18200
  const href = (view, cursor) => {
17970
18201
  const query = new URLSearchParams();
@@ -18007,6 +18238,10 @@ function workPage(chrome, data) {
18007
18238
  const view = {
18008
18239
  kind: 'tasks',
18009
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),
18010
18245
  rows: data.work.items.map(row => {
18011
18246
  const target = row.primaryAction?.target;
18012
18247
  const actionHref = row.primaryAction?.code === 'open-result' && target?.runId != null
@@ -18018,6 +18253,7 @@ function workPage(chrome, data) {
18018
18253
  project: data.multiProject || row.repo === null ? (row.repo === null ? 'Unplaced' : projectName(row.repo)) : null,
18019
18254
  age: relativeAge(row.updatedAt, data.now),
18020
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'),
18021
18257
  action: actionHref === null || row.primaryAction === null ? null : { label: row.primaryAction.label, href: actionHref },
18022
18258
  detail: needsYouDetail ? row.status.detail : null,
18023
18259
  problem: row.familyProblem,
@@ -19839,6 +20075,10 @@ function taskBodyParts(data) {
19839
20075
  : approval.approved
19840
20076
  ? "approved"
19841
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;
19842
20082
  const planCard = data.planDocument === null
19843
20083
  ? data.plan === "requested"
19844
20084
  ? data.revision != null && !("problem" in data.revision)
@@ -19852,10 +20092,10 @@ function taskBodyParts(data) {
19852
20092
  (approval.approved ? `<p class="meta">The agent can adapt this route when evidence changes; your approved outcome stays fixed.</p>` : "") +
19853
20093
  `</div><span class="plan-lock">${planStanding}</span>${approval.approved ? `</summary><div class="planner-plan-body">` : `</div>`}` +
19854
20094
  executionPlanHtml(displayedPlanDocument ?? data.planDocument) +
19855
- (approval.approved ? "" : planContractHtml(data.planContract ?? null, "full")) +
20095
+ (approval.approved || sheetStatesContract ? "" : planContractHtml(data.planContract ?? null, "full")) +
19856
20096
  (data.csrf === "" || data.planSha == null || approval.approved
19857
20097
  ? ""
19858
- : `<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">` +
19859
20099
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}"><input type="hidden" name="saw-plan" value="${escape(data.planSha)}">` +
19860
20100
  `<label>plan details <span class="meta">Keep the five headings. Approval locks this version for the build.</span>` +
19861
20101
  `<textarea name="plan-document" rows="14">${escape(data.planDocument)}</textarea></label>` +
@@ -19882,20 +20122,10 @@ function taskBodyParts(data) {
19882
20122
  // The approval form restates every field the digest binds — an operator
19883
20123
  // approves what is on this form, not what is elsewhere on the page — and
19884
20124
  // requires the token typed again. The session got you here; only the
19885
- // token agrees.
19886
- // The ceremony is the page's first card when a scope waits for its yes
19887
- // (task page pass): it states the wait, restates every term the digest
19888
- // binds, and puts the approve act above the fold — the consent-sheet
19889
- // shape. An unapprovable scope gets the problem and the edit road
19890
- // instead of a password it cannot use.
19891
- const approvalProfile = scope?.profile ?? null;
19892
- const approvalPermission = approvalProfile === null
19893
- ? null
19894
- : approvalProfile.provider === "claude"
19895
- ? approvalProfile.permissionArgv === "bypassPermissions" ? "Full access" : "Auto permissions"
19896
- : approvalProfile.provider === "gemini"
19897
- ? approvalProfile.approvalArgv === "yolo" ? "Full access" : "Auto permissions"
19898
- : 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.
19899
20129
  const consentDoor = consentDoorOf(scope, data.route);
19900
20130
  const revisionInApproval = scope !== null && !approval.approved && data.plan !== "requested" &&
19901
20131
  scope.profileState !== "unresolved" && consentDoor.open && data.dispatch?.action !== "repair-dependency" &&
@@ -19910,79 +20140,27 @@ function taskBodyParts(data) {
19910
20140
  `<p class="ceremony-road"><a class="button-link" href="#scope">edit the scope to fix it →</a></p></div>`
19911
20141
  : !consentDoor.open
19912
20142
  ? consentClosedHtml(task.id, consentDoor, "task")
19913
- : [
19914
- `<form method="post" action="${taskHref(task.id)}/approve" class="card approve-form approval-card" id="approve">`,
19915
- `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
19916
- `<input type="hidden" name="nonce" value="${escape(data.nonce)}">`,
19917
- `<input type="hidden" name="digest" value="${escape(data.approvalDigest ?? scope.digest)}">`,
19918
- `<input type="text" name="username" autocomplete="username" class="visually-hidden" tabindex="-1" aria-hidden="true">`,
19919
- `<div class="ceremony-head"><span class="approval-title"><strong>approve exactly this:</strong></span><a href="#scope">Edit details</a></div>`,
19920
- // No duplicate summary card or skip-ahead approval action.
19921
- // Every signed term still follows, before password confirmation.
19922
- `<p class="meta approval-orient" data-approval-orient>Nothing starts until you approve.</p>`,
19923
- `<div class="approval-terms" id="approval-terms">`,
19924
- // The deliverable INSIDE the ceremony (mate arc §10): a yes on a
19925
- // scout task authorizes a read-only session and a report, never
19926
- // a branch — said where the signature is given.
19927
- data.deliverable === "report"
19928
- ? `<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>`
19929
- : "",
19930
- // The filer INSIDE the ceremony (MCP spec v6): a coordinator's
19931
- // request is signed knowing whose it is — and until this
19932
- // signature, nothing plans, claims, or runs it.
19933
- data.coordinator === null || data.coordinator === undefined
19934
- ? ""
19935
- : `<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>`,
19936
- `<p class="approval-label">goal</p><p class="approval-goal">${escape(scope.goal)}</p>`,
19937
- scope.candidate ? `<p class="approval-label">Saved commit</p><p style="overflow-wrap:anywhere"><code>${escape(scope.candidate)}</code></p>` : "",
19938
- revisionCard,
19939
- `<div class="approval-boundaries">`,
19940
- `<div class="approval-boundary"><p class="approval-label">not this</p><p>${scope.outOfScope === null ? "<em>no exclusions</em>" : escape(scope.outOfScope)}</p></div>`,
19941
- `<div class="approval-boundary"><p class="approval-label">touches</p>${approvalPathsHtml(scope.touches)}</div>`,
19942
- `</div>`,
19943
- acceptanceCeremonyHtml(scope.acceptance),
19944
- // The contract amendment INSIDE the ceremony (contract handoff,
19945
- // task 1): what the yes accepts that the operator did not file.
19946
- data.plan === "drafted" ? planContractHtml(data.planContract ?? null, "ceremony") : "",
19947
- `</div>`,
19948
- `<div class="approval-chips"><span class="approval-chip">quality · <strong>${escape(qualityModeTitle(scope.qualityMode ?? "default"))}</strong></span>` +
19949
- (approvalPermission === null ? "" : `<span class="approval-chip">${escape(approvalPermission)}</span>`) +
19950
- `</div>`,
19951
- // The revision feedback above includes its lineage: the source,
19952
- // carried terms, re-resolved terms, and grants that never inherit.
19953
- // The AGENTS inside the ceremony (v47/v48): who plans, builds,
19954
- // repairs, and reviews, and why — said where the yes is given, in
19955
- // the same block chat and /next show. Availability is volatile and
19956
- // stays outside these terms; the runtime limits the profile seals
19957
- // are one tap away, never in the way.
19958
- agentsCeremonyHtml(data.route),
19959
- runtimeDetailsHtml(scope),
19960
- scope.budgetMicrousd === null
19961
- ? ""
19962
- : `<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>`,
19963
- // One yes covers BOTH documents (finding 31): the race terms are
19964
- // restated on the same card the password signs, or they are not
19965
- // approved at all.
19966
- data.raceTerms === null || data.raceTerms === undefined
19967
- ? ""
19968
- : data.raceTerms.kind === "comparison"
19969
- ? `<p><strong>and this comparison:</strong></p>` +
19970
- `<p class="recap" style="margin-top:0">${data.raceTerms.n} agents build this independently — ` +
19971
- `${data.raceTerms.agents.map(agent => `${escape(agent.provider)} · ${escape(agent.model)}`).join(" vs ")}. ` +
19972
- `No dollar caps exist on a comparison — each agent runs until it finishes or stops making progress; ` +
19973
- `spend lands measured only where the harness reports dollars (` +
19974
- `${data.raceTerms.agents.filter(agent => agent.provider === "claude").length} of ${data.raceTerms.n} lanes here). ` +
19975
- `You will compare the results and pick one.</p>`
19976
- : `<p><strong>and this tournament:</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
- `Each may spend $${(data.raceTerms.perAgentBudgetMicrousd / 1_000_000).toFixed(2)} plus a ` +
19980
- `$${(data.raceTerms.overrunReserveMicrousd / 1_000_000).toFixed(2)} overrun reserve; the whole tournament is capped at ` +
19981
- `$${(data.raceTerms.totalBudgetMicrousd / 1_000_000).toFixed(2)}. You will compare the results and pick one.</p>`,
19982
- `<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>`,
19983
- `<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>`,
19984
- `</form>`,
19985
- ].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
+ });
19986
20164
  // The attended road (Phase 2E): beside the approval, never replacing it.
19987
20165
  // The mint button leads to the CONFIRM screen where every term renders
19988
20166
  // and the password signs; an open authorization shows its state, its
@@ -20507,7 +20685,7 @@ function taskBodyParts(data) {
20507
20685
  `<p class="meta"><strong>Plan first</strong> drafts it from the repository, or <a href="#scope">write it yourself</a>.</p></div>`
20508
20686
  : "";
20509
20687
  const approvalHtml = dependencyChoiceNeeded || approveForm === "" ? "" : data.dispatch?.action === "approve-scope"
20510
- ? `<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>`
20511
20689
  : `<details class="task-secondary-approval"><summary>Updated approval terms</summary>${approveForm}</details>`;
20512
20690
  const optionsHtml = `${identity}${dispatchStatus}${dependencyChoiceNeeded ? "" : actsBar}`;
20513
20691
  const optionsOpen = data.assignment?.primaryAction?.code === "unhold" || data.assignment?.primaryAction?.code === "retry-task";
@@ -21197,7 +21375,7 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
21197
21375
  taskHref: taskHref(view.taskId), chatHref: taskChatHref(view.taskId),
21198
21376
  status: { label: status.label, tone: status.tone, token: status.token },
21199
21377
  problem: view.detail === null ? view.historyProblem : null,
21200
- 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: "",
21201
21379
  notes: view.notes.map(one => ({ author: one.author, at: one.createdAt, note: one.note })),
21202
21380
  };
21203
21381
  if (run === null || view.detail === null) {
@@ -21238,6 +21416,17 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
21238
21416
  ? { action: `${taskHref(view.taskId)}/complete`, receipt: assignment.receipt.digest, run: run.id } : null;
21239
21417
  if (complete !== null)
21240
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
+ }) };
21241
21430
  if (view.notes.length > 0) {
21242
21431
  parts.push(`<section class="card cockpit-section" data-cockpit-section="notes"><h3>operator notes</h3>` +
21243
21432
  view.notes.map(one => `<p class="row"><span class="meta">${escape(one.author)} · ${whenTime(one.createdAt)}</span> ${escape(one.note)}</p>`).join("\n") +
@@ -21246,7 +21435,7 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
21246
21435
  return {
21247
21436
  html: `<section class="cockpit-detail">${parts.join("\n")}</section>`,
21248
21437
  selected: {
21249
- ...selected, complete, panel: panel.panel, contest,
21438
+ ...selected, complete, decision, panel: panel.panel, contest,
21250
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}` },
21251
21440
  },
21252
21441
  };
@@ -22222,6 +22411,7 @@ function resultPanelParts(detail, o) {
22222
22411
  const acceptable = personChecks.length > 0 && humanReview && o.csrf !== "" && (current != null || detail.assignment == null);
22223
22412
  const youCheck = personChecks.length === 0 ? null : {
22224
22413
  lines: [...new Set(personChecks)],
22414
+ items: personCheckItems(proof, patchOk ? patch.text : null, shown, runId),
22225
22415
  accept: acceptable && need?.accept == null ? { action: `${taskHref(detail.taskId)}/accept-proof`, run: run.id, returnTo: o.returnTo } : null,
22226
22416
  };
22227
22417
  const youCheckHtml = youCheck === null ? "" :
@@ -22530,6 +22720,37 @@ function resultPanelParts(detail, o) {
22530
22720
  };
22531
22721
  return { html, panel };
22532
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
+ }
22533
22754
  /** A Needs you result's one action (needs-you.ts): a link to the act that resolves it, or Confirm it
22534
22755
  * stopped behind the password. Null under every other headline. */
22535
22756
  export function needActionOf(assignment, status, csrf, returnTo, result) {