toolroll 0.9.11 → 0.9.13

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 (49) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/assignment-ui.js +1 -1
  3. package/dist/assignment.d.ts +3 -0
  4. package/dist/assignment.js +3 -1
  5. package/dist/browser/workspace.css +2 -2
  6. package/dist/browser/workspace.js +25 -23
  7. package/dist/browser-crew.js +1 -1
  8. package/dist/browser-workspace.d.ts +79 -0
  9. package/dist/chat-delivery.js +17 -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.js +80 -4
  28. package/dist/result-acts.d.ts +44 -0
  29. package/dist/result-acts.js +36 -0
  30. package/dist/result-review.js +1 -1
  31. package/dist/serve.d.ts +6 -1
  32. package/dist/serve.js +500 -239
  33. package/dist/setup-guide.d.ts +2 -2
  34. package/dist/store.js +8 -0
  35. package/dist/surface.js +8 -1
  36. package/dist/task-status.d.ts +6 -0
  37. package/dist/task-status.js +19 -2
  38. package/dist/team-cli.js +1 -1
  39. package/dist/telegram-progress.js +7 -0
  40. package/dist/telegram.js +34 -3
  41. package/dist/test-temp.js +1 -1
  42. package/dist/toolroll-update.d.ts +3 -0
  43. package/dist/toolroll-update.js +13 -2
  44. package/dist/work-index.d.ts +9 -0
  45. package/dist/work-index.js +57 -20
  46. package/dist/work-summary.d.ts +4 -1
  47. package/dist/workspace-ui.d.ts +29 -0
  48. package/dist/workspace-ui.js +32 -3
  49. package/package.json +1 -1
package/dist/serve.js CHANGED
@@ -56,12 +56,14 @@ import { knowledgeHtml, knowledgeContextHtml, KNOWLEDGE_CSS, decisionsHtml, memo
56
56
  import { listDecisions, recordDecision, retireDecision, searchMemory } from "./project-memory.js";
57
57
  import { decideProposal, listProposals, memoryStatus } from "./memory-pass.js";
58
58
  import { learningHtml } from "./workspace-ui.js";
59
+ import { resultActsOf } from "./result-acts.js";
59
60
  import { createSessionEndpoint } from './session-server.js';
60
61
  import { handleTeamHttp } from './team-http.js';
61
62
  import { teamWorkspaceHtml } from './team-ui.js';
62
63
  import { createTeamRuntime } from './team-runtime.js';
63
64
  import { prepareWorkspaceRevision, WorkspaceValidatorCache } from "./workspace-revision.js";
64
65
  import { workIndexPage, workCountsByProject, WorkIndexCursorError, WORK_INDEX_MAX_LIMIT } from "./work-index.js";
66
+ import { leadActivity } from "./lead-voice.js";
65
67
  import { openWorkDecisionOf } from "./work-summary.js";
66
68
  import { assignmentOf, checkAssignmentAsOperator } from './assignment.js';
67
69
  import { demoChatHtml, demoThreadHtml, DEMO_CHAT_SCRIPT } from "./demo-chat.js";
@@ -73,7 +75,7 @@ import { leadBriefHtml, LEAD_CONTEXT_CSS } from './lead-context.js';
73
75
  import { assignmentCatchUp } from './assignment-brief.js';
74
76
  import { assignmentActionHref, assignmentCardOf, assignmentStatusOf, assignmentSummaryHtml, assignmentWithEvidence, ASSIGNMENT_CSS } from './assignment-ui.js';
75
77
  import { assignmentStageOf, pullRequestFactOf, stageOfCode, stageOfDispatch, statusDetailsHtml, statusIconSvg, statusWhyHtml, taskStatusOf, TASK_STATUS_CSS } from './task-status.js';
76
- import { NEEDS } from './needs-you.js';
78
+ import { ASKS, ASK_LABEL, NEEDS } from './needs-you.js';
77
79
  import { assignmentPresentationOf, historicalAssessmentReason, shortenedMaterialReason } from './assignment-presentation.js';
78
80
  import { ledgerBody } from "./ledger-view.js";
79
81
  import { EVIDENCE_PACK_CSS, evidencePack, evidencePackHtml, exportDay, ledgerExportChunks } from "./evidence-pack.js";
@@ -162,7 +164,7 @@ import { starterRecipes, savedRecipes, findRecipe, importRecipe, exportRecipe, c
162
164
  import { recipeFromForm, recipeLibraryHtml, recipeEditorHtml, workflowPreviewHtml, recipeScript, RECIPE_CSS, recipeDefinitionPreviewHtml, recipeRunHtml, recipeAnswersFromForm } from "./recipe-ui.js";
163
165
  import { EVIDENCE_CAPS, readVerifiedArtifact, readVerifiedReport, readVerifiedProofForRun, storeEvidence, writeEvidenceFile, scanForSecrets } from "./evidence.js";
164
166
  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";
167
+ import { WORK_VIEWS, REVIEW_TOKENS, RESULT_DECISION_SENTENCE, acceptWordsOf, cantAcceptYetOf, parseWorkView, resultStatusOf, resultHeadlineOf, receiptHeadingOf, receiptPublicationWords, reviewFactsOf, workStatusOf, primaryDestinationOf, needsPerson, dispatchActionLabel, } from "./workspace-ui.js";
166
168
  import { PRICED_BUILD_MODELS } from "./pricing.js";
167
169
  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
170
  import { parseReport } from "./scout-report.js";
@@ -408,6 +410,8 @@ class PersistentSessions extends Map {
408
410
  }
409
411
  hashOf(session) { const id = this.ids.get(session); return id === undefined ? null : PersistentSessions.hash(id); }
410
412
  }
413
+ /** How long a stop waits for teammates and the lead's follow pass before closing anyway. */
414
+ export const SHUTDOWN_WAIT_MS = 5_000;
411
415
  export function createDecisionServer(options) {
412
416
  const { store, evidenceRoot } = options;
413
417
  let coding = options.codingWorkspace ?? null;
@@ -1758,7 +1762,9 @@ export function createDecisionServer(options) {
1758
1762
  }
1759
1763
  // Exact result links carry their own read context. Check the stored
1760
1764
  // placement against both account and instance access before using it;
1761
- // viewing a result never changes the session's selected project.
1765
+ // viewing a result never changes the session's selected project, and
1766
+ // the page's project switch keeps showing the person's own choice.
1767
+ const chosenProject = project;
1762
1768
  if (url.pathname === "/review" && url.searchParams.has("result")) {
1763
1769
  const wanted = url.searchParams.get("result") ?? "";
1764
1770
  const ref = wanted.length > 0 && wanted.length <= 64 && !hasForbiddenControls(wanted) ? store.lookupRef(wanted) : null;
@@ -2026,7 +2032,7 @@ export function createDecisionServer(options) {
2026
2032
  needsVerification: store
2027
2033
  .listCompletedWorkScoped(project, 10, admission)
2028
2034
  .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)
2035
+ assignmentOf(store, one.taskId, now, workAccess(), evidenceRoot) === null)
2030
2036
  .map(one => ({
2031
2037
  taskId: one.taskId,
2032
2038
  title: one.title,
@@ -2364,7 +2370,7 @@ export function createDecisionServer(options) {
2364
2370
  // Admission binds BEFORE the SQL limit (the done page's own rule),
2365
2371
  // and every row is re-proved against the ceiling before ranking.
2366
2372
  const resultRow = (row) => {
2367
- const assignment = assignmentOf(store, row.taskId, now, { principal: "operator", repos: admissionList(), includeUnplaced: visible(null) }, evidenceRoot);
2373
+ const assignment = assignmentOf(store, row.taskId, now, workAccess(), evidenceRoot);
2368
2374
  return { ...row, proofReasons: row.runId === null ? [] : store.proofVerdictFor(row.runId)?.reasons ?? [], ciFailing: ciFailingFor(row.runId, row.prNumber),
2369
2375
  assignment: assignment?.activeTaskId === row.taskId && (assignment.receipt?.runId ?? null) === row.runId ? assignment : null };
2370
2376
  };
@@ -2387,11 +2393,14 @@ export function createDecisionServer(options) {
2387
2393
  const selectedRow = wantedId === null ? ranked[0] ?? null : chosen;
2388
2394
  const csrf = who.via === "cookie" ? who.session.csrf : "";
2389
2395
  const expectedRun = url.searchParams.get("run");
2396
+ // An exact link to a result this person can't see (or that never existed) reads the same either way.
2397
+ if (expectedRun !== null && wantedId !== null && chosen === null)
2398
+ return refuse(response, who, 404, "No such result in your projects.", "/work");
2390
2399
  if (expectedRun !== null && (!/^[1-9]\d*$/.test(expectedRun) || selectedRow?.runId !== Number(expectedRun))) {
2391
2400
  return sendScreen(response, 409, screen("Result changed", '<h1>Result changed</h1><p>This acceptance link no longer matches the current result. Review the current task before accepting.</p><p class="refusal-back"><a class="button-link" href="/review">Review results</a></p>', { chrome: chromeFor(project, "runs") }));
2392
2401
  }
2393
2402
  const selected = selectedRow === null ? null : reviewCockpitViewOf(selectedRow, who, now);
2394
- const reviewPage = reviewCockpitPage(chromeFor(project, "runs"), {
2403
+ const reviewPage = reviewCockpitPage(chromeFor(wantedId === null ? project : chosenProject, "runs"), {
2395
2404
  queue: ranked,
2396
2405
  queueCap: REVIEW_QUEUE_CAP,
2397
2406
  selected,
@@ -2859,7 +2868,7 @@ export function createDecisionServer(options) {
2859
2868
  // v103: a task's evidence pack, as a printable page or JSON (the whole family, sealed ledger entries included).
2860
2869
  const evidence = matchTaskPath(url.pathname, "/evidence$");
2861
2870
  if (evidence !== null) {
2862
- const access = { principal: "operator", repos: admissionList(), includeUnplaced: visible(null) };
2871
+ const access = workAccess();
2863
2872
  const family = store.taskFamilyOf(evidence.taskId, access.repos, access.includeUnplaced);
2864
2873
  if (family === null || !visible(family.root.repo))
2865
2874
  return refuse(response, who, 404, "no such task", "/tasks");
@@ -2883,7 +2892,7 @@ export function createDecisionServer(options) {
2883
2892
  const to = new Date(Date.parse(last) + 86_400_000).toISOString();
2884
2893
  if (to <= from || Date.parse(to) - Date.parse(from) > 366 * 86_400_000)
2885
2894
  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) };
2895
+ const access = workAccess();
2887
2896
  store.recordAction({ at: now.toISOString(), actor: who.name, repo: null, taskId: null, runId: null, action: "ledger exported", outcome: "exported", source: "access",
2888
2897
  detail: `${from.slice(0, 10)} to ${last.slice(0, 10)}` });
2889
2898
  // A piece at a time (a page of entries, or one pack), yielding between them, and stopping if the reader's access
@@ -3017,6 +3026,14 @@ export function createDecisionServer(options) {
3017
3026
  }
3018
3027
  return respond(response, 200, "application/json", JSON.stringify({ text: window.text, nextOffset: window.nextOffset, final: !live && window.eof }));
3019
3028
  }
3029
+ // One result page (2026-10-02): a builder's finished result opens on
3030
+ // its task's result page, titled with the task. The raw run record
3031
+ // waits there under Details, and here with ?record=1.
3032
+ if (url.searchParams.get("record") === null && !running && found.role === "builder" && (found.outcome === "built" || found.outcome === "no-change") &&
3033
+ completedRowFor(taskId, null)?.runId === found.id) {
3034
+ const tab = parseResultTab(url.searchParams.get("tab"));
3035
+ return redirect(response, `${reviewHref(taskId, found.id)}${tab === "summary" ? "" : `&tab=${tab}`}`);
3036
+ }
3020
3037
  // Pollers exist only for a LIVE run — an orphaned null-outcome run
3021
3038
  // would otherwise be refetched forever (finding 15). The nonce is
3022
3039
  // sendScreen's business now.
@@ -3597,7 +3614,7 @@ export function createDecisionServer(options) {
3597
3614
  if (repos.length > 0) {
3598
3615
  try {
3599
3616
  fleetSnapshot = store.chatSnapshot(repos, now);
3600
- const summaries = workIndexPage(store, now, { principal: 'operator', repos, includeUnplaced: false }, { limit: 100 }).items;
3617
+ const summaries = workIndexPage(store, now, { principal: 'operator', repos, includeUnplaced: false, viewer: who.name }, { limit: 100 }).items;
3601
3618
  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
3619
  fleetSnapshot.attentionCount = needsYouBadge(null);
3603
3620
  }
@@ -4891,8 +4908,6 @@ export function createDecisionServer(options) {
4891
4908
  const pageHtml = s.body;
4892
4909
  const extras = s.workspace ?? {};
4893
4910
  const notices = [...(extras.notices ?? [])];
4894
- if (s.chrome.demo)
4895
- notices.unshift(DEMO_BANNER);
4896
4911
  if (s.chrome.modeBanner)
4897
4912
  notices.push(s.chrome.modeBanner.words);
4898
4913
  if (s.chrome.updateWaiting)
@@ -4907,10 +4922,12 @@ export function createDecisionServer(options) {
4907
4922
  notices.push('Crew updates are unavailable. Open Tasks to inspect saved work.');
4908
4923
  }
4909
4924
  // "Wake me only for these": the navigation carries the count of tasks
4910
- // waiting on a person across everything this person may see.
4925
+ // waiting on a person behind its Tasks link — the open project's, or
4926
+ // everything this person may see — so it matches that page's Needs you tab.
4911
4927
  let needsYou = 0;
4912
4928
  try {
4913
- needsYou = (requestFacts.workCounts ?? workCountsByProject(store, clock(), workAccess())).reduce((sum, one) => sum + one.totals['needs-you'], 0);
4929
+ needsYou = (requestFacts.workCounts ?? workCountsByProject(store, clock(), workAccess()))
4930
+ .filter(one => s.chrome?.project == null || one.repo === s.chrome.project).reduce((sum, one) => sum + one.totals['needs-you'], 0);
4914
4931
  }
4915
4932
  catch {
4916
4933
  needsYou = 0;
@@ -4947,6 +4964,7 @@ export function createDecisionServer(options) {
4947
4964
  projects: browserProjectsOf(s.chrome.projects ?? []), ...crew,
4948
4965
  conversation, ...(extras.team ? { team: extras.team } : {}), focus: extras.focus ?? null, result: extras.result ?? null,
4949
4966
  catchUpHtml: extras.catchUpHtml ?? '', controlsHtml: extras.controlsHtml ?? '', notices, view: extras.view ?? null,
4967
+ ...(s.chrome.demo ? { demo: { text: DEMO_BANNER, short: DEMO_BANNER_SHORT } } : {}),
4950
4968
  pageHtml: extras.pageHtml === undefined ? (conversation === null ? pageHtml : null) : extras.pageHtml,
4951
4969
  navigation: [...browserNavigationOf(currentPath, s.chrome.project, needsYou), { label: 'Workspace tools', href: '/menu', active: path.pathname === '/menu' }],
4952
4970
  chats,
@@ -5013,7 +5031,7 @@ export function createDecisionServer(options) {
5013
5031
  * phase, four counts, plan-window use (never dollars: subscriptions don't
5014
5032
  * bill per run) and Catch up's items, each tagged with its tab. Reads only. */
5015
5033
  function chatHomeOf(who, repos, now) {
5016
- const access = { principal: "operator", repos: admissionList(), includeUnplaced: visible(null) };
5034
+ const access = workAccess();
5017
5035
  const admitted = (repo) => repo === null ? visible(null) : repos.includes(repo);
5018
5036
  const agents = store.liveRuns(now).filter(run => admitted(run.repo)).map(run => {
5019
5037
  const root = familyOf(run.taskId)?.root ?? null;
@@ -5042,8 +5060,13 @@ export function createDecisionServer(options) {
5042
5060
  })(),
5043
5061
  }));
5044
5062
  const windows = who.via === "cookie" && store.isInstanceOperator(who.name) ? limitsView(store.providerLimits(), [], { project: projectName, teammate: id => `Teammate ${id}` }, now) : null;
5063
+ // One line: what this person's lead is doing now and when it last acted.
5064
+ const activity = leadActivity(store, who.name);
5065
+ const leadTask = activity?.taskId == null ? null : store.lookupRef(activity.taskId);
5066
+ const lead = activity === null ? null : { doing: activity.doing, at: activity.at,
5067
+ href: leadTask == null || !admitted(leadTask.repo) ? null : taskHref(familyOf(activity.taskId)?.root.id ?? activity.taskId) };
5045
5068
  return {
5046
- agents, counts, catchUp, allHref: "/work",
5069
+ agents, counts, catchUp, allHref: "/work", ...(lead === null ? {} : { lead }),
5047
5070
  planUse: (windows?.tiles ?? []).map(one => ({ name: one.name, window: one.window, percent: one.percent, detail: one.detail, tone: one.tone })),
5048
5071
  };
5049
5072
  }
@@ -5141,7 +5164,8 @@ export function createDecisionServer(options) {
5141
5164
  };
5142
5165
  }
5143
5166
  function workAccess() {
5144
- return { principal: 'operator', repos: admissionList(), includeUnplaced: visible(null) };
5167
+ // The reader's own lead's claims read "Your lead is on it" (lead-voice.ts); nobody else's.
5168
+ return { principal: 'operator', repos: admissionList(), includeUnplaced: visible(null), viewer: requestContext.getStore()?.actor ?? null };
5145
5169
  }
5146
5170
  // All project counts are read together. No task artifacts or processes are
5147
5171
  // inspected to paint a badge or a project switcher.
@@ -5272,7 +5296,7 @@ export function createDecisionServer(options) {
5272
5296
  if (earlierLive.length > 0 && !status.views.includes("running"))
5273
5297
  status.views = [...status.views, "running"];
5274
5298
  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);
5299
+ const recordedAssignment = task.family === undefined ? null : assignmentOf(store, task.id, now, workAccess(), evidenceRoot);
5276
5300
  const assignment = freshAssignment(recordedAssignment, result ?? null);
5277
5301
  const assignmentStatus = assignment === null ? status : assignmentStatusOf(assignment, status);
5278
5302
  if (status.views.includes("running") && !assignmentStatus.views.includes("running"))
@@ -5498,7 +5522,7 @@ export function createDecisionServer(options) {
5498
5522
  const planRevisions = ref === null ? null : revisionLedgerOf(ref.id);
5499
5523
  const milestoneProgress = ref === null ? null : progressOf(ref.id, planRevisions?.current?.document ?? null);
5500
5524
  const family = familyOf(taskId);
5501
- const recordedAssignment = family?.current.id !== taskId ? null : assignmentOf(store, taskId, now, { principal: "operator", repos: admissionList(), includeUnplaced: visible(null) }, evidenceRoot);
5525
+ const recordedAssignment = family?.current.id !== taskId ? null : assignmentOf(store, taskId, now, workAccess(), evidenceRoot);
5502
5526
  const assignment = freshAssignment(recordedAssignment, completion?.receipt ?? null);
5503
5527
  const progressRun = runs.find(one => runIsLive(one))?.id ?? completion?.runId ?? null;
5504
5528
  return {
@@ -5799,7 +5823,7 @@ export function createDecisionServer(options) {
5799
5823
  title: family.root.title,
5800
5824
  state: task.state,
5801
5825
  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),
5826
+ assignment: view?.assignment ?? assignmentOf(store, task.id, now, workAccess(), evidenceRoot),
5803
5827
  project: ref.repo === null ? null : projectName(ref.repo),
5804
5828
  now,
5805
5829
  dispatch: diagnoseTaskDispatch(store, taskId, now),
@@ -5827,6 +5851,7 @@ export function createDecisionServer(options) {
5827
5851
  raceTerms: view.raceTerms ?? null,
5828
5852
  revision: view.revision ?? null,
5829
5853
  coordinator: view.coordinator ?? null,
5854
+ repairChain: view.repairChain ?? null,
5830
5855
  },
5831
5856
  decisions: (view?.decisions ?? store.decisionsForTask(ref.id))
5832
5857
  .filter(one => one.state === "open" || one.state === "expired")
@@ -5864,7 +5889,7 @@ export function createDecisionServer(options) {
5864
5889
  }
5865
5890
  if (!publishing.on || who.via !== "cookie" || who.role !== "approver")
5866
5891
  return null;
5867
- const assignment = assignmentOf(store, taskId, clock(), { principal: "operator", repos: admissionList(), includeUnplaced: visible(null) }, evidenceRoot);
5892
+ const assignment = assignmentOf(store, taskId, clock(), workAccess(), evidenceRoot);
5868
5893
  const receipt = assignment?.receipt ?? null;
5869
5894
  if (assignment?.state !== "complete" || receipt === null || store.publicationForRun(receipt.runId) !== null || pullRequestBlocker(store, receipt.runId) !== null)
5870
5895
  return null;
@@ -10964,7 +10989,7 @@ export function createDecisionServer(options) {
10964
10989
  const completed = checkAssignmentAsOperator(store, taskId, digest, principal, now, evidenceRoot);
10965
10990
  if (!completed.ok)
10966
10991
  return taskScreen(response, who, taskId, completed.message, 409);
10967
- return redirect(response, reviewHref(taskId, Number(namedRun), taskRepoOf(ref.id)));
10992
+ return redirect(response, reviewHref(taskId, Number(namedRun)));
10968
10993
  }
10969
10994
  case "retry-review": {
10970
10995
  return taskScreen(response, who, taskId, "Separate agent reviews have retired. Open the saved result to mark it complete or request a revision.", 410);
@@ -11478,7 +11503,7 @@ export function createDecisionServer(options) {
11478
11503
  run,
11479
11504
  receipt,
11480
11505
  assignment: (() => {
11481
- const value = assignmentOf(store, taskId, now, { principal: "operator", repos: admissionList(), includeUnplaced: visible(null) }, evidenceRoot);
11506
+ const value = assignmentOf(store, taskId, now, workAccess(), evidenceRoot);
11482
11507
  return value?.receipt?.runId === run.id && value.activeTaskId === taskId ? freshAssignment(value, receipt) : null;
11483
11508
  })(),
11484
11509
  handoff,
@@ -11486,6 +11511,7 @@ export function createDecisionServer(options) {
11486
11511
  terminal,
11487
11512
  publication,
11488
11513
  pullRequestTo: pullRequestTargetOf(run.id),
11514
+ publishing: pullRequestTargetOf(run.id) !== null ? "pull-request" : publishingOf(store, store.refById(run.taskRef)?.repo ?? null).on ? "other" : "off",
11489
11515
  ciFailing: publication !== null && publication.prNumber !== null && store.hasOpenCiEpisode(publication.githubRepo, publication.prNumber),
11490
11516
  files,
11491
11517
  outsideTouches: files.filter(one => one.outsideTouches).map(one => one.path),
@@ -11665,6 +11691,19 @@ export function createDecisionServer(options) {
11665
11691
  });
11666
11692
  });
11667
11693
  // Keep native process custody until shutdown has verified agent/tool exit.
11694
+ // Coding custody does not depend on teammates or the lead's follow pass, so
11695
+ // it closes at once and never waits behind a model turn: a stop must release
11696
+ // the owner record well inside the service's exit window (Oct 2: deploys
11697
+ // over 0.9.11 found it still held by a killed process).
11698
+ let codingClosing = null;
11699
+ const closeCoding = () => {
11700
+ leadClosing = true;
11701
+ return codingClosing ??= Promise.resolve().then(() => coding?.close());
11702
+ };
11703
+ const bounded = (work) => new Promise(done => {
11704
+ const timer = setTimeout(done, SHUTDOWN_WAIT_MS);
11705
+ void Promise.resolve(work).catch(() => { }).finally(() => { clearTimeout(timer); done(); });
11706
+ });
11668
11707
  const closeServer = server.close.bind(server);
11669
11708
  server.close = ((callback) => {
11670
11709
  leadClosing = true;
@@ -11675,7 +11714,7 @@ export function createDecisionServer(options) {
11675
11714
  stream.end();
11676
11715
  chatStreams.clear();
11677
11716
  flowRooms.close();
11678
- void (async () => { await team.close(); await leadMaintenance?.stop(); await coding?.close(); })().then(() => closeServer(callback)).catch(error => {
11717
+ void Promise.all([closeCoding(), bounded(team.close()), bounded(leadMaintenance?.stop())]).then(() => closeServer(callback)).catch(error => {
11679
11718
  if (callback)
11680
11719
  callback(error instanceof Error ? error : Error('Coding session shutdown failed.'));
11681
11720
  else
@@ -11683,7 +11722,7 @@ export function createDecisionServer(options) {
11683
11722
  });
11684
11723
  return server;
11685
11724
  });
11686
- return Object.assign(server, { mintSignInLink });
11725
+ return Object.assign(server, { mintSignInLink, closeCoding });
11687
11726
  }
11688
11727
  // ---- path plumbing ---------------------------------------------------------
11689
11728
  /**
@@ -12103,6 +12142,199 @@ function acceptanceCeremonyHtml(criteria) {
12103
12142
  .join("") +
12104
12143
  `</ul>`);
12105
12144
  }
12145
+ /** "claude" + "opus" → "Claude Opus"; "claude-opus-5-5" → "Claude Opus 5.5"; anything else keeps its exact id. */
12146
+ function agentNameWords(provider, model) {
12147
+ const name = providerName(provider);
12148
+ if (model === null || model === "" || model === "default")
12149
+ return name;
12150
+ const bare = model.startsWith(`${provider}-`) ? model.slice(provider.length + 1) : model;
12151
+ const title = (word) => `${word[0].toUpperCase()}${word.slice(1)}`;
12152
+ if (/^[a-z]+$/.test(bare))
12153
+ return `${name} ${title(bare)}`;
12154
+ const versioned = /^([a-z]+)-(\d+(?:-\d+)*)$/.exec(bare);
12155
+ if (versioned !== null)
12156
+ return `${name} ${title(versioned[1])} ${versioned[2].replace(/-/g, ".")}`;
12157
+ return `${name} ${model}`;
12158
+ }
12159
+ /** The permission a sealed profile grants, in two or three words. */
12160
+ function permissionWordsOf(profile) {
12161
+ if (profile === null || profile === undefined)
12162
+ return null;
12163
+ return profile.provider === "claude"
12164
+ ? profile.permissionArgv === "bypassPermissions" ? "Full access" : "Auto permissions"
12165
+ : profile.provider === "gemini"
12166
+ ? profile.approvalArgv === "yolo" ? "Full access" : "Auto permissions"
12167
+ : profile.sandboxMode === "danger-full-access" ? "Full access" : "Workspace sandbox";
12168
+ }
12169
+ /** One plain sentence for a revision (approval critique, Oct 2):
12170
+ * "Fixes what build #9 missed: <criterion>." The notes, paths and lineage
12171
+ * stay in the Details fold. */
12172
+ function revisionSentence(revision, acceptance) {
12173
+ const build = `build #${revision.sourceRun}`;
12174
+ if (revision.kind === "ci-repair")
12175
+ return `Fixes the checks that failed in ${build}.`;
12176
+ const clean = (text) => oneLineOf(text, 280).replace(/[.\s]+$/, "");
12177
+ if (revision.kind === "criterion-repair") {
12178
+ const ids = revision.comments.flatMap(one => (/Unmet: ([^.]+)\./.exec(one.note)?.[1] ?? "").split(",").map(id => id.trim()).filter(id => id !== ""));
12179
+ const missed = ids.map(id => acceptance.find(one => one.id === id)?.statement ?? null).filter((one) => one !== null);
12180
+ if (missed.length > 0)
12181
+ return `Fixes what ${build} missed: ${missed.map(clean).join("; ")}.`;
12182
+ }
12183
+ const notes = revision.comments.map(one => clean(one.note)).filter(one => one !== "");
12184
+ return notes.length === 0 ? `Revises ${build}.` : `Fixes what ${build} missed: ${notes.join("; ")}.`;
12185
+ }
12186
+ /** A planner's amendment, said plainly and kept in view: approving binds
12187
+ * the amended terms, so this is never folded away. */
12188
+ function approvalAmendmentHtml(view) {
12189
+ if (view === null || view === undefined || "problem" in view || !view.filed && view.revision !== true || view.changes.length === 0)
12190
+ return "";
12191
+ const line = (change) => {
12192
+ switch (change.field) {
12193
+ case "goal": return `Goal was: ${escape(change.before)}`;
12194
+ 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)}`;
12195
+ case "touches": return `${change.kind === "added" ? "Adds the path" : "Drops the path"} <span class="mono">${escape(change.path)}</span>`;
12196
+ 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 ?? "")}`;
12197
+ }
12198
+ };
12199
+ const count = `${view.changes.length} change${view.changes.length === 1 ? "" : "s"}`;
12200
+ return (`<div class="approval-amendment" id="contract-amendment">` +
12201
+ `<p><strong>${view.revision === true ? `The plan makes ${count} from your notes` : `The plan makes ${count} to what you filed`}</strong></p>` +
12202
+ `<ul>${view.changes.map(one => `<li>${line(one)}</li>`).join("")}</ul>` +
12203
+ (view.amendment === null ? "" : `<p class="meta">Why: ${escape(view.amendment)}</p>`) +
12204
+ `</div>`);
12205
+ }
12206
+ /**
12207
+ * The approval sheet (approval critique, Oct 2): the plan open, in plain
12208
+ * rows — Goal, Changes, Won't touch, Done when — one line of who builds,
12209
+ * one Approve & start beside the password, and every remaining signed
12210
+ * term (lineage, routing, runtime limits, the seal) in one Details fold.
12211
+ * Presentation only: the same hidden fields, nonce, digest and password
12212
+ * post to the same route, and the task page and chat card share it.
12213
+ */
12214
+ function approvalSheetHtml(input) {
12215
+ const { scope, route, raceTerms } = input;
12216
+ // A machine-drafted repair appends its brief to the signed goal; the row
12217
+ // shows the goal and one sentence says what the repair fixes. The whole
12218
+ // signed goal stays in Details.
12219
+ 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);
12220
+ const goalShown = repairText === null ? scope.goal : scope.goal.slice(0, repairText.index);
12221
+ 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("; ");
12222
+ const repairBuild = input.revision?.sourceRun ?? input.repairChain?.sourceRun ?? null;
12223
+ const builtBy = repairBuild === null ? "the last build" : `build #${repairBuild}`;
12224
+ const sentence = input.revision !== null && (repairText === null || input.revision.comments.length > 0) ? revisionSentence(input.revision, scope.acceptance)
12225
+ : repairText === null ? null
12226
+ : repairText[2] !== undefined ? `Fixes what ${builtBy} missed: ${statementsOf(repairText[2])}.`
12227
+ : repairText[3] !== undefined ? `Collects the evidence ${builtBy} was missing for: ${statementsOf(repairText[3])}. The saved code stays the same.`
12228
+ : `Fixes the checks that failed in ${builtBy}.`;
12229
+ const plan = input.planDocument === null ? null : parseExecutionPlanDocument(input.planDocument);
12230
+ const milestones = plan !== null && plan.ok ? plan.document.milestones : [];
12231
+ const permission = permissionWordsOf(scope.profile);
12232
+ const money = (micro) => `$${(micro / 1_000_000).toFixed(2)}`;
12233
+ const row = (label, body) => `<div class="approval-row"><dt>${label}</dt><dd>${body}</dd></div>`;
12234
+ const changes = (milestones.length > 0 ? `<ol class="approval-steps">${milestones.map(one => `<li>${escape(one)}</li>`).join("")}</ol>` : "") +
12235
+ (scope.touches.length > 0
12236
+ ? `<p>Only in these paths:</p>${approvalPathsHtml(scope.touches)}`
12237
+ : `<p${milestones.length > 0 ? ` class="meta"` : ""}>Any file in the project.</p>`);
12238
+ const doneWhen = scope.acceptance.length === 0
12239
+ ? `<p>You decide when you review the result.</p>`
12240
+ : `<ul class="approval-done">${scope.acceptance.map(one => `<li>${escape(one.statement)}</li>`).join("")}</ul>`;
12241
+ // Who builds, in one line: the builder and planner by name, the cap, and full access when granted.
12242
+ const legs = route?.projection?.legs ?? [];
12243
+ const legOf = (phase) => legs.find(one => one.phase === phase);
12244
+ const who = [];
12245
+ const builder = legOf("build");
12246
+ if (raceTerms === null) {
12247
+ if (builder !== undefined)
12248
+ who.push(`Builder ${agentNameWords(builder.provider, builder.model)}`);
12249
+ else if (route?.legacy != null)
12250
+ who.push(`Builder ${agentNameWords(route.legacy.provider, route.legacy.model)}`);
12251
+ else if (scope.profile != null)
12252
+ who.push(`Builder ${agentNameWords(scope.profile.provider, scope.profile.model)}`);
12253
+ }
12254
+ const planner = legOf("plan");
12255
+ if (planner !== undefined)
12256
+ who.push(`Planner ${agentNameWords(planner.provider, planner.model)}`);
12257
+ if (scope.budgetMicrousd !== null)
12258
+ who.push(`Cap ${money(scope.budgetMicrousd)} per attempt`);
12259
+ if (permission === "Full access")
12260
+ who.push("Full access");
12261
+ const race = raceTerms === null
12262
+ ? ""
12263
+ : `<p class="approval-race">${raceTerms.n} agents build this separately: ${raceTerms.agents.map(one => escape(agentNameWords(one.provider, one.model))).join(" and ")}. ` +
12264
+ (raceTerms.kind === "comparison"
12265
+ ? "No dollar cap; you compare the results and pick one.</p>"
12266
+ : `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>`);
12267
+ const after = input.deliverable === "report"
12268
+ ? "An agent investigates without changing the repository. You'll hear when its report is ready."
12269
+ : raceTerms !== null
12270
+ ? "Each agent starts in its own branch. You'll hear when the results are ready to compare."
12271
+ : "An agent starts in its own branch. You'll hear when it's ready to review.";
12272
+ const submit = raceTerms === null ? "Approve & start" : raceTerms.kind === "comparison" ? "Approve comparison" : "Approve tournament";
12273
+ // Everything else the digest binds, one tap away.
12274
+ const detail = (title, body, attrs = "") => body === "" ? "" : `<section class="approval-detail"${attrs}><h3>${title}</h3>${body}</section>`;
12275
+ const revisionDetail = input.revision === null ? "" : detail("Earlier build", `<div class="revision-card" data-revision-feedback>` +
12276
+ `<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>` +
12277
+ (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>`) +
12278
+ revisionLineageHtml(input.revision.lineage) +
12279
+ `</div>`);
12280
+ const contract = input.planContract;
12281
+ const contractDetail = input.planDocument === null || approvalAmendmentHtml(contract) !== "" ? ""
12282
+ : contract === null || contract === undefined ? `<p class="meta">No record compares this plan with what was filed. Check the rows above against your request.</p>`
12283
+ : "problem" in contract ? `<p class="meta">${escape(contract.problem)} · <a href="/r/${contract.run}">run ${contract.run}</a></p>`
12284
+ : !contract.filed && contract.revision !== true ? `<p class="meta">Nothing was filed before planning, so every term is the planner's proposal.</p>`
12285
+ : `<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>`;
12286
+ const criteriaDetail = scope.acceptance.length === 0 ? ""
12287
+ : `<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>`;
12288
+ const projection = route?.projection ?? null;
12289
+ const agentsDetail = route === null || route === undefined ? ""
12290
+ : projection === null
12291
+ ? route.legacy === null ? "" : `<p>${escape(agentsSummaryWords(route))}</p>`
12292
+ : `<p>${escape(projection.summary)}</p>` +
12293
+ `<p class="meta">${escape(riskTitle(route.riskLevel))}: ${escape(riskConsequence(route.riskLevel))}. Uses ${escape(projection.postureWords)}.</p>` +
12294
+ (projection.demands.length === 0 ? "" : `<ul class="meta">${projection.demands.map(one => `<li>${escape(one)}</li>`).join("")}</ul>`) +
12295
+ `<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>` +
12296
+ `<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>` +
12297
+ `<p class="meta">These exact agents are part of what you approve. Changing any of them asks for a fresh approval.</p>`;
12298
+ const limits = profileWords(scope) +
12299
+ `<p class="meta">Checks level: ${escape(qualityModeTitle(scope.qualityMode ?? "default"))}${permission === null ? "" : ` · ${escape(permission)}`}</p>` +
12300
+ (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>`);
12301
+ const details = `<details class="approval-details"><summary>Details</summary>` +
12302
+ revisionDetail +
12303
+ (repairText === null ? "" : detail("Signed goal", `<p>${escape(scope.goal)}</p>`)) +
12304
+ detail("Plan record", contractDetail) +
12305
+ detail("Signed criteria", criteriaDetail) +
12306
+ detail("Why these agents", agentsDetail, ` id="${input.surface === "task" ? "approval-agents" : "chat-approval-agents"}"`) +
12307
+ detail("Runtime limits", limits) +
12308
+ (scope.candidate ? detail("Saved commit", `<p><code class="approval-commit">${escape(scope.candidate)}</code></p>`) : "") +
12309
+ detail("Seal", `<p class="meta">Approval binds to this exact wording. <span class="seal mono">signs ${shortDigest(scope.digest)}</span></p>`) +
12310
+ `</details>`;
12311
+ return (`<form method="post" action="${input.action}" class="approve-form approval-sheet"${input.surface === "task" ? ` id="approve"` : ""}${input.sticky ? " data-sticky" : ""}>` +
12312
+ `<input type="hidden" name="csrf" value="${escape(input.csrf)}">` +
12313
+ `<input type="hidden" name="nonce" value="${escape(input.nonce)}">` +
12314
+ `<input type="hidden" name="digest" value="${escape(input.digest)}">` +
12315
+ (input.returnTo === null ? "" : `<input type="hidden" name="return" value="${escape(input.returnTo)}">`) +
12316
+ `<input type="text" name="username" autocomplete="username" class="visually-hidden" tabindex="-1" aria-hidden="true">` +
12317
+ `<h2 class="approval-sheet-title">${input.deliverable === "report" ? "The investigation" : "The plan"}</h2>` +
12318
+ (sentence === null ? "" : `<p class="approval-revision">${escape(sentence)}</p>`) +
12319
+ (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>`) +
12320
+ (input.deliverable === "report" ? `<p class="approval-note">Read-only: it reports back and changes nothing in the repository.</p>` : "") +
12321
+ `<dl class="approval-rows">` +
12322
+ row("Goal", `<p class="approval-goal">${escape(goalShown)}</p>`) +
12323
+ row("Changes", changes) +
12324
+ row("Won’t touch", `<p>${scope.outOfScope === null ? "Nothing is ruled out." : escape(scope.outOfScope)}</p>`) +
12325
+ row("Done when", doneWhen) +
12326
+ `</dl>` +
12327
+ approvalAmendmentHtml(contract) +
12328
+ race +
12329
+ (who.length === 0 ? "" : `<p class="approval-who">${who.map(escape).join(" · ")}</p>`) +
12330
+ `<div class="approval-act" id="${input.surface === "task" ? "approval-confirm" : "chat-approval-confirm"}">` +
12331
+ `<label class="approval-password"><span class="visually-hidden">Your password</span><input type="password" name="token" autocomplete="current-password" placeholder="Your password"></label>` +
12332
+ `<button type="submit" data-primary-action>${submit}</button></div>` +
12333
+ `<p class="approval-after">${after}</p>` +
12334
+ `<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>` +
12335
+ details +
12336
+ `</form>`);
12337
+ }
12106
12338
  function consentDoorOf(scope, route) {
12107
12339
  if (scope === null)
12108
12340
  return { open: false, title: "There is no scope to approve yet", why: "write the scope first", road: "scope" };
@@ -12374,6 +12606,8 @@ function agentsStripHtml(view, taskId) {
12374
12606
  }
12375
12607
  /** The demo's one-line promise, on every page. */
12376
12608
  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.`;
12609
+ /** The demo notice on a phone: one line. */
12610
+ const DEMO_BANNER_SHORT = "Demo: sample projects. Nothing calls a model or spends.";
12377
12611
  /** Every character that could open a tag or an attribute, dead at the sink. */
12378
12612
  function escape(text) {
12379
12613
  return text
@@ -12488,7 +12722,7 @@ ${THEME_MAPPING}
12488
12722
  --font-mono: "Geist Mono", ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
12489
12723
  /* Phone spacing steps (760px and narrower): side gutter, gap under the
12490
12724
  header, gap between page blocks, between rows, and inside a card. */
12491
- --so-phone-gutter: 14px; --so-phone-gap: 12px; --so-phone-block: 14px; --so-phone-row: 8px; --so-phone-card: 12px;
12725
+ --so-phone-gutter: 16px; --so-phone-gap: 12px; --so-phone-block: 14px; --so-phone-row: 8px; --so-phone-card: 12px;
12492
12726
  }
12493
12727
  @media (prefers-color-scheme: dark) {
12494
12728
  :root:not([data-theme="light"]) {
@@ -12864,7 +13098,7 @@ ${THEME_DARK}
12864
13098
  /* Thumbnails, never a full-width poster: auto-fill leaves a lone
12865
13099
  screenshot at thumbnail size (UI polish 2026-09-13). */
12866
13100
  .receipt-visuals { display: grid; grid-template-columns: repeat(auto-fill, minmax(8rem, 14rem)); gap: .55rem; margin-top: .75rem; }
12867
- .receipt-shot { display: grid; gap: .35rem; color: var(--muted-foreground); font-size: .7rem; text-decoration: none; }
13101
+ .receipt-shot { display: grid; gap: .35rem; color: var(--muted-foreground); font-size: .75rem; text-decoration: none; }
12868
13102
  .receipt-shot img { display: block; width: 100%; aspect-ratio: 16 / 10; object-fit: cover; border: 1px solid var(--glass-border); border-radius: calc(var(--radius) - 3px); background: var(--muted); }
12869
13103
  @media (hover: hover) and (pointer: fine) { .receipt-shot:hover { color: var(--foreground); } }
12870
13104
  .receipt-caveats, .receipt-coverage { margin-top: .8rem; padding: .7rem .8rem; border-left: 1px solid var(--border); border-radius: 0 calc(var(--radius) - 3px) calc(var(--radius) - 3px) 0; background: color-mix(in srgb, var(--muted) 62%, transparent); font-size: .78rem; }
@@ -12997,7 +13231,7 @@ ${THEME_DARK}
12997
13231
  }
12998
13232
  .diff-file[open] > summary::before { transform: rotate(45deg) translateY(-.1rem); }
12999
13233
  .diff-file-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font: 500 .75rem/1.4 var(--font-mono); }
13000
- .diff-file-counts { display: inline-flex; gap: .45rem; flex: none; margin-left: auto; font: 500 .68rem/1 var(--font-mono); }
13234
+ .diff-file-counts { display: inline-flex; gap: .45rem; flex: none; margin-left: auto; font: 500 .75rem/1 var(--font-mono); }
13001
13235
  .diff-file-counts b { color: var(--success); font-weight: 500; }
13002
13236
  .diff-file-counts i { color: var(--destructive); font-style: normal; }
13003
13237
  .diff-rename { margin: 0; padding: .4rem .85rem; border-top: 1px solid var(--glass-border); }
@@ -13005,12 +13239,12 @@ ${THEME_DARK}
13005
13239
  .diff-hunk-head {
13006
13240
  overflow-x: auto; padding: .42rem .85rem; border-top: 1px solid var(--glass-border); border-bottom: 1px solid var(--glass-border);
13007
13241
  background: color-mix(in srgb, var(--running) 8%, var(--card)); color: color-mix(in srgb, var(--running) 72%, var(--foreground));
13008
- font: 500 .68rem/1.4 var(--font-mono); white-space: pre;
13242
+ font: 500 .75rem/1.4 var(--font-mono); white-space: pre;
13009
13243
  }
13010
13244
  .diff-lines { max-width: 100%; overflow-x: auto; background: color-mix(in srgb, var(--background) 48%, var(--card)); }
13011
13245
  .diff-line {
13012
13246
  display: grid; grid-template-columns: 2rem 3.2rem 3.2rem minmax(max-content, 1fr); align-items: stretch;
13013
- min-width: max-content; min-height: 1.8rem; font: 400 .72rem/1.55 var(--font-mono);
13247
+ min-width: max-content; min-height: 1.8rem; font: 400 .75rem/1.55 var(--font-mono);
13014
13248
  }
13015
13249
  @media (hover: hover) and (pointer: fine) { .diff-line:hover { background: color-mix(in srgb, var(--foreground) 4%, transparent); } }
13016
13250
  .diff-line code { display: flex; min-width: 0; padding: .3rem .75rem .3rem .6rem; color: inherit; white-space: pre; }
@@ -13548,7 +13782,9 @@ ${THEME_DARK}
13548
13782
  .approval-title { display: grid; gap: .15rem; }
13549
13783
  .approval-kicker { color: var(--muted-foreground); font: 500 .66rem/1.3 var(--font-mono); letter-spacing: .06em; text-transform: uppercase; }
13550
13784
  .approval-title strong { font-size: 1.05rem; letter-spacing: -.02em; }
13551
- .approval-label { margin: .9rem 0 .2rem; color: var(--muted-foreground); font: 500 .66rem/1.3 var(--font-mono); letter-spacing: .06em; text-transform: uppercase; }
13785
+ .approval-label { display: block; margin: .9rem 0 .2rem; color: var(--muted-foreground); font: 500 12px/1.4 var(--font-sans); }
13786
+ .approval-label::first-letter, .planner-plan .eyebrow::first-letter { text-transform: uppercase; }
13787
+ .planner-plan .eyebrow { font: 500 12px/1.4 var(--font-sans); letter-spacing: 0; text-transform: none; }
13552
13788
  .approval-goal { margin: 0; font-size: 1rem; line-height: 1.55; white-space: pre-wrap; }
13553
13789
  .approval-boundaries { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .6rem; margin-top: .75rem; }
13554
13790
  .approval-boundary { padding: .7rem .8rem; border: 1px solid var(--glass-border); border-radius: calc(var(--radius) - 3px); background: color-mix(in srgb, var(--muted) 45%, transparent); }
@@ -13634,6 +13870,66 @@ ${THEME_DARK}
13634
13870
  .approval-confirm { scroll-margin-top: 5rem; }
13635
13871
  .approval-paths { margin: 0; padding: 0; list-style: none; display: grid; gap: .15rem; }
13636
13872
  .approval-paths li { color: var(--muted-foreground); font-size: .8rem; overflow-wrap: anywhere; }
13873
+ /* The approval sheet (approval critique, Oct 2): the plan open in plain
13874
+ rows, who builds in one line, one Approve & start beside the password,
13875
+ and every other signed term in one Details fold at the end. Sentence
13876
+ case, Geist, 12px and up; prose capped at 75ch. */
13877
+ .approval-sheet { display: grid; gap: 16px; margin: 0; padding: 20px; border: 1px solid var(--border); border-radius: 10px; background: var(--card); }
13878
+ .approval-sheet.approve-form > :not(input) { margin: 0; }
13879
+ .approval-sheet-title { margin: 0; font-size: 15px; font-weight: 600; line-height: 1.4; letter-spacing: -.01em; }
13880
+ .approval-revision, .approval-note, .approval-race { max-width: 75ch; font-size: 14px; line-height: 1.6; overflow-wrap: anywhere; }
13881
+ .approval-note { color: var(--muted-foreground); }
13882
+ .approval-rows { display: grid; gap: 14px; margin: 0; }
13883
+ .approval-row { display: grid; grid-template-columns: 7.5rem minmax(0, 1fr); gap: 16px; }
13884
+ .approval-row dt { margin: 0; color: var(--muted-foreground); font-size: 13px; font-weight: 500; line-height: 1.6; }
13885
+ .approval-row dd { min-width: 0; max-width: 75ch; margin: 0; font-size: 14px; line-height: 1.6; overflow-wrap: anywhere; }
13886
+ .approval-row dd p, .approval-sheet .approval-goal { margin: 0; font-size: 14px; line-height: 1.6; }
13887
+ .approval-row dd :is(ol, ul) { display: grid; gap: 4px; margin: 0; padding-left: 1.25rem; }
13888
+ .approval-row dd ol + p { margin-top: 6px; }
13889
+ .approval-row dd .approval-paths { margin-top: 2px; padding-left: 0; }
13890
+ .approval-row dd .approval-paths li { font-size: 13px; }
13891
+ .approval-amendment { max-width: 75ch; padding: 12px 14px; border-radius: 8px; background: var(--muted); font-size: 14px; line-height: 1.55; overflow-wrap: anywhere; }
13892
+ .approval-amendment p { margin: 0; }
13893
+ .approval-amendment ul { display: grid; gap: 4px; margin: 6px 0; padding-left: 1.25rem; }
13894
+ .approval-who { max-width: 75ch; color: var(--muted-foreground); font-size: 13px; line-height: 1.5; }
13895
+ .approval-act { display: grid; grid-template-columns: minmax(0, 20rem) auto; justify-content: start; align-items: center; gap: 8px; }
13896
+ .approval-password { margin: 0; }
13897
+ .approval-act input[type=password] { margin: 0; font-family: var(--font-sans); }
13898
+ .approval-act button { margin: 0; min-height: 2.125rem; white-space: nowrap; }
13899
+ .approval-after { max-width: 75ch; color: var(--muted-foreground); font-size: 13px; line-height: 1.5; }
13900
+ .approval-secondary { display: flex; flex-wrap: wrap; gap: 8px; }
13901
+ .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; }
13902
+ .approval-link + .approval-link { border-color: transparent; background: none; color: var(--muted-foreground); }
13903
+ @media (hover: hover) and (pointer: fine) { .approval-link:hover { background: var(--muted); color: var(--foreground); } }
13904
+ details.approval-details { margin: 0; padding: 0; border: 0; border-top: 1px solid var(--border); border-radius: 0; background: none; }
13905
+ details.approval-details[open] { padding-bottom: 0; }
13906
+ .approval-details > summary { display: flex; align-items: center; min-height: 40px; padding: 4px 0 0; font-size: 13px; }
13907
+ .approval-detail { max-width: 75ch; margin: 12px 0 0; font-size: 13px; line-height: 1.55; overflow-wrap: anywhere; }
13908
+ .approval-detail h3 { margin: 0 0 4px; font-size: 13px; font-weight: 600; letter-spacing: 0; text-transform: none; }
13909
+ .approval-detail p, .approval-detail ul, .approval-detail dl { margin: 4px 0; }
13910
+ .approval-detail ul { padding-left: 1.25rem; }
13911
+ .approval-detail .revision-card { margin: 0; padding: 0; border: 0; background: none; }
13912
+ .approval-roles { display: grid; gap: 8px; }
13913
+ .approval-roles > div { display: grid; grid-template-columns: 6rem minmax(0, 1fr); gap: 12px; }
13914
+ .approval-roles dt { color: var(--muted-foreground); font-weight: 500; }
13915
+ .approval-roles dd { margin: 0; }
13916
+ .approval-roles dd ul { margin: 2px 0 0; color: var(--muted-foreground); }
13917
+ .chat-plan .approval-sheet { padding: 0; border: 0; background: none; }
13918
+ @media (max-width: 760px) {
13919
+ .approval-sheet { gap: 12px; padding: 14px; }
13920
+ .approval-row { grid-template-columns: 1fr; gap: 2px; }
13921
+ .approval-act { grid-template-columns: 1fr; }
13922
+ .approval-act input[type=password], .approval-act button { min-height: 44px; font-size: 16px; }
13923
+ .approval-act button { width: 100%; }
13924
+ /* The one act stays under the thumb: sticky at the bottom, clear of the home indicator. */
13925
+ .approval-sheet[data-sticky] .approval-act {
13926
+ position: sticky; bottom: 0; z-index: 20; margin: 0 -14px; padding: 10px 14px calc(10px + env(safe-area-inset-bottom, 0px));
13927
+ border-top: 1px solid var(--border); background: var(--card);
13928
+ }
13929
+ .approval-link { min-height: 44px; padding: 0 16px; }
13930
+ .approval-details > summary { min-height: 44px; }
13931
+ .approval-roles > div { grid-template-columns: 1fr; gap: 2px; }
13932
+ }
13637
13933
  .run-facts-details { margin-top: 1.25rem; }
13638
13934
  .run-facts-details > summary { display: flex; align-items: center; justify-content: space-between; gap: .75rem; }
13639
13935
  .run-facts-details > summary .meta { font-size: .75rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@@ -13867,7 +14163,7 @@ ${THEME_DARK}
13867
14163
  .result-pin[open] > summary::before { content: "▾"; }
13868
14164
  .result-pin > summary .meta { font-weight: 400; }
13869
14165
  .result-pin .diff-comment-target { margin: .25rem 0 .5rem; }
13870
- .chat-approval-section, .revision-card, .revision-lineage, .task-history, .diff-comment p { min-width: 0; overflow-wrap: anywhere; }
14166
+ .revision-card, .revision-lineage, .task-history, .diff-comment p { min-width: 0; overflow-wrap: anywhere; }
13871
14167
  .result-feedback-history > summary, .task-history > summary, .task-history a { display: inline-flex; align-items: center; min-height: 44px; padding: .25rem .5rem; }
13872
14168
  .task-history > summary::before { content: "▸"; margin-right: .35rem; }
13873
14169
  .task-history[open] > summary::before { content: "▾"; }
@@ -14332,11 +14628,10 @@ ${THEME_DARK}
14332
14628
  .check-progress[data-final="passed"] { color: var(--success); }
14333
14629
  .check-progress[data-final="failed"] { color: var(--destructive); }
14334
14630
  .check-progress[data-final="unknown"] { color: var(--warning); }
14335
- .receipt-actions [data-primary-action], .task-plan-review .button-link { min-height: 44px; }
14631
+ .receipt-actions [data-primary-action] { min-height: 44px; }
14336
14632
  .task-status-reason { margin-top: .75rem; }
14337
14633
  .task-status-reason summary, .task-status-details > summary { color: var(--muted-foreground); }
14338
14634
  .task-status-details > summary { min-height: 44px; }
14339
- .task-plan-review > summary { min-height: 44px; display: flex; align-items: center; cursor: pointer; font-weight: 650; }
14340
14635
  .task-status-details, #task-control-details { margin: 1rem 0; }
14341
14636
  .task-control-copy { min-width: 0; overflow-wrap: anywhere; }
14342
14637
  [data-primary-action] { max-width: 100%; }
@@ -14351,16 +14646,9 @@ ${THEME_DARK}
14351
14646
  .chat-action-card > summary strong { font-size: .92rem; }
14352
14647
  .chat-action-card > summary small { color: var(--muted-foreground); font-size: .68rem; font-weight: 400; }
14353
14648
  .chat-action-card[open] > summary { border-bottom: 1px solid var(--border); }
14354
- .chat-approval-form { display: grid; gap: .85rem; padding: 1.05rem; }
14355
- .chat-approval-section { display: grid; gap: .3rem; }
14356
- .chat-approval-section > p, .chat-approval-section > pre { margin: 0; }
14357
- .chat-approval-form .approval-boundaries { margin: 0; }
14358
14649
  .chat-run-details { padding: .65rem .75rem; border: 1px solid var(--border); border-radius: calc(var(--radius) - 3px); background: color-mix(in srgb,var(--muted) 45%,transparent); }
14359
14650
  .chat-run-details > summary { color: var(--muted-foreground); cursor: pointer; font-size: .68rem; }
14360
14651
  .chat-run-details > .meta { margin-bottom: 0; }
14361
- .chat-approval-form .approval-confirm { align-items: end; margin-top: .15rem; }
14362
- .chat-approval-form .approval-confirm label { flex: 1 1 18rem; }
14363
- .chat-approval-form .approval-confirm button { min-height: 2.65rem; }
14364
14652
  .chat-decisions { display: grid; gap: .7rem; }
14365
14653
  .chat-section-head { margin: .15rem .15rem 0; }
14366
14654
  .chat-section-head h2 { margin: .2rem 0 0; font-size: 1rem; }
@@ -14557,18 +14845,9 @@ ${THEME_DARK}
14557
14845
  /* The concise plan (package 2): a title, the outcome, one line of
14558
14846
  counts and limits, then one Review plan disclosure over the unchanged
14559
14847
  exact-terms form. */
14560
- .chat-plan-head h2 { margin: 0; font-size: 1rem; letter-spacing: -.025em; }
14561
- .chat-plan-outcome { margin: .65rem 0 .55rem; font-size: .88rem; line-height: 1.5; overflow-wrap: anywhere; }
14562
- .chat-plan-facts { margin: 0; color: var(--muted-foreground); font-size: .74rem; line-height: 1.5; overflow-wrap: anywhere; }
14563
- .chat-plan > details.chat-approval { margin: .85rem 0 0; padding: .85rem 0 0; border: 0; border-top: 1px solid var(--border); border-radius: 0; background: transparent; }
14564
- .chat-plan > details.chat-approval > summary { display: flex; align-items: center; gap: .75rem; cursor: pointer; list-style: none; }
14565
- .chat-plan > details.chat-approval > summary::-webkit-details-marker { display: none; }
14566
- .chat-plan > details.chat-approval > summary .button-link { flex: 0 0 auto; white-space: nowrap; min-height: 44px; }
14567
- .chat-plan > details.chat-approval > .chat-approval-form { padding: .85rem 0 0; }
14568
- .chat-plan .chat-approval-lead { margin: 0 0 .85rem; }
14569
14848
  .chat-approval-stale { margin: .75rem 0 0; }
14570
14849
  .chat-approval-stale a { font-weight: 600; }
14571
- form.approve-form[data-stale="1"] .approval-confirm { opacity: .55; }
14850
+ form.approve-form[data-stale="1"] .approval-confirm, form.approve-form[data-stale="1"] .approval-act { opacity: .55; }
14572
14851
  .proposal-filed { display: flex; align-items: center; flex-wrap: wrap; gap: .6rem; margin: .5rem 0 0; }
14573
14852
  .chat-thinking { display: flex; align-items: center; gap: .75rem; padding: .75rem .85rem; }
14574
14853
  .chat-thinking p { flex: 1; margin: 0; }
@@ -14695,10 +14974,6 @@ ${THEME_DARK}
14695
14974
  .chat-action-card > summary { align-items: flex-start; padding: .85rem; }
14696
14975
  .chat-action-card > summary .button-link { min-height: 2.1rem; padding-inline: .6rem; font-size: .65rem; white-space: nowrap; }
14697
14976
  .chat-action-card > summary small { max-width: 14rem; }
14698
- .chat-approval-form { padding: .85rem; }
14699
- .chat-approval-form .approval-boundaries { grid-template-columns: 1fr; }
14700
- .chat-approval-form .approval-confirm { display: grid; }
14701
- .chat-approval-form .approval-confirm button { width: 100%; }
14702
14977
  .chat-budget { gap: .3rem; margin: .65rem 0 .9rem; }
14703
14978
  .chat-budget > span { padding: .25rem .48rem; }
14704
14979
  .chat-budget > span:first-child { max-width: 100%; overflow: hidden; text-overflow: ellipsis; }
@@ -15012,6 +15287,15 @@ button { min-height: 44px; }
15012
15287
  chips flow after it, never above the name. */
15013
15288
  .queue-card p.row > a:first-of-type { flex: 1 1 12rem; min-width: 0; }
15014
15289
  }
15290
+ /* A phone reads a long changed line wrapped, never scrolled sideways: the
15291
+ line keeps its numbers and marker, the code column takes what is left. */
15292
+ @media (max-width: 760px) {
15293
+ .diff-lines { overflow-x: visible; }
15294
+ .diff-line, .diff-review .diff-line { min-width: 0; grid-template-columns: 2.75rem 2.65rem 2.65rem minmax(0, 1fr); }
15295
+ .diff-review[data-mode="view"] .diff-line { grid-template-columns: 0 2.65rem 2.65rem minmax(0, 1fr); }
15296
+ .diff-line code { white-space: pre-wrap; overflow-wrap: anywhere; }
15297
+ .diff-hunk-head { overflow-x: visible; white-space: pre-wrap; overflow-wrap: anywhere; }
15298
+ }
15015
15299
  @media (max-width: 30rem) {
15016
15300
  .dependency-repair-actions .dependency-repair-replace { grid-template-columns: minmax(0, 1fr); }
15017
15301
  .dependency-repair-actions .dependency-repair-replace > button[type=submit] { width: 100%; }
@@ -15155,6 +15439,7 @@ const INBOX_TABS_CSS = '.inbox-tabs{display:inline-flex;gap:2px;max-width:100%;o
15155
15439
  '.inbox-tabs a[aria-current="page"]{background:var(--so-paper);color:var(--so-ink);box-shadow:var(--so-pill-shadow)}' +
15156
15440
  '.inbox-tab-count{min-width:18px;padding:0 5px;border-radius:9999px;font:500 11px/18px var(--so-mono,ui-monospace,monospace);font-variant-numeric:tabular-nums;text-align:center}' +
15157
15441
  '.inbox-tab-count--needs{background:var(--so-signal);color:var(--so-on-signal)}' +
15442
+ '.inbox-ask{margin:18px 0 0}.inbox-ask>h2{display:flex;align-items:baseline;gap:8px;margin:0 0 4px;font-size:15px;font-weight:600}.inbox-ask>h2 .count{font:500 12px var(--so-mono,ui-monospace,monospace);font-variant-numeric:tabular-nums;color:var(--so-muted)}.inbox-ask h3{font-size:13px;font-weight:600;margin:12px 0 4px}' +
15158
15443
  '.inbox-unread{display:none;position:absolute;top:4px;right:3px;width:6px;height:6px;border-radius:50%;background:var(--so-signal)}' +
15159
15444
  '@media (max-width:760px){.inbox-tabs{display:flex;width:100%}.inbox-tabs a{flex:1;justify-content:center;min-height:44px;padding:0 6px}.inbox-unread{display:block}}';
15160
15445
  const WORKSPACE_STYLE = styleAsset(STYLE + INBOX_TABS_CSS + APPROVAL_RULES_CSS + SPEND_CSS + RETENTION_CSS + STORAGE_CSS + UPDATES_CSS + LIMITS_CSS + MONITORING_CSS + INTEGRATIONS_CSS + BACKUP_CSS + EXPORT_CSS + PROJECT_DELETE_CSS + POLICY_CSS + EVIDENCE_PACK_CSS + THEME_CONTROLS_CSS + CODING_CSS + CODING_SHIPPING_CSS + RECIPE_CSS + SKILLS_CSS + TOOLS_CSS + FLOWS_CSS + TEAMMATE_CSS + KITS_CSS + STARTERS_CSS + GALLERY_CSS + SSO_CSS + CREDENTIALS_CSS + KNOWLEDGE_CSS + MODELS_CSS + CHAT_POLISH_CSS + TRANSITIONS_CSS + WORKSPACE_MOTION_CSS + ASSIGNMENT_CSS + TASK_STATUS_CSS + LEAD_CONTEXT_CSS + PULL_REQUEST_SETTINGS_CSS + CHECK_SETTINGS_CSS + '.learning{min-width:0;overflow-wrap:anywhere}.learning .card{min-width:0}.learning code,.learning blockquote,.learning pre{white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word}.learning button,.learning summary,.learning .button-link{min-height:44px}.learning button{white-space:nowrap}.learning summary{padding:12px 0;cursor:pointer}.learning form{margin:12px 0}.learning select{max-width:100%}.learning blockquote{margin:8px 0}.learning ul{padding-left:20px}');
@@ -15880,13 +16165,17 @@ function inboxPage(chrome, data) {
15880
16165
  const listRows = (rows) => rows.map(one => `<p class="row"><a href="${taskHref(one.taskId)}">${escape(one.title)}</a>${chip(one.repo)} <span class="meta">${escape(one.detail)}</span></p>`).join("\n");
15881
16166
  const ready = (data.ready ?? []).length === 0 ? "" : `<h2>ready to review</h2>` + listRows(data.ready ?? []);
15882
16167
  const running = (data.running ?? []).length === 0 ? "" : `<h2>running now</h2>` + listRows(data.running ?? []);
15883
- const counts = {
15884
- "needs-you": data.decisions.length + data.approvals.length + data.requeueables.length + data.cancelledBlockers.length + data.gaps.length,
16168
+ const asks = { decide: data.decisions.length + data.approvals.length, unblock: data.requeueables.length + data.cancelledBlockers.length + data.gaps.length };
16169
+ const counts = { ...asks,
16170
+ "needs-you": asks.decide + asks.unblock,
15885
16171
  ready: data.needsVerification.length + (data.ready ?? []).length,
15886
16172
  running: (data.running ?? []).length,
15887
16173
  all: 0,
15888
16174
  };
15889
16175
  counts.all = counts["needs-you"] + counts.ready + counts.running;
16176
+ // Each ask is a small heading with its count over its sections (their own headings one step down).
16177
+ const askGroup = (ask, count, sections) => count === 0 ? ""
16178
+ : `<section class="inbox-ask" data-ask="${ask}"><h2>${ASK_LABEL[ask]} <span class="count">${count}</span></h2>${sections.join("").replace(/<(\/?)h2>/g, "<$1h3>")}</section>`;
15890
16179
  const tabs = data.tab === undefined ? "" : `<nav class="inbox-tabs" aria-label="Inbox views">` + INBOX_TABS.map(one => `<a href="/inbox?tab=${one.id}"${one.id === tab ? ` aria-current="page"` : ""} data-inbox-tab="${one.id}">${one.label}` +
15891
16180
  `<span class="inbox-tab-count${one.id === "needs-you" && counts[one.id] > 0 ? " inbox-tab-count--needs" : ""}">${counts[one.id]}</span>` +
15892
16181
  `${one.id !== tab && (data.unread ?? []).includes(one.id) ? `<span class="inbox-unread" aria-label="new"></span>` : ""}</a>`).join("") + `</nav>`;
@@ -15900,14 +16189,11 @@ function inboxPage(chrome, data) {
15900
16189
  wizard,
15901
16190
  empty || !shows("needs-you") ? "" : `<p><a class="new-task" style="display:inline-block" href="/next">clear the queue \u2192 one thing at a time</a></p>`,
15902
16191
  empty && data.wizard === null && shows("needs-you") && counts.all === 0 ? `<div class="card"><p><strong>Nothing needs you.</strong></p><p class="meta">The queue is either working or waiting on its own timers. <a href="/board">Watch the board</a> or <a href="/activity">read the activity report</a>.</p></div>` : tabEmpty,
15903
- shows("needs-you") ? decisions : "",
15904
- shows("needs-you") ? approvals : "",
15905
- shows("needs-you") ? requeueables : "",
15906
- shows("ready") ? needsVerification : "",
15907
- shows("ready") ? ready : "",
16192
+ // What waits on a person, grouped by what it asks: Decide, then Review, then Unblock.
16193
+ shows("needs-you") ? askGroup("decide", counts.decide, [approvals, decisions]) : "",
16194
+ shows("ready") ? askGroup("review", counts.ready, [needsVerification, ready]) : "",
16195
+ shows("needs-you") ? askGroup("unblock", counts.unblock, [requeueables, cancelled, gaps]) : "",
15908
16196
  shows("running") ? running : "",
15909
- shows("needs-you") ? cancelled : "",
15910
- shows("needs-you") ? gaps : "",
15911
16197
  data.rollup
15912
16198
  ? `<p class="meta">requirement gaps are checked one project at a time \u2014 open a project to see and fill its gaps · <a href="/projects">open a project</a></p>`
15913
16199
  : "",
@@ -16530,80 +16816,31 @@ function taskChatApproval(focus, csrf) {
16530
16816
  if (approval.nonce === "") {
16531
16817
  return `<section class="card chat-action-card" id="task-chat-action"><span class="eyebrow">approval needs attention</span><h2>The agent setup isn’t ready yet</h2>${profileWords(scope)}<a class="button-link" href="${taskHref(focus.id)}#scope">Fix the agent setup →</a></section>`;
16532
16818
  }
16533
- const profile = scope.profile ?? null;
16534
- const permission = profile === null
16535
- ? null
16536
- : profile.provider === "claude"
16537
- ? profile.permissionArgv === "bypassPermissions" ? "Full access" : "Auto permissions"
16538
- : profile.provider === "gemini"
16539
- ? profile.approvalArgv === "yolo" ? "Full access" : "Auto permissions"
16540
- : profile.sandboxMode === "danger-full-access" ? "Full access" : "Workspace sandbox";
16541
- const revision = approval.revision === null
16542
- ? ""
16543
- : `<div class="chat-approval-section"><span class="approval-label">${approval.revision.kind === "ci-repair" ? "CI repair" : approval.revision.kind === "criterion-repair" ? "criterion repair" : "revision notes"}</span>` +
16544
- (approval.revision.comments.length === 0 ? "" : `<ul class="recap">${approval.revision.comments.map(one => `<li>${one.path === null ? "" : `<span class="mono">${escape(one.path)}${one.line === null ? "" : `:${one.line}`}</span> · `}${escape(one.note)} <span class="meta">— ${escape(one.author)}</span></li>`).join("")}</ul>`) +
16545
- // The link back (package 3): the original result stays on record
16546
- // and one tap away from the revision it produced, in both directions.
16547
- `<p class="meta"><a href="${escape(chatResultHref(focus.id, approval.revision.sourceRun))}" data-revision-source>Original result: build #${approval.revision.sourceRun} →</a></p>` +
16548
- revisionLineageHtml(approval.revision.lineage) +
16549
- `</div>`;
16550
- const race = approval.raceTerms === null
16551
- ? ""
16552
- : `<div class="chat-approval-section"><span class="approval-label">${approval.raceTerms.kind === "comparison" ? "comparison" : "tournament"}</span>` +
16553
- `<p>${approval.raceTerms.agents.length} agents build independently: ${approval.raceTerms.agents.map(one => `<span class="mono">${escape(one.provider)} · ${escape(one.model)}</span>`).join(" vs ")}.</p>` +
16554
- (approval.raceTerms.kind === "comparison"
16555
- ? `<p class="meta">No dollar caps; every result and its evidence is kept for you to compare.</p>`
16556
- : `<p class="meta">$${(approval.raceTerms.perAgentBudgetMicrousd / 1_000_000).toFixed(2)} per agent plus $${(approval.raceTerms.overrunReserveMicrousd / 1_000_000).toFixed(2)} reserve; $${(approval.raceTerms.totalBudgetMicrousd / 1_000_000).toFixed(2)} total.</p>`) +
16557
- `</div>`;
16558
- // The concise plan (package 2, revised on the operator's screenshot
16559
- // feedback and AGENTS.md): a short title, the outcome in one bounded
16560
- // line, one compact line of counts and limits, and ONE Review plan
16561
- // action — no eyebrow, no second sentence saying the same thing, no
16562
- // footer. Every word is a signed fact the exact terms below restate in
16563
- // full; the password and approval instructions live inside the
16564
- // expanded review, where Approve & start is the only submit.
16565
- const plan = approval.planDocument === null ? null : parseExecutionPlanDocument(approval.planDocument);
16566
- const milestones = plan !== null && plan.ok ? plan.document.milestones : [];
16567
- const count = (n, singular) => `${n} ${singular}${n === 1 ? "" : "s"}`;
16568
- const facts = [
16569
- milestones.length > 0 ? count(milestones.length, "step") : count(scope.touches.length, "path"),
16570
- count(scope.acceptance.length, "check"),
16571
- ];
16572
- if (approval.deliverable === "report")
16573
- facts.push("Report only");
16574
- if (scope.budgetMicrousd !== null)
16575
- facts.push(`$${(scope.budgetMicrousd / 1_000_000).toFixed(2)} attempt cap`);
16576
- if (permission !== null)
16577
- facts.push(permission);
16578
- if (approval.raceTerms !== null)
16579
- facts.push(`${approval.raceTerms.agents.length}-agent ${approval.raceTerms.kind}`);
16819
+ // The same sheet as the task page (approval critique, Oct 2): the plan
16820
+ // open in plain rows, one Approve & start, the rest in Details.
16580
16821
  return (`<section class="card chat-action-card chat-plan" id="task-chat-action" data-approval="${escape(approval.digest)}">` +
16581
- `<p class="chat-plan-outcome">${escape(oneLineOf(scope.goal, 200))}</p>` +
16582
- `<p class="chat-plan-facts">${facts.map(escape).join(" · ")}</p>` +
16583
- `<details class="chat-approval">` +
16584
- `<summary data-primary-action><span class="button-link">Approve plan</span></summary>` +
16585
- `<form method="post" action="${taskHref(focus.executionId)}/approve" class="chat-approval-form approve-form">` +
16586
- `<input type="hidden" name="csrf" value="${escape(csrf)}">` +
16587
- `<input type="hidden" name="nonce" value="${escape(approval.nonce)}">` +
16588
- `<input type="hidden" name="digest" value="${escape(approval.digest)}">` +
16589
- `<input type="hidden" name="return" value="${escape(returnTo)}">` +
16590
- `<input type="text" name="username" autocomplete="username" class="visually-hidden" tabindex="-1" aria-hidden="true">` +
16591
- `<p class="meta chat-approval-lead">These are the exact terms. Nothing builds until your password approves them.${approval.coordinator === null ? "" : ` Filed by <span class="mono">${escape(approval.coordinator.label)}</span>${approval.coordinator.filedAgo === null ? "" : ` · ${escape(approval.coordinator.filedAgo)}`}.`}</p>` +
16592
- (approval.planDocument === null ? "" : `<div class="chat-approval-section"><span class="approval-label">proposed plan</span>${executionPlanHtml(approval.planDocument, true)}${planContractHtml(approval.planContract, "ceremony")}</div>`) +
16593
- (approval.deliverable === "report" ? `<p class="meta"><span class="badge">report only</span> This investigates and reports back without changing the repository.</p>` : "") +
16594
- `<div class="chat-approval-section"><span class="approval-label">goal</span><p class="approval-goal">${escape(scope.goal)}</p></div>` +
16595
- `<div class="approval-boundaries"><div class="approval-boundary"><p class="approval-label">not this</p><p>${scope.outOfScope === null ? "<em>no exclusions</em>" : escape(scope.outOfScope)}</p></div>` +
16596
- `<div class="approval-boundary"><p class="approval-label">may touch</p>${approvalPathsHtml(scope.touches)}</div></div>` +
16597
- acceptanceCeremonyHtml(scope.acceptance) + revision + race +
16598
- `<div class="approval-chips"><span class="approval-chip">quality · <strong>${escape(qualityModeTitle(scope.qualityMode ?? "default"))}</strong></span>` +
16599
- (permission === null ? "" : `<span class="approval-chip">${escape(permission)}</span>`) +
16600
- (scope.budgetMicrousd === null ? "" : `<span class="approval-chip">$${(scope.budgetMicrousd / 1_000_000).toFixed(2)} attempt cap</span>`) +
16601
- (focus.route === null || focus.route.projection === null ? "" : `<span class="approval-chip">${escape(riskTitle(focus.route.riskLevel))} · ${escape(focus.route.projection.postureWords)}</span>`) +
16602
- `</div>` +
16603
- agentsCeremonyHtml(focus.route) +
16604
- runtimeDetailsHtml(scope) +
16605
- `<div class="approval-confirm"><label>Your password <span class="meta">— confirms this exact scope</span><input type="password" name="token" autocomplete="current-password" placeholder="Password"></label>` +
16606
- `<button type="submit">Approve & start</button></div></form></details></section>`);
16822
+ approvalSheetHtml({
16823
+ surface: "chat",
16824
+ action: `${taskHref(focus.executionId)}/approve`,
16825
+ csrf,
16826
+ nonce: approval.nonce,
16827
+ digest: approval.digest,
16828
+ returnTo,
16829
+ scope,
16830
+ planDocument: approval.planDocument,
16831
+ planContract: approval.planDocument === null ? null : approval.planContract,
16832
+ revision: approval.revision,
16833
+ revisionSourceHref: approval.revision === null ? "" : chatResultHref(focus.id, approval.revision.sourceRun),
16834
+ repairChain: approval.repairChain,
16835
+ raceTerms: approval.raceTerms,
16836
+ route: focus.route,
16837
+ coordinator: approval.coordinator,
16838
+ deliverable: approval.deliverable,
16839
+ editHref: `${taskHref(focus.id)}#${approval.planDocument === null ? "scope" : "plan-edit"}`,
16840
+ notNowHref: "/chat",
16841
+ sticky: false,
16842
+ }) +
16843
+ `</section>`);
16607
16844
  }
16608
16845
  /** One live, server-derived journey from request to proof. The fragment is
16609
16846
  * safe to refresh independently, so an in-progress message is never lost. */
@@ -17952,9 +18189,9 @@ function statusActionHref(status, taskId, runId, prUrl, repo) {
17952
18189
  return null;
17953
18190
  switch (status.action.kind) {
17954
18191
  case "open-result": return runId === null ? taskHref(taskId) : `/r/${runId}`;
17955
- case "open-review": return repo === undefined ? reviewHref(taskId) : reviewHref(taskId, runId, repo);
18192
+ case "open-review": return repo === undefined ? reviewHref(taskId) : reviewHref(taskId, runId);
17956
18193
  case "open-run": return runId === null ? taskHref(taskId) : `/r/${runId}`;
17957
- case "open-pr": return safePrUrl(prUrl) ?? (repo === undefined ? reviewHref(taskId) : reviewHref(taskId, runId, repo));
18194
+ case "open-pr": return safePrUrl(prUrl) ?? (repo === undefined ? reviewHref(taskId) : reviewHref(taskId, runId));
17958
18195
  case "open-task":
17959
18196
  default: return taskHref(taskId);
17960
18197
  }
@@ -17972,6 +18209,7 @@ function statusLineHtml(status, extra = "") {
17972
18209
  function workDiagnosticsHtml(diagnostics) {
17973
18210
  return (diagnostics ?? []).map(one => `<p class="work-detail" data-work-diagnostic="${escape(one.token)}"><strong>${escape(one.label)}</strong> · ${escape(one.detail)}</p>`).join("");
17974
18211
  }
18212
+ const TASK_GROUP_LABEL = { ...ASK_LABEL, building: 'Building', rest: 'Recent' };
17975
18213
  function workPage(chrome, data) {
17976
18214
  const href = (view, cursor) => {
17977
18215
  const query = new URLSearchParams();
@@ -17987,7 +18225,7 @@ function workPage(chrome, data) {
17987
18225
  const rowHtml = (row) => {
17988
18226
  const target = row.primaryAction?.target;
17989
18227
  const actionHref = row.primaryAction?.code === 'open-result' && target?.runId != null
17990
- ? `/review?result=${encodeURIComponent(target.taskId)}&run=${target.runId}${row.repo === null ? '' : '&project=' + encodeURIComponent(row.repo)}`
18228
+ ? `/review?result=${encodeURIComponent(target.taskId)}&run=${target.runId}`
17991
18229
  : browserWorkActionHref(row);
17992
18230
  const action = actionHref === null || row.primaryAction === null ? '' : `<a class="work-action" data-primary-action href="${escape(actionHref)}">${escape(row.primaryAction.label)} →</a>`;
17993
18231
  const project = data.multiProject || row.repo === null ? `<span class="project-label">${row.repo === null ? 'Unplaced' : escape(projectName(row.repo))}</span>` : '';
@@ -18014,10 +18252,14 @@ function workPage(chrome, data) {
18014
18252
  const view = {
18015
18253
  kind: 'tasks',
18016
18254
  tabs: WORK_VIEWS.map(one => ({ label: one.label, href: href(one.key), count: data.work.totals[one.key], active: one.key === data.view })),
18255
+ needsYou: data.work.totals['needs-you'],
18256
+ groups: data.view !== 'all' && data.view !== 'needs-you' ? null
18257
+ : [...ASKS, ...(data.view === 'all' ? ['building', 'rest'] : [])]
18258
+ .map(key => ({ key, label: TASK_GROUP_LABEL[key], count: data.work.groups[key] })).filter(one => one.count > 0),
18017
18259
  rows: data.work.items.map(row => {
18018
18260
  const target = row.primaryAction?.target;
18019
18261
  const actionHref = row.primaryAction?.code === 'open-result' && target?.runId != null
18020
- ? `/review?result=${encodeURIComponent(target.taskId)}&run=${target.runId}${row.repo === null ? '' : '&project=' + encodeURIComponent(row.repo)}`
18262
+ ? `/review?result=${encodeURIComponent(target.taskId)}&run=${target.runId}`
18021
18263
  : browserWorkActionHref(row);
18022
18264
  const needsYouDetail = (row.status.views.includes('needs-you') || row.status.label === 'Failed') && !['write-scope', 'approve-scope'].includes(row.primaryAction?.code ?? '') && row.status.label !== 'Ready for review' && row.status.detail !== row.status.label && row.status.detail !== row.familyProblem;
18023
18265
  return {
@@ -18025,6 +18267,7 @@ function workPage(chrome, data) {
18025
18267
  project: data.multiProject || row.repo === null ? (row.repo === null ? 'Unplaced' : projectName(row.repo)) : null,
18026
18268
  age: relativeAge(row.updatedAt, data.now),
18027
18269
  status: { label: row.status.label, tone: row.status.tone, token: row.status.token },
18270
+ ask: row.ask, group: row.ask ?? (row.status.rank === 1 ? 'building' : 'rest'),
18028
18271
  action: actionHref === null || row.primaryAction === null ? null : { label: row.primaryAction.label, href: actionHref },
18029
18272
  detail: needsYouDetail ? row.status.detail : null,
18030
18273
  problem: row.familyProblem,
@@ -19846,6 +20089,10 @@ function taskBodyParts(data) {
19846
20089
  : approval.approved
19847
20090
  ? "approved"
19848
20091
  : "review before starting";
20092
+ // While the approval sheet is open it states any amendment; the plan
20093
+ // card does not repeat it (one #contract-amendment on the page).
20094
+ const sheetStatesContract = scope !== null && !approval.approved && data.plan === "drafted" && data.dispatch?.action !== "repair-dependency" &&
20095
+ !(data.revision != null && "problem" in data.revision) && scope.profileState !== "unresolved" && consentDoorOf(scope, data.route).open;
19849
20096
  const planCard = data.planDocument === null
19850
20097
  ? data.plan === "requested"
19851
20098
  ? data.revision != null && !("problem" in data.revision)
@@ -19859,10 +20106,10 @@ function taskBodyParts(data) {
19859
20106
  (approval.approved ? `<p class="meta">The agent can adapt this route when evidence changes; your approved outcome stays fixed.</p>` : "") +
19860
20107
  `</div><span class="plan-lock">${planStanding}</span>${approval.approved ? `</summary><div class="planner-plan-body">` : `</div>`}` +
19861
20108
  executionPlanHtml(displayedPlanDocument ?? data.planDocument) +
19862
- (approval.approved ? "" : planContractHtml(data.planContract ?? null, "full")) +
20109
+ (approval.approved || sheetStatesContract ? "" : planContractHtml(data.planContract ?? null, "full")) +
19863
20110
  (data.csrf === "" || data.planSha == null || approval.approved
19864
20111
  ? ""
19865
- : `<details class="plan-editor"><summary>Edit plan</summary><form method="post" action="${taskHref(task.id)}/plan-edit">` +
20112
+ : `<details class="plan-editor" id="plan-edit"><summary>Edit plan</summary><form method="post" action="${taskHref(task.id)}/plan-edit">` +
19866
20113
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}"><input type="hidden" name="saw-plan" value="${escape(data.planSha)}">` +
19867
20114
  `<label>plan details <span class="meta">Keep the five headings. Approval locks this version for the build.</span>` +
19868
20115
  `<textarea name="plan-document" rows="14">${escape(data.planDocument)}</textarea></label>` +
@@ -19889,20 +20136,10 @@ function taskBodyParts(data) {
19889
20136
  // The approval form restates every field the digest binds — an operator
19890
20137
  // approves what is on this form, not what is elsewhere on the page — and
19891
20138
  // requires the token typed again. The session got you here; only the
19892
- // token agrees.
19893
- // The ceremony is the page's first card when a scope waits for its yes
19894
- // (task page pass): it states the wait, restates every term the digest
19895
- // binds, and puts the approve act above the fold — the consent-sheet
19896
- // shape. An unapprovable scope gets the problem and the edit road
19897
- // instead of a password it cannot use.
19898
- const approvalProfile = scope?.profile ?? null;
19899
- const approvalPermission = approvalProfile === null
19900
- ? null
19901
- : approvalProfile.provider === "claude"
19902
- ? approvalProfile.permissionArgv === "bypassPermissions" ? "Full access" : "Auto permissions"
19903
- : approvalProfile.provider === "gemini"
19904
- ? approvalProfile.approvalArgv === "yolo" ? "Full access" : "Auto permissions"
19905
- : approvalProfile.sandboxMode === "danger-full-access" ? "Full access" : "Workspace sandbox";
20139
+ // token agrees. The sheet shows the plan open in plain rows with one
20140
+ // Approve & start; the rest of the signed terms sit in its Details fold.
20141
+ // An unapprovable scope gets the problem and the edit road instead of a
20142
+ // password it cannot use.
19906
20143
  const consentDoor = consentDoorOf(scope, data.route);
19907
20144
  const revisionInApproval = scope !== null && !approval.approved && data.plan !== "requested" &&
19908
20145
  scope.profileState !== "unresolved" && consentDoor.open && data.dispatch?.action !== "repair-dependency" &&
@@ -19917,79 +20154,27 @@ function taskBodyParts(data) {
19917
20154
  `<p class="ceremony-road"><a class="button-link" href="#scope">edit the scope to fix it →</a></p></div>`
19918
20155
  : !consentDoor.open
19919
20156
  ? consentClosedHtml(task.id, consentDoor, "task")
19920
- : [
19921
- `<form method="post" action="${taskHref(task.id)}/approve" class="card approve-form approval-card" id="approve">`,
19922
- `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
19923
- `<input type="hidden" name="nonce" value="${escape(data.nonce)}">`,
19924
- `<input type="hidden" name="digest" value="${escape(data.approvalDigest ?? scope.digest)}">`,
19925
- `<input type="text" name="username" autocomplete="username" class="visually-hidden" tabindex="-1" aria-hidden="true">`,
19926
- `<div class="ceremony-head"><span class="approval-title"><strong>approve exactly this:</strong></span><a href="#scope">Edit details</a></div>`,
19927
- // No duplicate summary card or skip-ahead approval action.
19928
- // Every signed term still follows, before password confirmation.
19929
- `<p class="meta approval-orient" data-approval-orient>Nothing starts until you approve.</p>`,
19930
- `<div class="approval-terms" id="approval-terms">`,
19931
- // The deliverable INSIDE the ceremony (mate arc §10): a yes on a
19932
- // scout task authorizes a read-only session and a report, never
19933
- // a branch — said where the signature is given.
19934
- data.deliverable === "report"
19935
- ? `<p class="meta"><span class="badge">scout</span> approving sends a read-only session to investigate this goal and deliver a report — no branch, nothing changes in the repository</p>`
19936
- : "",
19937
- // The filer INSIDE the ceremony (MCP spec v6): a coordinator's
19938
- // request is signed knowing whose it is — and until this
19939
- // signature, nothing plans, claims, or runs it.
19940
- data.coordinator === null || data.coordinator === undefined
19941
- ? ""
19942
- : `<p class="meta">filed by <span class="mono">${escape(data.coordinator.label)}</span>${data.coordinator.filedAgo === null ? "" : ` \u00b7 ${escape(data.coordinator.filedAgo)}`} — an agent asked for this; nothing plans, claims, or runs until you sign, and your signature runs THEIR request</p>`,
19943
- `<p class="approval-label">goal</p><p class="approval-goal">${escape(scope.goal)}</p>`,
19944
- scope.candidate ? `<p class="approval-label">Saved commit</p><p style="overflow-wrap:anywhere"><code>${escape(scope.candidate)}</code></p>` : "",
19945
- revisionCard,
19946
- `<div class="approval-boundaries">`,
19947
- `<div class="approval-boundary"><p class="approval-label">not this</p><p>${scope.outOfScope === null ? "<em>no exclusions</em>" : escape(scope.outOfScope)}</p></div>`,
19948
- `<div class="approval-boundary"><p class="approval-label">touches</p>${approvalPathsHtml(scope.touches)}</div>`,
19949
- `</div>`,
19950
- acceptanceCeremonyHtml(scope.acceptance),
19951
- // The contract amendment INSIDE the ceremony (contract handoff,
19952
- // task 1): what the yes accepts that the operator did not file.
19953
- data.plan === "drafted" ? planContractHtml(data.planContract ?? null, "ceremony") : "",
19954
- `</div>`,
19955
- `<div class="approval-chips"><span class="approval-chip">quality · <strong>${escape(qualityModeTitle(scope.qualityMode ?? "default"))}</strong></span>` +
19956
- (approvalPermission === null ? "" : `<span class="approval-chip">${escape(approvalPermission)}</span>`) +
19957
- `</div>`,
19958
- // The revision feedback above includes its lineage: the source,
19959
- // carried terms, re-resolved terms, and grants that never inherit.
19960
- // The AGENTS inside the ceremony (v47/v48): who plans, builds,
19961
- // repairs, and reviews, and why — said where the yes is given, in
19962
- // the same block chat and /next show. Availability is volatile and
19963
- // stays outside these terms; the runtime limits the profile seals
19964
- // are one tap away, never in the way.
19965
- agentsCeremonyHtml(data.route),
19966
- runtimeDetailsHtml(scope),
19967
- scope.budgetMicrousd === null
19968
- ? ""
19969
- : `<p class="meta">each build attempt has a $${(scope.budgetMicrousd / 1_000_000).toFixed(2)} agent-reported usage cap — on a subscription this is a work limiter, not an API charge</p>`,
19970
- // One yes covers BOTH documents (finding 31): the race terms are
19971
- // restated on the same card the password signs, or they are not
19972
- // approved at all.
19973
- data.raceTerms === null || data.raceTerms === undefined
19974
- ? ""
19975
- : data.raceTerms.kind === "comparison"
19976
- ? `<p><strong>and this comparison:</strong></p>` +
19977
- `<p class="recap" style="margin-top:0">${data.raceTerms.n} agents build this independently — ` +
19978
- `${data.raceTerms.agents.map(agent => `${escape(agent.provider)} · ${escape(agent.model)}`).join(" vs ")}. ` +
19979
- `No dollar caps exist on a comparison — each agent runs until it finishes or stops making progress; ` +
19980
- `spend lands measured only where the harness reports dollars (` +
19981
- `${data.raceTerms.agents.filter(agent => agent.provider === "claude").length} of ${data.raceTerms.n} lanes here). ` +
19982
- `You will compare the results and pick one.</p>`
19983
- : `<p><strong>and this tournament:</strong></p>` +
19984
- `<p class="recap" style="margin-top:0">${data.raceTerms.n} agents build this independently — ` +
19985
- `${data.raceTerms.agents.map(agent => `${escape(agent.provider)} · ${escape(agent.model)}`).join(" vs ")}. ` +
19986
- `Each may spend $${(data.raceTerms.perAgentBudgetMicrousd / 1_000_000).toFixed(2)} plus a ` +
19987
- `$${(data.raceTerms.overrunReserveMicrousd / 1_000_000).toFixed(2)} overrun reserve; the whole tournament is capped at ` +
19988
- `$${(data.raceTerms.totalBudgetMicrousd / 1_000_000).toFixed(2)}. You will compare the results and pick one.</p>`,
19989
- `<div class="approval-confirm" id="approval-confirm"><label>Your password <span class="meta">— confirms exactly the terms above</span><input type="password" name="token" autocomplete="current-password" placeholder="Password"></label>`,
19990
- `<div class="sticky-actions"><button type="submit">${data.raceTerms === null || data.raceTerms === undefined ? "Approve & start" : data.raceTerms.kind === "comparison" ? "Approve comparison" : "Approve tournament"}</button></div></div>`,
19991
- `</form>`,
19992
- ].join("\n");
20157
+ : approvalSheetHtml({
20158
+ surface: "task",
20159
+ action: `${taskHref(task.id)}/approve`,
20160
+ csrf: data.csrf,
20161
+ nonce: data.nonce,
20162
+ digest: data.approvalDigest ?? scope.digest,
20163
+ returnTo: null,
20164
+ scope,
20165
+ planDocument: data.plan === "drafted" ? data.planDocument : null,
20166
+ planContract: data.plan === "drafted" ? data.planContract ?? null : null,
20167
+ revision: data.revision == null || "problem" in data.revision ? null : data.revision,
20168
+ revisionSourceHref: data.revision == null || "problem" in data.revision ? "" : `/r/${data.revision.sourceRun}`,
20169
+ repairChain: data.repairChain ?? null,
20170
+ raceTerms: data.raceTerms ?? null,
20171
+ route: data.route,
20172
+ coordinator: data.coordinator ?? null,
20173
+ deliverable: data.deliverable ?? "branch",
20174
+ editHref: data.csrf !== "" && data.planSha != null && data.planDocument !== null ? "#plan-edit" : "#scope",
20175
+ notNowHref: "/work",
20176
+ sticky: data.dispatch?.action === "approve-scope",
20177
+ });
19993
20178
  // The attended road (Phase 2E): beside the approval, never replacing it.
19994
20179
  // The mint button leads to the CONFIRM screen where every term renders
19995
20180
  // and the password signs; an open authorization shows its state, its
@@ -20514,7 +20699,7 @@ function taskBodyParts(data) {
20514
20699
  `<p class="meta"><strong>Plan first</strong> drafts it from the repository, or <a href="#scope">write it yourself</a>.</p></div>`
20515
20700
  : "";
20516
20701
  const approvalHtml = dependencyChoiceNeeded || approveForm === "" ? "" : data.dispatch?.action === "approve-scope"
20517
- ? `<details class="task-plan-review"><summary data-primary-action><span class="button-link">Approve plan</span></summary>${approveForm}</details>`
20702
+ ? `<section class="task-plan-review" aria-label="Approve the plan">${approveForm}</section>`
20518
20703
  : `<details class="task-secondary-approval"><summary>Updated approval terms</summary>${approveForm}</details>`;
20519
20704
  const optionsHtml = `${identity}${dispatchStatus}${dependencyChoiceNeeded ? "" : actsBar}`;
20520
20705
  const optionsOpen = data.assignment?.primaryAction?.code === "unhold" || data.assignment?.primaryAction?.code === "retry-task";
@@ -20970,7 +21155,9 @@ export function orderChangedFiles(files, proofCitesPaths) {
20970
21155
  ? churn(b) - churn(a)
20971
21156
  : a.path.localeCompare(b.path));
20972
21157
  }
20973
- const reviewHref = (taskId, runId = null, repo = null) => `/review?result=${encodeURIComponent(taskId)}${runId === null ? "" : `&run=${runId}`}${repo === null ? "" : `&project=${encodeURIComponent(repo)}`}`;
21158
+ /** The one result page's address: the task and its run, never a project path
21159
+ * (the page finds the result's project itself and keeps the person's own). */
21160
+ const reviewHref = (taskId, runId = null) => `/review?result=${encodeURIComponent(taskId)}${runId === null ? "" : `&run=${runId}`}`;
20974
21161
  /** The receipt's own proof-state word, so the cockpit and the task page
20975
21162
  * never disagree on the state's name or its precedence: the stored
20976
21163
  * verdict decides, whatever the run's outcome — a no-change run with a
@@ -21095,7 +21282,7 @@ function reviewCockpitPage(chrome, data) {
21095
21282
  const why = status === null
21096
21283
  ? reasons.length === 0 ? "" : `<span class="cockpit-why">${escape(reasons[0])}${reasons.length > 1 ? ` · +${reasons.length - 1} more` : ""}</span>`
21097
21284
  : `<span class="cockpit-why">${statusLineHtml(status)}</span>`;
21098
- return (`<li data-review-priority="${row.priority.band}"><a class="cockpit-row${current ? " current" : ""}" href="${escape(reviewHref(row.taskId, row.runId, row.repo))}"${current ? ` aria-current="page"` : ""}>` +
21285
+ return (`<li data-review-priority="${row.priority.band}"><a class="cockpit-row${current ? " current" : ""}" href="${escape(reviewHref(row.taskId, row.runId))}"${current ? ` aria-current="page"` : ""}>` +
21099
21286
  `<span class="cockpit-row-head"><strong>${escape(row.title)}</strong></span>` +
21100
21287
  `<span class="cockpit-row-meta">${whenTime(row.completedAt)}${row.runId === null ? " · no build" : row.outcome === "no-change" ? " · no change" : ""}${row.prNumber === null ? "" : ` · PR #${row.prNumber}`}</span>` +
21101
21288
  (row.historyProblem ? `<span class="cockpit-why" data-history-problem>History unavailable</span>` : why) +
@@ -21124,7 +21311,7 @@ function reviewCockpitPage(chrome, data) {
21124
21311
  const status = row.assignment == null ? null : assignmentPresentationOf(row.assignment).status;
21125
21312
  const reasons = row.priority.reasons;
21126
21313
  return {
21127
- title: row.title, href: reviewHref(row.taskId, row.runId, row.repo), at: row.completedAt,
21314
+ title: row.title, href: reviewHref(row.taskId, row.runId), at: row.completedAt,
21128
21315
  status: status === null ? null : { label: status.label, tone: status.tone },
21129
21316
  notes: [...(row.historyProblem ? ["History unavailable"] : status === null && reasons.length > 0 ? [reasons[0]] : []), ...(status !== null && row.ciFailing ? ["CI is failing"] : [])],
21130
21317
  current: selected !== null && selected.taskId === row.taskId,
@@ -21204,8 +21391,17 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
21204
21391
  taskHref: taskHref(view.taskId), chatHref: taskChatHref(view.taskId),
21205
21392
  status: { label: status.label, tone: status.tone, token: status.token },
21206
21393
  problem: view.detail === null ? view.historyProblem : null,
21207
- next: reviewNextActionOf(view, csrf), complete: null, checks: null, intent: intentView, noRun: null, panel: null, contest: "",
21394
+ next: reviewNextActionOf(view, csrf), complete: null, decision: null, checks: null, intent: intentView, noRun: null, panel: null, contest: "",
21208
21395
  notes: view.notes.map(one => ({ author: one.author, at: one.createdAt, note: one.note })),
21396
+ acts: { primary: null, secondary: null, line: null }, runChecks: null,
21397
+ // The raw run record lives under Details now (2026-10-02): /r/<id> for this result redirects here.
21398
+ record: run === null ? null : { build: run.id, href: `/r/${run.id}?record=1`, facts: [
21399
+ { label: "Agent", value: [run.provider, run.model].filter(Boolean).join(" · ") },
21400
+ { label: "Worker", value: run.runner },
21401
+ ...(run.branch === null ? [] : [{ label: "Branch", value: run.branch }]),
21402
+ ...(run.finishedAt === null ? [] : [{ label: "Finished", value: when(run.finishedAt) }]),
21403
+ ...(run.ranMinutes === null ? [] : [{ label: "Ran", value: `${run.ranMinutes} min` }]),
21404
+ ] },
21209
21405
  };
21210
21406
  if (run === null || view.detail === null) {
21211
21407
  parts.push(`<section class="card cockpit-section" id="verification" data-cockpit-section="proof"><h3>verification</h3>` +
@@ -21245,6 +21441,38 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
21245
21441
  ? { action: `${taskHref(view.taskId)}/complete`, receipt: assignment.receipt.digest, run: run.id } : null;
21246
21442
  if (complete !== null)
21247
21443
  parts.push(completionForm(view.taskId, run.id, complete.receipt, csrf, view.detail?.pullRequestTo ?? null));
21444
+ // The one decision, after the evidence: Accept only when every requirement is met and the checks passed.
21445
+ const acceptsHere = complete !== null || panel.panel.need?.accept != null;
21446
+ const matrix = proof === null || proof.proofProblem !== null ? [] : proof.matrix;
21447
+ const blocked = cantAcceptYetOf(proof?.verdict ?? null, proof?.reasons ?? [], accepted);
21448
+ const words = !acceptsHere ? null : { sentence: RESULT_DECISION_SENTENCE, ...acceptWordsOf({
21449
+ checks: checks === undefined ? null : checks.running != null ? "running" : checks.level === "off" && checks.status !== "passed" ? "off" : checks.status,
21450
+ unmet: matrix.filter(row => row.state !== "pass" && row.state !== "manual-review").length,
21451
+ yours: complete === null || accepted ? 0 : panel.panel.youCheck?.lines.length ?? 0,
21452
+ action: complete !== null ? "complete" : "accept",
21453
+ publishing: view.detail?.publishing ?? "other",
21454
+ proof: proof !== null && proof.proof !== null && proof.proofProblem === null,
21455
+ }) };
21456
+ // Refuted: plain Accept stays as allowed, in outline, and the one line before the acts (acts.line) says why.
21457
+ const decision = words === null || blocked === null ? words : { ...words, label: "Accept", ready: false };
21458
+ // Run checks: no check ran on this result (or its saved one can't be read), the project has one, and an approver may run it.
21459
+ const followUps = view.detail.followUps ?? null;
21460
+ const checksRunning = checks?.running != null || (followUps?.checks.some(one => one.state === "waiting" || one.state === "running") ?? false);
21461
+ const runChecks = canRetryReview && csrf !== "" && followUps !== null && (followUps.full || followUps.quick) && !checksRunning &&
21462
+ (checks === undefined || checks.status === "not-run" || checks.status === "unavailable")
21463
+ // Back on this result's Checks tab, where #follow-ups shows the run it started.
21464
+ ? { action: `/r/${run.id}/checks`, level: followUps.full ? "full" : "quick", returnTo: `${here}&run=${run.id}&tab=checks` } : null;
21465
+ const need = panel.panel.need;
21466
+ const nextKind = selected.next?.kind;
21467
+ const acts = resultActsOf({
21468
+ accept: decision === null ? null : { ready: decision.ready },
21469
+ runChecks: runChecks !== null,
21470
+ checksRunning,
21471
+ blocked,
21472
+ canRequest: panel.panel.canRequest && panel.panel.request !== null,
21473
+ need: need === null || need.accept != null ? null : need.rebuild != null ? "rebuild" : need.confirm !== null ? "confirm-stopped" : null,
21474
+ next: nextKind === "revise" || nextKind === "draft-repair" ? nextKind : null,
21475
+ });
21248
21476
  if (view.notes.length > 0) {
21249
21477
  parts.push(`<section class="card cockpit-section" data-cockpit-section="notes"><h3>operator notes</h3>` +
21250
21478
  view.notes.map(one => `<p class="row"><span class="meta">${escape(one.author)} · ${whenTime(one.createdAt)}</span> ${escape(one.note)}</p>`).join("\n") +
@@ -21253,7 +21481,7 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
21253
21481
  return {
21254
21482
  html: `<section class="cockpit-detail">${parts.join("\n")}</section>`,
21255
21483
  selected: {
21256
- ...selected, complete, panel: panel.panel, contest,
21484
+ ...selected, complete, decision, panel: panel.panel, contest, acts, runChecks,
21257
21485
  checks: checks === undefined ? null : { detail: checks.detail, problem: checks.status === "failed" || checks.status === "unavailable", logHref: checks.logArtifactId === null ? null : `/r/${run.id}/evidence/${checks.logArtifactId}` },
21258
21486
  },
21259
21487
  };
@@ -22162,7 +22390,7 @@ function resultPanelParts(detail, o) {
22162
22390
  // Needs you: the action that resolves it comes first; Request changes stays beside it, never alone.
22163
22391
  // On the result itself, "Review result" would link here: accepting it is what resolves it.
22164
22392
  const need = needActionOf(current, taskStatus, o.csrf, o.returnTo, {
22165
- accepted: proof?.accepted != null, humanReview, run: run.id, action: `${taskHref(detail.taskId)}/accept-proof`,
22393
+ accepted: proof?.accepted != null, humanReview, run: run.id, action: `${taskHref(detail.taskId)}/accept-proof`, refuted: proof?.verdict === "refuted",
22166
22394
  // What Store.finalResultReason refuses: a verified, attested, accepted or published result.
22167
22395
  rebuildable: proof?.accepted == null && proof?.verdict !== "verified" && proof?.verdict !== "attested" && !(detail.publication !== null && ["pushed", "opened"].includes(detail.publication.state))
22168
22396
  });
@@ -22229,6 +22457,7 @@ function resultPanelParts(detail, o) {
22229
22457
  const acceptable = personChecks.length > 0 && humanReview && o.csrf !== "" && (current != null || detail.assignment == null);
22230
22458
  const youCheck = personChecks.length === 0 ? null : {
22231
22459
  lines: [...new Set(personChecks)],
22460
+ items: personCheckItems(proof, patchOk ? patch.text : null, shown, runId),
22232
22461
  accept: acceptable && need?.accept == null ? { action: `${taskHref(detail.taskId)}/accept-proof`, run: run.id, returnTo: o.returnTo } : null,
22233
22462
  };
22234
22463
  const youCheckHtml = youCheck === null ? "" :
@@ -22537,6 +22766,37 @@ function resultPanelParts(detail, o) {
22537
22766
  };
22538
22767
  return { html, panel };
22539
22768
  }
22769
+ /** Changed lines shown beside one "You check this one" item: at most this many per file, three files. */
22770
+ const CHECK_EXCERPT_LINES = 12;
22771
+ /** Each requirement only a person can confirm, with the evidence to judge
22772
+ * it by, inline: the changed lines in the files it cites (citing none, the
22773
+ * change's first file, labelled as not cited), the screenshots it cites
22774
+ * (none cited: every validated one), and the agent's own note. */
22775
+ function personCheckItems(proof, patchText, shots, runId) {
22776
+ const rows = proof.matrix.filter(row => row.state === "manual-review");
22777
+ if (rows.length === 0)
22778
+ return proof.reasons.some(reason => manualReviewCriterionOf(reason) !== null)
22779
+ ? [{ id: "you-check", statement: "", words: personCheckWords(null), note: null, excerpts: [], shots: [] }] : [];
22780
+ const diff = patchText === null ? null : parseReviewDiff(patchText);
22781
+ const excerptOf = (file, cited) => {
22782
+ const changed = file.hunks.flatMap(hunk => hunk.lines).filter((line) => line.kind === "addition" || line.kind === "deletion");
22783
+ 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) };
22784
+ };
22785
+ return rows.map(row => {
22786
+ const refs = row.answered ?? [];
22787
+ const paths = refs.filter(one => one.kind === "changed-path").map(one => one.ref);
22788
+ const files = diff === null ? [] : paths.length > 0 ? diff.files.filter(file => paths.includes(file.path)).map(file => excerptOf(file, true))
22789
+ : diff.files.slice(0, 1).map(file => excerptOf(file, false));
22790
+ const cited = refs.filter(one => one.kind === "screenshot").map(one => one.ref);
22791
+ const pictures = cited.length > 0 ? shots.filter(shot => cited.includes(shot.path)) : shots;
22792
+ const notes = refs.filter(one => one.kind === "manual-review").map(one => one.ref.trim()).filter(one => one !== "");
22793
+ return {
22794
+ id: row.id, statement: row.statement, words: personCheckWords(row.statement), note: notes.length === 0 ? null : notes.join(" "),
22795
+ excerpts: files.filter(one => one.lines.length > 0).slice(0, 3),
22796
+ shots: pictures.slice(0, 4).map(shot => ({ src: `/r/${runId}/evidence/${shot.artifactId}`, href: `/r/${runId}/evidence/${shot.artifactId}`, caption: shot.caption })),
22797
+ };
22798
+ });
22799
+ }
22540
22800
  /** A Needs you result's one action (needs-you.ts): a link to the act that resolves it, or Confirm it
22541
22801
  * stopped behind the password. Null under every other headline. */
22542
22802
  export function needActionOf(assignment, status, csrf, returnTo, result) {
@@ -22548,7 +22808,7 @@ export function needActionOf(assignment, status, csrf, returnTo, result) {
22548
22808
  if (action?.code === "retry-task" && status.need.key === "rebuild" && csrf !== "")
22549
22809
  return { label: status.need.action.label, ...rebuild };
22550
22810
  if (result !== undefined) {
22551
- if (status.need.key === "review-result" && !result.accepted && csrf !== "")
22811
+ if ((status.need.key === "review-result" || result.refuted === true) && !result.accepted && csrf !== "")
22552
22812
  return { label: result.humanReview ? "Accept result" : "Accept with exception", href: null, confirm: null,
22553
22813
  accept: { action: result.action, run: result.run, returnTo, note: result.humanReview ? null : "Why is this safe to accept?" } };
22554
22814
  // The task page sends the person here: never back to it. What this page can do leads — Accept
@@ -22558,8 +22818,9 @@ export function needActionOf(assignment, status, csrf, returnTo, result) {
22558
22818
  return { label: "Accept result", href: null, confirm: null, accept: { action: result.action, run: result.run, returnTo, note: null } };
22559
22819
  if (here && csrf !== "" && result.rebuildable === true)
22560
22820
  return { label: NEEDS.rebuild.action.label, ...rebuild };
22821
+ // Nothing here resolves it: no act at all, never a link back to the task (which sends the person here).
22561
22822
  if (status.need.key === "review-result" || here)
22562
- return { label: "Open the task", href: taskHref(assignment.rootId), confirm: null };
22823
+ return null;
22563
22824
  }
22564
22825
  const confirm = action?.code === "confirm-stopped" && action.target.runId !== null && csrf !== ""
22565
22826
  ? { action: `${taskHref(assignment.rootId)}/confirm-stopped`, run: action.target.runId, returnTo, checked: needsCheck(assignment) } : null;
@@ -22588,7 +22849,7 @@ function resultNeedAction(need, o, canRequest) {
22588
22849
  function conciseOutcomeOf(conclusion) {
22589
22850
  const flat = oneLineOf(conclusion, 2_000);
22590
22851
  const sentence = /^[\s\S]*?[.!?](?=\s|$)/.exec(flat)?.[0] ?? flat;
22591
- const bounded = sentence.length <= 180 ? sentence : `${sentence.slice(0, 180).replace(/\s+\S*$/, "")}…`;
22852
+ const bounded = sentence.length <= 140 ? sentence : `${sentence.slice(0, 140).replace(/\s+\S*$/, "")}…`;
22592
22853
  return bounded.trim();
22593
22854
  }
22594
22855
  /** Exactly one primary act for the result (repair 2026-09-14), chosen from