toolroll 0.9.14 → 0.9.16

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 (84) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/assignment-brief.d.ts +10 -0
  3. package/dist/assignment-brief.js +49 -26
  4. package/dist/assignment-ui.d.ts +1 -0
  5. package/dist/assignment-ui.js +6 -4
  6. package/dist/assignment.d.ts +34 -1
  7. package/dist/assignment.js +86 -7
  8. package/dist/browser/workspace.css +2 -2
  9. package/dist/browser/workspace.js +38 -38
  10. package/dist/browser-crew.d.ts +2 -1
  11. package/dist/browser-crew.js +3 -1
  12. package/dist/browser-workspace.d.ts +93 -18
  13. package/dist/chat-actions.d.ts +1 -1
  14. package/dist/chat-actions.js +6 -4
  15. package/dist/chat-ask.d.ts +28 -0
  16. package/dist/chat-ask.js +77 -0
  17. package/dist/chat-channel.js +6 -1
  18. package/dist/chat-controls.d.ts +8 -0
  19. package/dist/chat-controls.js +3 -0
  20. package/dist/chat-delivery-state.d.ts +7 -2
  21. package/dist/chat-delivery-state.js +15 -2
  22. package/dist/chat-delivery.js +20 -3
  23. package/dist/chat-quiet.js +4 -2
  24. package/dist/chat-voice.d.ts +6 -9
  25. package/dist/chat-voice.js +11 -5
  26. package/dist/demo.js +82 -2
  27. package/dist/disclosure.d.ts +6 -0
  28. package/dist/disclosure.js +13 -0
  29. package/dist/discord-chat.js +10 -0
  30. package/dist/lead-commitments.d.ts +84 -0
  31. package/dist/lead-commitments.js +208 -0
  32. package/dist/lead-context.d.ts +32 -3
  33. package/dist/lead-context.js +133 -19
  34. package/dist/lead-follow.js +3 -0
  35. package/dist/lead-identity.d.ts +23 -0
  36. package/dist/lead-identity.js +33 -0
  37. package/dist/lead-status.js +2 -2
  38. package/dist/lead-voice.d.ts +12 -9
  39. package/dist/lead-voice.js +25 -16
  40. package/dist/ledger-view.js +3 -2
  41. package/dist/mate-cli.d.ts +1 -1
  42. package/dist/mate-cli.js +8 -2
  43. package/dist/mate-contract.d.ts +1 -1
  44. package/dist/mate-contract.js +11 -4
  45. package/dist/mate-progress.js +5 -0
  46. package/dist/mate-tools.d.ts +18 -1
  47. package/dist/mate-tools.js +250 -6
  48. package/dist/mate.d.ts +9 -0
  49. package/dist/mate.js +34 -38
  50. package/dist/needs-you.d.ts +48 -0
  51. package/dist/needs-you.js +93 -1
  52. package/dist/operate.js +2 -1
  53. package/dist/project-delete.js +3 -1
  54. package/dist/result-acts.d.ts +63 -5
  55. package/dist/result-acts.js +53 -3
  56. package/dist/result-review.d.ts +4 -3
  57. package/dist/result-review.js +7 -3
  58. package/dist/retention.js +6 -0
  59. package/dist/serve.d.ts +3 -0
  60. package/dist/serve.js +1220 -694
  61. package/dist/slack-chat.js +11 -1
  62. package/dist/store.d.ts +51 -0
  63. package/dist/store.js +115 -1
  64. package/dist/surface.js +2 -2
  65. package/dist/task-status.d.ts +26 -19
  66. package/dist/task-status.js +59 -21
  67. package/dist/team-runtime.js +2 -1
  68. package/dist/teams-chat.js +4 -1
  69. package/dist/telegram-mate.d.ts +4 -1
  70. package/dist/telegram-mate.js +17 -1
  71. package/dist/telegram-progress.js +3 -2
  72. package/dist/telegram.js +43 -3
  73. package/dist/test-temp.js +1 -1
  74. package/dist/transitions-recipes.d.ts +1 -1
  75. package/dist/transitions-recipes.js +5 -9
  76. package/dist/when-html.d.ts +24 -2
  77. package/dist/when-html.js +98 -11
  78. package/dist/work-index.d.ts +5 -1
  79. package/dist/work-index.js +22 -9
  80. package/dist/work-summary.d.ts +1 -1
  81. package/dist/workspace-motion.js +3 -3
  82. package/dist/workspace-ui.d.ts +44 -20
  83. package/dist/workspace-ui.js +63 -37
  84. package/package.json +1 -1
package/dist/serve.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { checkLeadIdentity, leadIdentityOf, leadNameOf, LEAD_NAME_MAX, LEAD_PERSONA_MAX } from "./lead-identity.js";
1
2
  import { withActor } from "./actor.js";
2
3
  import { repositoryContext, repositoryContextRead } from './repository-context.js';
3
4
  import { ADD_TESTS_ACTION, followUpChecksOf, requestFollowUpChecks, fileAddTestsTask, resultCheckLevel } from './result-follow-ups.js';
@@ -6,6 +7,7 @@ import { repositoryContextHtml } from './repository-context-ui.js';
6
7
  import { browserAssetsAvailable, browserWorkspaceDocument, serveBrowserAsset } from './browser-shell.js';
7
8
  import { browserCrewOf, browserCrewFromIndex, browserWorkActionHref, browserProjectsOf, browserNavigationOf } from './browser-workspace.js';
8
9
  import { configureLeadFollow, leadFollowStatus, runLeadFollowPass } from './lead-follow.js';
10
+ import { cancelCommitment, conditionWords, openCommitments } from './lead-commitments.js';
9
11
  import { startMaintenance } from './maintenance.js';
10
12
  import { codingHandoffPreview, createCodingHandoff } from './coding-handoff.js';
11
13
  import { codingShippingHtml, CODING_SHIPPING_CSS } from './coding-shipping-ui.js';
@@ -56,7 +58,7 @@ import { knowledgeHtml, knowledgeContextHtml, KNOWLEDGE_CSS, decisionsHtml, memo
56
58
  import { listDecisions, recordDecision, retireDecision, searchMemory } from "./project-memory.js";
57
59
  import { decideProposal, listProposals, memoryStatus } from "./memory-pass.js";
58
60
  import { learningHtml } from "./workspace-ui.js";
59
- import { resultActsOf } from "./result-acts.js";
61
+ import { acceptWithChecksOf, resultActsOf } from "./result-acts.js";
60
62
  import { createSessionEndpoint } from './session-server.js';
61
63
  import { handleTeamHttp } from './team-http.js';
62
64
  import { teamWorkspaceHtml } from './team-ui.js';
@@ -65,7 +67,7 @@ import { prepareWorkspaceRevision, WorkspaceValidatorCache } from "./workspace-r
65
67
  import { workIndexPage, workCountsByProject, WorkIndexCursorError, WORK_INDEX_MAX_LIMIT } from "./work-index.js";
66
68
  import { leadActivity } from "./lead-voice.js";
67
69
  import { openWorkDecisionOf } from "./work-summary.js";
68
- import { assignmentOf, checkAssignmentAsOperator } from './assignment.js';
70
+ import { acceptAndCompleteAsOperator, assignmentOf, checkAssignmentAsOperator, owedAcceptance } from './assignment.js';
69
71
  import { demoChatHtml, demoThreadHtml, DEMO_CHAT_SCRIPT } from "./demo-chat.js";
70
72
  import { pullRequestSettingsHtml, PULL_REQUEST_SETTINGS_CSS } from './pull-request-ui.js';
71
73
  import { checkSettingsHtml, CHECK_SETTINGS_CSS } from './check-levels-ui.js';
@@ -74,8 +76,8 @@ import { checkPublishing, completeAndOpenPullRequest, mergeAsPerson, pullRequest
74
76
  import { leadBriefHtml, LEAD_CONTEXT_CSS } from './lead-context.js';
75
77
  import { assignmentCatchUp } from './assignment-brief.js';
76
78
  import { assignmentActionHref, assignmentCardOf, assignmentStatusOf, assignmentSummaryHtml, assignmentWithEvidence, ASSIGNMENT_CSS } from './assignment-ui.js';
77
- import { assignmentStageOf, pullRequestFactOf, stageOfCode, stageOfDispatch, statusDetailsHtml, statusIconSvg, statusWhyHtml, taskStatusOf, TASK_STATUS_CSS } from './task-status.js';
78
- import { ASKS, ASK_LABEL, NEEDS, NO_REASON_RECORDED, failedAttemptSentence, isInternalErrorReason, latestFinishedAttempt, runReasonWords } from './needs-you.js';
79
+ import { assignmentStageOf, demoChecksOf, requirementWordOf, STATUS_MORE, pullRequestFactOf, requirementsOf, stageOfCode, stageOfDispatch, statusDetailsHtml, statusIconSvg, statusWhyHtml, taskStatusOf, TASK_STATUS_CSS } from './task-status.js';
80
+ import { ASKS, ASK_LABEL, GENERAL_SUGGESTION, NEEDS, NO_REASON_RECORDED, failedAttemptSentence, retryNoteOf, failingCheckSuggestion, isInternalErrorReason, latestFinishedAttempt, missedRequirementLine, missedRequirementSuggestion, runReasonWords, stopSuggestionOf } from './needs-you.js';
79
81
  import { assignmentPresentationOf, historicalAssessmentReason, shortenedMaterialReason } from './assignment-presentation.js';
80
82
  import { ledgerBody } from "./ledger-view.js";
81
83
  import { EVIDENCE_PACK_CSS, evidencePack, evidencePackHtml, exportDay, ledgerExportChunks } from "./evidence-pack.js";
@@ -86,6 +88,7 @@ import { createResultRevision, requestResultChanges } from "./result-actions.js"
86
88
  import { CHAT_CONTROLS, chatControlHref, chatResultHref as sharedResultHref, isChatControl } from "./chat-controls.js";
87
89
  import { CHAT_TASK_ACTIONS, isChatTaskAction } from "./chat-task-actions.js";
88
90
  import { WORKSPACE_MOTION_CSS, WORKSPACE_MOTION_SCRIPT } from "./workspace-motion.js";
91
+ import { DISCLOSURE_CSS } from "./disclosure.js";
89
92
  import { styleAsset } from "./style-asset.js";
90
93
  import { MOBILE_VIEWPORT_SCRIPT } from "./mobile-viewport.js";
91
94
  import { authorizePlanUnderMode, applyModeToNewFiling, planAutoPending } from "./plan-auto.js";
@@ -164,7 +167,7 @@ import { starterRecipes, savedRecipes, findRecipe, importRecipe, exportRecipe, c
164
167
  import { recipeFromForm, recipeLibraryHtml, recipeEditorHtml, workflowPreviewHtml, recipeScript, RECIPE_CSS, recipeDefinitionPreviewHtml, recipeRunHtml, recipeAnswersFromForm } from "./recipe-ui.js";
165
168
  import { EVIDENCE_CAPS, readVerifiedArtifact, readVerifiedReport, readVerifiedProofForRun, storeEvidence, writeEvidenceFile, scanForSecrets } from "./evidence.js";
166
169
  import { GOAL_ASSESSMENT_PENDING, reviewConflict, manualReviewOnly, manualReviewCriterionOf, personCheckWords, plainReasonWords, dispatchStatusToken, passFraction, semanticCoverage, coverageWords, coverageStateWords } from "./proof.js";
167
- import { WORK_VIEWS, REVIEW_TOKENS, RESULT_DECISION_SENTENCE, acceptWordsOf, cantAcceptYetOf, evidenceProblemOf, lastErrorLineOf, reportMismatchesOf, ACCEPT_NEEDS_REASON, MISMATCH_HEADLINE, parseWorkView, resultStatusOf, resultHeadlineOf, receiptHeadingOf, receiptPublicationWords, reviewFactsOf, workStatusOf, primaryDestinationOf, needsPerson, dispatchActionLabel, } from "./workspace-ui.js";
170
+ import { WORK_VIEWS, REVIEW_TOKENS, RESULT_DECISION_SENTENCE, acceptWordsOf, buildProgressOf, cantAcceptYetOf, earlierAttemptsWords, evidenceProblemOf, lastErrorLineOf, missedRequirementOf, reportMismatchesOf, ACCEPT_NEEDS_REASON, MISMATCH_HEADLINE, parseWorkView, resultStatusOf, resultHeadlineOf, receiptHeadingOf, receiptPublicationWords, reviewFactsOf, workStatusOf, primaryDestinationOf, needsPerson, dispatchActionLabel, } from "./workspace-ui.js";
168
171
  import { PRICED_BUILD_MODELS } from "./pricing.js";
169
172
  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";
170
173
  import { parseReport } from "./scout-report.js";
@@ -184,7 +187,7 @@ import { dirname } from "node:path";
184
187
  import { AsyncLocalStorage } from "node:async_hooks";
185
188
  import { loadOrCreateVapidKeys, validatePushEndpoint } from "./push.js";
186
189
  import { parseGithubRepo, previewGithubRepo, cloneGithubRepo, listGithubRepos, isLargeRepo } from "./onboard.js";
187
- import { verifiedAuthor, LEAD_THREAD, isDigestTime } from "./store.js";
190
+ import { verifiedAuthor, LEAD_THREAD, isDigestTime, MATE_ASK_OTHER } from "./store.js";
188
191
  import { digestTimes } from "./digest-times.js";
189
192
  import { updateRepos, addRepos, removeRepos } from "./repos.js";
190
193
  import { run as execRun } from "./exec.js";
@@ -240,7 +243,7 @@ import { parseProjectConcurrency, PROJECT_CONCURRENCY_DEFAULT, projectConcurrenc
240
243
  import { buildReviewOf, findingWords } from "./review-switch.js";
241
244
  import { deleteProject, holdingsWords, projectHoldings, projectRunning } from "./project-delete.js";
242
245
  import { POLICY_CSS, policyHtml } from "./policy-ui.js";
243
- import { checkPolicy, parseList, policyParts } from "./policy.js";
246
+ import { checkPolicy, levelOfProfile, parseList, policyParts } from "./policy.js";
244
247
  import { LIMITS_CSS, limitsHtml, limitsView } from "./limits-ui.js";
245
248
  import { budgetHoldWords, budgetLabel, budgetStates, monthNamed, monthOf, spendItems, teammateNames as teammateNamesOf, usd as spendUsd } from "./spend.js";
246
249
  import { targetOf } from "./monitoring.js";
@@ -2104,6 +2107,30 @@ export function createDecisionServer(options) {
2104
2107
  return refuse(response, who, 400, 'This task page has expired. Open the first page.', '/work');
2105
2108
  throw error;
2106
2109
  }
2110
+ // A failed row says what its latest attempt missed, as its task page does (a stop reason already reads so), from
2111
+ // the database alone: a row reads no check log. A live build's row says which step it is on, or the step it is
2112
+ // stuck on, from that live attempt's own progress only, never a stopped attempt's; the step's words come from its
2113
+ // current plan (one saved file, for live rows only), and without it the row names the step by number.
2114
+ work = { ...work, items: work.items.map((item) => {
2115
+ if (item.liveRunId !== null) {
2116
+ const ref = store.lookupRef(item.activeTaskId);
2117
+ const recorded = ref === null ? null : store.latestCheckpointForRun(item.liveRunId);
2118
+ if (ref === null || recorded === null || recorded.taskRef !== ref.id)
2119
+ return item;
2120
+ const plan = store.currentPlanRevision(ref.id);
2121
+ const parsed = plan === null ? null : (() => { const doc = revisionDocOf(plan); return doc === null ? null : parseExecutionPlanDocument(doc.document); })();
2122
+ const words = new Map(parsed?.ok === true ? milestonesOf(parsed.document).map(one => [one.id, one.description]) : []);
2123
+ const steps = buildProgressOf(recorded.snapshot.milestones.map(one => ({ description: words.get(one.id) ?? null, state: one.state, note: one.note ?? null })));
2124
+ return steps === null ? item : { ...item, progress: steps.line };
2125
+ }
2126
+ if (item.state !== "failed")
2127
+ return item;
2128
+ const family = familyOf(item.activeTaskId);
2129
+ if (family === null || family.problem !== null)
2130
+ return item;
2131
+ const failure = failureOf(family.versions.flatMap(version => store.runsFor(version.refId)), null, false);
2132
+ return failure.kind === "reason" ? item : { ...item, status: { ...item.status, detail: failure.line } };
2133
+ }) };
2107
2134
  const page = workPage(chromeFor(project, "work", undefined, rollup ? "all" : undefined), {
2108
2135
  view,
2109
2136
  ...(url.searchParams.has('project') && project !== null ? { projectFilter: project } : {}),
@@ -2234,7 +2261,7 @@ export function createDecisionServer(options) {
2234
2261
  if (selected !== null) {
2235
2262
  const view = taskViewData(selected, who, null);
2236
2263
  detail = view === null
2237
- ? `<p class="workbench-mobile-back"><a href="/workbench">← all work</a></p><div class="card"><p class="meta">no such task — it may have been outside this console's view</p></div>`
2264
+ ? `<p class="workbench-mobile-back"><a href="/workbench">← all work</a></p><div class="card"><p class="meta">No such task — it may have been outside this console's view</p></div>`
2238
2265
  : `<p class="workbench-mobile-back"><a href="/workbench">← all work</a></p>${taskBody({ ...view, degraded: "pane" })}`;
2239
2266
  }
2240
2267
  return sendScreen(response, 200, screen("portfolio", detail, {
@@ -2389,10 +2416,15 @@ export function createDecisionServer(options) {
2389
2416
  // The ranked queue itself stays bounded and says so.
2390
2417
  const beyond = wantedId === null || inQueue !== null ? null : completedRowFor(wantedId, project);
2391
2418
  const beyondRow = beyond === null ? null : resultRow(beyond);
2392
- const chosen = inQueue ?? (beyondRow === null ? null : { ...beyondRow, priority: reviewPriorityOf(beyondRow) });
2419
+ const completedChoice = inQueue ?? (beyondRow === null ? null : { ...beyondRow, priority: reviewPriorityOf(beyondRow) });
2420
+ const expectedRun = url.searchParams.get("run");
2421
+ const namedRun = expectedRun !== null && /^[1-9]\d*$/.test(expectedRun) ? Number(expectedRun) : null;
2422
+ // A build that failed (or stopped) has its own result page too: the run named, or a failed task's latest attempt.
2423
+ const attempt = wantedId === null || (completedChoice !== null && (namedRun === null || completedChoice.runId === namedRun)) ? null : attemptRowFor(wantedId, namedRun, project);
2424
+ const attemptRow = attempt === null ? null : resultRow(attempt);
2425
+ const chosen = attemptRow !== null ? { ...attemptRow, priority: reviewPriorityOf(attemptRow) } : completedChoice;
2393
2426
  const selectedRow = wantedId === null ? ranked[0] ?? null : chosen;
2394
2427
  const csrf = who.via === "cookie" ? who.session.csrf : "";
2395
- const expectedRun = url.searchParams.get("run");
2396
2428
  // An exact link to a result this person can't see (or that never existed) reads the same either way.
2397
2429
  if (expectedRun !== null && wantedId !== null && chosen === null)
2398
2430
  return refuse(response, who, 404, "No such result in your projects.", "/work");
@@ -2400,11 +2432,34 @@ export function createDecisionServer(options) {
2400
2432
  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") }));
2401
2433
  }
2402
2434
  const selected = selectedRow === null ? null : reviewCockpitViewOf(selectedRow, who, now);
2435
+ // A build that delivered nothing reads Failed here, and so does a failed task's delivered result: what went wrong
2436
+ // is the card and Retry the ink act. That result may still be accepted, but only in outline, as Accept anyway, with
2437
+ // a reason.
2438
+ const delivered = selected?.run?.outcome === "built" || selected?.run?.outcome === "no-change";
2439
+ const failedTask = selected === null ? null : store.getTask(selected.taskId);
2440
+ if (selected !== null && attemptRow !== null && selected.run !== null && (!delivered || failedTask?.state === "failed")) {
2441
+ const attemptRun = store.getRun(selected.run.id);
2442
+ const runs = attemptRun === null ? [] : store.runsFor(attemptRun.taskRef);
2443
+ const latest = latestFinishedAttempt(runs);
2444
+ // Retry from here only when the task failed, nothing holds it, and this is its latest attempt or a result it delivered.
2445
+ const retry = failedTask?.state === "failed" && (latest?.id === selected.run.id || delivered) && csrf !== "" && who.role === "approver" && store.currentLiveLease(attemptRun.taskRef, now) === null
2446
+ ? { action: `${taskHref(selected.taskId)}/requeue` } : null;
2447
+ // Accept anyway: the same acceptance this task's latest result takes, never for an older one or one already accepted.
2448
+ const family = delivered ? familyOf(selected.taskId) : null;
2449
+ const acceptAnyway = delivered && csrf !== "" && who.role === "approver" && family?.current.id === selected.taskId && family.problem === null &&
2450
+ runs.find(runIsTaskResult)?.id === selected.run.id && store.proofAcceptance(selected.run.id) === null
2451
+ ? { action: `${taskHref(selected.taskId)}/accept-proof`, run: selected.run.id } : null;
2452
+ if (attemptRun !== null) {
2453
+ // A delivered result didn't fail itself: the task's failure is its latest attempt's.
2454
+ const failure = delivered ? failureOf(runs, null) : explainAttempt(attemptRun, null);
2455
+ selected.failure = { ...failure, retry: retry === null ? null : { ...retry, note: retryNoteOf(failure.suggestion) }, ...(acceptAnyway === null ? {} : { acceptAnyway }) };
2456
+ }
2457
+ }
2403
2458
  const reviewPage = reviewCockpitPage(chromeFor(wantedId === null ? project : chosenProject, "runs"), {
2404
2459
  queue: ranked,
2405
2460
  queueCap: REVIEW_QUEUE_CAP,
2406
2461
  selected,
2407
- beyondQueue: beyond !== null,
2462
+ beyondQueue: beyond !== null && attemptRow === null,
2408
2463
  // A deep link to a result this console cannot show — not done,
2409
2464
  // not admitted, or never existed — says so in one sentence and
2410
2465
  // shows the top of the queue; the three cases read identically.
@@ -2412,6 +2467,7 @@ export function createDecisionServer(options) {
2412
2467
  csrf,
2413
2468
  canRetryReview: who.via === "cookie" && who.role === "approver",
2414
2469
  noted: url.searchParams.get("noted") !== null,
2470
+ refusal: Object.hasOwn(RESULT_REFUSALS, url.searchParams.get("refused") ?? "") ? RESULT_REFUSALS[url.searchParams.get("refused")] : null,
2415
2471
  tab: parseResultTab(url.searchParams.get("tab")),
2416
2472
  user: who.name,
2417
2473
  now,
@@ -2547,7 +2603,7 @@ export function createDecisionServer(options) {
2547
2603
  const csrf = who.via === "cookie" ? who.session.csrf : "";
2548
2604
  const hasGrant = store.hasMergeCapableGrant(project, now);
2549
2605
  const current = live === null || liveTerms === null
2550
- ? `<div class="card"><h2>locked</h2><p class="meta">no mode is signed — every action asks for your password. That is the default.</p></div>`
2606
+ ? `<div class="card"><h2>Locked</h2><p class="meta">No mode is signed — every action asks for your password. That is the default.</p></div>`
2551
2607
  : [
2552
2608
  `<div class="card">`,
2553
2609
  `<h2 style="margin-top:0">${escape(live.name)} <span class="meta">signed by ${personChip(live.signedBy)}</span></h2>`,
@@ -2557,7 +2613,7 @@ export function createDecisionServer(options) {
2557
2613
  who.role === "approver"
2558
2614
  ? `<form method="post" action="/mode/revoke" class="row">` +
2559
2615
  `<input type="hidden" name="csrf" value="${escape(csrf)}">` +
2560
- `<button type="submit">end this mode now</button>` +
2616
+ `<button type="submit">End this mode now</button>` +
2561
2617
  `<span class="meta">one click — every act it covered falls back to its own ceremony</span></form>`
2562
2618
  : "",
2563
2619
  `</div>`,
@@ -2569,31 +2625,31 @@ export function createDecisionServer(options) {
2569
2625
  `<h2 style="margin-top:0">${live === null ? "sign a mode" : "replace it — a renewal is a new signature"}</h2>`,
2570
2626
  `<form method="post" action="/mode/confirm">`,
2571
2627
  `<input type="hidden" name="csrf" value="${escape(csrf)}">`,
2572
- `<label>preset<select name="name"><option value="standard">standard — approvals stay with me</option><option value="hands-off">hands-off — your filings auto-approve, full permissions</option></select></label>`,
2573
- `<label>days <span class="meta">(1\u2013${MODE_MAX_DAYS})</span><input type="number" name="days" value="1" min="1" max="${MODE_MAX_DAYS}"></label>`,
2574
- `<label>merges<select name="publication">` +
2628
+ `<label>Preset<select name="name"><option value="standard">standard — approvals stay with me</option><option value="hands-off">hands-off — your filings auto-approve, full permissions</option></select></label>`,
2629
+ `<label>Days <span class="meta">(1\u2013${MODE_MAX_DAYS})</span><input type="number" name="days" value="1" min="1" max="${MODE_MAX_DAYS}"></label>`,
2630
+ `<label>Merges<select name="publication">` +
2575
2631
  `<option value="notify">wait for me — even under a merge grant</option>` +
2576
2632
  (hasGrant ? `<option value="automerge">merge themselves when CI is green on the exact commit</option>` : "") +
2577
2633
  `</select></label>` +
2578
2634
  (hasGrant ? "" : `<span class="meta">self-merging needs a merge-capable publication grant first</span>`),
2579
- `<label>my filings<select name="auto-approve">` +
2635
+ `<label>My filings<select name="auto-approve">` +
2580
2636
  `<option value="">the preset's default (standard: wait for approval; hands-off: auto-approve)</option>` +
2581
2637
  `<option value="1">approve the moment I file them</option>` +
2582
2638
  `<option value="0">wait for their own approval</option>` +
2583
2639
  `</select></label>`,
2584
- `<label>planner approval<select name="plan-auto">` +
2640
+ `<label>Planner approval<select name="plan-auto">` +
2585
2641
  `<option value="0">wait for my approval</option>` +
2586
2642
  `<option value="1">auto-approve plans that preserve my filed contract</option>` +
2587
2643
  `</select><span class="meta">Requires automatic filing approval. File the goal, paths, and acceptance criteria upfront. Changed scope and unanswered questions still pause.</span></label>`,
2588
- `<label>if a subscription runs out<select name="allow-paid-fallback">` +
2644
+ `<label>If a subscription runs out<select name="allow-paid-fallback">` +
2589
2645
  `<option value="">never switch to a paid API key on its own (the default, every preset)</option>` +
2590
2646
  `<option value="1">allow the approved fallback — spend moves to that account</option>` +
2591
2647
  `</select></label>`,
2592
- `<button type="submit">read the full terms</button>`,
2648
+ `<button type="submit">Read the full terms</button>`,
2593
2649
  `</form>`,
2594
2650
  `</div>`,
2595
2651
  ].join("\n");
2596
- return sendScreen(response, 200, screen("mode", [`<h1>operating mode</h1><p>Authorize this project's automatic approvals once, for up to ${MODE_MAX_DAYS} days. Select the touchpoints below; the signature covers the exact choices. Existing modes keep their original terms.</p><p class="meta">Scope changes, agent questions, and requested revisions still require your attention. Automatic merging also needs a publication grant.</p>`, current, signForm].join("\n"), { chrome: chromeFor(project, "mode") }));
2652
+ return sendScreen(response, 200, screen("mode", [`<h1>Operating mode</h1><p>Authorize this project's automatic approvals once, for up to ${MODE_MAX_DAYS} days. Select the touchpoints below; the signature covers the exact choices. Existing modes keep their original terms.</p><p class="meta">Scope changes, agent questions, and requested revisions still require your attention. Automatic merging also needs a publication grant.</p>`, current, signForm].join("\n"), { chrome: chromeFor(project, "mode") }));
2597
2653
  }
2598
2654
  if (url.pathname === "/people") {
2599
2655
  // Approvers see everyone (D7's ceiling: every fact already passed
@@ -2635,13 +2691,13 @@ export function createDecisionServer(options) {
2635
2691
  ? ""
2636
2692
  : `<p class="row">watching now: ${attended.map(session => `<a href="/t/${escape(session.taskId)}">${escape(session.taskId)}</a>`).join(", ")}</p>`,
2637
2693
  acts.length === 0
2638
- ? `<p class="meta">no recorded acts yet</p>`
2694
+ ? `<p class="meta">No recorded acts yet</p>`
2639
2695
  : acts.map(act => `<p class="row meta">${escape(act.kind)} ${escape(act.subject)} \u00b7 ${escape(act.at.slice(0, 16).replace("T", " "))}</p>`).join("\n"),
2640
2696
  approverView && one.revokedAt === null && one.name !== who.name
2641
2697
  ? `<form method="post" action="/people/revoke" class="row">` +
2642
2698
  `<input type="hidden" name="csrf" value="${escape(csrf)}"><input type="hidden" name="name" value="${escape(one.name)}">` +
2643
- `<label>your password<input type="password" name="token" autocomplete="current-password"></label>` +
2644
- `<button type="submit">remove ${escape(one.name)}'s sign-in</button>` +
2699
+ `<label>Your password<input type="password" name="token" autocomplete="current-password"></label>` +
2700
+ `<button type="submit">Remove ${escape(one.name)}'s sign-in</button>` +
2645
2701
  `<span class="meta">ends their access and everything it signed \u2014 history stays</span></form>`
2646
2702
  : "",
2647
2703
  `</div>`,
@@ -2651,20 +2707,20 @@ export function createDecisionServer(options) {
2651
2707
  const inviteRows = invites.map(one => `<p class="row">${escape(one.role)} invite · ${one.projects === null ? "all projects" : one.projects.map(repo => escape(projectName(repo))).join(", ")} \u00b7 from ${personChip(one.mintedBy)} \u00b7 expires ${escape(one.expiresAt.slice(0, 16).replace("T", " "))}${one.attempts > 0 ? ` \u00b7 ${one.attempts} failed attempt${one.attempts === 1 ? "" : "s"}` : ""}` +
2652
2708
  ` <form method="post" action="/people/invite-revoke" style="display:inline">` +
2653
2709
  `<input type="hidden" name="csrf" value="${escape(csrf)}"><input type="hidden" name="id" value="${one.id}">` +
2654
- `<label>password <input type="password" name="token" autocomplete="current-password" style="width:8rem"></label>` +
2655
- `<button type="submit">cancel it</button></form></p>`);
2710
+ `<label>Password <input type="password" name="token" autocomplete="current-password" style="width:8rem"></label>` +
2711
+ `<button type="submit">Cancel it</button></form></p>`);
2656
2712
  const inviteCard = !approverView
2657
2713
  ? ""
2658
2714
  : [
2659
2715
  `<div class="card">`,
2660
- `<h2 style="margin-top:0">invite someone</h2>`,
2661
- inviteRows.length === 0 ? `<p class="meta">no open invites</p>` : inviteRows.join("\n"),
2716
+ `<h2 style="margin-top:0">Invite someone</h2>`,
2717
+ inviteRows.length === 0 ? `<p class="meta">No open invites</p>` : inviteRows.join("\n"),
2662
2718
  `<form method="post" action="/people/invite" class="row">`,
2663
2719
  `<input type="hidden" name="csrf" value="${escape(csrf)}">`,
2664
- `<label>they can<select name="role"><option value="viewer">Viewer — read work</option><option value="approver">Operator — create, manage and approve work</option></select></label>`,
2720
+ `<label>They can<select name="role"><option value="viewer">Viewer — read work</option><option value="approver">Operator — create, manage and approve work</option></select></label>`,
2665
2721
  accessFields(project === null ? [] : [project]),
2666
- `<label>your password<input type="password" name="token" autocomplete="current-password"></label>`,
2667
- `<button type="submit">make an invite link</button>`,
2722
+ `<label>Your password<input type="password" name="token" autocomplete="current-password"></label>`,
2723
+ `<button type="submit">Make an invite link</button>`,
2668
2724
  `<span class="meta">single-use, expires in 72 hours</span>`,
2669
2725
  `</form>`,
2670
2726
  `</div>`,
@@ -2786,7 +2842,7 @@ export function createDecisionServer(options) {
2786
2842
  `<p class="row"><a href="/r/${run.id}" class="mono">#${run.id}</a> <a href="${taskHref(run.taskId)}"><strong>${escape(run.title)}</strong></a>${projectChip(run.repo)} ` +
2787
2843
  `<span class="right meta mono">${escape(run.runner)} \u00b7 ${escape(run.role)} \u00b7 ${escape(stage)} \u00b7 ${escape(relativeAge(run.startedAt, now))}</span></p>`,
2788
2844
  options.localRunner === undefined
2789
- ? `<p class="meta">the transcript is readable on the machine that runs the worker \u2014 start the console there with <code>toolroll up</code></p>`
2845
+ ? `<p class="meta">The transcript is readable on the machine that runs the worker \u2014 start the console there with <code>toolroll up</code></p>`
2790
2846
  : `<pre class="recap plan-doc live-pane" id="live-transcript-${run.id}" data-initial-offset="0">${escape(tail)}</pre><p class="meta" id="live-transcript-${run.id}-state"></p>`,
2791
2847
  `</section>`,
2792
2848
  ].join("\n");
@@ -2799,7 +2855,7 @@ export function createDecisionServer(options) {
2799
2855
  return sendScreen(response, 200, screen("peek", [
2800
2856
  `<h1>Peek</h1>`,
2801
2857
  panes.length === 0
2802
- ? `<p class="meta">no agent is working right now \u2014 this page follows them the moment one starts</p>`
2858
+ ? `<p class="meta">No agent is working right now \u2014 this page follows them the moment one starts</p>`
2803
2859
  : panes.join("\n"),
2804
2860
  ].join("\n"), { chrome: chromeFor(project, "runs"), functional: { script, fetches: true } }));
2805
2861
  }
@@ -2823,19 +2879,19 @@ export function createDecisionServer(options) {
2823
2879
  ...(said === null ? [] : [`<p class="meta">${escape(said)}</p>`]),
2824
2880
  `<div id="fleet-region">${body}</div>`,
2825
2881
  `<p class="meta" id="fleet-region-stamp"></p>`,
2826
- `<h2>register a worker</h2>`,
2882
+ `<h2>Register a worker</h2>`,
2827
2883
  `<form method="post" action="/fleet/runner/register" class="card">` +
2828
2884
  `<input type="hidden" name="csrf" value="${escape(csrf)}">` +
2829
- `<label>name<input type="text" name="name" placeholder="builder-2" maxlength="60" required></label>` +
2830
- `<label>capacity<input type="text" name="capacity" inputmode="numeric" value="1" aria-label="capacity"></label>` +
2831
- `<label>your password, typed again<input type="password" name="token" autocomplete="current-password" required></label>` +
2832
- `<button type="submit">register — its token is shown once</button></form>`,
2833
- `<details class="arm-danger"><summary>retire a worker</summary>` +
2885
+ `<label>Name<input type="text" name="name" placeholder="builder-2" maxlength="60" required></label>` +
2886
+ `<label>Capacity<input type="text" name="capacity" inputmode="numeric" value="1" aria-label="capacity"></label>` +
2887
+ `<label>Your password, typed again<input type="password" name="token" autocomplete="current-password" required></label>` +
2888
+ `<button type="submit">Register — its token is shown once</button></form>`,
2889
+ `<details class="arm-danger"><summary>Retire a worker</summary>` +
2834
2890
  `<form method="post" action="/fleet/runner/retire" class="card">` +
2835
2891
  `<input type="hidden" name="csrf" value="${escape(csrf)}">` +
2836
- `<label>which worker<input type="text" name="name" placeholder="builder-1" required></label>` +
2837
- `<label>your password, typed again<input type="password" name="token" autocomplete="current-password" required></label>` +
2838
- `<button type="submit" class="danger">retire it</button></form></details>`,
2892
+ `<label>Which worker<input type="text" name="name" placeholder="builder-1" required></label>` +
2893
+ `<label>Your password, typed again<input type="password" name="token" autocomplete="current-password" required></label>` +
2894
+ `<button type="submit" class="danger">Retire it</button></form></details>`,
2839
2895
  ].join("\n"),
2840
2896
  // The password fields make this screen sensitive: sendScreen strips
2841
2897
  // the chrome additions and keeps the reorder poller — the named
@@ -2861,9 +2917,10 @@ export function createDecisionServer(options) {
2861
2917
  const version = url.searchParams.get("version");
2862
2918
  if (version !== null && !family.versions.some(one => one.id === version))
2863
2919
  return refuse(response, who, 404, "That version is not available for this task.", taskHref(family.root.id));
2920
+ // A version's own address lands on its family's page, still asking for the plan editor when it did (Chat's Edit plan).
2864
2921
  if (task.taskId !== family.root.id)
2865
- return redirect(response, `${taskHref(family.root.id)}?version=${encodeURIComponent(task.taskId)}`);
2866
- return taskScreen(response, who, version ?? family.current.id, null, 200);
2922
+ return redirect(response, `${taskHref(family.root.id)}?version=${encodeURIComponent(task.taskId)}${url.searchParams.get("edit") === "plan" ? "&edit=plan" : ""}`);
2923
+ return taskScreen(response, who, version ?? family.current.id, null, 200, undefined, undefined, url.searchParams.get("edit") === "plan");
2867
2924
  }
2868
2925
  // v103: a task's evidence pack, as a printable page or JSON (the whole family, sealed ledger entries included).
2869
2926
  const evidence = matchTaskPath(url.pathname, "/evidence$");
@@ -3034,6 +3091,11 @@ export function createDecisionServer(options) {
3034
3091
  const tab = parseResultTab(url.searchParams.get("tab"));
3035
3092
  return redirect(response, `${reviewHref(taskId, found.id)}${tab === "summary" ? "" : `&tab=${tab}`}`);
3036
3093
  }
3094
+ // A build that failed or stopped opens on its own result page too: its changes, its checks and what went wrong.
3095
+ if (url.searchParams.get("record") === null && !running && (found.role === "builder" || found.role === "scout") && attemptRowFor(taskId, found.id, null) !== null) {
3096
+ const tab = parseResultTab(url.searchParams.get("tab"));
3097
+ return redirect(response, `${reviewHref(taskId, found.id)}${tab === "summary" ? "" : `&tab=${tab}`}`);
3098
+ }
3037
3099
  // Pollers exist only for a LIVE run — an orphaned null-outcome run
3038
3100
  // would otherwise be refetched forever (finding 15). The nonce is
3039
3101
  // sendScreen's business now.
@@ -3424,7 +3486,7 @@ export function createDecisionServer(options) {
3424
3486
  const fragments = known === version
3425
3487
  ? null
3426
3488
  : {
3427
- thread: mateThreadHtml({ ...rows, focusTask, csrf, now, problem: takeMateNote(csrf, session.id) }),
3489
+ thread: mateThreadHtml({ ...rows, focusTask, csrf, now, problem: takeMateNote(csrf, session.id), chatProject }),
3428
3490
  after: mateAfterComposerHtml({ messages: rows.messages, pending: rows.pending, focusTask, csrf }),
3429
3491
  live: focusTask === null ? null : taskChatLiveRegion(focusTask, csrf, true, rows.pending !== null),
3430
3492
  };
@@ -3533,6 +3595,8 @@ export function createDecisionServer(options) {
3533
3595
  })();
3534
3596
  const roomId = url.searchParams.get('conversation');
3535
3597
  const resultLink = (href) => roomId ? href + '&conversation=' + encodeURIComponent(roomId) : href;
3598
+ // Accept and finish is here: the one act that accepts the person's own checks and finishes the task.
3599
+ const finishes = resultRun !== null && who.role === 'approver' && focusTask?.assignment?.state === 'ready-to-check' && focusTask.assignment.receipt?.runId === resultRun.id;
3536
3600
  const resultPanel = resultRun === null
3537
3601
  ? null
3538
3602
  : resultPanelHtml(resultDetailOf(resultRun, who, now), {
@@ -3545,9 +3609,9 @@ export function createDecisionServer(options) {
3545
3609
  hrefFor: one => resultLink(chatResultHref(focusTask?.id ?? "", resultRun.id, one)),
3546
3610
  returnTo: resultLink(chatResultHref(focusTask?.id ?? "", resultRun.id)),
3547
3611
  back: { href: roomId ? "/chat?conversation=" + encodeURIComponent(roomId) : taskChatHref(focusTask?.id ?? ""), label: "Back to chat" },
3548
- }) + (who.role === 'approver' && focusTask?.assignment?.state === 'ready-to-check'
3549
- && focusTask.assignment.receipt?.runId === resultRun.id
3550
- ? completionForm(focusTask.assignment.receipt.taskId, resultRun.id, focusTask.assignment.receipt.digest, who.session.csrf, pullRequestTargetOf(resultRun.id)) : '');
3612
+ finishes,
3613
+ }) + (finishes && focusTask?.assignment?.receipt != null
3614
+ ? completionForm(focusTask.assignment.receipt.taskId, resultRun.id, focusTask.assignment.receipt.digest, who.session.csrf, pullRequestTargetOf(resultRun.id), owedAcceptanceOf(focusTask.assignment.receipt)) : '');
3551
3615
  const focusProblem = requestedTask !== null && focusTask === null
3552
3616
  ? "That task is not available in this workspace."
3553
3617
  : requestedResult !== null && focusTask !== null && resultRun === null
@@ -4072,8 +4136,9 @@ export function createDecisionServer(options) {
4072
4136
  returnTo: "/settings/lead",
4073
4137
  };
4074
4138
  const signedIn = localSignIn?.states.claude === "connected" ? "Claude Code" : localSignIn?.states.codex === "connected" ? "Codex" : null;
4139
+ const promises = openCommitments(store, who.name, 50).map(one => ({ id: one.id, what: one.what, when: conditionWords(store, one.condition), until: one.expiresAt }));
4075
4140
  return sendScreen(response, 200, screen("Lead", leadSettingsHtml({ config, facts, words: leadWords(), signedIn, command: agentSignInCommand(),
4076
- said: url.searchParams.get("said") }), { chrome: chromeFor(project, "settings") }));
4141
+ said: url.searchParams.get("said"), saved: url.searchParams.get("saved") === "1", identity: leadIdentityOf(store, who.name), promises }), { chrome: chromeFor(project, "settings") }));
4077
4142
  }
4078
4143
  if (url.pathname === "/settings/telegram") {
4079
4144
  // Any approver pairs their OWN phone here; the bot token stays on /settings.
@@ -4372,8 +4437,16 @@ export function createDecisionServer(options) {
4372
4437
  // One reply runs at a time per person; this thread shows it only when
4373
4438
  // the reply is its own.
4374
4439
  const live = store.liveMateTurnFor(who.name);
4440
+ const messages = store.listMateMessages(opened.thread.id, 40);
4441
+ // The lead's question with buttons, while its reply is the last word in the thread.
4442
+ const last = messages.at(-1);
4443
+ const ask = last?.role === "assistant" && last.turn !== null && store.getMateTurn(last.turn)?.approver === who.name ? store.mateAskOpen(last.turn, now) : null;
4444
+ // Answered questions stay readable above the answer; only their buttons go.
4445
+ const asks = new Map(messages.flatMap(one => { const asked = one.role === "assistant" && one.turn !== null ? store.mateAsk(one.turn) : null; return asked === null ? [] : [[asked.turn, asked]]; }));
4375
4446
  return {
4376
- messages: store.listMateMessages(opened.thread.id, 40),
4447
+ messages,
4448
+ ask,
4449
+ asks,
4377
4450
  proposals,
4378
4451
  decisions: decisionsFor(store, [...proposals, ...coordinatorProposals]),
4379
4452
  coordinatorProposals,
@@ -4448,7 +4521,7 @@ export function createDecisionServer(options) {
4448
4521
  const rows = mateConversationRows(who, principal, focusTask, now, chatProject);
4449
4522
  return {
4450
4523
  sessionId: session.id, user: session.approver, version: mateChatVersion({ ...rows, focusTask }),
4451
- messages: mateBrowserMessages(rows, who.session.csrf, back),
4524
+ messages: mateBrowserMessages(rows, who.session.csrf, back, { task: focusTask?.id ?? null, project: chatProject }),
4452
4525
  pendingTurnId: rows.pending?.id ?? null, requestId: randomBytes(16).toString("hex"), maxChars: MATE_MESSAGE_MAX_CHARS,
4453
4526
  taskId: focusTask?.id ?? null, resultRunId, project: focusTask === null ? chatProject : null,
4454
4527
  };
@@ -4916,7 +4989,8 @@ export function createDecisionServer(options) {
4916
4989
  try {
4917
4990
  const project = s.chrome.active === 'chat' ? null : s.chrome.project;
4918
4991
  crew = extras.team?.tasks ? browserCrewFromIndex(extras.team.tasks, extras.team.selected?.id) : requestFacts.workCrew?.project === project ? browserCrewFromIndex(requestFacts.workCrew.page)
4919
- : browserCrewOf(store, clock(), { principal: 'operator', repos: managedRepos(), includeUnplaced: false }, { evidenceRoot, project });
4992
+ // The reader's own lead's claims read "<name> is on it" here too.
4993
+ : browserCrewOf(store, clock(), { principal: 'operator', repos: managedRepos(), includeUnplaced: false, viewer: requestFacts.actor ?? null }, { evidenceRoot, project });
4920
4994
  }
4921
4995
  catch {
4922
4996
  notices.push('Crew updates are unavailable. Open Tasks to inspect saved work.');
@@ -4958,6 +5032,7 @@ export function createDecisionServer(options) {
4958
5032
  const request = requestFacts.workspaceRequest;
4959
5033
  const workspace = {
4960
5034
  version: 1, path: currentPath, title: s.title, user: requestFacts.actor ?? '', csrf: requestFacts.csrf, sensitive,
5035
+ ...(requestFacts.actor ? { leadName: leadNameOf(store, requestFacts.actor) } : {}),
4961
5036
  refreshUrl: path.pathname + path.search,
4962
5037
  receipt: conversation !== null && request && REQUEST_TOKEN.test(request)
4963
5038
  ? { request, received: store.mateRequestReceipt(conversation.sessionId, request) !== null } : null,
@@ -5063,7 +5138,7 @@ export function createDecisionServer(options) {
5063
5138
  // One line: what this person's lead is doing now and when it last acted.
5064
5139
  const activity = leadActivity(store, who.name);
5065
5140
  const leadTask = activity?.taskId == null ? null : store.lookupRef(activity.taskId);
5066
- const lead = activity === null ? null : { doing: activity.doing, at: activity.at,
5141
+ const lead = activity === null ? null : { name: activity.name, doing: activity.doing, at: activity.at,
5067
5142
  href: leadTask == null || !admitted(leadTask.repo) ? null : taskHref(familyOf(activity.taskId)?.root.id ?? activity.taskId) };
5068
5143
  return {
5069
5144
  agents, counts, catchUp, allHref: "/work", ...(lead === null ? {} : { lead }),
@@ -5164,7 +5239,7 @@ export function createDecisionServer(options) {
5164
5239
  };
5165
5240
  }
5166
5241
  function workAccess() {
5167
- // The reader's own lead's claims read "Your lead is on it" (lead-voice.ts); nobody else's.
5242
+ // The reader's own lead's claims read "<name> is on it" (lead-voice.ts); nobody else's.
5168
5243
  return { principal: 'operator', repos: admissionList(), includeUnplaced: visible(null), viewer: requestContext.getStore()?.actor ?? null };
5169
5244
  }
5170
5245
  // All project counts are read together. No task artifacts or processes are
@@ -5317,7 +5392,7 @@ export function createDecisionServer(options) {
5317
5392
  .map(task => `<a class="item${task.id === currentId ? " current" : ""}" href="${taskHref(task.id)}">` +
5318
5393
  `<span class="t">${escape(task.title)}</span></a>`)
5319
5394
  .join("\n");
5320
- return `<h2>tasks</h2>\n${items === "" ? `<p class="meta">none yet</p>` : items}`;
5395
+ return `<h2>Tasks</h2>\n${items === "" ? `<p class="meta">None yet</p>` : items}`;
5321
5396
  }
5322
5397
  /**
5323
5398
  * The one liveness fact (round-4 findings 14/15): a run is being built
@@ -5350,7 +5425,7 @@ export function createDecisionServer(options) {
5350
5425
  `<span class="m">${runOutcomeBadge(run, live.has(run.id))}` +
5351
5426
  `<span class="mono">${whenTime(run.startedAt)}</span></span></a>`)
5352
5427
  .join("\n");
5353
- return `<h2>builds</h2>\n${items === "" ? `<p class="meta">none yet</p>` : items}`;
5428
+ return `<h2>Builds</h2>\n${items === "" ? `<p class="meta">None yet</p>` : items}`;
5354
5429
  }
5355
5430
  /**
5356
5431
  * The opener: every project inside the ceiling, most recently opened
@@ -5834,6 +5909,8 @@ export function createDecisionServer(options) {
5834
5909
  // available, computed fresh only when this call has no `who`.
5835
5910
  planRevisions,
5836
5911
  milestoneProgress: view?.milestoneProgress ?? progressOf(ref.id, planRevisions?.current?.document ?? null),
5912
+ // What a failed task missed, said as the task page says it.
5913
+ failure: task.state === "failed" ? failureOf(family.versions.flatMap(version => store.runsFor(version.refId)), view?.assignment ?? assignmentOf(store, task.id, now, workAccess(), evidenceRoot)) : null,
5837
5914
  claimed: view?.claimed ?? store.hasLiveClaim(ref.id, now),
5838
5915
  liveRun: live === null ? null : { id: live.id, runner: live.runner, startedAt: live.startedAt, phase: live.phase },
5839
5916
  checkProgress: view?.checkProgress ?? (live === null && latest === null ? null : store.checkProgress((live ?? latest).id)),
@@ -5895,34 +5972,69 @@ export function createDecisionServer(options) {
5895
5972
  return null;
5896
5973
  return { taskId: rootId, view: null, offer: { taskId: receipt.taskId, runId: receipt.runId, digest: receipt.digest }, target };
5897
5974
  }
5898
- /** What went wrong with the latest finished attempt (whatever its outcome, never an older failure), in one line: a
5899
- * failing check's last error line (linked to that line in its saved log), else the attempt's recorded reason in plain
5900
- * words, else that no reason was recorded. A reason that is machine output reads as an internal error, its detail
5901
- * behind a link to that line of the attempt's record; nothing links to this task's own page. */
5902
- function failureOf(runs, assignment) {
5975
+ /** What went wrong with the latest finished attempt (whatever its outcome, never an older failure). */
5976
+ function failureOf(runs, assignment, readLog = true) {
5903
5977
  const last = latestFinishedAttempt(runs);
5904
5978
  if (last === null)
5905
- return { line: NO_REASON_RECORDED, link: null };
5906
- const receipt = assignment?.receipt?.runId === last.id ? assignment.receipt : null;
5907
- if (receipt !== null && receipt.checks.status === "failed" && receipt.checks.logArtifactId !== null) {
5908
- const artifact = store.artifactsFor(last.id).find(one => one.id === receipt.checks.logArtifactId) ?? null;
5979
+ return { kind: "reason", line: NO_REASON_RECORDED, evidence: null, suggestion: GENERAL_SUGGESTION, link: null };
5980
+ const full = store.getRun(last.id);
5981
+ return full === null ? { kind: "reason", line: failedAttemptSentence(last.reason), evidence: null, suggestion: stopSuggestionOf(last.reason), link: null }
5982
+ : explainAttempt(full, assignment?.receipt?.runId === last.id ? assignment.receipt : null, readLog);
5983
+ }
5984
+ /** One finished attempt's failure in plain words: a failing check's last error line (linked to that line in its
5985
+ * saved log), else the first signed requirement it missed with the evidence line behind it, else its recorded reason
5986
+ * in plain words (or that none was recorded). Each comes with one suggestion of what to change, and a link to the
5987
+ * attempt's own result page, where its changes, checks and failure are; machine output reads as an internal error,
5988
+ * its detail behind a link to that line of the attempt's record. `readLog` false (a list's rows) reads no saved
5989
+ * check log: the requirement or the reason, from the database alone. */
5990
+ function explainAttempt(run, receipt, readLog = true) {
5991
+ const taskId = store.externalIdFor(run.taskRef) ?? "";
5992
+ const page = `${reviewHref(taskId)}&run=${run.id}`;
5993
+ const see = taskId === "" ? null : { label: `See build #${run.id}`, href: page };
5994
+ const artifacts = store.artifactsFor(run.id);
5995
+ const logArtifact = !readLog ? null : receipt?.checks.status === "failed" && receipt.checks.logArtifactId !== null
5996
+ ? artifacts.find(one => one.id === receipt.checks.logArtifactId) ?? null
5997
+ : [...artifacts].reverse().find(one => one.kind === "check-log") ?? null;
5998
+ if (logArtifact !== null) {
5909
5999
  let read = null;
5910
6000
  try {
5911
- read = artifact === null ? null : readVerifiedArtifact(evidenceRoot, artifact);
6001
+ read = readVerifiedArtifact(evidenceRoot, logArtifact);
5912
6002
  }
5913
6003
  catch {
5914
6004
  read = null;
5915
6005
  }
5916
- const found = read !== null && read.ok ? lastErrorLineOf(read.content.toString("utf8")) : null;
6006
+ const text = read !== null && read.ok ? read.content.toString("utf8") : null;
6007
+ const exit = text === null ? null : /^\(exit (-?[0-9]+)\)$/m.exec(text);
6008
+ const failedCheck = receipt?.checks.status === "failed" || (exit !== null && exit !== undefined && Number(exit[1]) !== 0);
6009
+ const found = text !== null && failedCheck ? lastErrorLineOf(text) : null;
5917
6010
  if (found !== null) {
5918
- return { line: oneLineOf(found.text, 140), link: { label: `Check output, line ${found.line}`,
5919
- href: `${reviewHref(receipt.taskId)}&run=${last.id}&tab=checks#check-log-L${found.line}` } };
6011
+ const line = oneLineOf(found.text, 140);
6012
+ return { kind: "check", line: `The check failed: ${line}`, evidence: null, suggestion: failingCheckSuggestion(line),
6013
+ link: taskId === "" ? null : { label: `Check output, line ${found.line}`, href: `${page}&tab=checks#check-log-L${found.line}` } };
5920
6014
  }
5921
6015
  }
5922
- return { line: failedAttemptSentence(last.reason),
5923
- link: isInternalErrorReason(last.reason) ? { label: "The recorded error", href: `/r/${last.id}?record=1#run-reason-detail` } : null };
6016
+ const verdict = store.proofVerdictFor(run.id);
6017
+ const missed = verdict === null ? null : missedRequirementOf(verdict.matrix);
6018
+ if (missed !== null) {
6019
+ return { kind: "requirement", line: missedRequirementLine(missed.statement), evidence: missed.evidence === null ? null : oneLineOf(missed.evidence, 240),
6020
+ suggestion: missedRequirementSuggestion(missed.statement), link: see };
6021
+ }
6022
+ const internal = isInternalErrorReason(run.reason);
6023
+ return { kind: "reason", line: failedAttemptSentence(run.reason), evidence: null, suggestion: stopSuggestionOf(run.reason),
6024
+ link: internal ? { label: "The recorded error", href: `/r/${run.id}?record=1#run-reason-detail` } : see };
6025
+ }
6026
+ /** Run checks in place on a result's commit: an approver's browser session, outside the demo, when the project has a
6027
+ * check and none is waiting or running on it. Comes back to `returnTo`. */
6028
+ function runChecksHere(runId, who, returnTo) {
6029
+ if (runId === null || who.via !== "cookie" || who.role !== "approver" || store.isDemo())
6030
+ return null;
6031
+ const run = store.getRun(runId);
6032
+ const followUps = run === null ? null : followUpsFor(store, evidenceRoot, run, clock());
6033
+ if (followUps === null || !(followUps.quick || followUps.full) || followUps.checks.some(one => one.state === "waiting" || one.state === "running"))
6034
+ return null;
6035
+ return { action: `/r/${runId}/checks`, level: followUps.full ? "full" : "quick", returnTo };
5924
6036
  }
5925
- function taskScreen(response, who, taskId, problem, status, scopeDraft, cancelDraft) {
6037
+ function taskScreen(response, who, taskId, problem, status, scopeDraft, cancelDraft, editPlan = false) {
5926
6038
  const admittedFamily = familyOf(taskId);
5927
6039
  if (admittedFamily?.problem != null)
5928
6040
  return sendScreen(response, status, screen(admittedFamily.root.title, `<h1>${escape(admittedFamily.root.title)}</h1><p>${escape(admittedFamily.current.state)}</p><p class="problem" data-history-problem>${escape(admittedFamily.problem)}</p>`, { chrome: chromeFor(admittedFamily.root.repo, "tasks") }));
@@ -5937,6 +6049,8 @@ export function createDecisionServer(options) {
5937
6049
  presentedData.scopeDraft = scopeDraft;
5938
6050
  if (cancelDraft !== undefined)
5939
6051
  presentedData.cancelDraft = cancelDraft;
6052
+ if (editPlan)
6053
+ presentedData.editPlan = true;
5940
6054
  presentedData.pullRequest = taskPullRequestOf(taskId, who);
5941
6055
  // The thread reads the whole family: the original, its revisions, and every attempt across them.
5942
6056
  if (family !== null && family.problem === null) {
@@ -5946,6 +6060,8 @@ export function createDecisionServer(options) {
5946
6060
  }
5947
6061
  // What went wrong reads the family's latest finished attempt, as the Tasks row does.
5948
6062
  presentedData.failure = failureOf(presentedData.family?.runs ?? data.runs, data.assignment ?? null);
6063
+ presentedData.runChecks = runChecksHere(data.assignment?.receipt?.runId ?? null, who, taskHref(taskId));
6064
+ presentedData.demo = store.isDemo();
5949
6065
  const paneProject = restricted() ? store.lookupRef(taskId)?.repo ?? null : who.via === "cookie" ? who.session.project : null;
5950
6066
  const page = taskPage(paneProject === null && !unscopedMode
5951
6067
  ? chromeFor(paneProject, "tasks")
@@ -6126,7 +6242,7 @@ export function createDecisionServer(options) {
6126
6242
  return peekSay(seen.reason);
6127
6243
  const stamp = clock().toISOString().slice(11, 19);
6128
6244
  const parts = [
6129
- `<p class="meta">best-effort look at ${escape(stamp)} UTC — files can change mid-read</p>`,
6245
+ `<p class="meta">Best-effort look at ${escape(stamp)} UTC — files can change mid-read</p>`,
6130
6246
  ];
6131
6247
  const changed = seen.rows.filter(one => one.kind === "changed");
6132
6248
  const deleted = seen.rows.filter(one => one.kind === "deleted");
@@ -6151,7 +6267,7 @@ export function createDecisionServer(options) {
6151
6267
  for (const row of deleted)
6152
6268
  parts.push(line(row, "−"));
6153
6269
  if (fresh.total > 0) {
6154
- parts.push(`<p class="meta">new files · ${fresh.total}</p>`);
6270
+ parts.push(`<p class="meta">New files · ${fresh.total}</p>`);
6155
6271
  for (const row of fresh.rows) {
6156
6272
  parts.push(row.collapsed
6157
6273
  ? `<p class="row mono">+ ${peekName(row.label)} <span class="meta">collapsed names — ${row.count} files</span></p>`
@@ -6162,7 +6278,7 @@ export function createDecisionServer(options) {
6162
6278
  }
6163
6279
  }
6164
6280
  if (unchecked.length > 0) {
6165
- parts.push(`<p class="meta">not verified this look — absence above does not mean unchanged:</p>`);
6281
+ parts.push(`<p class="meta">Not verified this look — absence above does not mean unchanged:</p>`);
6166
6282
  for (const row of unchecked)
6167
6283
  parts.push(line(row, "?"));
6168
6284
  }
@@ -6173,7 +6289,7 @@ export function createDecisionServer(options) {
6173
6289
  // The byte cap is enforced AFTER escaping (finding 32): an oversize
6174
6290
  // rendering is replaced whole by its exact counts.
6175
6291
  fragment =
6176
- `<p class="meta">best-effort look at ${escape(stamp)} UTC</p>` +
6292
+ `<p class="meta">Best-effort look at ${escape(stamp)} UTC</p>` +
6177
6293
  `<p class="row">${changed.length} changed · ${deleted.length} deleted · ${fresh.total} new · ${unchecked.length} unverified — too much to render live; the final diff will hold the detail</p>`;
6178
6294
  }
6179
6295
  peekCache.set(key, { fragment, at: Date.now() });
@@ -8604,7 +8720,7 @@ export function createDecisionServer(options) {
8604
8720
  // THE CEREMONY (M1): every resolved term in words, and the password
8605
8721
  // signs exactly this digest — a drifted form refuses at /mode/sign.
8606
8722
  const nonce = mintApprovalNonce(who.name, "mode-sign", `${project}:${digest}`);
8607
- const bodyHtml = `<h1 style="overflow-wrap:anywhere">sign the ${escape(name)} mode for ${escape(project)}</h1>` +
8723
+ const bodyHtml = `<h1 style="overflow-wrap:anywhere">Sign the ${escape(name)} mode for ${escape(project)}</h1>` +
8608
8724
  `<form method="post" action="/mode/sign" class="card approve-form">` +
8609
8725
  `<input type="hidden" name="csrf" value="${escape(who.session.csrf)}">` +
8610
8726
  `<input type="hidden" name="nonce" value="${escape(nonce)}">` +
@@ -8613,12 +8729,12 @@ export function createDecisionServer(options) {
8613
8729
  .map(field => `<input type="hidden" name="${field}" value="${escape(body.get(field) ?? "")}">`)
8614
8730
  .join("") +
8615
8731
  `<input type="hidden" name="expiry" value="${escape(expiry)}">` +
8616
- `<p><strong>your password signs exactly this:</strong></p>` +
8732
+ `<p><strong>Your password signs exactly this:</strong></p>` +
8617
8733
  modeWords(terms)
8618
8734
  .map(words => `<p class="recap" style="margin-top:.4rem">${escape(words)}</p>`)
8619
8735
  .join("") +
8620
- `<label>your password, typed again<input type="password" name="token" autocomplete="current-password"></label>` +
8621
- `<div class="sticky-actions"><button type="submit">sign it</button></div>` +
8736
+ `<label>Your password, typed again<input type="password" name="token" autocomplete="current-password"></label>` +
8737
+ `<div class="sticky-actions"><button type="submit">Sign it</button></div>` +
8622
8738
  `</form>` +
8623
8739
  `<p class="meta"><a href="/mode">back — sign nothing</a></p>`;
8624
8740
  return sendScreen(response, 200, screen("mode", bodyHtml, { chrome: chromeFor(project, "mode") }));
@@ -9105,6 +9221,25 @@ export function createDecisionServer(options) {
9105
9221
  }
9106
9222
  // Turn the lead on with the agent signed in on this computer: its membership spends no dollars, so like starting a
9107
9223
  // conversation it needs no second password; the full form (Advanced) still asks for one.
9224
+ // Settings → Lead: the name and persona this person's lead speaks with.
9225
+ if (url.pathname === "/settings/lead/identity") {
9226
+ if (who.via !== "cookie" || who.role !== "approver")
9227
+ return refuse(response, who, 403, "An approver names their lead.", "/settings/lead");
9228
+ const checked = checkLeadIdentity(body.get("name") ?? "", body.get("persona") ?? "");
9229
+ if (!checked.ok)
9230
+ return redirect(response, chatReturnWithSaid("/settings/lead", checked.message));
9231
+ store.setLeadConfig(who.name, checked.identity.name, checked.identity.persona, now);
9232
+ return redirect(response, "/settings/lead?saved=1");
9233
+ }
9234
+ // Settings → Lead: stop the lead following up on one of your promises.
9235
+ if (url.pathname === "/settings/lead/promise/cancel") {
9236
+ if (who.via !== "cookie" || who.role !== "approver")
9237
+ return refuse(response, who, 403, "An approver manages the lead's promises.", "/settings/lead");
9238
+ const id = Number(body.get("promise"));
9239
+ const done = Number.isSafeInteger(id) && cancelCommitment(store, who.name, id, who.name, "Cancelled in Settings.", now);
9240
+ // The promise leaving the list says it was cancelled; only a refusal needs words.
9241
+ return redirect(response, done ? "/settings/lead" : chatReturnWithSaid("/settings/lead", "That promise was already closed."));
9242
+ }
9108
9243
  if (url.pathname === "/settings/lead/on") {
9109
9244
  if (who.via !== "cookie" || who.role !== "approver")
9110
9245
  return refuse(response, who, 403, "An approver turns the lead on.", "/settings/lead");
@@ -9418,7 +9553,7 @@ export function createDecisionServer(options) {
9418
9553
  }
9419
9554
  const opened = store.openMateThread(who.name, principal.ceilingDigest, now, chatScopeOf(focusTask, chatProject));
9420
9555
  const onProgress = beginLiveTurn(opened.thread.id);
9421
- void runMateTurn({ store, who: principal, session: mateSession, thread: opened.thread, config: enabled.config, key: enabled.key, message, onProgress, ...(requestId === null ? {} : { requestId }), ...(focusTask === null && chatProject !== null ? { context: `Current project: ${projectName(chatProject)} (${chatProject}). Keep this conversation about that project unless the operator explicitly asks to broaden it; use it as the repo for project tools.` } : {}), ...(focusTask === null ? {} : { context: `Current task: ${focusTask.id}. Read it with get_task before answering or proposing changes. Read its currentExecution next and bind new actions to that exact execution. Never replace the target of a prior proposal with a newer revision. Keep this turn about that task unless the operator explicitly asks to broaden it.${resultContext}${modeContext}` }), fetcher: chatFetcher, ...(options.subscriptionChatRunner === undefined ? {} : { subscriptionRunner: options.subscriptionChatRunner }), clock, evidenceRoot })
9556
+ void runMateTurn({ store, who: principal, session: mateSession, thread: opened.thread, config: enabled.config, key: enabled.key, message, onProgress, channel: "console", ...(requestId === null ? {} : { requestId }), ...(focusTask === null && chatProject !== null ? { context: `Current project: ${projectName(chatProject)} (${chatProject}). Keep this conversation about that project unless the operator explicitly asks to broaden it; use it as the repo for project tools.` } : {}), ...(focusTask === null ? {} : { context: `Current task: ${focusTask.id}. Read it with get_task before answering or proposing changes. Read its currentExecution next and bind new actions to that exact execution. Never replace the target of a prior proposal with a newer revision. Keep this turn about that task unless the operator explicitly asks to broaden it.${resultContext}${modeContext}` }), fetcher: chatFetcher, ...(options.subscriptionChatRunner === undefined ? {} : { subscriptionRunner: options.subscriptionChatRunner }), clock, evidenceRoot })
9422
9557
  .then(outcome => {
9423
9558
  if (!outcome.ok)
9424
9559
  noteMate(who.session.csrf, "turn" in outcome ? outcome.turn : null, outcome.message);
@@ -9936,7 +10071,9 @@ export function createDecisionServer(options) {
9936
10071
  const found = store.getRun(id);
9937
10072
  if (found === null || !visible(taskRepoOf(found.taskRef)))
9938
10073
  return refuse(response, who, 404, "no such run");
9939
- const back = resultReturnTarget(body.get("return"), id);
10074
+ // The task page that offered Run checks in place is a return target too: this run's own task, or its family's root.
10075
+ const runTask = store.externalIdFor(found.taskRef);
10076
+ const back = resultReturnTarget(body.get("return"), id, runTask === null ? [] : [runTask, familyOf(runTask)?.root.id ?? runTask]);
9940
10077
  if (who.via !== "cookie")
9941
10078
  return refuse(response, who, 403, "Sign in with a browser session to do this.", back);
9942
10079
  if (followUp[2] === "checks") {
@@ -9948,7 +10085,8 @@ export function createDecisionServer(options) {
9948
10085
  const asked = requestFollowUpChecks(store, { runId: id, level, actor: who.name }, now);
9949
10086
  if (!asked.ok)
9950
10087
  return refuse(response, who, 409, asked.message, back);
9951
- return redirect(response, `${back.split("#")[0]}#follow-ups`);
10088
+ // Back where it was asked: the task page's status row says the checks are running; a result page shows them under #follow-ups.
10089
+ return redirect(response, back.startsWith("/t/") ? back : `${back.split("#")[0]}#follow-ups`);
9952
10090
  }
9953
10091
  const filed = fileAddTestsTask(store, evidenceRoot, { runId: id, actor: who.name, filedVia: "console", ...(admissionList() === null ? {} : { admittedRepos: admissionList() }) }, now);
9954
10092
  if (!filed.ok)
@@ -10162,7 +10300,7 @@ export function createDecisionServer(options) {
10162
10300
  const quickMode = quickRef?.repo == null ? null : store.activeMode(quickRef.repo, clock());
10163
10301
  const quickTerms = quickMode === null ? null : modeTermsFromJson(quickMode.termsJson);
10164
10302
  const quick = quickTerms?.quickMint === true && quickMode !== null && quickMode.signedBy === who.name;
10165
- const body = `<h1>run ${escape(terms.taskId)} once, while you watch</h1>` +
10303
+ const body = `<h1>Run ${escape(terms.taskId)} once, while you watch</h1>` +
10166
10304
  `<form method="post" action="${taskHref(terms.taskId)}/attend" class="card approve-form">` +
10167
10305
  `<input type="hidden" name="csrf" value="${escape(csrf)}">` +
10168
10306
  `<input type="hidden" name="nonce" value="${escape(nonce)}">` +
@@ -10175,10 +10313,10 @@ export function createDecisionServer(options) {
10175
10313
  (inputs.posture === undefined ? "" : `<input type="hidden" name="posture" value="${escape(inputs.posture)}">`) +
10176
10314
  (terms.parentRun == null ? "" : `<input type="hidden" name="parent" value="${terms.parentRun}">`) +
10177
10315
  (terms.followup == null ? "" : `<input type="hidden" name="followup" value="${escape(terms.followup)}">`) +
10178
- `<p><strong>your password signs exactly this:</strong></p>` +
10179
- `<p class="meta">goal</p><p class="recap" style="margin-top:0">${escape(scope?.goal ?? "")}</p>` +
10180
- `<p class="meta">not this</p><p class="recap" style="margin-top:0">${scope?.outOfScope == null ? "<em>no exclusions</em>" : escape(scope.outOfScope)}</p>` +
10181
- `<p class="meta">touches · ${scope === null || scope.touches.length === 0 ? "anything" : scope.touches.map(one => escape(one)).join(", ")}</p>` +
10316
+ `<p><strong>Your password signs exactly this:</strong></p>` +
10317
+ `<p class="meta">Goal</p><p class="recap" style="margin-top:0">${escape(scope?.goal ?? "")}</p>` +
10318
+ `<p class="meta">Not this</p><p class="recap" style="margin-top:0">${scope?.outOfScope == null ? "<em>no exclusions</em>" : escape(scope.outOfScope)}</p>` +
10319
+ `<p class="meta">Touches · ${scope === null || scope.touches.length === 0 ? "anything" : scope.touches.map(one => escape(one)).join(", ")}</p>` +
10182
10320
  (() => {
10183
10321
  const profile = JSON.parse(terms.profileJson).profile;
10184
10322
  const posture = profile?.permissionArgv === "bypassPermissions"
@@ -10186,21 +10324,21 @@ export function createDecisionServer(options) {
10186
10324
  : profile?.permissionArgv === "auto"
10187
10325
  ? "safe unattended permissions — routine project commands and edits proceed; risky acts stop"
10188
10326
  : "legacy acceptEdits — edits proceed, commands that ask are denied unattended";
10189
- return `<p class="meta">runs on</p><p class="recap" style="margin-top:0">claude · ${escape(String(profile?.model ?? ""))} — ${posture}</p>`;
10327
+ return `<p class="meta">Runs on</p><p class="recap" style="margin-top:0">claude · ${escape(String(profile?.model ?? ""))} — ${posture}</p>`;
10190
10328
  })() +
10191
10329
  (terms.parentRun == null
10192
10330
  ? ""
10193
- : `<p class="meta">continues</p><p class="recap" style="margin-top:0">attempt <a href="/r/${terms.parentRun}" class="mono">#${terms.parentRun}</a>, from exactly where it finished</p>` +
10194
- `<p class="meta">the follow-up — this is the instruction</p><p class="recap" style="margin-top:0">${escape(terms.followup ?? "")}</p>`) +
10195
- `<p class="meta">repository · head</p><p class="recap mono" style="margin-top:0">${escape(terms.repo)} @ ${escape(terms.head.slice(0, 12))}</p>` +
10196
- `<p class="meta">worker</p><p class="recap" style="margin-top:0">${escape(terms.runner)} (this machine)</p>` +
10197
- `<p class="meta">spending</p><p class="recap" style="margin-top:0">up to about $${(terms.budgetMicrousd / 1_000_000).toFixed(2)} — the agent stops as soon as its total crosses this; the final step may run a little past it</p>` +
10198
- `<p class="meta">conversation</p><p class="recap" style="margin-top:0">at most ${terms.maxSessionTurns} messages to the agent, this whole session; each may work up to ${Math.round(terms.turnTimeoutSeconds / 60)} minutes — one that runs past that ends the whole session. If it needs a repair, the repair uses the same session, model, and clock.</p>` +
10199
- `<p class="meta">while you watch — a signed term</p><p class="recap" style="margin-top:0">your console being open is what keeps it running — any page of it, this one included; close the console and the session winds down within a minute. Everything ends by ${whenTime(terms.absoluteExpiry)} regardless. One attempt; it never converts into unattended work.</p>` +
10331
+ : `<p class="meta">Continues</p><p class="recap" style="margin-top:0">attempt <a href="/r/${terms.parentRun}" class="mono">#${terms.parentRun}</a>, from exactly where it finished</p>` +
10332
+ `<p class="meta">The follow-up — this is the instruction</p><p class="recap" style="margin-top:0">${escape(terms.followup ?? "")}</p>`) +
10333
+ `<p class="meta">Repository · head</p><p class="recap mono" style="margin-top:0">${escape(terms.repo)} @ ${escape(terms.head.slice(0, 12))}</p>` +
10334
+ `<p class="meta">Worker</p><p class="recap" style="margin-top:0">${escape(terms.runner)} (this machine)</p>` +
10335
+ `<p class="meta">Spending</p><p class="recap" style="margin-top:0">up to about $${(terms.budgetMicrousd / 1_000_000).toFixed(2)} — the agent stops as soon as its total crosses this; the final step may run a little past it</p>` +
10336
+ `<p class="meta">Conversation</p><p class="recap" style="margin-top:0">at most ${terms.maxSessionTurns} messages to the agent, this whole session; each may work up to ${Math.round(terms.turnTimeoutSeconds / 60)} minutes — one that runs past that ends the whole session. If it needs a repair, the repair uses the same session, model, and clock.</p>` +
10337
+ `<p class="meta">While you watch — a signed term</p><p class="recap" style="margin-top:0">your console being open is what keeps it running — any page of it, this one included; close the console and the session winds down within a minute. Everything ends by ${whenTime(terms.absoluteExpiry)} regardless. One attempt; it never converts into unattended work.</p>` +
10200
10338
  (quick
10201
- ? `<p class="meta">your signed mode covers this mint — no password; the mode is re-proved as you confirm, and the session is stamped with its signature</p>`
10202
- : `<label>your password, typed again — a signed-in session alone cannot authorize work<input type="password" name="token" autocomplete="current-password"></label>`) +
10203
- `<div class="sticky-actions"><button type="submit">run it while I watch</button></div>` +
10339
+ ? `<p class="meta">Your signed mode covers this mint — no password; the mode is re-proved as you confirm, and the session is stamped with its signature</p>`
10340
+ : `<label>Your password, typed again — a signed-in session alone cannot authorize work<input type="password" name="token" autocomplete="current-password"></label>`) +
10341
+ `<div class="sticky-actions"><button type="submit">Run it while I watch</button></div>` +
10204
10342
  `</form>` +
10205
10343
  `<p class="meta"><a href="${taskHref(terms.taskId)}">back to the task</a></p>`;
10206
10344
  // The chrome binds to the AUTHORITY repository (surfaces round 1,
@@ -10748,6 +10886,13 @@ export function createDecisionServer(options) {
10748
10886
  if (budgetUsd !== null && (!Number.isFinite(budgetUsd) || budgetUsd <= 0)) {
10749
10887
  return taskScreen(response, who, taskId, "the dollar cap is a positive amount", 400);
10750
10888
  }
10889
+ // The approval sheet's in-place editor carries the cap it was shown
10890
+ // exactly, in millionths, and "none" for no limit (never a default).
10891
+ const exactBudgetGiven = body.get("budget-microusd");
10892
+ const exactBudget = exactBudgetGiven === null ? undefined : exactBudgetGiven === "none" ? null : /^[1-9][0-9]{0,14}$/.test(exactBudgetGiven) ? Number(exactBudgetGiven) : NaN;
10893
+ if (Number.isNaN(exactBudget))
10894
+ return taskScreen(response, who, taskId, "the dollar cap is a positive amount", 400);
10895
+ const inPlace = body.has("requirement-new");
10751
10896
  // The tournament controls (operator request): a count of 2–4 files
10752
10897
  // race terms BESIDE the scope — validated and priced BEFORE anything
10753
10898
  // saves, so a bad tournament never half-lands on a good scope.
@@ -10802,94 +10947,120 @@ export function createDecisionServer(options) {
10802
10947
  if (plannedRace !== null && store.mirrorByTask(taskId) !== null) {
10803
10948
  return taskScreen(response, who, taskId, "external work races in a follow-up release — file the tournament on a local task", 409);
10804
10949
  }
10805
- const saved = store.transact(() => {
10806
- if ((plannedRace !== null || plannedComparison !== null) && store.openAuthorizationFor(ref.id) !== null) {
10807
- return { ok: false, status: 409, message: "an attended authorization is open on this task — revoke it before filing a tournament or comparison" };
10808
- }
10809
- // C1/M3: the signer's own credentialed filing auto-approves —
10810
- // coverage is asked INSIDE this transaction, the escalated
10811
- // default and budget ride the filing, and the seal commits
10812
- // atomically with it. Plain scopes only: a tournament or
10813
- // comparison keeps its own human ceremony.
10814
- // COOKIE ONLY (C1's channel table; surfaces round 1, finding 1):
10815
- // bearer credentials are for machines, and a machine road must
10816
- // never spend the signer's mode.
10817
- const coverage = who.via === "cookie" && plannedRace === null && plannedComparison === null
10818
- ? modeFilingCoverage(store, ref.repo, who.name, now)
10819
- : null;
10820
- const proposed = proposeGuarded(store, {
10821
- taskId, author: who.name,
10822
- goal: body.get("goal") ?? "",
10823
- outOfScope: body.get("not") ?? null,
10824
- touches: (body.get("touches") ?? "").split(/[\n,]/),
10825
- acceptance: acceptanceLinesToInput((body.get("acceptance") ?? "").split("\n")),
10826
- permissionMode,
10827
- qualityMode,
10828
- ...(budgetUsd !== null
10829
- ? { budgetMicrousd: Math.round(budgetUsd * 1_000_000) }
10830
- : coverage?.defaultBudgetMicrousd != null
10831
- ? { budgetMicrousd: coverage.defaultBudgetMicrousd }
10832
- : {}),
10833
- ...(coverage?.escalated === true ? { posture: "escalated" } : {}),
10834
- sawDigest: sawDigest === null || sawDigest === "" ? null : sawDigest,
10835
- taskRef: ref.id,
10836
- now,
10837
- });
10838
- if (!proposed.ok) {
10839
- return {
10840
- ok: false,
10841
- status: proposed.reason === "changed" || proposed.reason === "claimed" ? 409 : 400,
10842
- message: proposed.message ?? `scope not saved: ${proposed.reason}`,
10843
- };
10844
- }
10845
- if (coverage !== null && ref.plan === "requested") {
10846
- authorizePlanUnderMode(store, taskId, who.name, now);
10847
- }
10848
- else if (coverage !== null) {
10849
- const filedScope = store.getScope(taskId);
10850
- if (filedScope?.profileState === "resolved") {
10851
- const sealedUnderMode = store.sealScopeApproval(taskId, who.name, now, {}, { kind: "mode", modeDigest: coverage.digest });
10852
- // The quarantine speaks at the caller (review finding 7).
10853
- if (!sealedUnderMode) {
10854
- return { ok: false, status: 200, message: "scope saved — coordinator-filed: mode coverage cannot admit it; sign the scope" };
10950
+ let saved;
10951
+ try {
10952
+ saved = store.transact(() => {
10953
+ if ((plannedRace !== null || plannedComparison !== null) && store.openAuthorizationFor(ref.id) !== null) {
10954
+ return { ok: false, status: 409, message: "an attended authorization is open on this task — revoke it before filing a tournament or comparison" };
10955
+ }
10956
+ // C1/M3: the signer's own credentialed filing auto-approves —
10957
+ // coverage is asked INSIDE this transaction, the escalated
10958
+ // default and budget ride the filing, and the seal commits
10959
+ // atomically with it. Plain scopes only: a tournament or
10960
+ // comparison keeps its own human ceremony.
10961
+ // COOKIE ONLY (C1's channel table; surfaces round 1, finding 1):
10962
+ // bearer credentials are for machines, and a machine road must
10963
+ // never spend the signer's mode.
10964
+ const coverage = who.via === "cookie" && plannedRace === null && plannedComparison === null
10965
+ ? modeFilingCoverage(store, ref.repo, who.name, now)
10966
+ : null;
10967
+ const before = store.getScope(taskId);
10968
+ const proposed = proposeGuarded(store, {
10969
+ taskId, author: who.name,
10970
+ goal: body.has("goal-brief") ? `${(body.get("goal") ?? "").trim()}${body.get("goal-brief")}` : body.get("goal") ?? "",
10971
+ outOfScope: body.get("not") ?? null,
10972
+ touches: (body.get("touches") ?? "").split(/[\n,]/),
10973
+ // The approval sheet's in-place editor sends Done when line by line.
10974
+ acceptance: inPlace
10975
+ ? requirementsFromEditor(body, before?.acceptance ?? [])
10976
+ : acceptanceLinesToInput((body.get("acceptance") ?? "").split("\n")),
10977
+ permissionMode,
10978
+ qualityMode,
10979
+ ...(exactBudget !== undefined
10980
+ ? exactBudget === null ? {} : { budgetMicrousd: exactBudget }
10981
+ : budgetUsd !== null
10982
+ ? { budgetMicrousd: Math.round(budgetUsd * 1_000_000) }
10983
+ : coverage?.defaultBudgetMicrousd != null
10984
+ ? { budgetMicrousd: coverage.defaultBudgetMicrousd }
10985
+ : {}),
10986
+ ...(coverage?.escalated === true ? { posture: "escalated" } : {}),
10987
+ sawDigest: sawDigest === null || sawDigest === "" ? null : sawDigest,
10988
+ taskRef: ref.id,
10989
+ now,
10990
+ });
10991
+ if (!proposed.ok) {
10992
+ return {
10993
+ ok: false,
10994
+ status: proposed.reason === "changed" || proposed.reason === "claimed" ? 409 : 400,
10995
+ message: proposed.message ?? (proposed.reason === "acceptance-required" ? "Not saved: add at least one requirement under Done when." : `scope not saved: ${proposed.reason}`),
10996
+ };
10997
+ }
10998
+ // Editing the plan in place never changes what the agent may do:
10999
+ // a save that would (a legacy setting, a newer policy or mode)
11000
+ // rolls back whole.
11001
+ if (inPlace && before?.profile != null && proposed.scope.profile != null && levelOfProfile(before.profile) !== levelOfProfile(proposed.scope.profile)) {
11002
+ throw new PermissionsWouldChange();
11003
+ }
11004
+ if (coverage !== null && ref.plan === "requested") {
11005
+ authorizePlanUnderMode(store, taskId, who.name, now);
11006
+ }
11007
+ else if (coverage !== null) {
11008
+ const filedScope = store.getScope(taskId);
11009
+ if (filedScope?.profileState === "resolved") {
11010
+ const sealedUnderMode = store.sealScopeApproval(taskId, who.name, now, {}, { kind: "mode", modeDigest: coverage.digest });
11011
+ // The quarantine speaks at the caller (review finding 7).
11012
+ if (!sealedUnderMode) {
11013
+ return { ok: false, status: 200, message: "scope saved — coordinator-filed: mode coverage cannot admit it; sign the scope" };
11014
+ }
10855
11015
  }
10856
11016
  }
10857
- }
10858
- if (plannedComparison !== null && plannedComparison.ok) {
11017
+ if (plannedComparison !== null && plannedComparison.ok) {
11018
+ store.fileTournamentTerms({
11019
+ taskRef: ref.id,
11020
+ kind: "comparison",
11021
+ raceDigest: plannedComparison.plan.comparisonDigest,
11022
+ agents: plannedComparison.plan.agents,
11023
+ perAgentBudgetMicrousd: 0,
11024
+ overrunReserveMicrousd: 0,
11025
+ totalBudgetMicrousd: 0,
11026
+ priceVersion: 0,
11027
+ publicationPolicy: plannedComparison.plan.publicationPolicy,
11028
+ }, now);
11029
+ return { ok: true };
11030
+ }
11031
+ if (plannedRace === null) {
11032
+ // Switching back to "one agent" withdraws a standing race OR
11033
+ // comparison — the deactivated row survives as history, and
11034
+ // the approval card returns to the scope alone.
11035
+ store.retractTournamentTerms(ref.id);
11036
+ return { ok: true };
11037
+ }
10859
11038
  store.fileTournamentTerms({
10860
11039
  taskRef: ref.id,
10861
- kind: "comparison",
10862
- raceDigest: plannedComparison.plan.comparisonDigest,
10863
- agents: plannedComparison.plan.agents,
10864
- perAgentBudgetMicrousd: 0,
10865
- overrunReserveMicrousd: 0,
10866
- totalBudgetMicrousd: 0,
10867
- priceVersion: 0,
10868
- publicationPolicy: plannedComparison.plan.publicationPolicy,
11040
+ raceDigest: plannedRace.raceDigest,
11041
+ agents: plannedRace.agents,
11042
+ perAgentBudgetMicrousd: plannedRace.perAgentBudgetMicrousd,
11043
+ overrunReserveMicrousd: plannedRace.overrunReserveMicrousd,
11044
+ totalBudgetMicrousd: plannedRace.totalBudgetMicrousd,
11045
+ priceVersion: plannedRace.priceVersion,
11046
+ publicationPolicy: plannedRace.publicationPolicy,
10869
11047
  }, now);
10870
11048
  return { ok: true };
10871
- }
10872
- if (plannedRace === null) {
10873
- // Switching back to "one agent" withdraws a standing race OR
10874
- // comparison — the deactivated row survives as history, and
10875
- // the approval card returns to the scope alone.
10876
- store.retractTournamentTerms(ref.id);
10877
- return { ok: true };
10878
- }
10879
- store.fileTournamentTerms({
10880
- taskRef: ref.id,
10881
- raceDigest: plannedRace.raceDigest,
10882
- agents: plannedRace.agents,
10883
- perAgentBudgetMicrousd: plannedRace.perAgentBudgetMicrousd,
10884
- overrunReserveMicrousd: plannedRace.overrunReserveMicrousd,
10885
- totalBudgetMicrousd: plannedRace.totalBudgetMicrousd,
10886
- priceVersion: plannedRace.priceVersion,
10887
- publicationPolicy: plannedRace.publicationPolicy,
10888
- }, now);
10889
- return { ok: true };
10890
- });
10891
- if (!saved.ok)
10892
- return taskScreen(response, who, taskId, saved.message, saved.status, body);
11049
+ });
11050
+ }
11051
+ catch (error) {
11052
+ if (!(error instanceof PermissionsWouldChange))
11053
+ throw error;
11054
+ saved = { ok: false, status: 409, message: "Not saved: this would change what the agent may do. Change permissions in Details, under edit the scope." };
11055
+ }
11056
+ if (!saved.ok) {
11057
+ // Refused because the plan moved on: the draft comes back with the version now on file, so saving it
11058
+ // again (having read the reason) can succeed instead of being refused the same way.
11059
+ const draft = new URLSearchParams(body);
11060
+ if (saved.status === 409)
11061
+ draft.set("sawDigest", store.getScope(taskId)?.digest ?? "");
11062
+ return taskScreen(response, who, taskId, saved.message, saved.status, draft);
11063
+ }
10893
11064
  return redirect(response, taskHref(taskId));
10894
11065
  }
10895
11066
  case "approve": {
@@ -11003,6 +11174,14 @@ export function createDecisionServer(options) {
11003
11174
  }
11004
11175
  note = validated.note;
11005
11176
  }
11177
+ // A failed task's result is accepted only anyway, on purpose: with a reason.
11178
+ if (note === null && store.getTask(taskId)?.state === "failed") {
11179
+ // Posted from the result page: back there, where the reason field is, with the refusal said on it.
11180
+ const back = body.get("return") === null ? null : safeReturn(body.get("return"));
11181
+ if (back !== null && back.startsWith("/review?"))
11182
+ return redirect(response, withRefusal(back, "reason"));
11183
+ return taskScreen(response, who, taskId, ACCEPT_ANYWAY_NEEDS_REASON, 400);
11184
+ }
11006
11185
  store.acceptProof(latest.id, verifiedAuthor(who.name), note, now);
11007
11186
  return redirect(response, body.get("return") === null ? taskHref(taskId) : safeReturn(body.get("return")));
11008
11187
  }
@@ -11018,6 +11197,23 @@ export function createDecisionServer(options) {
11018
11197
  if (current === null || current.receipt?.taskId !== taskId || !/^[0-9]{1,15}$/.test(namedRun) || current.receipt.runId !== Number(namedRun)) {
11019
11198
  return taskScreen(response, who, taskId, "This result changed. Open the current result before marking it complete.", 409);
11020
11199
  }
11200
+ if (body.get("accept") === "1") {
11201
+ // Accept and finish: the person's acceptance and the completion of what it accepted, in one request
11202
+ // and one transaction. The receipt named is the one read, before accepting.
11203
+ const rawNote = (body.get("note") ?? "").trim();
11204
+ let note = null;
11205
+ if (rawNote !== "") {
11206
+ const validated = validateNote(rawNote);
11207
+ if (!validated.ok)
11208
+ return taskScreen(response, who, taskId, validated.problem, 400);
11209
+ note = validated.note;
11210
+ }
11211
+ const publish = body.get("publish") === "1";
11212
+ const finished = acceptAndCompleteAsOperator(store, taskId, { runId: Number(namedRun), receiptDigest: digest, note }, principal, now, evidenceRoot, publish ? accepted => completeAndOpenPullRequest(store, { taskId, digest: accepted, runId: Number(namedRun), who: principal, root: evidenceRoot }, now) : undefined);
11213
+ if (!finished.ok)
11214
+ return taskScreen(response, who, taskId, finished.message, finished.reason === "needs-reason" ? 400 : 409);
11215
+ return redirect(response, publish ? `${taskHref(familyOf(taskId)?.root.id ?? taskId)}#merge` : body.get("return") === null ? reviewHref(taskId, Number(namedRun)) : safeReturn(body.get("return")));
11216
+ }
11021
11217
  if (body.get("publish") === "1") {
11022
11218
  // "Complete and open a pull request": the same exact-receipt completion, plus the owed PR for this
11023
11219
  // exact commit in one transaction. The watch process pushes and opens it; the task shows its progress.
@@ -11438,6 +11634,36 @@ export function createDecisionServer(options) {
11438
11634
  const run = store.runsFor(ref.id).find(one => (one.outcome === "built" || one.outcome === "no-change") &&
11439
11635
  one.finishedAt !== null &&
11440
11636
  (one.role === "builder" || one.role === "scout")) ?? null;
11637
+ return resultRowOf(task, ref.repo, family.problem, run);
11638
+ }
11639
+ /**
11640
+ * A build attempt that didn't deliver a result (failed, refused, stopped,
11641
+ * never finished) still has a result page: its changes, its checks and what
11642
+ * went wrong. The row for one: the run named by `runId` when it is an ended
11643
+ * builder or scout attempt of this task that isn't a delivered result, or a
11644
+ * failed task's latest attempt; with no run named, that latest attempt. The
11645
+ * same admission as a completed row: in this project (or none) and inside
11646
+ * the ceiling. Null otherwise, which reads like a task that never existed.
11647
+ */
11648
+ function attemptRowFor(taskId, runId, project) {
11649
+ const family = familyOf(taskId);
11650
+ const task = store.getTask(taskId);
11651
+ const ref = store.lookupRef(taskId);
11652
+ if (family === null || task === null || ref === null)
11653
+ return null;
11654
+ if (!(project === null || ref.repo === null || ref.repo === project) || !visible(ref.repo))
11655
+ return null;
11656
+ const attempts = store.runsFor(ref.id).filter(one => (one.role === "builder" || one.role === "scout") && (one.finishedAt !== null || one.outcome !== null));
11657
+ const latest = task.state === "failed" ? latestFinishedAttempt(attempts) : null;
11658
+ const run = runId !== null ? attempts.find(one => one.id === runId) ?? null : latest;
11659
+ // A delivered result has its completed row; a failed task's latest attempt is what failed, whatever it ended as.
11660
+ if (run === null || runIsLive(run) || ((run.outcome === "built" || run.outcome === "no-change") && run.id !== latest?.id))
11661
+ return null;
11662
+ return resultRowOf(task, ref.repo, family.problem, run);
11663
+ }
11664
+ function resultRowOf(task, repo, historyProblem, run) {
11665
+ const ref = { repo };
11666
+ const family = { problem: historyProblem };
11441
11667
  const publication = run === null ? null : store.publicationForRun(run.id);
11442
11668
  const verdict = run === null ? null : store.proofVerdictFor(run.id);
11443
11669
  return {
@@ -11628,6 +11854,7 @@ export function createDecisionServer(options) {
11628
11854
  intent,
11629
11855
  plan,
11630
11856
  contest: contestOf(),
11857
+ ...(store.isDemo() ? { demo: true } : {}),
11631
11858
  };
11632
11859
  if (run === null) {
11633
11860
  return { ...base, run: null, detail: null, reviewRetry: null, review: null, notes: [] };
@@ -11949,8 +12176,8 @@ export function editorFileHref(worktree, path, line) {
11949
12176
  function matrixStateBadge(state) {
11950
12177
  // Red belongs to a Failed headline alone (task-status.ts); an unmet requirement is an amber note.
11951
12178
  const cls = state === "pass" ? "badge-done" : state === "failed" ? "badge-note" : state === "missing" ? "badge-note" : "badge-manual-review";
11952
- const word = state === "pass" ? "pass" : state === "missing" ? "missing" : state === "failed" ? "failed" : "manual review";
11953
- return `<span class="badge ${cls}" data-matrix-state="${escape(state)}">${escape(word)}</span>`;
12179
+ // The same words as the Checks tab and the card's Requirements row (task-status.ts).
12180
+ return `<span class="badge ${cls}" data-matrix-state="${escape(state)}">${escape(requirementWordOf({ state }))}</span>`;
11954
12181
  }
11955
12182
  /** v40: the bounded repair chain's own line — presented as the user-facing
11956
12183
  * recovery it represents, without exposing the internal workflow name.
@@ -12051,8 +12278,9 @@ function criterionMatrixHtml(matrix, options = {}) {
12051
12278
  const awaitingAssessment = row.assessment?.evidenceState === "pass" && row.review === null;
12052
12279
  const confirmed = row.assessment !== undefined && state === "pass";
12053
12280
  // Plain words (task-status brief): the retired assessment step is not a state a person acts on.
12054
- const label = awaitingAssessment ? "Met" : confirmed ? "Confirmed" : state === "pass" ? "Met" : state === "manual-review" ? "You check" : state === "missing" ? "Not shown yet" : "Not met";
12055
- const cls = awaitingAssessment ? "badge-done" : state === "pass" ? "badge-done" : state === "manual-review" ? "badge-manual-review" : "badge-note";
12281
+ // The card's Requirements row reads the same words from the same source (task-status.ts), so they agree.
12282
+ const label = requirementWordOf(row, options.verdict);
12283
+ const cls = label === "Met" ? "badge-done" : label === "You check" ? "badge-manual-review" : "badge-note";
12056
12284
  const warnings = [];
12057
12285
  // Only replace the known boilerplate. Other recorded failure details
12058
12286
  // remain verbatim, so concision cannot hide a different problem.
@@ -12122,10 +12350,10 @@ function planContractHtml(view, mode) {
12122
12350
  if ("problem" in view) {
12123
12351
  return `<div class="contract-panel contract-problem"><p class="approval-label">filed contract</p><p class="meta">${escape(view.problem)} · <a href="/r/${view.run}">run ${view.run}</a></p></div>`;
12124
12352
  }
12125
- const stale = view.current ? "" : `<p class="meta">the scope was edited after this draft landed — the record below describes the draft as the planner proposed it</p>`;
12353
+ const stale = view.current ? "" : `<p class="meta">The scope was edited after this draft landed — the record below describes the draft as the planner proposed it</p>`;
12126
12354
  if (!view.filed) {
12127
12355
  return mode === "full"
12128
- ? `<div class="contract-panel contract-drafted"><p class="approval-label">filed contract</p><p class="meta">no scope was filed before planning — the planner drafted this contract from the title and the repository; review every term as new</p>${stale}</div>`
12356
+ ? `<div class="contract-panel contract-drafted"><p class="approval-label">filed contract</p><p class="meta">No scope was filed before planning — the planner drafted this contract from the title and the repository; review every term as new</p>${stale}</div>`
12129
12357
  : `<p class="meta contract-note">no scope was filed before planning — every term above is the planner's proposal</p>`;
12130
12358
  }
12131
12359
  if (view.revision === true) {
@@ -12135,19 +12363,19 @@ function planContractHtml(view, mode) {
12135
12363
  }
12136
12364
  return (`<div class="contract-panel contract-amended"${mode === "full" ? ` id="contract-amendment"` : ""}><p class="approval-label">updated plan</p>` +
12137
12365
  `<p><strong>${view.changes.length} change${view.changes.length === 1 ? "" : "s"} from your notes</strong> <span class="meta">— approving accepts the updated terms ${mode === "full" ? "in the scope" : "above"} · <a href="/r/${view.run}">run ${view.run}</a></span></p>` +
12138
- (view.amendment === null ? "" : `<p class="recap contract-reason"><strong>why:</strong> ${escape(view.amendment)}</p>`) +
12366
+ (view.amendment === null ? "" : `<p class="recap contract-reason"><strong>Why:</strong> ${escape(view.amendment)}</p>`) +
12139
12367
  `<ul class="recap contract-changes">${view.changes.map(contractChangeHtml).join("")}</ul>` +
12140
12368
  stale +
12141
12369
  `</div>`);
12142
12370
  }
12143
12371
  if (view.changes.length === 0) {
12144
- return `<div class="contract-panel contract-preserved"><p class="approval-label">filed contract</p><p><strong>preserved exactly</strong> <span class="meta">the plan reproduces the filed goal, exclusions, touches, and acceptance criteria — approving binds the terms you filed · <a href="/r/${view.run}">run ${view.run}</a></span></p>${stale}</div>`;
12372
+ return `<div class="contract-panel contract-preserved"><p class="approval-label">filed contract</p><p><strong>Preserved exactly</strong> <span class="meta">the plan reproduces the filed goal, exclusions, touches, and acceptance criteria — approving binds the terms you filed · <a href="/r/${view.run}">run ${view.run}</a></span></p>${stale}</div>`;
12145
12373
  }
12146
12374
  return (`<div class="contract-panel contract-amended"${mode === "full" ? ` id="contract-amendment"` : ""}><p class="approval-label">filed contract · amendment proposed</p>` +
12147
12375
  `<p><strong>${view.changes.length} change${view.changes.length === 1 ? "" : "s"} to what you filed</strong> <span class="meta">— approving binds the AMENDED terms shown ${mode === "full" ? "in the scope" : "above"}, not the ones you filed · <a href="/r/${view.run}">run ${view.run}</a></span></p>` +
12148
12376
  (view.amendment === null
12149
- ? `<p class="meta">the planner stated no reason for the amendment</p>`
12150
- : `<p class="recap contract-reason"><strong>why:</strong> ${escape(view.amendment)}</p>`) +
12377
+ ? `<p class="meta">The planner stated no reason for the amendment</p>`
12378
+ : `<p class="recap contract-reason"><strong>Why:</strong> ${escape(view.amendment)}</p>`) +
12151
12379
  `<ul class="recap contract-changes">${view.changes.map(contractChangeHtml).join("")}</ul>` +
12152
12380
  stale +
12153
12381
  `</div>`);
@@ -12176,7 +12404,7 @@ function approvalPathsHtml(touches) {
12176
12404
  function acceptanceCeremonyHtml(criteria) {
12177
12405
  if (criteria.length === 0)
12178
12406
  return "";
12179
- return (`<p class="meta">acceptance</p><ul class="recap acceptance-rubric">` +
12407
+ return (`<p class="meta">Acceptance</p><ul class="recap acceptance-rubric">` +
12180
12408
  criteria
12181
12409
  .map(c => `<li><code>${escape(c.id)}</code> ${escape(c.statement)} <span class="meta">[requires: ${c.evidence.map(escape).join(", ")}]</span></li>`)
12182
12410
  .join("") +
@@ -12294,10 +12522,20 @@ function approvalSheetHtml(input) {
12294
12522
  const planner = legOf("plan");
12295
12523
  if (planner !== undefined)
12296
12524
  who.push(`Planner ${agentNameWords(planner.provider, planner.model)}`);
12297
- if (scope.budgetMicrousd !== null)
12298
- who.push(`Cap ${money(scope.budgetMicrousd)} per attempt`);
12299
- if (permission === "Full access")
12300
- who.push("Full access");
12525
+ // What the yes allows, in plain words, right above it: the agent's
12526
+ // permissions, the per-attempt limit, and an earlier version still running.
12527
+ const allowing = [];
12528
+ const allowed = permissionPlainWordsOf(scope.profile);
12529
+ if (allowed !== null)
12530
+ allowing.push(allowed);
12531
+ if (raceTerms === null)
12532
+ allowing.push(scope.budgetMicrousd === null ? "no attempt limit" : `up to ${money(scope.budgetMicrousd)} per attempt`);
12533
+ const earlier = earlierVersionsWords(input.earlier.active, input.earlier.running);
12534
+ if (earlier !== null)
12535
+ allowing.push(earlier);
12536
+ const yoursToCheck = scope.acceptance.filter(one => one.evidence.includes("manual-review")).map(one => one.statement.trim().replace(/[.\s]+$/, ""));
12537
+ const consent = (allowing.length === 0 ? "" : `<p class="approval-allowing" data-approval-allowing>You’re allowing: ${allowing.map(escape).join(" · ")}</p>`) +
12538
+ (yoursToCheck.length === 0 ? "" : `<p class="approval-you-check" data-approval-you-check>You’ll check: ${yoursToCheck.map(escape).join("; ")}</p>`);
12301
12539
  const race = raceTerms === null
12302
12540
  ? ""
12303
12541
  : `<p class="approval-race">${raceTerms.n} agents build this separately: ${raceTerms.agents.map(one => escape(agentNameWords(one.provider, one.model))).join(" and ")}. ` +
@@ -12324,7 +12562,7 @@ function approvalSheetHtml(input) {
12324
12562
  : !contract.filed && contract.revision !== true ? `<p class="meta">Nothing was filed before planning, so every term is the planner's proposal.</p>`
12325
12563
  : `<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>`;
12326
12564
  const criteriaDetail = scope.acceptance.length === 0 ? ""
12327
- : `<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>`;
12565
+ : `<ul class="approval-signed-criteria">${scope.acceptance.map(one => `<li><code>${escape(one.id)}</code> ${escape(one.statement)} <span class="meta">shown by ${one.evidence.map(kind => escape(EVIDENCE_PLAIN[kind] ?? kind)).join(", ")}</span></li>`).join("")}</ul>`;
12328
12566
  const projection = route?.projection ?? null;
12329
12567
  const agentsDetail = route === null || route === undefined ? ""
12330
12568
  : projection === null
@@ -12338,7 +12576,7 @@ function approvalSheetHtml(input) {
12338
12576
  const limits = profileWords(scope) +
12339
12577
  `<p class="meta">Checks level: ${escape(qualityModeTitle(scope.qualityMode ?? "default"))}${permission === null ? "" : ` · ${escape(permission)}`}</p>` +
12340
12578
  (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>`);
12341
- const details = `<details class="approval-details"><summary>Details</summary>` +
12579
+ const details = `<details class="approval-details"><summary>Plan details</summary>` +
12342
12580
  revisionDetail +
12343
12581
  (repairText === null ? "" : detail("Signed goal", `<p>${escape(scope.goal)}</p>`)) +
12344
12582
  detail("Plan record", contractDetail) +
@@ -12348,6 +12586,53 @@ function approvalSheetHtml(input) {
12348
12586
  (scope.candidate ? detail("Saved commit", `<p><code class="approval-commit">${escape(scope.candidate)}</code></p>`) : "") +
12349
12587
  detail("Seal", `<p class="meta">Approval binds to this exact wording. <span class="seal mono">signs ${shortDigest(scope.digest)}</span></p>`) +
12350
12588
  `</details>`;
12589
+ const passwordNote = input.surface === "task" ? "approval-password-note" : "chat-approval-password-note";
12590
+ // Edit plan, in place (task page): the rows become fields and save
12591
+ // through the scope's own route. The fields belong to a form after this
12592
+ // one (forms never nest), so Approve never carries them.
12593
+ const edit = input.edit;
12594
+ const draft = edit?.draft ?? null;
12595
+ const editForm = "plan-editor-form";
12596
+ // Each field opens tall enough for what it holds (up to ten lines).
12597
+ const field = (name, value, rows, label, hint = "") => `<label class="approval-field"><span class="approval-field-label">${label}</span>${hint === "" ? "" : `<span class="approval-field-hint">${hint}</span>`}` +
12598
+ `<textarea name="${name}" rows="${Math.min(10, Math.max(rows, value.split("\n").reduce((sum, line) => sum + Math.max(1, Math.ceil(line.length / 80)), 0)))}" form="${editForm}">${escape(value)}</textarea></label>`;
12599
+ // Done when, one line per requirement in the signed order; the server
12600
+ // matches them back by position (the digest guard keeps that order).
12601
+ const requirement = (name, value, label, placeholder = "") => `<li><input type="text" name="${name}" value="${escape(value)}" aria-label="${label}"${placeholder === "" ? "" : ` placeholder="${placeholder}"`} form="${editForm}"></li>`;
12602
+ const requirements = draft?.getAll("requirement") ?? scope.acceptance.map(one => one.statement);
12603
+ const added = draft?.get("requirement-new") ?? "";
12604
+ const editor = edit === null ? null :
12605
+ `<details class="approval-edit" id="plan-editor"${draft === null && !edit.open ? "" : " open"}>` +
12606
+ `<summary class="approval-link"><span class="approval-edit-open">Edit plan</span><span class="approval-edit-close">Cancel</span></summary>` +
12607
+ `<div class="approval-editor">` +
12608
+ (edit.problem === null ? "" : `<p class="problem" role="alert">${escape(edit.problem)}</p>`) +
12609
+ field("goal", draft?.get("goal") ?? goalShown, 3, "Goal") +
12610
+ field("touches", draft?.get("touches") ?? scope.touches.join("\n"), 2, "Changes", "Only in these paths, one per line. Leave empty for any file.") +
12611
+ field("not", draft?.get("not") ?? scope.outOfScope ?? "", 2, "Won’t touch") +
12612
+ `<fieldset class="approval-field"><legend class="approval-field-label">Done when</legend>` +
12613
+ `<span class="approval-field-hint">Clear a line to drop it.</span><ul class="approval-requirements">` +
12614
+ requirements.map((one, index) => requirement("requirement", one, `Requirement ${index + 1}`)).join("") +
12615
+ requirement("requirement-new", added, "Add a requirement", "Add a requirement") +
12616
+ `</ul></fieldset>` +
12617
+ `<div class="approval-edit-act"><button type="submit" form="${editForm}">Save plan</button>` +
12618
+ // A plan with written steps keeps its own editor for them.
12619
+ (edit.stepsHref === null ? "" : `<a class="approval-link" href="${escape(edit.stepsHref)}">Edit steps</a>`) + `</div>` +
12620
+ `</div></details>`;
12621
+ const keptMode = permissionModeOfProfile(scope.profile);
12622
+ const editorForm = edit === null ? "" :
12623
+ `<form method="post" action="${edit.action}" id="${editForm}" class="approval-editor-form">` +
12624
+ `<input type="hidden" name="csrf" value="${escape(input.csrf)}">` +
12625
+ // A refused save keeps the version its draft was edited from, so a
12626
+ // plan that changed meanwhile is never silently overwritten.
12627
+ `<input type="hidden" name="sawDigest" value="${escape(draft?.get("sawDigest") ?? scope.digest)}">` +
12628
+ // The terms the editor doesn't show ride along unchanged: a repair's
12629
+ // brief after its goal (it stays in Details), permissions, checks, cap.
12630
+ (repairText === null ? "" : `<input type="hidden" name="goal-brief" value="${escape(repairText[0])}">`) +
12631
+ (keptMode === null ? "" : `<input type="hidden" name="permission-mode" value="${keptMode}">`) +
12632
+ `<input type="hidden" name="quality-mode" value="${escape(scope.qualityMode ?? "default")}">` +
12633
+ // The cap rides in exact millionths ("none" keeps no limit).
12634
+ `<input type="hidden" name="budget-microusd" value="${scope.budgetMicrousd === null ? "none" : String(scope.budgetMicrousd)}">` +
12635
+ `</form>`;
12351
12636
  return (`<form method="post" action="${input.action}" class="approve-form approval-sheet"${input.surface === "task" ? ` id="approve"` : ""}${input.sticky ? " data-sticky" : ""}>` +
12352
12637
  `<input type="hidden" name="csrf" value="${escape(input.csrf)}">` +
12353
12638
  `<input type="hidden" name="nonce" value="${escape(input.nonce)}">` +
@@ -12367,13 +12652,75 @@ function approvalSheetHtml(input) {
12367
12652
  approvalAmendmentHtml(contract) +
12368
12653
  race +
12369
12654
  (who.length === 0 ? "" : `<p class="approval-who">${who.map(escape).join(" · ")}</p>`) +
12655
+ consent +
12370
12656
  `<div class="approval-act" id="${input.surface === "task" ? "approval-confirm" : "chat-approval-confirm"}">` +
12371
- `<label class="approval-password"><span class="visually-hidden">Your password</span><input type="password" name="token" autocomplete="current-password" placeholder="Your password"></label>` +
12657
+ `<label class="approval-password"><span class="visually-hidden">Your password</span><input type="password" name="token" autocomplete="current-password" placeholder="Your password" aria-describedby="${passwordNote}"></label>` +
12658
+ `<p class="approval-password-note" id="${passwordNote}">Your password signs this approval.</p>` +
12372
12659
  `<button type="submit" data-primary-action>${submit}</button></div>` +
12373
12660
  `<p class="approval-after">${after}</p>` +
12374
- `<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>` +
12661
+ `<div class="approval-secondary">${editor ?? `<a class="approval-link" href="${escape(input.editHref)}">Edit plan</a>`}<a class="approval-link" href="${escape(input.notNowHref)}">Not now</a></div>` +
12375
12662
  details +
12376
- `</form>`);
12663
+ `</form>` +
12664
+ editorForm);
12665
+ }
12666
+ /** Earlier versions of a task still in flight, in plain words: queued
12667
+ * while they only wait, running once one has started; null when none. */
12668
+ export function earlierVersionsWords(active, running) {
12669
+ if (active <= 0)
12670
+ return null;
12671
+ const still = running >= active ? "running" : running <= 0 ? "queued" : "queued or running";
12672
+ return active === 1 ? `an earlier version is still ${still}` : `${active} earlier versions are still ${still}`;
12673
+ }
12674
+ /** An in-place plan edit that would change the agent's permissions: thrown to roll the save back. */
12675
+ class PermissionsWouldChange extends Error {
12676
+ }
12677
+ /** Evidence kinds in plain words, for the signed criteria in Details. */
12678
+ const EVIDENCE_PLAIN = { check: "the project check", screenshot: "screenshots", "changed-path": "changed files", "manual-review": "your own check" };
12679
+ /** What an agent may do on its own, in a few plain words. */
12680
+ function permissionPlainWordsOf(profile) {
12681
+ if (profile === null || profile === undefined)
12682
+ return null;
12683
+ const full = "full access, nothing asks first";
12684
+ if (profile.provider === "claude") {
12685
+ return profile.permissionArgv === "bypassPermissions" ? full
12686
+ : profile.permissionArgv === "auto" ? "file edits and routine commands; anything risky stops"
12687
+ : "file edits only; commands are refused";
12688
+ }
12689
+ if (profile.provider === "gemini")
12690
+ return profile.approvalArgv === "yolo" ? full : "file edits only; other tools are refused";
12691
+ return profile.sandboxMode === "danger-full-access" ? full : "file edits and commands inside the project only";
12692
+ }
12693
+ /** The permission choice a sealed profile carries, as the scope form names it. */
12694
+ function permissionModeOfProfile(profile) {
12695
+ if (profile === null || profile === undefined)
12696
+ return null;
12697
+ return profile.provider === "claude"
12698
+ ? profile.permissionArgv === "bypassPermissions" ? "bypassPermissions" : "auto"
12699
+ : profile.provider === "gemini"
12700
+ ? profile.approvalArgv === "yolo" ? "bypassPermissions" : "auto"
12701
+ : profile.sandboxMode === "danger-full-access" ? "bypassPermissions" : "auto";
12702
+ }
12703
+ /** Done when, from the in-place editor: each line keeps its requirement's
12704
+ * id by position, and its evidence and guidance while its words stand; a
12705
+ * rewritten one is yours to check, like an added one; an emptied one is
12706
+ * dropped. */
12707
+ function requirementsFromEditor(body, current) {
12708
+ const used = new Set(current.map(one => one.id));
12709
+ let next = 1;
12710
+ while (used.has(`c${next}`))
12711
+ next++;
12712
+ const plain = (raw) => raw.replace(/\s+/g, " ").trim();
12713
+ const kept = body.getAll("requirement").slice(0, current.length).flatMap((raw, index) => {
12714
+ const statement = plain(raw);
12715
+ const criterion = current[index];
12716
+ if (statement === "")
12717
+ return [];
12718
+ return statement === plain(criterion.statement)
12719
+ ? [{ id: criterion.id, statement: criterion.statement, evidence: [...criterion.evidence], how: criterion.how }]
12720
+ : [{ id: criterion.id, statement, evidence: ["manual-review"], how: null }];
12721
+ });
12722
+ const added = plain(body.get("requirement-new") ?? "");
12723
+ return added === "" ? kept : [...kept, { id: `c${next}`, statement: added, evidence: ["manual-review"], how: null }];
12377
12724
  }
12378
12725
  function consentDoorOf(scope, route) {
12379
12726
  if (scope === null)
@@ -12426,31 +12773,31 @@ function consentClosedHtml(taskId, door, surface) {
12426
12773
  }
12427
12774
  function profileWords(scope) {
12428
12775
  if (scope.profileState === "unresolved") {
12429
- return `<p class="meta"><strong>filed but unapprovable</strong> — ${escape(scope.unresolvedReason ?? "the scope cannot say exactly what would run")}. Restate the scope to fix it.</p>`;
12776
+ return `<p class="meta"><strong>Filed but unapprovable</strong> — ${escape(scope.unresolvedReason ?? "the scope cannot say exactly what would run")}. Restate the scope to fix it.</p>`;
12430
12777
  }
12431
12778
  const profile = scope.profile ?? null;
12432
12779
  if (profile === null) {
12433
12780
  return (scope.digestVersion ?? 1) < 2
12434
- ? `<p class="meta">approved before routing was bound — pinned at upgrade to the configuration of that day</p>`
12781
+ ? `<p class="meta">Approved before routing was bound — pinned at upgrade to the configuration of that day</p>`
12435
12782
  : "";
12436
12783
  }
12437
12784
  const repair = profile.repairModel === "inherit" ? "same model" : profile.repairModel;
12438
12785
  const base = profile.provider === "claude"
12439
- ? `<p class="meta">runs on <span class="mono">claude · ${escape(profile.model)}</span> — ${profile.permissionArgv === "bypassPermissions"
12786
+ ? `<p class="meta">Runs on <span class="mono">claude · ${escape(profile.model)}</span> — ${profile.permissionArgv === "bypassPermissions"
12440
12787
  ? "FULL permissions; claude runs with --dangerously-skip-permissions and nothing asks"
12441
12788
  : profile.permissionArgv === "auto"
12442
12789
  ? "safe unattended permissions; routine project commands and edits proceed, risky acts stop"
12443
- : "legacy acceptEdits; edits proceed, commands that ask are denied unattended"}, ${profile.maxTurns}-turn runaway breaker, ${Math.round(profile.timeoutSeconds / 60)} min ${profile.timeoutKind === "idle" ? "without progress" : "per attempt"}; repairs on ${escape(repair)}, ${profile.repairMaxTurns} turns / ${Math.round(profile.repairTimeoutSeconds / 60)} min</p>`
12790
+ : "legacy acceptEdits; edits proceed, commands that ask are denied unattended"}, stops after ${profile.maxTurns} turns, ${Math.round(profile.timeoutSeconds / 60)} min ${profile.timeoutKind === "idle" ? "without progress" : "per attempt"}; repairs on ${escape(repair)}, ${profile.repairMaxTurns} turns / ${Math.round(profile.repairTimeoutSeconds / 60)} min</p>`
12444
12791
  : profile.provider === "gemini"
12445
- ? `<p class="meta">runs on <span class="mono">gemini · ${escape(profile.model)}</span> — ${profile.approvalArgv === "yolo" ? "Full access via --approval-mode yolo; every tool auto-approved" : "Auto via --approval-mode auto_edit; edits auto-approved, other tools refused"}, no turn limit (${Math.round(profile.timeoutSeconds / 60)} min ${profile.timeoutKind === "idle" ? "without progress" : "per attempt"}), spend reported in tokens only; repairs on ${escape(repair)}, ${Math.round(profile.repairTimeoutSeconds / 60)} min</p>`
12446
- : `<p class="meta">runs on <span class="mono">${escape(profile.provider)} · ${escape(profile.model)}</span> — ${profile.sandboxMode === "danger-full-access" ? "FULL permissions via --dangerously-bypass-approvals-and-sandbox; nothing asks" : "workspace-write sandbox"}, no turn limit (${Math.round(profile.timeoutSeconds / 60)} min ${profile.timeoutKind === "idle" ? "without progress" : "per attempt"}); repairs on ${escape(repair)}, ${Math.round(profile.repairTimeoutSeconds / 60)} min</p>`;
12792
+ ? `<p class="meta">Runs on <span class="mono">gemini · ${escape(profile.model)}</span> — ${profile.approvalArgv === "yolo" ? "Full access via --approval-mode yolo; every tool auto-approved" : "Auto via --approval-mode auto_edit; edits auto-approved, other tools refused"}, no turn limit (${Math.round(profile.timeoutSeconds / 60)} min ${profile.timeoutKind === "idle" ? "without progress" : "per attempt"}), spend reported in tokens only; repairs on ${escape(repair)}, ${Math.round(profile.repairTimeoutSeconds / 60)} min</p>`
12793
+ : `<p class="meta">Runs on <span class="mono">${escape(profile.provider)} · ${escape(profile.model)}</span> — ${profile.sandboxMode === "danger-full-access" ? "FULL permissions via --dangerously-bypass-approvals-and-sandbox; nothing asks" : "workspace-write sandbox"}, no turn limit (${Math.round(profile.timeoutSeconds / 60)} min ${profile.timeoutKind === "idle" ? "without progress" : "per attempt"}); repairs on ${escape(repair)}, ${Math.round(profile.repairTimeoutSeconds / 60)} min</p>`;
12447
12794
  // The fallback chain rides EVERY surface these words sign (F+G review,
12448
12795
  // finding 2): the digest binds the whole chain, so the password form —
12449
12796
  // task page and /next alike — states every entry, credential included.
12450
12797
  const chain = chainFromJson(scope.proposedChainJson ?? null);
12451
12798
  const chainLine = chain === null || chain.length < 2
12452
12799
  ? ""
12453
- : `<p class="meta">if its subscription runs out: ${chain
12800
+ : `<p class="meta">If its subscription runs out: ${chain
12454
12801
  .slice(1)
12455
12802
  .map(one => `falls back to <span class="mono">${escape(one.profile.provider)} · ${escape(one.profile.model)}</span> — ${one.authMode === "api-key" ? "your API key; spend moves to that account" : "its subscription login"}`)
12456
12803
  .join("; ")}</p>`;
@@ -12479,9 +12826,9 @@ function agentsSummaryWords(view) {
12479
12826
  /** Neutral metadata badges: risk, posture, and standing. */
12480
12827
  function agentsBadgesHtml(view) {
12481
12828
  const p = view.projection;
12482
- return (`<span class="badge">${escape(riskTitle(view.riskLevel))}</span>` +
12483
- (p === null ? "" : `<span class="badge">${escape(p.postureWords)}</span>`) +
12484
- `<span class="badge">${escape(agentsStandingWords(view))}</span>`);
12829
+ return (`<span class="badge">${escape(sentenceCase(riskTitle(view.riskLevel)))}</span>` +
12830
+ (p === null ? "" : `<span class="badge">${escape(sentenceCase(p.postureWords))}</span>`) +
12831
+ `<span class="badge">${escape(sentenceCase(agentsStandingWords(view)))}</span>`);
12485
12832
  }
12486
12833
  /** Volatile availability, per provider on the route — shown beside the
12487
12834
  * agents, never inside the approval's terms. */
@@ -12506,7 +12853,7 @@ function agentsWhyHtml(projection, summaryLabel = "Why these agents") {
12506
12853
  .map(leg => {
12507
12854
  const chosen = chosenWords(leg);
12508
12855
  return (`<dt>${escape(ROLE_NOUN[leg.phase])}</dt>` +
12509
- `<dd><span class="mono">${escape(leg.provider)} · ${escape(leg.model)}</span> <span class="badge">${escape(chosen)}</span>` +
12856
+ `<dd><span class="mono">${escape(leg.provider)} · ${escape(leg.model)}</span> <span class="badge">${escape(sentenceCase(chosen))}</span>` +
12510
12857
  `<ul class="agents-reasons">${leg.reasons.map(reason => `<li>${escape(reason)}</li>`).join("")}${leg.problem === null ? "" : `<li><strong>${escape(leg.problem)}</strong></li>`}</ul></dd>`);
12511
12858
  })
12512
12859
  .join("") +
@@ -12527,7 +12874,7 @@ function agentsCeremonyHtml(view) {
12527
12874
  }
12528
12875
  return (`<div class="agents-ceremony"><p class="approval-label">agents</p>` +
12529
12876
  `<p class="agents-summary">${escape(view.projection.summary)}</p>` +
12530
- `<div class="agents-badges"><span class="badge">${escape(riskTitle(view.riskLevel))}</span><span class="badge">${escape(view.projection.postureWords)}</span></div>` +
12877
+ `<div class="agents-badges"><span class="badge">${escape(sentenceCase(riskTitle(view.riskLevel)))}</span><span class="badge">${escape(sentenceCase(view.projection.postureWords))}</span></div>` +
12531
12878
  `<p class="meta agents-risk-line">${escape(riskTitle(view.riskLevel))}: ${escape(riskConsequence(view.riskLevel))}.</p>` +
12532
12879
  agentsWhyHtml(view.projection) +
12533
12880
  `<p class="meta">These exact agents are part of what you approve; changing any of them asks for a fresh approval.</p></div>`);
@@ -12554,7 +12901,7 @@ function routineAgentsHtml(routine, approved, ceremony = false) {
12554
12901
  const projection = projectRoute(route, () => null);
12555
12902
  return (`<div class="agents-ceremony"><p class="approval-label">agents</p>` +
12556
12903
  `<p class="agents-summary">${escape(agentsSummary(route))}</p>` +
12557
- `<div class="agents-badges"><span class="badge">${escape(riskTitle(route.risk))}</span><span class="badge">${escape(postureWords(route))}</span><span class="badge">${approved ? "frozen by the approval" : "frozen when you approve"}</span></div>` +
12904
+ `<div class="agents-badges"><span class="badge">${escape(sentenceCase(riskTitle(route.risk)))}</span><span class="badge">${escape(sentenceCase(postureWords(route)))}</span><span class="badge">${approved ? "frozen by the approval" : "frozen when you approve"}</span></div>` +
12558
12905
  (ceremony ? agentsWhyHtml(projection) : "") +
12559
12906
  `<p class="meta">${approved ? "Every firing runs on exactly these agents; a configuration change cannot re-route it." : "Approving freezes exactly these agents for every firing; a configuration change afterwards cannot re-route one."}</p></div>`);
12560
12907
  }
@@ -12584,7 +12931,7 @@ function agentsCardHtml(taskId, view, csrf, canEdit) {
12584
12931
  ? ""
12585
12932
  : `<ul class="agents-overrides">${view.overrides
12586
12933
  .map(one => `<li><span>${escape(ROLE_NOUN[one.phase])} → <span class="mono">${escape(one.provider)} · ${escape(one.model)}</span> <span class="meta">by ${escape(one.by)} ${whenTime(one.at)}</span></span>` +
12587
- (canEdit && view.editable ? `<form method="post" action="${taskHref(taskId)}/route" class="agents-clear">${hidden}<input type="hidden" name="clear-phase" value="${escape(one.phase)}"><button type="submit" class="secondary" aria-label="clear the ${escape(ROLE_NOUN[one.phase].toLowerCase())} choice">clear</button></form>` : "") +
12934
+ (canEdit && view.editable ? `<form method="post" action="${taskHref(taskId)}/route" class="agents-clear">${hidden}<input type="hidden" name="clear-phase" value="${escape(one.phase)}"><button type="submit" class="secondary" aria-label="clear the ${escape(ROLE_NOUN[one.phase].toLowerCase())} choice">Clear</button></form>` : "") +
12588
12935
  `</li>`)
12589
12936
  .join("")}</ul>`;
12590
12937
  // The controls (v48): a risk choice that says what each level does, and
@@ -12614,7 +12961,7 @@ function agentsCardHtml(taskId, view, csrf, canEdit) {
12614
12961
  ? `<p class="meta">${escape(view.editableWhy ?? "the agents cannot change right now")}</p>`
12615
12962
  : `<details class="agents-change"><summary>Change agents</summary>` +
12616
12963
  `<form method="post" action="${taskHref(taskId)}/route" class="agents-form-risk">${hidden}` +
12617
- `<label>risk<select name="risk" aria-label="declared risk">${RISK_LEVELS.map(one => `<option value="${one}"${one === view.riskLevel ? " selected" : ""}>${escape(riskTitle(one))}</option>`).join("")}</select></label>` +
12964
+ `<label>Risk<select name="risk" aria-label="declared risk">${RISK_LEVELS.map(one => `<option value="${one}"${one === view.riskLevel ? " selected" : ""}>${escape(riskTitle(one))}</option>`).join("")}</select></label>` +
12618
12965
  `<button type="submit" class="secondary">Set risk</button></form>` +
12619
12966
  riskGuide +
12620
12967
  `<div class="agents-role-forms">${roleForms.join("")}</div>` +
@@ -13937,9 +14284,34 @@ ${THEME_DARK}
13937
14284
  .approval-act input[type=password] { margin: 0; font-family: var(--font-sans); }
13938
14285
  .approval-act button { margin: 0; min-height: 2.125rem; white-space: nowrap; }
13939
14286
  .approval-after { max-width: 75ch; color: var(--muted-foreground); font-size: 13px; line-height: 1.5; }
13940
- .approval-secondary { display: flex; flex-wrap: wrap; gap: 8px; }
14287
+ .approval-allowing, .approval-you-check { max-width: 75ch; font-size: 14px; line-height: 1.55; overflow-wrap: anywhere; }
14288
+ .approval-sheet .approval-allowing + .approval-you-check { margin-top: -10px; }
14289
+ .approval-act > button { grid-column: 2; grid-row: 1; }
14290
+ .approval-password-note { grid-column: 1; grid-row: 2; margin: 0; color: var(--muted-foreground); font-size: 13px; line-height: 1.5; }
14291
+ .approval-secondary { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
14292
+ details.approval-edit, details.approval-edit[open] { margin: 0; padding: 0; border: 0; border-radius: 0; background: none; }
14293
+ /* Edit plan, in place: the rows become fields where they stood; the
14294
+ approval act and Details step aside until it is saved or cancelled. */
14295
+ .approval-sheet { position: relative; }
14296
+ .approval-edit > summary { list-style: none; cursor: pointer; }
14297
+ .approval-edit > summary::-webkit-details-marker { display: none; }
14298
+ .approval-edit-close, .approval-edit[open] .approval-edit-open { display: none; }
14299
+ .approval-edit[open] .approval-edit-close { display: inline; }
14300
+ .approval-edit[open] > summary { position: absolute; top: 14px; right: 20px; }
14301
+ .approval-sheet:has(.approval-edit[open]) > :not(.approval-sheet-title, .approval-secondary) { display: none; }
14302
+ .approval-secondary:has(> .approval-edit[open]) { display: block; }
14303
+ .approval-secondary:has(> .approval-edit[open]) > a { display: none; }
14304
+ .approval-editor { display: grid; gap: 14px; }
14305
+ .approval-editor .problem { margin: 0; }
14306
+ .approval-field { display: grid; gap: 4px; min-width: 0; margin: 0; padding: 0; border: 0; }
14307
+ .approval-field-label { padding: 0; color: var(--muted-foreground); font-size: 13px; font-weight: 500; line-height: 1.6; }
14308
+ .approval-field-hint { color: var(--muted-foreground); font-size: 12px; line-height: 1.5; }
14309
+ .approval-field textarea, .approval-field input[type=text] { width: 100%; max-width: 75ch; margin: 0; font-family: var(--font-sans); font-size: 14px; line-height: 1.55; }
14310
+ .approval-requirements { display: grid; gap: 6px; margin: 2px 0 0; padding: 0; list-style: none; }
14311
+ .approval-edit-act { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
14312
+ .approval-edit-act button { margin: 0; min-height: 2.125rem; white-space: nowrap; }
13941
14313
  .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; }
13942
- .approval-link + .approval-link { border-color: transparent; background: none; color: var(--muted-foreground); }
14314
+ .approval-secondary > .approval-link:not(:first-child) { border-color: transparent; background: none; color: var(--muted-foreground); }
13943
14315
  @media (hover: hover) and (pointer: fine) { .approval-link:hover { background: var(--muted); color: var(--foreground); } }
13944
14316
  details.approval-details { margin: 0; padding: 0; border: 0; border-top: 1px solid var(--border); border-radius: 0; background: none; }
13945
14317
  details.approval-details[open] { padding-bottom: 0; }
@@ -13959,6 +14331,7 @@ ${THEME_DARK}
13959
14331
  .approval-sheet { gap: 12px; padding: 14px; }
13960
14332
  .approval-row { grid-template-columns: 1fr; gap: 2px; }
13961
14333
  .approval-act { grid-template-columns: 1fr; }
14334
+ .approval-act > button, .approval-password-note { grid-column: auto; grid-row: auto; }
13962
14335
  .approval-act input[type=password], .approval-act button { min-height: 44px; font-size: 16px; }
13963
14336
  .approval-act button { width: 100%; }
13964
14337
  /* The one act stays under the thumb: sticky at the bottom, clear of the home indicator. */
@@ -13967,6 +14340,9 @@ ${THEME_DARK}
13967
14340
  border-top: 1px solid var(--border); background: var(--card);
13968
14341
  }
13969
14342
  .approval-link { min-height: 44px; padding: 0 16px; }
14343
+ .approval-edit[open] > summary { top: 6px; right: 6px; }
14344
+ .approval-field textarea, .approval-field input[type=text] { font-size: 16px; }
14345
+ .approval-edit-act button { width: 100%; min-height: 44px; font-size: 16px; }
13970
14346
  .approval-details > summary { min-height: 44px; }
13971
14347
  .approval-roles > div { grid-template-columns: 1fr; gap: 2px; }
13972
14348
  }
@@ -15482,7 +15858,7 @@ const INBOX_TABS_CSS = '.inbox-tabs{display:inline-flex;gap:2px;max-width:100%;o
15482
15858
  '.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}' +
15483
15859
  '.inbox-unread{display:none;position:absolute;top:4px;right:3px;width:6px;height:6px;border-radius:50%;background:var(--so-signal)}' +
15484
15860
  '@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}}';
15485
- 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}');
15861
+ 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 + DISCLOSURE_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}');
15486
15862
  /**
15487
15863
  * The board's liveness: fetch this page's own fragment on a timer and swap
15488
15864
  * it in place — no flicker, no scroll reset, no long-lived stream to manage,
@@ -15737,7 +16113,7 @@ FINISH: unreviewed and undocumented is unfinished; this build ends with the fini
15737
16113
  * absent from sensitive pages. Navigation help in plain words — no key
15738
16114
  * ever posts. */
15739
16115
  const KBD_HELP = `<div class="kbd-help" hidden role="dialog" aria-label="keyboard shortcuts" tabindex="-1">` +
15740
- `<h2>keyboard shortcuts</h2><table>` +
16116
+ `<h2>Keyboard shortcuts</h2><table>` +
15741
16117
  `<tr><td><kbd>/</kbd></td><td>jump to a page or an open task</td></tr>` +
15742
16118
  `<tr><td><kbd>g</kbd> then <kbd>i</kbd></td><td>go to the inbox</td></tr>` +
15743
16119
  `<tr><td><kbd>g</kbd> then <kbd>b</kbd></td><td>go to the board</td></tr>` +
@@ -15812,7 +16188,7 @@ function shell(title, body, options = {}) {
15812
16188
  : `<span class="scope-status">` +
15813
16189
  (peekCounts.waiting > 0 ? `<a class="hot" href="/work?view=needs-you">${peekCounts.waiting} needs you</a>` : `<a href="/work?view=needs-you">0 needs you</a>`) +
15814
16190
  `<a href="/runs">${peekCounts.running} live</a><a href="/board?view=order">${peekCounts.queued} queued</a></span>`;
15815
- const scopeName = effectiveScope === "project" && chrome.project !== null ? escape(projectName(chrome.project)) : "all projects";
16191
+ const scopeName = effectiveScope === "project" && chrome.project !== null ? escape(projectName(chrome.project)) : "All projects";
15816
16192
  // The switcher (board pass): the scope's name opens a menu of every
15817
16193
  // enrolled project — one tap to open one, or to widen to all — as plain
15818
16194
  // POST forms carrying the session's csrf, returning to this screen.
@@ -15827,7 +16203,7 @@ function shell(title, body, options = {}) {
15827
16203
  const allCurrent = effectiveScope !== "project";
15828
16204
  const rows = [
15829
16205
  `<form method="post" action="/projects/select">${hidden}<input type="hidden" name="path" value="">` +
15830
- `<button type="submit"${allCurrent ? ' class="current" aria-current="true"' : ""}>all projects</button></form>`,
16206
+ `<button type="submit"${allCurrent ? ' class="current" aria-current="true"' : ""}>All projects</button></form>`,
15831
16207
  ...(chrome.projects ?? []).map(one => `<form method="post" action="/projects/open">${hidden}<input type="hidden" name="path" value="${escape(one.path)}">` +
15832
16208
  `<button type="submit"${!allCurrent && chrome.project === one.path ? ' class="current" aria-current="true"' : ""}>${escape(one.name)}</button></form>`),
15833
16209
  ];
@@ -15871,10 +16247,10 @@ function shell(title, body, options = {}) {
15871
16247
  primaryItem("flows", "/flows", "Flows"),
15872
16248
  primaryItem("projects", "/projects", "Projects"),
15873
16249
  `</nav>`,
15874
- ...(chrome.active === "code" ? [] : [`<a class="new-task" href="/tasks/new" aria-label="new task">+ new task</a>`]),
16250
+ ...(chrome.active === "code" ? [] : [`<a class="new-task" href="/tasks/new" aria-label="New task">+ New task</a>`]),
15875
16251
  `<nav class="nav-groups">`,
15876
- navGroup("tools", "work tools", workToolRows(chrome.projectScoped, chrome.chat, chrome.code), TOOL_KEYS.has(chrome.active)),
15877
- navGroup("settings", "settings", settingsRows(chrome.projectScoped, chrome.settings), SETTINGS_KEYS.has(chrome.active)),
16252
+ navGroup("tools", "Work tools", workToolRows(chrome.projectScoped, chrome.chat, chrome.code), TOOL_KEYS.has(chrome.active)),
16253
+ navGroup("settings", "Settings", settingsRows(chrome.projectScoped, chrome.settings), SETTINGS_KEYS.has(chrome.active)),
15878
16254
  `</nav>`,
15879
16255
  `<span class="grow"></span>`,
15880
16256
  `</aside>`,
@@ -15883,11 +16259,11 @@ function shell(title, body, options = {}) {
15883
16259
  // must not pass as production (adoption review, finding 8; the FENCE is
15884
16260
  // the refuseDemo gate in operate.ts, this is the honest label).
15885
16261
  const demoBanner = (chrome.demo === true
15886
- ? `<div class="banner"><span class="badge">demo</span>${escape(DEMO_BANNER.replace(/^Demo: /, ""))}</div>`
16262
+ ? `<div class="banner"><span class="badge">Demo</span>${escape(DEMO_BANNER.replace(/^Demo: /, ""))}</div>`
15887
16263
  : "") +
15888
16264
  (chrome.modeBanner === undefined
15889
16265
  ? ""
15890
- : `<div class="banner"><span class="badge badge-running">mode</span>${escape(chrome.modeBanner.words)} \u00b7 <a href="/mode">the terms \u00b7 end it</a></div>`) +
16266
+ : `<div class="banner"><span class="badge badge-running">Mode</span>${escape(chrome.modeBanner.words)} \u00b7 <a href="/mode">the terms \u00b7 end it</a></div>`) +
15891
16267
  (chrome.signIn ?? []).map(one => `<div class="banner sign-in-banner" data-sign-in="${escape(one.provider)}"><strong>${escape(one.title)}</strong> \u00b7 run <code>${escape(one.command)}</code> on this computer, then resume.${one.detail === "" ? "" : ` ${escape(one.detail)}`}` +
15892
16268
  `<form method="post" action="${escape(one.resumeHref)}" class="inline"><input type="hidden" name="csrf" value="${escape(chrome.csrf ?? "")}"><button type="submit">${escape(one.resumeLabel)}</button></form></div>`).join("") +
15893
16269
  (chrome.updateWaiting === undefined
@@ -16109,7 +16485,7 @@ function inboxPage(chrome, data) {
16109
16485
  /** The row's project, worn openly in the roll-up — null is UNPLACED,
16110
16486
  * said as such, never a silent missing chip (finding 13). */
16111
16487
  const chip = (repo) => !data.rollup ? "" : repo === null || repo === undefined
16112
- ? ` <span class="badge">unplaced</span>`
16488
+ ? ` <span class="badge">Unplaced</span>`
16113
16489
  : ` <span class="badge">${escape(projectName(repo))}</span>`;
16114
16490
  const empty = data.decisions.length + data.approvals.length + data.requeueables.length +
16115
16491
  data.cancelledBlockers.length + data.gaps.length + data.needsVerification.length === 0;
@@ -16118,19 +16494,19 @@ function inboxPage(chrome, data) {
16118
16494
  // the card itself (portfolio arc §2).
16119
16495
  const decisions = data.decisions.length === 0
16120
16496
  ? ""
16121
- : `<h2>answer a question</h2><p class="hint">an agent stopped mid-build to ask — nothing proceeds until you answer</p>` +
16497
+ : `<h2>Answer a question</h2><p class="hint">an agent stopped mid-build to ask — nothing proceeds until you answer</p>` +
16122
16498
  data.decisions
16123
16499
  .map(decision => data.interactive && !data.rollup
16124
16500
  ? decisionAnswerCard(decision, data.csrf, data.now, false)
16125
16501
  : `<a class="decide-card" href="/d/${decision.id}">` +
16126
16502
  `<p class="q">${escape(decision.question)}</p>` +
16127
16503
  `<span class="mono meta">${escape(decision.taskId)}</span>${chip(decision.repo)}` +
16128
- `${isOverdue(decision, data.now) ? ` <span class="badge badge-overdue">overdue</span>` : ""}` +
16504
+ `${isOverdue(decision, data.now) ? ` <span class="badge badge-overdue">Overdue</span>` : ""}` +
16129
16505
  `</a>`)
16130
16506
  .join("\n");
16131
16507
  const approvals = data.approvals.length === 0
16132
16508
  ? ""
16133
- : `<h2>approve a scope</h2><p class="hint">scopes waiting for your approval — each binds to the exact wording you sign</p>` +
16509
+ : `<h2>Approve a scope</h2><p class="hint">scopes waiting for your approval — each binds to the exact wording you sign</p>` +
16134
16510
  data.approvals
16135
16511
  .map(one => `<a class="decide-card" href="${taskHref(one.taskId)}">` +
16136
16512
  `<p class="q">${escape(one.title)}</p>` +
@@ -16140,7 +16516,7 @@ function inboxPage(chrome, data) {
16140
16516
  .join("\n");
16141
16517
  const requeueables = data.requeueables.length === 0
16142
16518
  ? ""
16143
- : `<h2>retry stalled work</h2><p class="hint">failed builds waiting for a person — retry clears the incidents and requeues</p>` +
16519
+ : `<h2>Retry stalled work</h2><p class="hint">failed builds waiting for a person — retry clears the incidents and requeues</p>` +
16144
16520
  data.requeueables
16145
16521
  .map(one => `<p class="row"><a href="${taskHref(one.taskId)}">${escape(one.taskId)}</a> ${escape(one.title)}${chip(one.repo)}` +
16146
16522
  `${one.incidentCount > 0 ? ` <span class="badge badge-failed">${one.incidentCount} incident${one.incidentCount > 1 ? "s" : ""}</span>` : ""}` +
@@ -16150,11 +16526,11 @@ function inboxPage(chrome, data) {
16150
16526
  : `<span class="right"><form method="post" action="${taskHref(one.taskId)}/requeue" class="inline">` +
16151
16527
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
16152
16528
  `<input type="hidden" name="return" value="inbox">` +
16153
- `<button type="submit">retry</button></form></span></p>`))
16529
+ `<button type="submit">Retry</button></form></span></p>`))
16154
16530
  .join("\n");
16155
16531
  const needsVerification = data.needsVerification.length === 0
16156
16532
  ? ""
16157
- : `<h2>needs review</h2><p class="hint">finished, but the evidence needs your attention before accepting</p>` +
16533
+ : `<h2>Needs review</h2><p class="hint">finished, but the evidence needs your attention before accepting</p>` +
16158
16534
  data.needsVerification
16159
16535
  .map(one => `<p class="row"><a href="${taskHref(one.taskId)}">${escape(one.taskId)}</a> ${escape(one.title)}${chip(one.repo)}` +
16160
16536
  ` <span class="badge badge-failed">${one.verdict === "refuted" ? "conflicting evidence" : "missing evidence"}</span>${criterionMatrixSummary(one.matrix ?? [])}` +
@@ -16163,7 +16539,7 @@ function inboxPage(chrome, data) {
16163
16539
  .join("\n");
16164
16540
  const cancelled = data.cancelledBlockers.length === 0
16165
16541
  ? ""
16166
- : `<h2>choose how waiting tasks continue</h2><p class="hint">these tasks were waiting for work that was cancelled — open one and choose what happens next</p>` +
16542
+ : `<h2>Choose how waiting tasks continue</h2><p class="hint">these tasks were waiting for work that was cancelled — open one and choose what happens next</p>` +
16167
16543
  data.cancelledBlockers
16168
16544
  .map(one => `<p class="row"><a href="${taskHref(one.exampleDependent)}">${escape(one.exampleDependent)}</a>${chip(one.repo)} ` +
16169
16545
  `<span class="meta">${one.dependentCount > 1 ? `one of ${one.dependentCount} tasks waiting` : "waiting"} for cancelled task ${escape(one.blockerId)}` +
@@ -16171,7 +16547,7 @@ function inboxPage(chrome, data) {
16171
16547
  .join("\n");
16172
16548
  const gaps = data.gaps.length === 0
16173
16549
  ? ""
16174
- : `<h2>supply a requirement</h2><p class="hint">approved work is ready except for these — fill one and its tasks start</p>` +
16550
+ : `<h2>Supply a requirement</h2><p class="hint">approved work is ready except for these — fill one and its tasks start</p>` +
16175
16551
  data.gaps
16176
16552
  .map(gap => `<p class="row"><a href="/caps">${escape(gap.key)}</a> ` +
16177
16553
  `<span class="meta">frees ${gap.unblocks.length} task${gap.unblocks.length > 1 ? "s" : ""}</span>` +
@@ -16203,8 +16579,8 @@ function inboxPage(chrome, data) {
16203
16579
  const tab = data.tab ?? "all";
16204
16580
  const shows = (one) => tab === "all" || tab === one;
16205
16581
  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");
16206
- const ready = (data.ready ?? []).length === 0 ? "" : `<h2>ready to review</h2>` + listRows(data.ready ?? []);
16207
- const running = (data.running ?? []).length === 0 ? "" : `<h2>running now</h2>` + listRows(data.running ?? []);
16582
+ const ready = (data.ready ?? []).length === 0 ? "" : `<h2>Ready to review</h2>` + listRows(data.ready ?? []);
16583
+ const running = (data.running ?? []).length === 0 ? "" : `<h2>Running now</h2>` + listRows(data.running ?? []);
16208
16584
  const asks = { decide: data.decisions.length + data.approvals.length, unblock: data.requeueables.length + data.cancelledBlockers.length + data.gaps.length };
16209
16585
  const counts = { ...asks,
16210
16586
  "needs-you": asks.decide + asks.unblock,
@@ -16223,7 +16599,7 @@ function inboxPage(chrome, data) {
16223
16599
  ? `<div class="card"><p class="meta">${tab === "needs-you" ? "Nothing needs you." : tab === "ready" ? "No results are waiting for review." : "Nothing is running."}</p></div>` : "";
16224
16600
  return screen("inbox", [
16225
16601
  `<h1>Inbox</h1>`,
16226
- `<p class="meta">everything that waits on you \u2014 empty means the fleet is working</p>`,
16602
+ `<p class="meta">Everything that waits on you \u2014 empty means the fleet is working</p>`,
16227
16603
  tabs,
16228
16604
  noWorker,
16229
16605
  wizard,
@@ -16235,21 +16611,21 @@ function inboxPage(chrome, data) {
16235
16611
  shows("needs-you") ? askGroup("unblock", counts.unblock, [requeueables, cancelled, gaps]) : "",
16236
16612
  shows("running") ? running : "",
16237
16613
  data.rollup
16238
- ? `<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>`
16614
+ ? `<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>`
16239
16615
  : "",
16240
16616
  // Quick capture: the shortest path from "I want this done" to the
16241
16617
  // approve card — title and goal here, the yes on the next screen. The
16242
16618
  // one-shot form posts to the same guarded handler as the full page.
16243
- data.rollup ? "" : `<h2>capture new work</h2>`,
16619
+ data.rollup ? "" : `<h2>Capture new work</h2>`,
16244
16620
  data.rollup ? "" : `<form method="post" action="/tasks/add" class="card">`,
16245
16621
  ...(data.rollup
16246
16622
  ? []
16247
16623
  : [
16248
16624
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
16249
16625
  `<input type="hidden" name="projectRevision" value="${data.revision}">`,
16250
- `<label>what should get done<input type="text" name="title" placeholder="task title" maxlength="200"></label>`,
16251
- `<label>what success looks like <span class="meta">(becomes the scope you approve on the next screen)</span><textarea name="goal" rows="2"></textarea></label>`,
16252
- `<button type="submit">queue it \u2192 approve its scope next</button>`,
16626
+ `<label>What should get done<input type="text" name="title" placeholder="task title" maxlength="200"></label>`,
16627
+ `<label>What success looks like <span class="meta">(becomes the scope you approve on the next screen)</span><textarea name="goal" rows="2"></textarea></label>`,
16628
+ `<button type="submit">Queue it \u2192 approve its scope next</button>`,
16253
16629
  `</form>`,
16254
16630
  ]),
16255
16631
  ].join("\n"), {
@@ -16298,7 +16674,7 @@ function systemPage(chrome, data) {
16298
16674
  const agentLines = data.agents
16299
16675
  .map(one => {
16300
16676
  if (one.problem !== undefined) {
16301
- return `<p class="row"><span class="mono">${escape(one.phase)}</span> <span class="badge badge-failed">misconfigured</span> <span class="meta">${escape(one.problem)}</span></p>`;
16677
+ return `<p class="row"><span class="mono">${escape(one.phase)}</span> <span class="badge badge-failed">Misconfigured</span> <span class="meta">${escape(one.problem)}</span></p>`;
16302
16678
  }
16303
16679
  const who = one.source === "project"
16304
16680
  ? `chosen for this project${one.setBy === null ? "" : ` by ${escape(one.setBy)}`}`
@@ -16313,10 +16689,10 @@ function systemPage(chrome, data) {
16313
16689
  `<p class="meta" style="margin-top:0">${PHASE_SAID[one.phase] ?? ""}</p>`);
16314
16690
  })
16315
16691
  .join("\n");
16316
- const agentsCard = `<h2>agents</h2>` +
16317
- `<p class="meta">which AI provider runs each phase — changed from the terminal with your credentials (<code>toolroll config</code>), never by a browser click</p>` +
16692
+ const agentsCard = `<h2>Agents</h2>` +
16693
+ `<p class="meta">Which AI provider runs each phase — changed from the terminal with your credentials (<code>toolroll config</code>), never by a browser click</p>` +
16318
16694
  `<div class="card">${agentLines}` +
16319
- `<p class="meta">repair always stays on the provider that built — only its model can differ. A routine pins its agent the moment it fires; nothing after that can re-route it.</p>` +
16695
+ `<p class="meta">Repair always stays on the provider that built — only its model can differ. A routine pins its agent the moment it fires; nothing after that can re-route it.</p>` +
16320
16696
  `</div>`;
16321
16697
  return screen("system", [
16322
16698
  `<h1>System</h1>`,
@@ -16325,16 +16701,16 @@ function systemPage(chrome, data) {
16325
16701
  cards.length === 0
16326
16702
  ? `<p class="meta">No builder is connected yet. On the machine where the project lives, open that folder and run <code>toolroll up</code>.</p>`
16327
16703
  : `<div class="cards">${cards.join("")}</div>`,
16328
- data.outboxPending > 0 ? `<p class="meta">notifications: ${data.outboxPending} pending delivery</p>` : "",
16704
+ data.outboxPending > 0 ? `<p class="meta">Notifications: ${data.outboxPending} pending delivery</p>` : "",
16329
16705
  (data.externalWork ?? []).length === 0
16330
16706
  ? ""
16331
- : `<h2>external work</h2>` +
16707
+ : `<h2>External work</h2>` +
16332
16708
  (data.externalWork ?? [])
16333
16709
  .map(one => `<p class="row"><span class="mono">${escape(one.remoteRepo)}</span> ` +
16334
16710
  (one.blocked !== null
16335
- ? `<span class="badge badge-failed">dispatch blocked</span> <span class="meta">the tracker connection needs repair — \`toolroll sync\` says why</span>`
16711
+ ? `<span class="badge badge-failed">Dispatch blocked</span> <span class="meta">the tracker connection needs repair — \`toolroll sync\` says why</span>`
16336
16712
  : one.openEpisode !== null
16337
- ? `<span class="badge badge-failed">sync failing</span> <span class="meta">${escape(one.openEpisode)}</span>`
16713
+ ? `<span class="badge badge-failed">Sync failing</span> <span class="meta">${escape(one.openEpisode)}</span>`
16338
16714
  : `<span class="meta">syncing normally</span>`) +
16339
16715
  `</p>`)
16340
16716
  .join("\n"),
@@ -16437,7 +16813,7 @@ function boardBody(data, ciRed) {
16437
16813
  queuedHeads.get(`${card.assignedRunner ?? ""}|${card.repo ?? ""}`) === card.taskId
16438
16814
  ? `<span class="badge">${card.assignedRunner === null ? "next up" : `next for ${escape(card.assignedRunner)}`}</span>`
16439
16815
  : "",
16440
- card.assignedRunner === null ? "" : `<span class="badge">reserved</span>`,
16816
+ card.assignedRunner === null ? "" : `<span class="badge">Reserved</span>`,
16441
16817
  card.routineName === null ? "" : `<span class="badge">${escape(card.routineName)}</span>`,
16442
16818
  chip(card.repo).trim(),
16443
16819
  ]) +
@@ -16465,7 +16841,7 @@ function boardBody(data, ciRed) {
16465
16841
  ...(claim.branch === null ? [] : [["branch", `${claim.branch}${workspace === null ? "" : ` · ${workspace}`}`]]),
16466
16842
  ]) +
16467
16843
  chips([
16468
- card.attempt === null ? "" : `<span class="badge">attempt ${card.attempt}</span>`,
16844
+ card.attempt === null ? "" : `<span class="badge">Attempt ${card.attempt}</span>`,
16469
16845
  chip(card.repo).trim(),
16470
16846
  ]) +
16471
16847
  `</a>`);
@@ -16487,7 +16863,7 @@ function boardBody(data, ciRed) {
16487
16863
  ["usage", runCostWords({ authMode: row.authMode, costUsd: row.costUsd, tokensIn: null, tokensOut: null })],
16488
16864
  ]) +
16489
16865
  chips([
16490
- `<span class="badge badge-done">${row.outcome === "no-change" ? "no change" : "built"}</span>`,
16866
+ `<span class="badge badge-done">${row.outcome === "no-change" ? "No change" : "Built"}</span>`,
16491
16867
  pr.trim(),
16492
16868
  data.all && row.repo !== null ? `<span class="badge">${escape(projectName(row.repo))}</span>` : "",
16493
16869
  ]) +
@@ -16499,7 +16875,7 @@ function boardBody(data, ciRed) {
16499
16875
  : `<p class="meta board-scope">` +
16500
16876
  (data.all
16501
16877
  ? (data.project === null
16502
- ? `<strong>all projects</strong>`
16878
+ ? `<strong>All projects</strong>`
16503
16879
  : `<a href="/board">${escape(projectName(data.project))}</a> \u00b7 <strong>all projects</strong>`) +
16504
16880
  ` \u2014 every project this server serves, each card wearing its project`
16505
16881
  : `<strong>${escape(projectName(data.project))}</strong> \u00b7 <a href="/board?scope=all">all projects</a>`) +
@@ -16516,7 +16892,7 @@ function boardBody(data, ciRed) {
16516
16892
  const ago = (minutes) => minutes < 60 ? `${minutes}m` : minutes < 48 * 60 ? `${Math.round(minutes / 60)}h` : `${Math.round(minutes / (24 * 60))}d`;
16517
16893
  const deltaLine = data.delta === null
16518
16894
  ? ""
16519
- : `<p class="meta"><strong>since you last looked</strong> (${ago(data.delta.agoMinutes)} ago): ` +
16895
+ : `<p class="meta"><strong>Since you last looked</strong> (${ago(data.delta.agoMinutes)} ago): ` +
16520
16896
  [
16521
16897
  data.delta.built > 0 ? `<span class="good">${data.delta.built} built</span>` : "",
16522
16898
  data.delta.failed > 0 ? `<span class="bad">${data.delta.failed} failed</span>` : "",
@@ -16527,7 +16903,7 @@ function boardBody(data, ciRed) {
16527
16903
  `</p>`;
16528
16904
  return [
16529
16905
  `<h1>Board</h1>`,
16530
- data.all ? "" : `<p class="meta board-view"><strong>state</strong> \u00b7 <a href="/board?view=order">order \u2192</a> <span class="meta">drag to reorder, or to reserve a task for one worker</span></p>`,
16906
+ data.all ? "" : `<p class="meta board-view"><strong>State</strong> \u00b7 <a href="/board?view=order">order \u2192</a> <span class="meta">drag to reorder, or to reserve a task for one worker</span></p>`,
16531
16907
  deltaLine,
16532
16908
  toggle,
16533
16909
  `<div class="board">`,
@@ -16549,14 +16925,14 @@ function donePage(chrome, rows, ciRed) {
16549
16925
  const pr = row.prNumber === null
16550
16926
  ? row.publicationState === null
16551
16927
  ? ""
16552
- : ` <span class="badge">${escape(row.publicationState)}</span>`
16928
+ : ` <span class="badge">${escape(sentenceCase(row.publicationState))}</span>`
16553
16929
  : ` <a href="${escape(row.prUrl ?? "#")}" class="badge badge-open">PR #${row.prNumber}</a>` +
16554
16930
  (ciRed(row.prNumber) ? ` <span class="badge badge-failed">CI failing</span>` : "");
16555
16931
  const needsVerification = (row.proofVerdict === "short" || row.proofVerdict === "refuted") && !row.proofAccepted
16556
16932
  ? ` <span class="badge badge-failed">${row.proofVerdict === "refuted" ? "conflicting evidence" : "missing evidence"}</span>`
16557
16933
  : "";
16558
16934
  return (`<div class="card"><p><a href="${taskHref(row.taskId)}"><strong>${escape(row.title)}</strong></a>` +
16559
- `${row.outcome === "no-change" ? ` <span class="badge">no change needed</span>` : ""}${pr}${needsVerification}${criterionMatrixSummary(row.proofMatrix)}</p>` +
16935
+ `${row.outcome === "no-change" ? ` <span class="badge">No change needed</span>` : ""}${pr}${needsVerification}${criterionMatrixSummary(row.proofMatrix)}</p>` +
16560
16936
  `${row.handoff === null ? "" : `<p class="meta">${escape(row.handoff.length > 200 ? row.handoff.slice(0, 200) + "\u2026" : row.handoff)}</p>`}` +
16561
16937
  `<p class="meta mono">${escape(row.taskId)} \u00b7 ${whenTime(row.completedAt)}${row.ranMinutes === null ? "" : ` \u00b7 ran ${row.ranMinutes}m`}${row.provider === null ? "" : ` \u00b7 ${escape(runCostWords({ authMode: row.authMode, costUsd: row.costUsd, tokensIn: null, tokensOut: null }))}`}` +
16562
16938
  ` \u00b7 <a href="${reviewHref(row.taskId)}">review \u2192</a></p></div>`);
@@ -16622,9 +16998,9 @@ function trackRow(track, all) {
16622
16998
  `<p>${trackStrip(fires)}` +
16623
16999
  `<span class="right meta">$${spend.costUsd.toFixed(2)} this week${spend.unmeasuredRuns > 0 ? ` — measured on ${spend.totalRuns - spend.unmeasuredRuns} of ${spend.totalRuns} builds` : ""}${routine.costCeilingUsd === null ? "" : ` of $${routine.costCeilingUsd.toFixed(2)}`}</span></p>` +
16624
17000
  (blocker !== null
16625
- ? `<p class="meta">stopped behind <a href="${taskHref(blocker.taskId)}" class="mono">${escape(blocker.taskId)}</a> (${escape(blocker.state)})</p>`
17001
+ ? `<p class="meta">Stopped behind <a href="${taskHref(blocker.taskId)}" class="mono">${escape(blocker.taskId)}</a> (${escape(blocker.state)})</p>`
16626
17002
  : latest?.instanceTaskId !== undefined && latest.instanceTaskId !== null
16627
- ? `<p class="meta">latest: <a href="${taskHref(latest.instanceTaskId)}" class="mono">${escape(latest.instanceTaskId)}</a>${latest.instanceState === null ? "" : ` (${escape(latest.instanceState)})`}</p>`
17003
+ ? `<p class="meta">Latest: <a href="${taskHref(latest.instanceTaskId)}" class="mono">${escape(latest.instanceTaskId)}</a>${latest.instanceState === null ? "" : ` (${escape(latest.instanceState)})`}</p>`
16628
17004
  : "") +
16629
17005
  `</div>`);
16630
17006
  }
@@ -16741,12 +17117,13 @@ function checkProgressHtml(progress, id = "check-progress") {
16741
17117
  : Object.values(progress.suites).every(one => one.state === "passed") ? "passed" : "unknown";
16742
17118
  return `<p id="${id}" class="check-progress mono" data-check-progress data-final="${escape(state)}" role="status" aria-live="polite">${escape(progress.line)}</p>`;
16743
17119
  }
16744
- function taskControlDetailsHtml(control, taskId, csrf, surface, inert = false) {
16745
- const html = taskControlHtml(control, taskId, csrf, surface, inert);
17120
+ function taskControlDetailsHtml(control, taskId, csrf, surface, inert = false, stopElsewhere = false) {
17121
+ const html = taskControlHtml(control, taskId, csrf, surface, inert, stopElsewhere);
16746
17122
  return control.kind === "paused" || control.kind === "review-stopped" || control.kind === "stopping"
16747
17123
  ? `<details id="task-control-details"><summary>${control.kind === "paused" ? "Preserved work and resume" : "Stop details"}</summary>${html}</details>` : html;
16748
17124
  }
16749
- function taskControlHtml(control, taskId, csrf, surface, inert = false) {
17125
+ /** `stopElsewhere`: the Building card already carries this exact run's Stop, so the control card keeps only its details. */
17126
+ function taskControlHtml(control, taskId, csrf, surface, inert = false, stopElsewhere = false) {
16750
17127
  if (control.kind === "none")
16751
17128
  return "";
16752
17129
  const back = `<input type="hidden" name="return" value="${surface}">`;
@@ -16760,7 +17137,7 @@ function taskControlHtml(control, taskId, csrf, surface, inert = false) {
16760
17137
  if (control.kind === "stop") {
16761
17138
  return (`<section class="card task-control" id="task-control" data-task-control="stop" data-control-run="${control.run}" aria-label="stop this attempt">` +
16762
17139
  `<div class="task-control-copy"><details><summary>Stop details · ${escape(role(control))} #${control.run}</summary><p class="meta">Stopping ends only this attempt's own processes. Its branch, uncommitted work, evidence, and decisions stay preserved; other tasks keep running.</p></details></div>` +
16763
- (guarded
17140
+ (stopElsewhere ? "" : guarded
16764
17141
  ? `<form method="post" action="${taskHref(taskId)}/stop" class="inline task-control-form task-stop-form"><input type="hidden" name="csrf" value="${escape(csrf)}"><input type="hidden" name="run" value="${control.run}">${back}<button type="submit" class="danger task-control-button">Stop</button></form>`
16765
17142
  : `<button type="button" class="danger task-control-button" disabled>Stop</button>`) +
16766
17143
  `</section>`);
@@ -16800,7 +17177,7 @@ function resumeCeremonyPage(chrome, data) {
16800
17177
  `<p class="row">Continues saved work${control.committed ? " and its commit" : ""} under the current approved scope. The next attempt needs fresh evidence; the earlier handoff cannot count as a new result.</p>`,
16801
17178
  `<p class="row">Resuming grants no new approval or publishing permission. Scope, budget, agents, risk, fallback, verification, and review limits still apply.</p>`,
16802
17179
  `<details><summary>Stop record and saved work</summary><p>Run #${control.run} was stopped by <span class="mono">${escape(control.stop.requestedBy)}</span> at ${escape(stopWhen(control.stop.requestedAt))}${control.stop.settledAt === null ? "" : ` and settled ${escape(stopWhen(control.stop.settledAt))} (${escape(control.stop.settlement ?? "?")})`}.</p>${control.worktree === null ? "" : `<p class="mono">${escape(control.worktree)}</p>`}</details>`,
16803
- data.approved ? "" : `<p class="row problem-words"><strong>the scope is not approved as it stands</strong> — resuming lifts the pause, but no worker spends until the scope is approved again</p>`,
17180
+ data.approved ? "" : `<p class="row problem-words"><strong>The scope is not approved as it stands</strong> — resuming lifts the pause, but no worker spends until the scope is approved again</p>`,
16804
17181
  data.gate === null ? "" : `<p class="row"><strong>Before work starts:</strong> ${escape(data.gate.summary)} — ${escape(data.gate.detail)}</p>`,
16805
17182
  `</div>`,
16806
17183
  `<form method="post" action="${taskHref(data.taskId)}/resume" class="card resume-form">`,
@@ -16876,12 +17253,23 @@ function taskChatApproval(focus, csrf) {
16876
17253
  route: focus.route,
16877
17254
  coordinator: approval.coordinator,
16878
17255
  deliverable: approval.deliverable,
16879
- editHref: `${taskHref(focus.id)}#${approval.planDocument === null ? "scope" : "plan-edit"}`,
17256
+ // Edit plan opens the task page's sheet with its fields open for editing.
17257
+ editHref: approval.raceTerms !== null ? `${taskHref(focus.id)}#${approval.planDocument === null ? "scope" : "plan-edit"}` : `${taskHref(focus.id)}?edit=plan#plan-editor`,
16880
17258
  notNowHref: "/chat",
16881
17259
  sticky: false,
17260
+ earlier: { active: focus.assignment?.earlierActive ?? 0, running: focus.assignment?.earlierRunning ?? 0 },
17261
+ edit: null,
16882
17262
  }) +
16883
17263
  `</section>`);
16884
17264
  }
17265
+ /** The chat's status sentence, worded as the task page words it: what a failed attempt missed, or which step a live
17266
+ * build is on (and the step it is stuck on). Undefined keeps the shared status sentence. */
17267
+ function chatStatusSentence(focus) {
17268
+ if (focus.state === "failed" && focus.failure != null)
17269
+ return [focus.failure.line, focus.failure.evidence].filter(Boolean).join(" ");
17270
+ const steps = focus.liveRun === null ? null : buildProgressOf(focus.milestoneProgress);
17271
+ return steps === null ? undefined : steps.line;
17272
+ }
16885
17273
  /** One live, server-derived journey from request to proof. The fragment is
16886
17274
  * safe to refresh independently, so an in-progress message is never lost. */
16887
17275
  function taskChatLiveRegion(focus, csrf, fragment = false, inert = false, approvalHref = taskChatHref(focus.id) + "#task-chat-action") {
@@ -16897,7 +17285,7 @@ function taskChatLiveRegion(focus, csrf, fragment = false, inert = false, approv
16897
17285
  return (`<section id="task-chat-live" aria-live="polite" data-task="${escape(focus.id)}" data-execution="${escape(focus.executionId)}" data-source="/chat/task-status?task=${encodeURIComponent(focus.id)}" data-poll="${polling ? "1" : "0"}" data-approval="${escape(focus.approval?.digest ?? "")}" data-plan="${escape(focus.plan ?? "")}">` +
16898
17286
  (focus.guide === true && focus.state !== "cancelled" ? firstTaskJourneyHtml(focus) : "") +
16899
17287
  `<div class="task-live-summary">` +
16900
- (focus.assignment !== null ? assignmentSummaryHtml(focus.assignment, { workStatus: focus.status, hideAction: focus.approval !== null && focus.dispatch?.action === "approve-scope", ...(receiptLeads ? { resultHref: chatResultHref(focus.id, focus.result.runId) } : {}) }) + (receiptLeads ? completionReceiptCard(focus.result, focus.id, "chat", focus.status, focus.assignment, false) : "") : receiptLeads ? completionReceiptCard(focus.result, focus.id, "chat", focus.status) : taskStatusCard(focus.status, focus.id, focus.dispatch, focus.liveRun?.id ?? null, focus.approval !== null && focus.dispatch?.action === "approve-scope")) +
17288
+ (focus.assignment !== null ? assignmentSummaryHtml(focus.assignment, { workStatus: focus.status, hideAction: focus.approval !== null && focus.dispatch?.action === "approve-scope", sentence: chatStatusSentence(focus), ...(receiptLeads ? { resultHref: chatResultHref(focus.id, focus.result.runId) } : {}) }) + (receiptLeads ? completionReceiptCard(focus.result, focus.id, "chat", focus.status, focus.assignment, false) : "") : receiptLeads ? completionReceiptCard(focus.result, focus.id, "chat", focus.status) : taskStatusCard(focus.status, focus.id, focus.dispatch, focus.liveRun?.id ?? null, focus.approval !== null && focus.dispatch?.action === "approve-scope")) +
16901
17289
  checkProgressHtml(focus.checkProgress) +
16902
17290
  // The exact-run control (v52): the SAME component the task page
16903
17291
  // renders, refreshed with the live region — typed input in the
@@ -17008,7 +17396,7 @@ function chatFleetOverview(snapshot, projects, csrf, interactive) {
17008
17396
  return (`<section class="card chat-overview" aria-label="live portfolio overview" data-card-kind="fleet-overview">` +
17009
17397
  `<div class="chat-overview-head"><h2>${projects.length} project${projects.length === 1 ? "" : "s"}</h2>` +
17010
17398
  (interactive
17011
- ? `<form method="post" action="/chat" class="inline"><input type="hidden" name="csrf" value="${escape(csrf)}"><button type="submit" name="message" value="${escape(briefing)}" class="quiet">brief me</button></form>`
17399
+ ? `<form method="post" action="/chat" class="inline"><input type="hidden" name="csrf" value="${escape(csrf)}"><button type="submit" name="message" value="${escape(briefing)}" class="quiet">Brief me</button></form>`
17012
17400
  : `<a href="/board?scope=all" class="chat-overview-link">open board</a>`) +
17013
17401
  `</div>` +
17014
17402
  `<div class="chat-overview-stats">` +
@@ -17043,9 +17431,9 @@ function chatProjectRail(projects, csrf, inert) {
17043
17431
  const ask = `Give me a concise status for ${one.id} (${one.label}) and recommend the next reversible action.`;
17044
17432
  return (`<div class="chat-project-card">` +
17045
17433
  `<div class="chat-project-name"><span class="mono">${escape(one.id)}</span><strong>${escape(one.label)}</strong>` +
17046
- `<span class="badge">${escape(statusOf(peek))}</span></div>` +
17434
+ `<span class="badge">${escape(sentenceCase(statusOf(peek)))}</span></div>` +
17047
17435
  (peek === null
17048
- ? `<p class="meta">pulse unavailable</p>`
17436
+ ? `<p class="meta">Pulse unavailable</p>`
17049
17437
  : `<div class="chat-project-stats">` +
17050
17438
  `<span${peek.waiting > 0 ? ' class="hot"' : ""}><b>${peek.waiting}</b> need you</span>` +
17051
17439
  `<span><b>${peek.running}</b> live</span><span><b>${peek.queued}</b> queued</span>` +
@@ -17054,13 +17442,13 @@ function chatProjectRail(projects, csrf, inert) {
17054
17442
  (inert
17055
17443
  ? ""
17056
17444
  : `<form method="post" action="/chat" class="inline"><input type="hidden" name="csrf" value="${escape(csrf)}">` +
17057
- `<button type="submit" name="message" value="${escape(ask)}" class="quiet" aria-label="ask about ${escape(one.label)}">ask</button></form>`) +
17445
+ `<button type="submit" name="message" value="${escape(ask)}" class="quiet" aria-label="ask about ${escape(one.label)}">Ask</button></form>`) +
17058
17446
  `<form method="post" action="/projects/open" class="inline"><input type="hidden" name="csrf" value="${escape(csrf)}">` +
17059
17447
  `<input type="hidden" name="path" value="${escape(one.path)}"><input type="hidden" name="return" value="/board">` +
17060
- `<button type="submit" class="quiet" aria-label="open ${escape(one.label)} board">board</button></form></div></div>`);
17448
+ `<button type="submit" class="quiet" aria-label="open ${escape(one.label)} board">Board</button></form></div></div>`);
17061
17449
  }).join("");
17062
17450
  return (`<aside class="chat-projects" id="chat-project-panel" aria-label="projects in this conversation">` +
17063
- `<div class="chat-projects-head"><h2>projects</h2><span class="badge">${projects.length}</span>` +
17451
+ `<div class="chat-projects-head"><h2>Projects</h2><span class="badge">${projects.length}</span>` +
17064
17452
  `<button type="button" class="chat-project-close quiet" aria-label="close projects">×</button></div>` +
17065
17453
  `<div class="chat-project-list">${rows}</div></aside>`);
17066
17454
  }
@@ -17073,9 +17461,9 @@ function matePromptStarters(csrf, focus = null) {
17073
17461
  ["new task", "Help me define a new task. Ask only about choices that materially change the result; otherwise use your judgment and sensible reversible defaults."],
17074
17462
  ]
17075
17463
  : [
17076
- ["what’s happening", "Read this task and explain its current status, what is blocking it, and what should happen next."],
17077
- ["review results", "Review the evidence recorded for this task and tell me what is proven and what is still unverified."],
17078
- ["adjust the plan", "Read this task and propose a tighter scope if that would improve the outcome. Explain why before I confirm anything."],
17464
+ ["What’s happening", "Read this task and explain its current status, what is blocking it, and what should happen next."],
17465
+ ["Review results", "Review the evidence recorded for this task and tell me what is proven and what is still unverified."],
17466
+ ["Adjust the plan", "Read this task and propose a tighter scope if that would improve the outcome. Explain why before I confirm anything."],
17079
17467
  ];
17080
17468
  return `<div class="chat-prompts" aria-label="suggested questions">${prompts.map(([label, message]) => `<form method="post" action="/chat" class="inline"><input type="hidden" name="csrf" value="${escape(csrf)}">` +
17081
17469
  (focus === null ? "" : `<input type="hidden" name="task" value="${escape(focus.id)}">`) +
@@ -17205,6 +17593,14 @@ function leadSettingsHtml(data) {
17205
17593
  `<p><a href="/settings">Settings</a></p><h1>Lead</h1>`,
17206
17594
  data.said === null ? "" : `<p class="problem" role="status">${escape(data.said)}</p>`,
17207
17595
  `<section class="card lead-settings" data-lead-settings>${summary}</section>`,
17596
+ data.identity === undefined ? "" : `<form method="post" action="/settings/lead/identity" class="card lead-identity" data-lead-identity>` +
17597
+ `<input type="hidden" name="csrf" value="${escape(facts.csrf)}">` +
17598
+ `<label>Name your lead<input name="name" value="${escape(data.identity.name)}" maxlength="${LEAD_NAME_MAX}" autocomplete="off" required></label>` +
17599
+ `<label>Persona<textarea name="persona" rows="4" maxlength="${LEAD_PERSONA_MAX}">${escape(data.identity.persona)}</textarea></label>` +
17600
+ `<button type="submit">Save</button>${data.saved === true ? ` <span class="meta" role="status">Saved.</span>` : ""}</form>`,
17601
+ // What the lead promised to follow up on; it reports each once, here in chat, and drops it after 7 days.
17602
+ (data.promises ?? []).length === 0 ? "" : `<section class="card lead-promises" data-lead-promises><h2>Promises</h2><ul class="lead-promise-list">${data.promises.map(one => `<li data-promise="${one.id}"><p>${escape(one.what)}</p><p class="meta">${escape(one.when.charAt(0).toUpperCase() + one.when.slice(1))} · <span class="nowrap">until ${escape(new Date(one.until).toLocaleDateString("en-US", { month: "short", day: "numeric", timeZone: "UTC" }))}</span></p>` +
17603
+ `<form method="post" action="/settings/lead/promise/cancel" class="inline">${hidden}<input type="hidden" name="promise" value="${one.id}"><button type="submit" class="secondary">Cancel</button></form></li>`).join("")}</ul></section>`,
17208
17604
  `<details class="lead-advanced" data-lead-advanced><summary>Advanced</summary>`,
17209
17605
  leadConfigForm(config, facts),
17210
17606
  config === null ? "" : `<form method="post" action="/chat/config" class="inline">${hidden}<input type="hidden" name="off" value="1">` +
@@ -17224,23 +17620,23 @@ function leadConfigForm(current, data) {
17224
17620
  `<form method="post" action="/chat/config" class="card">`,
17225
17621
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
17226
17622
  `<input type="hidden" name="return" value="${escape(data.returnTo)}">`,
17227
- `<label>provider<select name="provider">`,
17623
+ `<label>Provider<select name="provider">`,
17228
17624
  `<option value="codex-subscription"${current?.provider === "codex-subscription" ? " selected" : ""}>Codex membership (logged-in CLI)</option>`,
17229
17625
  `<option value="claude-subscription"${current?.provider === "claude-subscription" ? " selected" : ""}>Anthropic membership (logged-in CLI)</option>`,
17230
17626
  `<option value="anthropic-api"${current?.provider === "anthropic-api" ? " selected" : ""}>anthropic-api (direct API)</option>`,
17231
17627
  `<option value="openrouter-api"${current?.provider === "openrouter-api" ? " selected" : ""}>openrouter-api (direct API)</option>`,
17232
17628
  `</select></label>`,
17233
- `<label>model <span class="meta">(use default for your membership's current model; direct API models need a pinned price)</span>` +
17629
+ `<label>Model <span class="meta">(use default for your membership's current model; direct API models need a pinned price)</span>` +
17234
17630
  `<input name="model" list="chat-models" value="${escape(current?.model ?? "default")}"><datalist id="chat-models">` +
17235
17631
  `${models.map(model => `<option value="${escape(model)}">${escape(labels.get(model) ?? "")}</option>`).join("")}</datalist></label>`,
17236
17632
  data.openrouterModels === null
17237
- ? `<p class="meta">with OPENROUTER_API_KEY in the serve environment, this list becomes OpenRouter's full live catalog — each model priced by the party that bills it</p>`
17633
+ ? `<p class="meta">With OPENROUTER_API_KEY in the serve environment, this list becomes OpenRouter's full live catalog — each model priced by the party that bills it</p>`
17238
17634
  : `<p class="meta">${data.openrouterModels.length} models live from OpenRouter's catalog; saving pins today's price — re-save to re-pin</p>`,
17239
17635
  currentSubscription
17240
- ? `<p class="meta"><strong>no dollar maximum.</strong> Membership chat uses the plan attached to the logged-in CLI; the conversation stays live until you end it, and the daily turn limit still applies.</p>`
17241
- : `<label>weekly ceiling <span class="meta">(direct API only; leave blank when choosing a membership)</span>` +
17636
+ ? `<p class="meta"><strong>No dollar maximum.</strong> Membership chat uses the plan attached to the logged-in CLI; the conversation stays live until you end it, and the daily turn limit still applies.</p>`
17637
+ : `<label>Weekly ceiling <span class="meta">(direct API only; leave blank when choosing a membership)</span>` +
17242
17638
  `<input type="text" name="weekly-usd" inputmode="decimal" style="width:8rem" value="${current === null ? "" : (current.weeklyCeilingMicrousd / 1_000_000).toFixed(2)}"></label>`,
17243
- `<label>daily turns <span class="meta">(default 50)</span>` +
17639
+ `<label>Daily turns <span class="meta">(default 50)</span>` +
17244
17640
  `<input type="text" name="daily-turns" inputmode="numeric" style="width:8rem" value="${current === null ? "" : String(current.dailyTurns)}"></label>`,
17245
17641
  currentSubscription
17246
17642
  ? `<p class="meta">Authenticate on this machine first with ${current?.provider === "codex-subscription" ? `<span class="mono">codex login</span>` : `the <span class="mono">claude</span> CLI`}. Toolroll reuses that cached login and never stores it.</p>`
@@ -17252,13 +17648,13 @@ function leadConfigForm(current, data) {
17252
17648
  : `${escape(one.provider)}: stored ${escape(one.tail ?? "")}`)
17253
17649
  .join(" · ")})</span>` +
17254
17650
  `<input type="password" name="key" placeholder="direct API only — leave empty to keep" autocomplete="off"></label>`,
17255
- `<label>your password <span class="meta">(typed again to change the provider)</span>` +
17651
+ `<label>Your password <span class="meta">(typed again to change the provider)</span>` +
17256
17652
  `<input type="password" name="token" autocomplete="current-password"></label>`,
17257
17653
  `<button type="submit">${current === null ? "turn chat on" : "save"}</button>`,
17258
17654
  `</form>`,
17259
17655
  currentSubscription
17260
17656
  ? `<p class="meta">The membership provider runs without repository tools in a temporary directory; Toolroll remains the only layer that can turn a proposed action into a confirmation card.</p>`
17261
- : `<p class="meta">a pasted key is written once to a mode-0600 file beside the database — never INTO the database, never shown again beyond its last characters; an environment variable (` +
17657
+ : `<p class="meta">A pasted key is written once to a mode-0600 file beside the database — never INTO the database, never shown again beyond its last characters; an environment variable (` +
17262
17658
  `<span class="mono">ANTHROPIC_API_KEY</span> / <span class="mono">OPENROUTER_API_KEY</span>) always wins when set</p>`,
17263
17659
  ].join("\n");
17264
17660
  }
@@ -17287,7 +17683,7 @@ function chatPage(chrome, data) {
17287
17683
  ? `<div class="card" id="latest" data-lead-off><p><strong>The lead is off.</strong></p><p class="meta">${data.canManage ? `Turn it on in${settingsLink}.` : "An approver can turn it on."}</p></div>`
17288
17684
  : code === "unpriced" || code === "no-key"
17289
17685
  ? `<div class="card" id="latest"><p><strong>The lead can’t run yet.</strong></p><p class="meta">${escape(data.enabled.why)}${data.canManage ? ` ·${settingsLink}` : ""}</p></div>`
17290
- : `<div class="card" id="latest"><p><strong>chat is off.</strong></p><p class="meta">${escape(data.enabled.why)}</p></div>`);
17686
+ : `<div class="card" id="latest"><p><strong>Chat is off.</strong></p><p class="meta">${escape(data.enabled.why)}</p></div>`);
17291
17687
  return screen("chat", chatWorkspace(parts.join("\n"), data.projects, data.csrf, true, data.focusTask, data.resultPanel ?? null), { chrome, functional: { script: CHAT_UI_SCRIPT + (data.focusTask === null ? "" : RESULT_REVIEW_SCRIPT), fetches: data.focusTask !== null } });
17292
17688
  }
17293
17689
  const config = data.enabled.config;
@@ -17303,7 +17699,7 @@ function chatPage(chrome, data) {
17303
17699
  parts.push(data.coordinatorProposals);
17304
17700
  parts.push(data.focusTask === null && !data.catchUp ? `<details class="chat-fleet-context">${chatOverviewSummaryHtml(data.projects, data.fleetSnapshot?.attentionCount)}${chatFleetOverview(data.fleetSnapshot, data.projects, data.csrf, false)}</details>` : "", chatLimitsHtml({ provider: config.provider, model: config.model, turnsToday: data.turnsToday, dailyTurns: config.dailyTurns, subscription, weekly: subscription ? null : { spent: data.weeklySpent, ceiling: config.weeklyCeilingMicrousd } }));
17305
17701
  for (const turn of data.latched) {
17306
- parts.push(`<div class="problem"><strong>unknown spend blocks chat.</strong> turn #${turn.id} may have cost up to ${chatMoney(turn.reservedMicrousd)} — ` +
17702
+ parts.push(`<div class="problem"><strong>Unknown spend blocks chat.</strong> turn #${turn.id} may have cost up to ${chatMoney(turn.reservedMicrousd)} — ` +
17307
17703
  `<a href="/chat/ack/${turn.id}">read and acknowledge it</a> to re-enable this credential.</div>`);
17308
17704
  }
17309
17705
  if (data.pending !== null) {
@@ -17318,7 +17714,7 @@ function chatPage(chrome, data) {
17318
17714
  }
17319
17715
  else if (last.reply !== null) {
17320
17716
  parts.push(`<div class="card" id="latest">${renderChatText(last.reply)}` +
17321
- (last.proposalsDiscarded ? `<p class="meta">a draft block in this answer was malformed and was discarded whole</p>` : "") +
17717
+ (last.proposalsDiscarded ? `<p class="meta">A draft block in this answer was malformed and was discarded whole</p>` : "") +
17322
17718
  `</div>`);
17323
17719
  }
17324
17720
  }
@@ -17326,21 +17722,21 @@ function chatPage(chrome, data) {
17326
17722
  for (const one of candidates) {
17327
17723
  const draft = one.draft;
17328
17724
  parts.push(`<div class="card">` +
17329
- `<p><strong>${draft.kind === "task" ? escape(draft.title) : escape(draft.name)}</strong> <span class="badge">draft ${escape(draft.kind)}</span></p>` +
17330
- `<p class="meta">drafted by the model from fleet context — nothing is filed; drafts do not survive a restart</p>` +
17725
+ `<p><strong>${draft.kind === "task" ? escape(draft.title) : escape(draft.name)}</strong> <span class="badge">Draft ${escape(draft.kind)}</span></p>` +
17726
+ `<p class="meta">Drafted by the model from fleet context — nothing is filed; drafts do not survive a restart</p>` +
17331
17727
  `<p style="white-space:pre-wrap">${escape(draft.goal)}</p>` +
17332
- (draft.outOfScope === null ? "" : `<p class="meta">not: ${escape(draft.outOfScope)}</p>`) +
17333
- (draft.touches.length > 0 ? `<p class="meta">touches: ${escape(draft.touches.join(", "))}</p>` : "") +
17334
- (draft.kind === "routine" ? `<p class="meta">schedule: ${escape(draft.schedule)}</p>` : "") +
17335
- `<p class="meta">repo: <span class="mono">${escape(projectName(one.repoPath))}</span></p>` +
17728
+ (draft.outOfScope === null ? "" : `<p class="meta">Not: ${escape(draft.outOfScope)}</p>`) +
17729
+ (draft.touches.length > 0 ? `<p class="meta">Touches: ${escape(draft.touches.join(", "))}</p>` : "") +
17730
+ (draft.kind === "routine" ? `<p class="meta">Schedule: ${escape(draft.schedule)}</p>` : "") +
17731
+ `<p class="meta">Repo: <span class="mono">${escape(projectName(one.repoPath))}</span></p>` +
17336
17732
  `<form method="post" action="/chat/file/${escape(one.key)}" class="inline">` +
17337
17733
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
17338
17734
  `<input type="password" name="token" placeholder="your password" autocomplete="current-password">` +
17339
- `<button type="submit">file unapproved</button></form>` +
17735
+ `<button type="submit">File unapproved</button></form>` +
17340
17736
  `</div>`);
17341
17737
  }
17342
17738
  if (!subscription && data.canManage && data.focusTask === null) {
17343
- parts.push(`<h2>ask</h2>`, `<form method="post" action="/chat" class="card">`, `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`, `<label>message<textarea name="message" rows="3" maxlength="2000"></textarea></label>`, `<label>your password <span class="meta">(every message — chat spends)</span><input type="password" name="token" autocomplete="current-password"></label>`, `<button type="submit">ask</button>`, `</form>`);
17739
+ parts.push(`<h2>Ask</h2>`, `<form method="post" action="/chat" class="card">`, `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`, `<label>Message<textarea name="message" rows="3" maxlength="2000"></textarea></label>`, `<label>Your password <span class="meta">(every message — chat spends)</span><input type="password" name="token" autocomplete="current-password"></label>`, `<button type="submit">Ask</button>`, `</form>`);
17344
17740
  }
17345
17741
  if (data.canManage) {
17346
17742
  parts.push(`<p class="meta" id="chat-settings"><a href="/settings/lead">Lead settings</a> · provider, model and limits</p>`);
@@ -17363,12 +17759,12 @@ function chatAckPage(chrome, turn, nonce, csrf) {
17363
17759
  `<p>Turn <span class="mono">#${turn.id}</span> on <span class="mono">${escape(turn.provider)} · ${escape(turn.model)}</span> ` +
17364
17760
  `may have started before it failed (${escape(turn.failureReason ?? "crashed")}), and its cost could not be measured.</p>`,
17365
17761
  `<p><strong>Acknowledging charges the reserved worst case, ${chatMoney(turn.reservedMicrousd)}, to the ledger and re-enables chat on this credential.</strong></p>`,
17366
- `<p class="meta">check the provider's own usage dashboard if you want the exact figure first; the ledger keeps whichever is known.</p>`,
17762
+ `<p class="meta">Check the provider's own usage dashboard if you want the exact figure first; the ledger keeps whichever is known.</p>`,
17367
17763
  `<form method="post" action="/chat/ack/${turn.id}">`,
17368
17764
  `<input type="hidden" name="csrf" value="${escape(csrf)}">`,
17369
17765
  `<input type="hidden" name="nonce" value="${escape(nonce)}">`,
17370
- `<label>your password<input type="password" name="token" autocomplete="current-password"></label>`,
17371
- `<button type="submit">accept the charge — re-enable chat</button>`,
17766
+ `<label>Your password<input type="password" name="token" autocomplete="current-password"></label>`,
17767
+ `<button type="submit">Accept the charge — re-enable chat</button>`,
17372
17768
  `</form>`,
17373
17769
  `</div>`,
17374
17770
  ].join("\n"), { chrome });
@@ -17575,10 +17971,10 @@ function proposalCardParts(view, csrf, inert, decision, returnTo = null) {
17575
17971
  const decisionId = typeof payload["decision"] === "number" ? payload["decision"] : 0;
17576
17972
  const pick = text("option");
17577
17973
  const options = decision === null
17578
- ? `<p class="meta">the decision is gone</p>`
17974
+ ? `<p class="meta">The decision is gone</p>`
17579
17975
  : `<ul class="answer-options">` +
17580
17976
  decision.options
17581
- .map(one => `<li${one.id === pick ? ' class="picked"' : ""}><strong>${escape(one.label)}</strong>${one.reversible ? "" : ' <span class="badge">irreversible</span>'}` +
17977
+ .map(one => `<li${one.id === pick ? ' class="picked"' : ""}><strong>${escape(one.label)}</strong>${one.reversible ? "" : ' <span class="badge">Irreversible</span>'}` +
17582
17978
  `${one.id === decision.recommendation ? ' <span class="meta">— the builder recommends this</span>' : ""}` +
17583
17979
  `${one.id === pick ? ' <span class="meta">— proposed</span>' : ""}` +
17584
17980
  `<p class="meta">${escape(one.consequence)}</p></li>`)
@@ -17608,13 +18004,13 @@ function proposalCardParts(view, csrf, inert, decision, returnTo = null) {
17608
18004
  : view.kind === "control"
17609
18005
  ? (isChatControl(payload["control"]) ? `<a class="button-link" href="${escape(chatControlHref(payload["control"], task, payload["run"], payload["project"]))}">${escape(CHAT_CONTROLS[payload["control"]].label)}</a>` : `<p>Control unavailable.</p>`)
17610
18006
  : view.kind === "cancel"
17611
- ? `<p class="meta">cancelling is armed on the task itself — <a href="${taskHref(task)}">open ${escape(task)}</a></p>` +
17612
- `<form method="post" action="${view.actionBase}/${view.id}/dismiss" class="inline"><input type="hidden" name="csrf" value="${escape(csrf)}">${returnField}<button type="submit" class="quiet">dismiss</button></form>`
18007
+ ? `<p class="meta">Cancelling is armed on the task itself — <a href="${taskHref(task)}">open ${escape(task)}</a></p>` +
18008
+ `<form method="post" action="${view.actionBase}/${view.id}/dismiss" class="inline"><input type="hidden" name="csrf" value="${escape(csrf)}">${returnField}<button type="submit" class="quiet">Dismiss</button></form>`
17613
18009
  : `<div class="acts">` +
17614
18010
  `<form method="post" action="${view.actionBase}/${view.id}/confirm" class="inline"><input type="hidden" name="csrf" value="${escape(csrf)}">${returnField}` +
17615
18011
  (irreversible ? `<label class="arm"><input type="checkbox" name="confirm" value="yes"> I understand this cannot be undone</label>` : "") +
17616
18012
  `<button type="submit">${escape(presentation.action)}</button></form>` +
17617
- `<form method="post" action="${view.actionBase}/${view.id}/dismiss" class="inline"><input type="hidden" name="csrf" value="${escape(csrf)}">${returnField}<button type="submit" class="quiet">dismiss</button></form>` +
18013
+ `<form method="post" action="${view.actionBase}/${view.id}/dismiss" class="inline"><input type="hidden" name="csrf" value="${escape(csrf)}">${returnField}<button type="submit" class="quiet">Dismiss</button></form>` +
17618
18014
  `</div>`;
17619
18015
  }
17620
18016
  else if (view.state === "pending") {
@@ -17645,7 +18041,7 @@ function proposalCardParts(view, csrf, inert, decision, returnTo = null) {
17645
18041
  const html = (`<article class="card proposal proposal-${escape(view.kind)} ${escape(view.state)}" data-card-kind="${escape(view.kind)}">` +
17646
18042
  `<header class="proposal-head"><span class="proposal-icon">${strokeIcon(presentation.icon)}</span>` +
17647
18043
  `<span><strong>${escape(presentation.label)}</strong><small>proposed by ${provenance}</small></span>` +
17648
- `<span class="badge ${stateClass}">${escape(view.state)}</span></header>` +
18044
+ `<span class="badge ${stateClass}">${escape(sentenceCase(view.state))}</span></header>` +
17649
18045
  `<div class="proposal-body">${what}</div>` +
17650
18046
  `<footer class="proposal-actions">${acts}</footer></article>`);
17651
18047
  // The same card as data (chat cards): one act, in the door's own words.
@@ -17693,18 +18089,35 @@ function relativeAge(iso, now) {
17693
18089
  function coordinatorProposalsSection(proposals, decisions, csrf, now, heading = true, returnTo = null) {
17694
18090
  if (proposals.length === 0)
17695
18091
  return "";
17696
- return ((heading ? `<h2>proposed by coordinators <span class="meta">${proposals.length}</span></h2>` : "") +
18092
+ return ((heading ? `<h2>Proposed by coordinators <span class="meta">${proposals.length}</span></h2>` : "") +
17697
18093
  `<div class="coordinator-proposals">` +
17698
18094
  proposals.map(one => coordinatorProposalCard(one, csrf, now, decisions.get(typeof one.payload["decision"] === "number" ? one.payload["decision"] : -1) ?? null, returnTo)).join("") +
17699
18095
  `</div>`);
17700
18096
  }
18097
+ /** The lead's question as buttons: each option sends itself as the next message (the same POST as a typed one);
18098
+ * "Something else" moves to the composer. Nothing shows while a reply is running. */
18099
+ function mateAskHtml(ask, csrf, target, composer, open = true) {
18100
+ if (!open)
18101
+ return `<div class="so-owner-ask" data-ask="${ask.turn}"><p class="so-owner-ask-question"><strong>${escape(ask.question)}</strong></p></div>`;
18102
+ const hidden = `<input type="hidden" name="csrf" value="${escape(csrf)}">` +
18103
+ (target.task !== null ? `<input type="hidden" name="task" value="${escape(target.task)}">` : target.project !== null ? `<input type="hidden" name="project" value="${escape(target.project)}">` : "");
18104
+ return `<div class="so-owner-ask" data-ask="${ask.turn}"><p class="so-owner-ask-question"><strong>${escape(ask.question)}</strong></p>` +
18105
+ `<div class="so-owner-ask-options" role="group" aria-label="Answer options">` +
18106
+ ask.options.map(option => `<form method="post" action="/chat" class="inline">${hidden}<button type="submit" name="message" value="${escape(option)}" class="so-suggestion quiet">${escape(option)}</button></form>`).join("") +
18107
+ `<label for="${escape(composer)}" class="so-suggestion so-owner-ask-other">${escape(MATE_ASK_OTHER)}</label></div></div>`;
18108
+ }
17701
18109
  /** The thread's messages as the React conversation renders them, each
17702
18110
  * with the proposal cards its turn produced; `back` is where a card's
17703
18111
  * confirm or dismiss returns. */
17704
- function mateBrowserMessages(rows, csrf, back) {
18112
+ function mateBrowserMessages(rows, csrf, back, target = { task: null, project: null }) {
18113
+ const open = rows.pending === null ? rows.ask?.turn ?? null : null;
18114
+ const asked = (turn) => {
18115
+ const ask = turn === null ? undefined : rows.asks?.get(turn);
18116
+ return ask === undefined ? "" : mateAskHtml(ask, csrf, target, target.task === null ? "lead-message" : "task-message", ask.turn === open);
18117
+ };
17705
18118
  return rows.messages.map(message => ({
17706
18119
  id: message.id, role: message.role, text: message.text,
17707
- html: message.role === 'operator' ? `<p>${escape(message.text)}</p>` : renderChatText(message.text),
18120
+ html: message.role === 'operator' ? `<p>${escape(message.text)}</p>` : renderChatText(message.text) + asked(message.turn),
17708
18121
  activity: message.activity, createdAt: message.createdAt,
17709
18122
  ...(() => {
17710
18123
  const parts = message.turn === null ? [] : rows.proposals.filter(one => one.turn === message.turn)
@@ -17727,6 +18140,7 @@ function mateChatVersion(rows) {
17727
18140
  rows.coordinatorProposals.map(one => [one.id, one.state, one.outcome === null ? null : JSON.stringify(one.outcome)]),
17728
18141
  [...rows.decisions.values()].map(one => [one.id, one.state, one.choice ?? null]),
17729
18142
  rows.pending === null ? null : [rows.pending.id, rows.pending.steps],
18143
+ rows.ask?.turn ?? null,
17730
18144
  rows.recent[0] === undefined ? null : [rows.recent[0].id, rows.recent[0].state, rows.recent[0].failureReason],
17731
18145
  focus === null
17732
18146
  ? null
@@ -17782,6 +18196,10 @@ function mateThreadHtml(data) {
17782
18196
  const cards = message.turn === null ? [] : (byTurn.get(message.turn) ?? []);
17783
18197
  parts.push(`<div class="msg mate" data-message-role="assistant" data-key="m${message.id}"${message.id === latestReply ? ' id="latest"' : ""}>` +
17784
18198
  renderChatText(message.text) +
18199
+ (() => {
18200
+ const ask = message.turn === null ? undefined : data.asks?.get(message.turn);
18201
+ return ask === undefined ? "" : mateAskHtml(ask, data.csrf, { task: data.focusTask?.id ?? null, project: data.chatProject ?? null }, "chat-message", data.pending === null && data.ask?.turn === ask.turn);
18202
+ })() +
17785
18203
  cards.map(one => mateProposalCard(one, data.csrf, inert, data.decisions.get(typeof one.payload["decision"] === "number" ? one.payload["decision"] : -1) ?? null, data.focusTask === null ? null : returnTo)).join("") +
17786
18204
  `<div class="chat-message-foot">${chatActivity(message.activity)}<time datetime="${escape(message.createdAt)}">${escape(relativeAge(message.createdAt, data.now))}</time></div>` +
17787
18205
  `</div>`);
@@ -17817,10 +18235,10 @@ function matePage(chrome, data) {
17817
18235
  `<span>this week ${chatMoney(data.weeklySpent)} of ${chatMoney(data.config.weeklyCeilingMicrousd)}</span>`) +
17818
18236
  `</div>`,
17819
18237
  `<p class="meta">Started ${escape(data.session.mintedAt.slice(0, 16).replace("T", " "))}Z. It stays open until you end it; only bounded recent context is sent to the model.</p>`,
17820
- `<form method="post" action="/chat/mate/end" class="inline"><input type="hidden" name="csrf" value="${escape(data.csrf)}"><input type="hidden" name="return" value="${escape(returnTo)}"><button type="submit" class="quiet">end the conversation and forget the thread</button></form>`,
18238
+ `<form method="post" action="/chat/mate/end" class="inline"><input type="hidden" name="csrf" value="${escape(data.csrf)}"><input type="hidden" name="return" value="${escape(returnTo)}"><button type="submit" class="quiet">End the conversation and forget the thread</button></form>`,
17821
18239
  data.recent.length === 0
17822
18240
  ? ""
17823
- : `<p class="meta">recent turns: ${data.recent
18241
+ : `<p class="meta">Recent turns: ${data.recent
17824
18242
  .map(turn => `<span class="mono">#${turn.id}</span> ${escape(turn.state)}${turn.failureReason === null ? "" : ` · ${escape(turn.failureReason)}`} · ${subscription ? "membership" : chatMoney(turn.settledMicrousd ?? turn.reservedMicrousd)}`)
17825
18243
  .join(" · ")}</p>`,
17826
18244
  `<p class="meta"><a href="/settings/lead">Lead settings</a> · provider, model and limits</p>`,
@@ -17837,17 +18255,17 @@ function matePage(chrome, data) {
17837
18255
  if (data.problem !== null)
17838
18256
  conversation.push(`<div class="problem">${escape(data.problem)}</div>`);
17839
18257
  for (const turn of data.latched) {
17840
- conversation.push(`<div class="problem"><strong>unknown spend blocks chat.</strong> turn #${turn.id} may have cost up to ${chatMoney(turn.reservedMicrousd)} — ` +
18258
+ conversation.push(`<div class="problem"><strong>Unknown spend blocks chat.</strong> turn #${turn.id} may have cost up to ${chatMoney(turn.reservedMicrousd)} — ` +
17841
18259
  `<a href="/chat/ack/${turn.id}">read and acknowledge it</a> to re-enable this credential.</div>`);
17842
18260
  }
17843
18261
  const lastMessage = data.messages.at(-1);
17844
18262
  const latestReply = data.pending === null && lastMessage?.role === "assistant" ? lastMessage.id : null;
17845
- conversation.push(mateThreadHtml({ ...data, problem: null }));
18263
+ conversation.push(mateThreadHtml({ ...data, problem: null, chatProject }));
17846
18264
  conversation.push(
17847
18265
  // The New update action (package 2): hidden until a live update lands
17848
18266
  // while the reader is above the latest message; a real button, so the
17849
18267
  // keyboard reaches it. Only this act moves the reader.
17850
- `<div class="chat-new-update-holder"><button type="button" class="chat-new-update" id="chat-new-update" hidden>New update ↓</button></div>`, `<form method="post" action="/chat" class="card composer" id="${latestReply === null && data.pending === null ? "latest" : "chat-composer"}" aria-label="message the mate" data-chat-session="${data.session.id}" data-chat-task="${escape(data.focusTask?.id ?? "")}" data-chat-user="${escape(data.session.approver)}" data-chat-busy="${data.pending === null ? "0" : "1"}" data-chat-version="${mateChatVersion(data)}" data-chat-approval="${escape(data.focusTask?.approval?.digest ?? "")}">`, `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`, `<input type="hidden" name="request" value="${randomBytes(16).toString("hex")}"><input type="hidden" name="request-session" value="${data.session.id}">`, data.focusTask === null ? "" : `<input type="hidden" name="task" value="${escape(data.focusTask.id)}">`, chatProject === null ? "" : `<input type="hidden" name="project" value="${escape(chatProject)}">`, data.resultRunId == null ? "" : `<input type="hidden" name="result" value="${data.resultRunId}">`, `<label>message<textarea name="message" rows="1" maxlength="${MATE_MESSAGE_MAX_CHARS}" placeholder="${data.focusTask !== null ? "Ask about this task…" : chatProject !== null ? `Ask about ${escape(projectName(chatProject))}…` : "Describe what you want done…"}"></textarea></label>`, `<button type="submit" aria-label="${data.pending === null ? "send message" : "wait for the current reply before sending"}"${data.pending === null ? "" : " disabled"}>send</button>`, `</form>`,
18268
+ `<div class="chat-new-update-holder"><button type="button" class="chat-new-update" id="chat-new-update" hidden>New update ↓</button></div>`, `<form method="post" action="/chat" class="card composer" id="${latestReply === null && data.pending === null ? "latest" : "chat-composer"}" aria-label="message the mate" data-chat-session="${data.session.id}" data-chat-task="${escape(data.focusTask?.id ?? "")}" data-chat-user="${escape(data.session.approver)}" data-chat-busy="${data.pending === null ? "0" : "1"}" data-chat-version="${mateChatVersion(data)}" data-chat-approval="${escape(data.focusTask?.approval?.digest ?? "")}">`, `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`, `<input type="hidden" name="request" value="${randomBytes(16).toString("hex")}"><input type="hidden" name="request-session" value="${data.session.id}">`, data.focusTask === null ? "" : `<input type="hidden" name="task" value="${escape(data.focusTask.id)}">`, chatProject === null ? "" : `<input type="hidden" name="project" value="${escape(chatProject)}">`, data.resultRunId == null ? "" : `<input type="hidden" name="result" value="${data.resultRunId}">`, `<label>Message<textarea id="chat-message" name="message" rows="1" maxlength="${MATE_MESSAGE_MAX_CHARS}" placeholder="${data.focusTask !== null ? "Ask about this task…" : chatProject !== null ? `Ask about ${escape(projectName(chatProject))}…` : "Describe what you want done…"}"></textarea></label>`, `<button type="submit" aria-label="${data.pending === null ? "send message" : "wait for the current reply before sending"}"${data.pending === null ? "" : " disabled"}>Send</button>`, `</form>`,
17851
18269
  // Concise pass (2026-09-13): the status line speaks only when there is
17852
18270
  // a state to report — a reply in progress here, the connection from
17853
18271
  // the continuity script — and the composer carries no second intro.
@@ -17859,7 +18277,7 @@ function matePage(chrome, data) {
17859
18277
  workspace: {
17860
18278
  conversation: {
17861
18279
  sessionId: data.session.id, user: data.session.approver, version: mateChatVersion(data),
17862
- messages: mateBrowserMessages(data, data.csrf, data.focusTask === null && chatProject === null ? null : returnTo),
18280
+ messages: mateBrowserMessages(data, data.csrf, data.focusTask === null && chatProject === null ? null : returnTo, { task: data.focusTask?.id ?? null, project: chatProject }),
17863
18281
  pendingTurnId: data.pending?.id ?? null, requestId: randomBytes(16).toString('hex'), maxChars: MATE_MESSAGE_MAX_CHARS,
17864
18282
  taskId: data.focusTask?.id ?? null, resultRunId: data.resultRunId ?? null, project: chatProject,
17865
18283
  },
@@ -17879,25 +18297,25 @@ function routinesPage(chrome, tracks, form) {
17879
18297
  const capture = chrome.project === null
17880
18298
  ? ""
17881
18299
  : [
17882
- `<h2>file a standing order</h2>`,
18300
+ `<h2>File a standing order</h2>`,
17883
18301
  form.problem === null ? "" : `<div class="problem">${escape(form.problem)}</div>`,
17884
18302
  `<form method="post" action="/routines/add" class="card">`,
17885
18303
  `<input type="hidden" name="csrf" value="${escape(form.csrf)}">`,
17886
18304
  `<input type="hidden" name="projectRevision" value="${form.revision}">`,
17887
18305
  fill === null
17888
- ? `<p class="meta">start from a template: ${TEMPLATES.filter(one => one.kind === "routine")
18306
+ ? `<p class="meta">Start from a template: ${TEMPLATES.filter(one => one.kind === "routine")
17889
18307
  .map(one => `<a href="/routines?template=${escape(one.name)}">${escape(one.name)}</a>`)
17890
18308
  .join(" · ")}</p>`
17891
18309
  : `<p class="meta">${form.values === undefined ? "pre-filled from a template — edit anything" : "Your entries are kept below"}; nothing fires until you approve the standing order</p>`,
17892
- `<label>name <span class="meta">(lowercase-with-dashes — it names each instance)</span><input type="text" name="name" placeholder="nightly-deps" maxlength="41" value="${fill === null ? "" : escape(fill.name)}"></label>`,
17893
- `<label>goal <span class="meta">(what every firing is allowed to do)</span><textarea name="goal" rows="2">${fill === null ? "" : escape(fill.goal)}</textarea></label>`,
17894
- `<label>not this <span class="meta">(optional)</span><input type="text" name="not" value="${fill === null ? "" : escape(fill.not)}"></label>`,
17895
- `<label>touches <span class="meta">(paths, comma-separated, optional)</span><input type="text" name="touches" value="${fill === null ? "" : escape(fill.touches)}"></label>`,
17896
- `<label>acceptance <span class="meta">(required — one criterion per line: <code>statement | evidence,kinds | how</code>; evidence kinds are check, screenshot, changed-path, manual-review; id is optional and auto-numbered)</span><textarea name="acceptance" rows="3" placeholder="The full test suite passes | check">${fill === null ? "" : escape(fill.acceptance)}</textarea></label>`,
18310
+ `<label>Name <span class="meta">(lowercase-with-dashes — it names each instance)</span><input type="text" name="name" placeholder="nightly-deps" maxlength="41" value="${fill === null ? "" : escape(fill.name)}"></label>`,
18311
+ `<label>Goal <span class="meta">(what every firing is allowed to do)</span><textarea name="goal" rows="2">${fill === null ? "" : escape(fill.goal)}</textarea></label>`,
18312
+ `<label>Not this <span class="meta">(optional)</span><input type="text" name="not" value="${fill === null ? "" : escape(fill.not)}"></label>`,
18313
+ `<label>Touches <span class="meta">(paths, comma-separated, optional)</span><input type="text" name="touches" value="${fill === null ? "" : escape(fill.touches)}"></label>`,
18314
+ `<label>Acceptance <span class="meta">(required — one criterion per line: <code>statement | evidence,kinds | how</code>; evidence kinds are check, screenshot, changed-path, manual-review; id is optional and auto-numbered)</span><textarea name="acceptance" rows="3" placeholder="The full test suite passes | check">${fill === null ? "" : escape(fill.acceptance)}</textarea></label>`,
17897
18315
  scheduleEditorHtml(fill?.schedule ?? null, form.values),
17898
- `<label>budget <span class="meta">(dollars per rolling 7 days, optional — needs a provider that reports cost)</span><input type="text" name="ceiling" inputmode="decimal" style="width:8rem" value="${escape(form.values?.get("ceiling") ?? "")}"></label>`,
17899
- `<button type="submit">file it \u2192 approve the standing order next</button>`,
17900
- `<p class="meta">filing is cheap — nothing fires until you approve the template on the next screen, password and all</p>`,
18316
+ `<label>Budget <span class="meta">(dollars per rolling 7 days, optional — needs a provider that reports cost)</span><input type="text" name="ceiling" inputmode="decimal" style="width:8rem" value="${escape(form.values?.get("ceiling") ?? "")}"></label>`,
18317
+ `<button type="submit">File it \u2192 approve the standing order next</button>`,
18318
+ `<p class="meta">Filing is cheap — nothing fires until you approve the template on the next screen, password and all</p>`,
17901
18319
  `</form>`,
17902
18320
  ].join("\n");
17903
18321
  const list = tracks.length === 0
@@ -17917,19 +18335,19 @@ function routineScreenPage(chrome, data) {
17917
18335
  const schedule = parseSchedule(routine.schedule);
17918
18336
  const scheduleSaid = schedule === null ? routine.schedule : describeSchedule(schedule);
17919
18337
  const terms = `<div class="card">` +
17920
- `<p class="meta">goal</p><p class="recap" style="margin-top:0">${escape(routine.goal)}</p>` +
17921
- `<p class="meta">not this</p><p class="recap" style="margin-top:0">${routine.outOfScope === null ? "<em>no exclusions</em>" : escape(routine.outOfScope)}</p>` +
17922
- `<p class="meta">touches · ${routine.touches.length === 0 ? "anything" : routine.touches.map(one => escape(one)).join(", ")}</p>` +
18338
+ `<p class="meta">Goal</p><p class="recap" style="margin-top:0">${escape(routine.goal)}</p>` +
18339
+ `<p class="meta">Not this</p><p class="recap" style="margin-top:0">${routine.outOfScope === null ? "<em>no exclusions</em>" : escape(routine.outOfScope)}</p>` +
18340
+ `<p class="meta">Touches · ${routine.touches.length === 0 ? "anything" : routine.touches.map(one => escape(one)).join(", ")}</p>` +
17923
18341
  (routine.acceptance.length === 0
17924
18342
  ? ""
17925
- : `<p class="meta">acceptance</p><ul class="recap">${routine.acceptance
18343
+ : `<p class="meta">Acceptance</p><ul class="recap">${routine.acceptance
17926
18344
  .map(c => `<li><code>${escape(c.id)}</code> ${escape(c.statement)} <span class="meta">[requires: ${c.evidence.map(escape).join(", ")}]</span></li>`)
17927
18345
  .join("")}</ul>`) +
17928
- `<p class="meta">needs · ${routine.requirements.length === 0 ? "nothing beyond the repository" : routine.requirements.map(one => escape(one)).join(", ")}</p>` +
18346
+ `<p class="meta">Needs · ${routine.requirements.length === 0 ? "nothing beyond the repository" : routine.requirements.map(one => escape(one)).join(", ")}</p>` +
17929
18347
  routineAgentsHtml(routine, approved) +
17930
- `<p class="meta">schedule · ${escape(scheduleSaid)}</p>` +
17931
- `<p class="meta">budget · ${routine.costCeilingUsd === null ? "no ceiling" : `$${routine.costCeilingUsd.toFixed(2)} per rolling 7 days`}</p>` +
17932
- `<p class="meta">one at a time — a firing skips while the previous instance is unfinished</p>` +
18348
+ `<p class="meta">Schedule · ${escape(scheduleSaid)}</p>` +
18349
+ `<p class="meta">Budget · ${routine.costCeilingUsd === null ? "no ceiling" : `$${routine.costCeilingUsd.toFixed(2)} per rolling 7 days`}</p>` +
18350
+ `<p class="meta">One at a time — a firing skips while the previous instance is unfinished</p>` +
17933
18351
  `</div>`;
17934
18352
  // THE CONSENT DOOR (v48): the password and the approve act exist only
17935
18353
  // where an exact, readable route can be restated — never over agents
@@ -17961,13 +18379,13 @@ function routineScreenPage(chrome, data) {
17961
18379
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
17962
18380
  `<input type="hidden" name="nonce" value="${escape(data.nonce)}">`,
17963
18381
  `<input type="hidden" name="digest" value="${escape(routine.digest)}">`,
17964
- `<p><strong>approve this standing order:</strong></p>`,
18382
+ `<p><strong>Approve this standing order:</strong></p>`,
17965
18383
  `<p class="recap">Each firing creates a task under exactly the terms above and BUILDS IT WITHOUT ASKING — ${escape(scheduleSaid)}, until you pause it. Questions and failures still reach you like any other work.</p>`,
17966
18384
  // The agents the yes freezes (v48): the same concise block every
17967
18385
  // approval shows, before the password.
17968
18386
  routineAgentsHtml(routine, false, true),
17969
- `<label>your password, typed again — a signed-in session alone cannot agree to standing work<input type="password" name="token" autocomplete="current-password"></label>` +
17970
- `<button type="submit">approve this routine</button>`,
18387
+ `<label>Your password, typed again — a signed-in session alone cannot agree to standing work<input type="password" name="token" autocomplete="current-password"></label>` +
18388
+ `<button type="submit">Approve this routine</button>`,
17971
18389
  `</form>`,
17972
18390
  ].join("\n");
17973
18391
  const verb = (name, label, danger = false) => `<form method="post" action="${routineHref(routine.id)}/${name}" class="inline">` +
@@ -17977,7 +18395,7 @@ function routineScreenPage(chrome, data) {
17977
18395
  ? `<form method="post" action="${routineHref(routine.id)}/run-now" class="inline">` +
17978
18396
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
17979
18397
  `<input type="password" name="token" autocomplete="current-password" class="inline" placeholder="your password" aria-label="password for run now" style="width:11rem"> ` +
17980
- `<button type="submit">run now</button></form>`
18398
+ `<button type="submit">Run now</button></form>`
17981
18399
  : "";
17982
18400
  const acts = `<div class="card">` +
17983
18401
  (routine.paused ? verb("resume", "resume") : verb("pause", "pause")) +
@@ -17991,7 +18409,7 @@ function routineScreenPage(chrome, data) {
17991
18409
  const said = fire.outcome === "fired"
17992
18410
  ? fire.instanceTaskId === null
17993
18411
  ? "fired"
17994
- : `<a href="${taskHref(fire.instanceTaskId)}" class="mono">${escape(fire.instanceTaskId)}</a>${fire.instanceState === null ? "" : ` <span class="badge badge-${escape(fire.instanceState)}">${escape(fire.instanceState)}</span>`}`
18412
+ : `<a href="${taskHref(fire.instanceTaskId)}" class="mono">${escape(fire.instanceTaskId)}</a>${fire.instanceState === null ? "" : ` <span class="badge badge-${escape(fire.instanceState)}">${escape(sentenceCase(fire.instanceState))}</span>`}`
17995
18413
  : `<span class="meta">skipped — ${escape(fire.reason ?? "")}</span>`;
17996
18414
  const slot = fire.scheduledFor.replace(/^manual:/, "");
17997
18415
  return `<p class="row">${said}<span class="right meta mono">${fire.reason === "manual" ? "run now · " : ""}${whenTime(slot)}</span></p>`;
@@ -18006,14 +18424,14 @@ function routineScreenPage(chrome, data) {
18006
18424
  ? `<div class="problem">stopped behind <a href="${taskHref(data.blocker.taskId)}" class="mono">${escape(data.blocker.taskId)}</a> (${escape(data.blocker.state)}) — the track resumes when it finishes or is cancelled</div>`
18007
18425
  : "",
18008
18426
  approved && !routine.paused && routine.nextFireAt !== null
18009
- ? `<p class="meta">next fire ${whenTime(routine.nextFireAt)}</p>`
18427
+ ? `<p class="meta">Next fire ${whenTime(routine.nextFireAt)}</p>`
18010
18428
  : "",
18011
- "<h2>the standing order</h2>",
18429
+ "<h2>The standing order</h2>",
18012
18430
  terms,
18013
18431
  approveForm,
18014
- "<h2>acts</h2>",
18432
+ "<h2>Acts</h2>",
18015
18433
  acts,
18016
- "<h2>firings</h2>",
18434
+ "<h2>Firings</h2>",
18017
18435
  ledger,
18018
18436
  ].join("\n"), { chrome });
18019
18437
  }
@@ -18032,21 +18450,21 @@ function homePage(chrome, data) {
18032
18450
  .map(decision => `<a class="decide-card" href="/d/${decision.id}">` +
18033
18451
  `<p class="q">${escape(decision.question)}</p>` +
18034
18452
  `<span class="mono meta">${escape(decision.taskId)}</span>` +
18035
- `${isOverdue(decision, data.now) ? ` <span class="badge badge-overdue">overdue</span>` : ""}` +
18453
+ `${isOverdue(decision, data.now) ? ` <span class="badge badge-overdue">Overdue</span>` : ""}` +
18036
18454
  `</a>`)
18037
18455
  .join("\n");
18038
18456
  const incidents = data.incidents.length === 0
18039
18457
  ? ""
18040
- : `<h2>incidents</h2><p class="hint">builds that stopped and need a person — resolve here, or open the task to retry it</p>` +
18458
+ : `<h2>Incidents</h2><p class="hint">builds that stopped and need a person — resolve here, or open the task to retry it</p>` +
18041
18459
  data.incidents
18042
18460
  .map(one => `<p class="row"><a href="${taskHref(one.taskId)}">${escape(one.taskId)}</a> — ${escape(incidentWords(one.kind))}` +
18043
18461
  `<span class="right"><form method="post" action="/i/${one.id}/resolve" class="inline">` +
18044
18462
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
18045
- `<button type="submit">resolve</button></form></span></p>`)
18463
+ `<button type="submit">Resolve</button></form></span></p>`)
18046
18464
  .join("\n");
18047
18465
  const stranded = data.stranded.length === 0
18048
18466
  ? ""
18049
- : `<h2>tasks waiting on failed work</h2><p class="hint">open a task and choose whether to try the failed work again, wait for something else, or continue without it</p>` +
18467
+ : `<h2>Tasks waiting on failed work</h2><p class="hint">open a task and choose whether to try the failed work again, wait for something else, or continue without it</p>` +
18050
18468
  data.stranded
18051
18469
  .map(one => `<p class="row"><a href="${taskHref(one.id)}">${escape(one.id)}</a> waits on ${one.blockedBy
18052
18470
  .map(blocker => `<a href="${taskHref(blocker)}">${escape(blocker)}</a>`)
@@ -18056,7 +18474,7 @@ function homePage(chrome, data) {
18056
18474
  ? ""
18057
18475
  : data.gaps.length === 0
18058
18476
  ? ""
18059
- : `<h2>missing requirements</h2><p class="hint">tools or credentials builds need — checked before any money is spent</p>` +
18477
+ : `<h2>Missing requirements</h2><p class="hint">tools or credentials builds need — checked before any money is spent</p>` +
18060
18478
  data.gaps
18061
18479
  .map(gap => `<p class="row"><a href="/caps">${escape(gap.key)}</a> — ${escape(gap.state)}<span class="right meta">how to fix →</span></p>`)
18062
18480
  .join("\n") +
@@ -18068,7 +18486,7 @@ function homePage(chrome, data) {
18068
18486
  // an operator actually watches this page, so it re-renders itself.
18069
18487
  const building = data.building.length === 0
18070
18488
  ? ""
18071
- : `<h2>building now</h2><p class="hint">live builds — this page refreshes itself every 10 seconds while anything runs</p><div class="cards">` +
18489
+ : `<h2>Building now</h2><p class="hint">live builds — this page refreshes itself every 10 seconds while anything runs</p><div class="cards">` +
18072
18490
  data.building
18073
18491
  .map(claim => `<a class="stat-card" href="${taskHref(claim.taskId)}" style="text-decoration:none">` +
18074
18492
  `<span class="k"><span class="dot dot-ok pulse"></span>${escape(claim.taskId)}</span>` +
@@ -18104,8 +18522,8 @@ function homePage(chrome, data) {
18104
18522
  : `last window: ${data.episode.built} built, ${data.episode.broke} broke \u00b7 ended ${whenTime(data.episode.endedAt)}`}</span></div>`;
18105
18523
  const fleetCards = [...runnerCards, watchCard, ...worktreeCards].filter(one => one !== "");
18106
18524
  const fleet = fleetCards.length === 0
18107
- ? `<h2>system status</h2><p class="hint">No builder is connected yet. On the machine where the project lives, open that folder and run <code>toolroll up</code>.</p>`
18108
- : `<h2>system status</h2><p class="hint">builders execute tasks in isolated temporary copies of each project</p><div class="cards">${fleetCards.join("")}</div>`;
18525
+ ? `<h2>System status</h2><p class="hint">No builder is connected yet. On the machine where the project lives, open that folder and run <code>toolroll up</code>.</p>`
18526
+ : `<h2>System status</h2><p class="hint">builders execute tasks in isolated temporary copies of each project</p><div class="cards">${fleetCards.join("")}</div>`;
18109
18527
  const startHere = data.taskCount === 0
18110
18528
  ? [
18111
18529
  `<div class="card">`,
@@ -18125,10 +18543,10 @@ function homePage(chrome, data) {
18125
18543
  : `<p class="meta"><strong>${escape(projectName(data.repo))}</strong> — the last 24 hours, honestly labeled: a rolling window, whatever your hours are</p>`,
18126
18544
  startHere,
18127
18545
  ledger,
18128
- `<p class="meta">spend: ${escape(consoleSpend(summary))}</p>`,
18129
- data.outboxPending > 0 ? `<p class="meta">notifications: ${data.outboxPending} pending delivery</p>` : "",
18546
+ `<p class="meta">Spend: ${escape(consoleSpend(summary))}</p>`,
18547
+ data.outboxPending > 0 ? `<p class="meta">Notifications: ${data.outboxPending} pending delivery</p>` : "",
18130
18548
  building,
18131
- `<h2>needs your decision</h2><p class="hint">an agent stopped mid-build to ask — nothing proceeds until you answer</p>`,
18549
+ `<h2>Needs your decision</h2><p class="hint">an agent stopped mid-build to ask — nothing proceeds until you answer</p>`,
18132
18550
  decide,
18133
18551
  incidents,
18134
18552
  stranded,
@@ -18197,21 +18615,21 @@ function taskComposerHtml(data) {
18197
18615
  `<details class="task-options"${prefill === null ? "" : " open"}>`,
18198
18616
  `<summary><span>Edit details</span><small>optional · defaults are remembered</small></summary>`,
18199
18617
  `<div class="task-options-grid">`,
18200
- `<label class="wide">goal <span class="meta">— provide upfront for automatic approval of an unchanged plan</span>` +
18618
+ `<label class="wide">Goal <span class="meta">— provide upfront for automatic approval of an unchanged plan</span>` +
18201
18619
  `<textarea name="goal" rows="3" placeholder="What success looks like">${prefill === null ? "" : escape(prefill.goal)}</textarea></label>`,
18202
- `<label class="wide">acceptance <span class="meta">— required with a goal; one per line: <code>statement | evidence,kinds | how</code></span>` +
18620
+ `<label class="wide">Acceptance <span class="meta">— required with a goal; one per line: <code>statement | evidence,kinds | how</code></span>` +
18203
18621
  `<textarea name="acceptance" rows="3" placeholder="Requests over the limit return 429 | check">${prefill === null ? "" : escape(prefill.acceptance)}</textarea></label>`,
18204
- `<label>not this <span class="meta">— optional boundary</span><input type="text" name="not" value="${prefill === null ? "" : escape(prefill.not)}"></label>`,
18205
- `<label>likely touches <span class="meta">— paths, comma-separated</span><input type="text" name="touches" value="${prefill === null ? "" : escape(prefill.touches)}"></label>`,
18622
+ `<label>Not this <span class="meta">— optional boundary</span><input type="text" name="not" value="${prefill === null ? "" : escape(prefill.not)}"></label>`,
18623
+ `<label>Likely touches <span class="meta">— paths, comma-separated</span><input type="text" name="touches" value="${prefill === null ? "" : escape(prefill.touches)}"></label>`,
18206
18624
  `<label class="wide task-check"><input type="checkbox" name="plan-first" value="1"${values === undefined || values.get("plan-first") === "1" ? " checked" : ""}><span><strong>Let the planner inspect first</strong><small class="meta">Recommended. It drafts the goal, acceptance criteria, and implementation approach, and asks only when a missing answer materially changes the work.</small></span></label>`,
18207
18625
  `<label class="wide task-check"><input type="checkbox" name="scout" value="1"${values?.get("scout") === "1" ? " checked" : ""}><span><strong>Research only</strong><small class="meta">Deliver a read-only report instead of changing the repository.</small></span></label>`,
18208
- `<label>task id <span class="meta">— optional</span><input type="text" name="id" value="${escape(values?.get("id") ?? "")}" placeholder="made from the request"></label>`,
18626
+ `<label>Task id <span class="meta">— optional</span><input type="text" name="id" value="${escape(values?.get("id") ?? "")}" placeholder="made from the request"></label>`,
18209
18627
  candidates.length === 0
18210
18628
  ? ""
18211
- : `<label>starts after <span class="meta">— optional</span><select name="after"><option value="">right away</option>` +
18629
+ : `<label>Starts after <span class="meta">— optional</span><select name="after"><option value="">right away</option>` +
18212
18630
  candidates.map(one => `<option value="${escape(one.id)}"${one.id === after ? " selected" : ""}>${escape(one.id)} — ${escape(one.title)}</option>`).join("") +
18213
18631
  `</select></label>`,
18214
- `<fieldset class="permission-field"><legend>agent permissions</legend>${permissionModeChoices("permission-mode", values?.get("permission-mode") === "bypassPermissions" ? "bypassPermissions" : values?.get("permission-mode") === "auto" ? "auto" : data.permissionDefault)}` +
18632
+ `<fieldset class="permission-field"><legend>Agent permissions</legend>${permissionModeChoices("permission-mode", values?.get("permission-mode") === "bypassPermissions" ? "bypassPermissions" : values?.get("permission-mode") === "auto" ? "auto" : data.permissionDefault)}` +
18215
18633
  `<p class="meta permission-note">Inherited from Settings. You can still change it on the proposed scope before approval.</p></fieldset>`,
18216
18634
  `</div>`,
18217
18635
  `</details>`,
@@ -18307,9 +18725,9 @@ function workPage(chrome, data) {
18307
18725
  project: data.multiProject || row.repo === null ? (row.repo === null ? 'Unplaced' : projectName(row.repo)) : null,
18308
18726
  age: relativeAge(row.updatedAt, data.now),
18309
18727
  status: { label: row.status.label, tone: row.status.tone, token: row.status.token },
18310
- ask: row.ask, group: row.ask ?? (row.status.rank === 1 ? 'building' : 'rest'),
18728
+ ask: row.ask, chip: row.chip, group: row.ask ?? (row.status.rank === 1 ? 'building' : 'rest'),
18311
18729
  action: actionHref === null || row.primaryAction === null ? null : { label: row.primaryAction.label, href: actionHref },
18312
- detail: needsYouDetail ? row.status.detail : null,
18730
+ detail: needsYouDetail ? row.status.detail : row.progress ?? null,
18313
18731
  problem: row.familyProblem,
18314
18732
  notes: (row.status.diagnostics ?? []).map(one => `${one.label} · ${one.detail}`),
18315
18733
  };
@@ -18337,12 +18755,12 @@ function tasksPage(chrome, tasks, state, csrf, problem, repo = null, prefill = n
18337
18755
  .join("\n");
18338
18756
  return screen("tasks", [
18339
18757
  "<h1>Tasks</h1>",
18340
- `<p class="meta">work you want done${repo === null ? "" : ` in <strong>${escape(projectName(repo))}</strong>`} \u2014 a task builds unattended only after its scope is approved; open one to write or approve its scope</p>`,
18758
+ `<p class="meta">Work you want done${repo === null ? "" : ` in <strong>${escape(projectName(repo))}</strong>`} \u2014 a task builds unattended only after its scope is approved; open one to write or approve its scope</p>`,
18341
18759
  repo === null ? "" : `<p class="meta path-words"><span class="mono">${escape(repo)}</span></p>`,
18342
18760
  problem === null ? "" : `<div class="problem">${escape(problem)}</div>`,
18343
- `<p class="meta">filter: <a href="/tasks">all</a> · ${filters}</p>`,
18761
+ `<p class="meta">Filter: <a href="/tasks">all</a> · ${filters}</p>`,
18344
18762
  rows,
18345
- `<h2>add a task</h2>`,
18763
+ `<h2>Add a task</h2>`,
18346
18764
  taskComposerHtml({ csrf, project: repo, prefill, permissionDefault, qualityDefault }),
18347
18765
  ].join("\n"), { chrome });
18348
18766
  }
@@ -18352,21 +18770,21 @@ function browsePage(chrome, data) {
18352
18770
  `<form method="post" action="/projects/open" class="inline">`,
18353
18771
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
18354
18772
  `<input type="hidden" name="path" value="${escape(path)}">`,
18355
- `<button type="submit">open</button>`,
18773
+ `<button type="submit">Open</button>`,
18356
18774
  `</form>`,
18357
18775
  ].join("");
18358
18776
  return screen("projects", [
18359
18777
  `<h1>Choose a folder</h1>`,
18360
18778
  `<p class="meta">git repositories float to the top and can be opened; anything else can be entered — only folders under ${data.roots.length === 1 ? `<span class="mono">${escape(projectName(data.root))}</span>` : "the configured roots"} are visible here</p>`,
18361
18779
  data.roots.length > 1
18362
- ? `<p class="meta">roots: ${data.roots.map(one => `<a href="/projects/browse?at=${encodeURIComponent(one)}" class="mono">${escape(projectName(one))}</a>`).join(" · ")}</p>`
18780
+ ? `<p class="meta">Roots: ${data.roots.map(one => `<a href="/projects/browse?at=${encodeURIComponent(one)}" class="mono">${escape(projectName(one))}</a>`).join(" · ")}</p>`
18363
18781
  : "",
18364
18782
  `<p class="mono meta">${escape(crumb)}</p>`,
18365
18783
  data.parent === null
18366
18784
  ? ""
18367
18785
  : `<p class="row"><a href="/projects/browse?at=${encodeURIComponent(data.parent)}">\u2190 up one level</a></p>`,
18368
18786
  data.entries.length === 0
18369
- ? `<p class="meta">no folders here</p>`
18787
+ ? `<p class="meta">No folders here</p>`
18370
18788
  : data.entries
18371
18789
  .map(one => `<p class="row">` +
18372
18790
  `<a href="/projects/browse?at=${encodeURIComponent(one.path)}"><strong>${escape(one.name)}</strong></a>` +
@@ -18384,7 +18802,7 @@ function browsePage(chrome, data) {
18384
18802
  * switching context must never depend on remembering which repo is open.
18385
18803
  */
18386
18804
  function workbenchRail(data) {
18387
- const workspace = (repo) => `<span class="badge">${repo === null ? "unplaced" : escape(projectName(repo))}</span>`;
18805
+ const workspace = (repo) => `<span class="badge">${repo === null ? "Unplaced" : escape(projectName(repo))}</span>`;
18388
18806
  const row = (card, reason) => `<a class="wb-row${card.taskId === data.selected ? " wb-selected" : ""}" href="/workbench?t=${encodeURIComponent(card.taskId)}"` +
18389
18807
  `${card.taskId === data.selected ? ` aria-current="true"` : ""}>` +
18390
18808
  `<span class="wb-title">${escape(card.title)}</span>` +
@@ -18394,7 +18812,7 @@ function workbenchRail(data) {
18394
18812
  (cards.length === 0 ? `<p class="meta">${empty}</p>` : cards.slice(0, 100).map(render).join("\n")) +
18395
18813
  `</section>`;
18396
18814
  const parts = [];
18397
- parts.push(`<div class="wb-rail-head"><div><span class="eyebrow">portfolio</span><h2>all projects</h2></div>` +
18815
+ parts.push(`<div class="wb-rail-head"><div><span class="eyebrow">portfolio</span><h2>All projects</h2></div>` +
18398
18816
  `<a href="/projects">manage →</a></div>`);
18399
18817
  parts.push(group("needs you", data.attention, "Nothing needs your input.", card => row(card, escape(card.reason))));
18400
18818
  parts.push(group("in progress", data.building, "No agent is working right now.", card => {
@@ -18406,23 +18824,23 @@ function workbenchRail(data) {
18406
18824
  parts.push(group("blocked & waiting", data.waiting, "Nothing is blocked or paused.", card => row(card, escape(card.reason))));
18407
18825
  parts.push(group("up next", data.queued, "The ready queue is empty.", card => row(card, `${escape(card.reason)}${card.assignedRunner === null ? "" : ` · reserved for ${escape(card.assignedRunner)}`}`)));
18408
18826
  if (data.done.length > 0) {
18409
- parts.push(`<section class="wb-group"><h2>just finished <span class="lane-count">${data.done.length}</span></h2>`);
18827
+ parts.push(`<section class="wb-group"><h2>Just finished <span class="lane-count">${data.done.length}</span></h2>`);
18410
18828
  parts.push(data.done
18411
18829
  .map(one => `<a class="wb-row${one.taskId === data.selected ? " wb-selected" : ""}" href="/workbench?t=${encodeURIComponent(one.taskId)}">` +
18412
18830
  `<span class="wb-title">${escape(one.title)}</span>` +
18413
18831
  `<span class="wb-meta"><span class="mono meta">${escape(one.taskId)}</span>${workspace(one.repo)} ` +
18414
- `<span class="badge badge-${one.outcome === "built" || one.outcome === "no-change" ? "done" : "failed"}">${escape(one.outcome ?? "?")}</span></span></a>`)
18832
+ `<span class="badge badge-${one.outcome === "built" || one.outcome === "no-change" ? "done" : "failed"}">${escape(sentenceCase(one.outcome ?? "?"))}</span></span></a>`)
18415
18833
  .join("\n"));
18416
18834
  parts.push(`</section>`);
18417
18835
  }
18418
18836
  if (data.saturated)
18419
- parts.push(`<p class="meta">more exists — this rail is capped; the <a href="/board?scope=all">board</a> holds the rest</p>`);
18837
+ parts.push(`<p class="meta">More exists — this rail is capped; the <a href="/board?scope=all">board</a> holds the rest</p>`);
18420
18838
  return parts.join("\n");
18421
18839
  }
18422
18840
  /** The project chip every all-scope row wears: null is UNPLACED, said so. */
18423
18841
  function projectChip(repo) {
18424
18842
  return repo === null || repo === undefined
18425
- ? ` <span class="badge">unplaced</span>`
18843
+ ? ` <span class="badge">Unplaced</span>`
18426
18844
  : ` <span class="badge">${escape(projectName(repo))}</span>`;
18427
18845
  }
18428
18846
  /**
@@ -18440,11 +18858,11 @@ function decisionAnswerCard(decision, csrf, now, chip, returnTo = null) {
18440
18858
  const options = decision.options
18441
18859
  .map(option => {
18442
18860
  const recommended = option.id === decision.recommendation
18443
- ? ` <span class="badge">recommended</span>`
18861
+ ? ` <span class="badge">Recommended</span>`
18444
18862
  : "";
18445
18863
  if (!option.reversible) {
18446
18864
  return (`<p class="decide-option"><a href="/d/${decision.id}${returnTo === null ? "" : `?return=${encodeURIComponent(returnTo)}`}">${escape(option.label)}</a>` +
18447
- ` <span class="badge badge-overdue">irreversible</span>${recommended}` +
18865
+ ` <span class="badge badge-overdue">Irreversible</span>${recommended}` +
18448
18866
  ` <span class="meta">${escape(option.consequence)}</span></p>`);
18449
18867
  }
18450
18868
  return (`<form class="decide-option decide-inline" method="post" action="/d/${decision.id}/answer">` +
@@ -18459,7 +18877,7 @@ function decisionAnswerCard(decision, csrf, now, chip, returnTo = null) {
18459
18877
  `<p class="q">${escape(decision.question)}</p>` +
18460
18878
  `<details class="decision-context"><summary>Context</summary><p class="meta">${escape(decision.recap)}</p><span class="meta mono">${escape(decision.taskId)}</span></details>` +
18461
18879
  `<p class="meta">${chip ? projectChip(decision.repo) : ""}` +
18462
- `${isOverdue(decision, now) ? ` <span class="badge badge-overdue">overdue</span>` : ""}` +
18880
+ `${isOverdue(decision, now) ? ` <span class="badge badge-overdue">Overdue</span>` : ""}` +
18463
18881
  ` <a href="/d/${decision.id}${returnTo === null ? "" : `?return=${encodeURIComponent(returnTo)}`}">View details →</a></p>` +
18464
18882
  `<div class="decide-options">${options}</div></div>`);
18465
18883
  }
@@ -18540,10 +18958,10 @@ function portfolioOverview(data) {
18540
18958
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
18541
18959
  `<input type="hidden" name="path" value="${escape(one.repo)}">` +
18542
18960
  `<input type="hidden" name="return" value="/board">` +
18543
- `<button type="submit">board →</button></form>`;
18961
+ `<button type="submit">Board →</button></form>`;
18544
18962
  return (`<div class="workspace-card${one.attention > 0 ? " hot" : ""}">` +
18545
18963
  `<div class="workspace-head"><span class="workspace-name">${escape(workspace(one.repo))}</span>` +
18546
- `<span class="badge ${status.cls}">${status.word}</span>${boardForm}${one.repo === null || data.csrf === "" ? "" : `<form method="post" action="/projects/open" class="inline">${hiddenFields({ csrf: data.csrf, path: one.repo, return: "/control" })}<button>set up →</button></form>`}</div>` +
18964
+ `<span class="badge ${status.cls}">${sentenceCase(status.word)}</span>${boardForm}${one.repo === null || data.csrf === "" ? "" : `<form method="post" action="/projects/open" class="inline">${hiddenFields({ csrf: data.csrf, path: one.repo, return: "/control" })}<button>Set up →</button></form>`}</div>` +
18547
18965
  `<div class="workspace-stats">` +
18548
18966
  `<span class="pulse-stat${one.attention > 0 ? " hot" : ""}"><b>${one.attention}</b> need you</span>` +
18549
18967
  `<span class="pulse-stat"><b>${one.building}</b> live</span>` +
@@ -18595,7 +19013,7 @@ function portfolioOverview(data) {
18595
19013
  // ---- running: current live claims, project chips on ----
18596
19014
  const liveRows = data.live
18597
19015
  .map(one => `<p class="row"><a href="${taskHref(one.taskId)}">${escape(one.taskId)}</a>${projectChip(one.repo)} ` +
18598
- `<span class="badge badge-running">running</span> ` +
19016
+ `<span class="badge badge-running">Running</span> ` +
18599
19017
  `<span class="mono meta">${escape(one.runner)}${one.model === null ? "" : ` · ${escape(one.model)}`} · ` +
18600
19018
  `<time data-elapsed-since="${escape(one.claimedAt)}"></time></span></p>`)
18601
19019
  .join("\n");
@@ -18603,8 +19021,8 @@ function portfolioOverview(data) {
18603
19021
  const chipClass = (outcome) => outcome === "built" || outcome === "no-change" ? "badge-done" : outcome === "failed" ? "badge-failed" : "";
18604
19022
  const ledgerRows = data.ledger
18605
19023
  .map(one => `<p class="row"><a href="/r/${one.runId}">${escape(one.title)}</a>${projectChip(one.repo)} ` +
18606
- `<span class="badge ${chipClass(one.outcome)}">${escape(one.outcome)}</span> ` +
18607
- `${one.role === "scout" ? `<a class="badge" href="${taskHref(one.taskId)}#report">report</a> ` : ""}` +
19024
+ `<span class="badge ${chipClass(one.outcome)}">${escape(sentenceCase(one.outcome))}</span> ` +
19025
+ `${one.role === "scout" ? `<a class="badge" href="${taskHref(one.taskId)}#report">Report</a> ` : ""}` +
18608
19026
  `<span class="mono meta">${one.provider === null ? "" : escape(one.provider)}${one.model === null ? "" : ` · ${escape(one.model)}`}` +
18609
19027
  `${one.ranMinutes === null ? "" : ` · ${one.ranMinutes}m`}` +
18610
19028
  ` · ${escape(runCostWords({ authMode: one.authMode, costUsd: one.costUsd, tokensIn: null, tokensOut: null }))}` +
@@ -18618,11 +19036,11 @@ function portfolioOverview(data) {
18618
19036
  })()}</span></p>`)
18619
19037
  .join("\n");
18620
19038
  return [
18621
- `<div class="control-room-head"><div><h1>portfolio</h1>` +
18622
- `<p class="meta">every project and live build in one place</p></div>` +
18623
- `<div class="actions"><a class="badge" href="/tasks/new">+ new task</a><a class="badge" href="/board?scope=all">full board →</a></div></div>`,
19039
+ `<div class="control-room-head"><div><h1>Portfolio</h1>` +
19040
+ `<p class="meta">Every project and live build in one place</p></div>` +
19041
+ `<div class="actions"><a class="badge" href="/tasks/new">+ New task</a><a class="badge" href="/board?scope=all">Full board →</a></div></div>`,
18624
19042
  data.saturated ? `<div class="problem">This overview reached its 200-task display cap; the task list holds the rest.</div>` : "",
18625
- `<h2>waits on you</h2>`,
19043
+ `<h2>Waits on you</h2>`,
18626
19044
  waitCount === 0
18627
19045
  ? `<div class="answered"><strong>Nothing needs you.</strong> <span class="meta">You can leave this open; live state updates in the rail.</span></div>`
18628
19046
  : [
@@ -18634,17 +19052,17 @@ function portfolioOverview(data) {
18634
19052
  `<p class="meta"><a href="/next">clear the queue → one thing at a time</a></p>`,
18635
19053
  ].filter(part => part !== "").join("\n"),
18636
19054
  data.gapsProject === null
18637
- ? `<p class="meta">requirement gaps are checked one project at a time — open a project to see and fill its gaps · <a href="/projects">open a project</a></p>`
19055
+ ? `<p class="meta">Requirement gaps are checked one project at a time — open a project to see and fill its gaps · <a href="/projects">open a project</a></p>`
18638
19056
  : "",
18639
- `<h2>project pulse</h2>`,
19057
+ `<h2>Project pulse</h2>`,
18640
19058
  `<p class="hint">one row per repository</p>`,
18641
19059
  pulseRows.length === 0
18642
19060
  ? `<div class="card"><p><strong>No active work yet.</strong></p><p class="meta">Queue a task and its progress will show here across every workspace.</p></div>`
18643
19061
  : `<div class="workspace-pulse">${workspaceRows}</div>`,
18644
- `<h2>the last 24 hours</h2>`,
19062
+ `<h2>The last 24 hours</h2>`,
18645
19063
  `<p class="hint">runs started in the last 24 hours</p>`,
18646
19064
  data.runs24.length === 0
18647
- ? `<p class="meta">no runs started in the window</p>`
19065
+ ? `<p class="meta">No runs started in the window</p>`
18648
19066
  : `<p class="row"><span class="meta">runs started</span> <span class="mono">${data.runs24.length}</span></p>` +
18649
19067
  `<p class="row"><span class="meta">outcomes</span> <span class="mono">${escape(outcomeWords)}</span></p>` +
18650
19068
  `<p class="row"><span class="meta">spend</span> <span class="mono">${escape(spendLine(summary))}</span></p>` +
@@ -18654,10 +19072,10 @@ function portfolioOverview(data) {
18654
19072
  (summary.tokens > 0
18655
19073
  ? `<p class="row"><span class="meta">tokens</span> <span class="mono">${summary.tokens.toLocaleString()}</span></p>`
18656
19074
  : ""),
18657
- `<h2>running</h2>`,
18658
- data.live.length === 0 ? `<p class="meta">no agent is working right now</p>` : liveRows,
18659
- `<h2>terminal runs started in the last 24 hours</h2>`,
18660
- data.ledger.length === 0 ? `<p class="meta">none yet</p>` : ledgerRows,
19075
+ `<h2>Running</h2>`,
19076
+ data.live.length === 0 ? `<p class="meta">No agent is working right now</p>` : liveRows,
19077
+ `<h2>Terminal runs started in the last 24 hours</h2>`,
19078
+ data.ledger.length === 0 ? `<p class="meta">None yet</p>` : ledgerRows,
18661
19079
  ].join("\n");
18662
19080
  }
18663
19081
  /** Dollars for a screen: micro-USD stated as money, unknowables in words. */
@@ -18753,7 +19171,7 @@ function contestPage(chrome, data) {
18753
19171
  const glance = summaries.length < 2
18754
19172
  ? ""
18755
19173
  : `<div class="scroll-x"><table class="contest-glance">` +
18756
- `<tr><td></td>${summaries.map(one => `<th>agent ${one.contestant.ordinal}<span class="meta"> · ${escape(one.contestant.provider)} · ${escape(one.contestant.model)}</span>${one.winner ? ` <span class="badge badge-done">picked</span>` : ""}</th>`).join("")}</tr>` +
19174
+ `<tr><td></td>${summaries.map(one => `<th>Agent ${one.contestant.ordinal}<span class="meta"> · ${escape(one.contestant.provider)} · ${escape(one.contestant.model)}</span>${one.winner ? ` <span class="badge badge-done">Picked</span>` : ""}</th>`).join("")}</tr>` +
18757
19175
  `<tr><td class="meta">outcome</td>${summaries.map(one => `<td>${escape(one.outcome)}</td>`).join("")}</tr>` +
18758
19176
  `<tr><td class="meta">changed</td>${summaries.map(one => `<td>${escape(one.diffWords)}</td>`).join("")}</tr>` +
18759
19177
  `<tr><td class="meta">time</td>${summaries.map(one => `<td>${one.minutes === null ? "—" : `${one.minutes} min`}</td>`).join("")}</tr>` +
@@ -18766,22 +19184,22 @@ function contestPage(chrome, data) {
18766
19184
  const agent = summary.agent;
18767
19185
  const parts = [
18768
19186
  `<div class="card${winner ? " picked" : ""}">`,
18769
- `<p><strong>agent ${contestant.ordinal}</strong> <span class="meta">${escape(contestant.provider)} · ${escape(contestant.model)}</span>` +
18770
- `${winner ? ` <span class="badge badge-done">picked</span>` : ""}</p>`,
19187
+ `<p><strong>Agent ${contestant.ordinal}</strong> <span class="meta">${escape(contestant.provider)} · ${escape(contestant.model)}</span>` +
19188
+ `${winner ? ` <span class="badge badge-done">Picked</span>` : ""}</p>`,
18771
19189
  `<p class="row">${escape(outcome)}` +
18772
19190
  `${minutes === null ? "" : ` <span class="meta">· ${minutes} min</span>`}` +
18773
19191
  `${asked > 0 ? ` <span class="meta">· asked ${asked} question${asked > 1 ? "s" : ""}</span>` : ""}` +
18774
19192
  `${run === null ? "" : ` <span class="meta">· <a href="/r/${run.id}">the build</a></span>`}</p>`,
18775
- `<p class="row"><strong>cost</strong> ${escape(cost)}</p>`,
18776
- run === null || run.handoff === null ? "" : `<p><strong>its own conclusion</strong></p><p class="recap">${escape(run.handoff)}</p>`,
18777
- diff === null ? `<p class="meta">no diff was captured</p>` : terminalDiffCard(diff, run === null ? 0 : run.id),
19193
+ `<p class="row"><strong>Cost</strong> ${escape(cost)}</p>`,
19194
+ run === null || run.handoff === null ? "" : `<p><strong>Its own conclusion</strong></p><p class="recap">${escape(run.handoff)}</p>`,
19195
+ diff === null ? `<p class="meta">No diff was captured</p>` : terminalDiffCard(diff, run === null ? 0 : run.id),
18778
19196
  ];
18779
19197
  if (picking) {
18780
19198
  if (agent.pickable) {
18781
- parts.push(`<form method="post" action="/contest/${contest.id}/arm" class="inline">`, `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`, `<input type="hidden" name="choice" value="${contestant.id}">`, `<button type="submit">pick this result…</button>`, `</form>`, `<p class="meta">picking continues to a confirmation screen — nothing happens yet</p>`);
19199
+ parts.push(`<form method="post" action="/contest/${contest.id}/arm" class="inline">`, `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`, `<input type="hidden" name="choice" value="${contestant.id}">`, `<button type="submit">Pick this result…</button>`, `</form>`, `<p class="meta">Picking continues to a confirmation screen — nothing happens yet</p>`);
18782
19200
  }
18783
19201
  else {
18784
- parts.push(`<p class="meta">cannot be picked — ${escape(agent.unpickableReason ?? "")}</p>`);
19202
+ parts.push(`<p class="meta">Cannot be picked — ${escape(agent.unpickableReason ?? "")}</p>`);
18785
19203
  }
18786
19204
  }
18787
19205
  parts.push(`</div>`);
@@ -18793,11 +19211,11 @@ function contestPage(chrome, data) {
18793
19211
  `only one result will be kept as the task's outcome; the rest stay archived with their evidence</p>`,
18794
19212
  data.problem === null ? "" : `<div class="problem">${escape(data.problem)}</div>`,
18795
19213
  `<p class="row"><strong>${escape((CONTEST_STATE_WORDS[contest.state] ?? "the tournament is in an unexpected state — the records have the detail").replace(/tournament/g, contestNoun(contest.kind)))}</strong></p>`,
18796
- contest.pickedBy === null ? "" : `<p class="meta">picked by ${escape(contest.pickedBy)} at ${whenTime(contest.pickedAt ?? "")}</p>`,
19214
+ contest.pickedBy === null ? "" : `<p class="meta">Picked by ${escape(contest.pickedBy)} at ${whenTime(contest.pickedAt ?? "")}</p>`,
18797
19215
  contest.kind === "comparison"
18798
- ? `<p class="row"><strong>spend</strong> ${escape(contestDollars(data.totalMicrousd))} measured on the lanes that report dollars` +
19216
+ ? `<p class="row"><strong>Spend</strong> ${escape(contestDollars(data.totalMicrousd))} measured on the lanes that report dollars` +
18799
19217
  `${data.anyUnknown ? ` <span class="meta">— the rest report tokens only</span>` : ""}</p>`
18800
- : `<p class="row"><strong>charged so far</strong> ${escape(contestDollars(data.totalMicrousd))}` +
19218
+ : `<p class="row"><strong>Charged so far</strong> ${escape(contestDollars(data.totalMicrousd))}` +
18801
19219
  `${data.anyUnknown ? ` <span class="meta">— includes at least one agent charged its full reservation because the exact figure was unknowable</span>` : ""}</p>`,
18802
19220
  glance,
18803
19221
  `<div class="contest-compare">${summaries.map(agentCard).join("\n")}</div>`,
@@ -18807,9 +19225,9 @@ function contestPage(chrome, data) {
18807
19225
  `<form method="post" action="/contest/${contest.id}/arm" class="inline">`,
18808
19226
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
18809
19227
  `<input type="hidden" name="act" value="abandon">`,
18810
- `<button type="submit">abandon the ${contestNoun(contest.kind)}…</button>`,
19228
+ `<button type="submit">Abandon the ${contestNoun(contest.kind)}…</button>`,
18811
19229
  `</form>`,
18812
- `<p class="meta">abandoning picks nothing: the task is marked failed (it can be re-queued), and every agent's branch and evidence is kept</p>`,
19230
+ `<p class="meta">Abandoning picks nothing: the task is marked failed (it can be re-queued), and every agent's branch and evidence is kept</p>`,
18813
19231
  `</div>`,
18814
19232
  ].join("\n")
18815
19233
  : "",
@@ -18826,7 +19244,7 @@ function contestCeremonyPage(chrome, data) {
18826
19244
  const back = `<p class="meta"><a href="/contest/${data.contestId}">back — decide nothing</a></p>`;
18827
19245
  if (data.kind === "abandon") {
18828
19246
  return screen("tournament", [
18829
- `<h1>abandon this ${contestNoun(data.contestKind)}?</h1>`,
19247
+ `<h1>Abandon this ${contestNoun(data.contestKind)}?</h1>`,
18830
19248
  `<div class="card">`,
18831
19249
  `<p class="row">${data.agents} agents ${data.contestKind === "comparison" ? "built independently" : "raced"} on <strong>${escape(data.taskTitle)}</strong>. Abandoning picks nothing:</p>`,
18832
19250
  `<p class="row">— the task is marked <strong>failed</strong> and can be re-queued later</p>`,
@@ -18838,20 +19256,20 @@ function contestCeremonyPage(chrome, data) {
18838
19256
  `<form method="post" action="/contest/${data.contestId}/abandon" class="card">`,
18839
19257
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
18840
19258
  `<input type="hidden" name="nonce" value="${escape(data.nonceValue)}">`,
18841
- `<label>your password, typed again<input type="password" name="token" autocomplete="current-password"></label>`,
18842
- `<button type="submit" class="danger">abandon the ${contestNoun(data.contestKind)}</button>`,
19259
+ `<label>Your password, typed again<input type="password" name="token" autocomplete="current-password"></label>`,
19260
+ `<button type="submit" class="danger">Abandon the ${contestNoun(data.contestKind)}</button>`,
18843
19261
  `</form>`,
18844
19262
  back,
18845
19263
  ].join("\n"), { chrome });
18846
19264
  }
18847
19265
  const agent = data.chosen;
18848
19266
  if (agent === undefined || agent.run === null)
18849
- return screen("tournament", `<p class="meta">nothing to confirm</p>`, { chrome });
19267
+ return screen("tournament", `<p class="meta">Nothing to confirm</p>`, { chrome });
18850
19268
  const run = agent.run;
18851
19269
  return screen("tournament", [
18852
- `<h1>pick agent ${agent.contestant.ordinal}'s result?</h1>`,
19270
+ `<h1>Pick agent ${agent.contestant.ordinal}'s result?</h1>`,
18853
19271
  `<div class="card">`,
18854
- `<p class="row"><strong>agent ${agent.contestant.ordinal}</strong> — ${escape(agent.contestant.provider)} · ${escape(agent.contestant.model)}</p>`,
19272
+ `<p class="row"><strong>Agent ${agent.contestant.ordinal}</strong> — ${escape(agent.contestant.provider)} · ${escape(agent.contestant.model)}</p>`,
18855
19273
  `<p class="row">its result becomes the outcome of <strong>${escape(data.taskTitle)}</strong> — the task is marked done, keyed to <a href="/r/${run.id}">this build</a></p>`,
18856
19274
  run.outcome === "no-change"
18857
19275
  ? `<p class="row">the result is a verified <strong>no change</strong> — the agent concluded nothing needed doing, and its checkout still matches the starting point</p>`
@@ -18861,8 +19279,8 @@ function contestCeremonyPage(chrome, data) {
18861
19279
  ? ""
18862
19280
  : `<p class="row">the diff being picked: <span class="mono">${escape(agent.diff.sha256.slice(0, 12))}</span> · ${agent.diff.bytesStored} bytes, verified</p>`,
18863
19281
  data.publication === null || data.publication === undefined
18864
- ? `<p class="row"><strong>nothing is published</strong> — the branch stays local to this machine</p>`
18865
- : `<p class="row"><strong>a ${data.publication.draft ? "draft " : ""}pull request will be opened</strong> on ` +
19282
+ ? `<p class="row"><strong>Nothing is published</strong> — the branch stays local to this machine</p>`
19283
+ : `<p class="row"><strong>A ${data.publication.draft ? "draft " : ""}pull request will be opened</strong> on ` +
18866
19284
  `<span class="mono">${escape(data.publication.githubRepo)}</span> from <span class="mono">${escape(data.publication.branch)}</span></p>`,
18867
19285
  data.contestKind === "comparison"
18868
19286
  ? `<p class="row">this agent's spend: ${agent.contestant.unknownSpend ? "unmeasured — its harness reports tokens, not dollars" : `${escape(contestDollars(agent.contestant.measuredMicrousd))} measured`}` +
@@ -18876,8 +19294,8 @@ function contestCeremonyPage(chrome, data) {
18876
19294
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
18877
19295
  `<input type="hidden" name="nonce" value="${escape(data.nonceValue)}">`,
18878
19296
  `<input type="hidden" name="choice" value="${agent.contestant.id}">`,
18879
- `<label>your password, typed again<input type="password" name="token" autocomplete="current-password"></label>`,
18880
- `<div class="sticky-actions"><button type="submit">yes — pick this result</button></div>`,
19297
+ `<label>Your password, typed again<input type="password" name="token" autocomplete="current-password"></label>`,
19298
+ `<div class="sticky-actions"><button type="submit">Yes — pick this result</button></div>`,
18881
19299
  `</form>`,
18882
19300
  back,
18883
19301
  ].filter(one => one !== "").join("\n"), { chrome });
@@ -18968,18 +19386,18 @@ function githubReposPage(chrome, data) {
18968
19386
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
18969
19387
  `<input type="hidden" name="repo" value="${escape(nameWithOwner)}">`,
18970
19388
  `<input type="hidden" name="root" value="0">`,
18971
- `<button type="submit">clone here →</button>`,
19389
+ `<button type="submit">Clone here →</button>`,
18972
19390
  `</form>`,
18973
19391
  ].join("");
18974
19392
  const rows = !data.listed.ok
18975
19393
  ? `<p class="meta">${escape(data.listed.message)}</p>`
18976
19394
  : data.listed.repos.length === 0
18977
- ? `<p class="meta">the signed-in GitHub account has no repositories to list</p>`
19395
+ ? `<p class="meta">The signed-in GitHub account has no repositories to list</p>`
18978
19396
  : data.listed.repos
18979
19397
  .map(repo => {
18980
19398
  const localPath = data.local.get(repo.nameWithOwner.toLowerCase()) ?? null;
18981
19399
  const action = localPath !== null && data.openProject === localPath
18982
- ? `<span class="badge badge-done">open now</span>`
19400
+ ? `<span class="badge badge-done">Open now</span>`
18983
19401
  : localPath !== null
18984
19402
  ? openForm(localPath, data.registered.has(localPath) ? "open →" : "add + open →")
18985
19403
  : data.cloneReady
@@ -18987,10 +19405,10 @@ function githubReposPage(chrome, data) {
18987
19405
  : `<span class="meta">choose a projects folder first</span>`;
18988
19406
  return [
18989
19407
  `<div class="card project-card">`,
18990
- `<div class="row"><strong>${escape(repo.nameWithOwner)}</strong>${repo.isPrivate ? ` <span class="badge">private</span>` : ""}`,
19408
+ `<div class="row"><strong>${escape(repo.nameWithOwner)}</strong>${repo.isPrivate ? ` <span class="badge">Private</span>` : ""}`,
18991
19409
  `<span class="right">${action}</span></div>`,
18992
19410
  localPath === null
18993
- ? `<p class="meta">not on this machine yet${/^\d{4}-\d{2}-\d{2}T/.test(repo.updatedAt) ? ` · pushed ${whenTime(repo.updatedAt)}` : ""}</p>`
19411
+ ? `<p class="meta">Not on this machine yet${/^\d{4}-\d{2}-\d{2}T/.test(repo.updatedAt) ? ` · pushed ${whenTime(repo.updatedAt)}` : ""}</p>`
18994
19412
  : `<p class="meta mono" style="overflow-wrap:anywhere;margin:.2rem 0">${escape(localPath)}</p>`,
18995
19413
  repo.description === "" ? "" : `<p class="meta">${escape(repo.description)}</p>`,
18996
19414
  `</div>`,
@@ -18998,8 +19416,8 @@ function githubReposPage(chrome, data) {
18998
19416
  })
18999
19417
  .join("\n");
19000
19418
  return screen("projects", [
19001
- `<h1>your GitHub repositories</h1>`,
19002
- `<p class="meta">repositories available to the GitHub account signed in on this machine — open one you already have, or clone a new one after a quick preview.</p>`,
19419
+ `<h1>Your GitHub repositories</h1>`,
19420
+ `<p class="meta">Repositories available to the GitHub account signed in on this machine — open one you already have, or clone a new one after a quick preview.</p>`,
19003
19421
  rows,
19004
19422
  `<p class="row" style="margin-top:.6rem"><a class="badge" href="/projects">← back to projects</a></p>`,
19005
19423
  ].join("\n"), { chrome });
@@ -19014,16 +19432,16 @@ function projectsPage(chrome, recent, candidates, open, csrf, problem, _unscoped
19014
19432
  ? `<p class="meta">${escape(onboard.why)}</p>`
19015
19433
  : [
19016
19434
  `<details class="project-add-more"${onboard.record === null ? '' : ' open'}><summary>${onboard.record === null ? 'Paste a GitHub link' : 'Review repository'}</summary>`,
19017
- `<p class="meta">paste a GitHub repository — you will preview it before anything is downloaded. It goes into your saved projects folder and the builder connects automatically. Large-file (LFS) objects are not downloaded.</p>`,
19435
+ `<p class="meta">Paste a GitHub repository — you will preview it before anything is downloaded. It goes into your saved projects folder and the builder connects automatically. Large-file (LFS) objects are not downloaded.</p>`,
19018
19436
  onboard.record === null
19019
19437
  ? [
19020
19438
  `<form method="post" action="/projects/onboard-preview" class="card">`,
19021
19439
  `<input type="hidden" name="csrf" value="${escape(csrf)}">`,
19022
- `<label>repository <input type="text" name="repo" placeholder="owner/name or https://github.com/owner/name"></label>`,
19440
+ `<label>Repository <input type="text" name="repo" placeholder="owner/name or https://github.com/owner/name"></label>`,
19023
19441
  onboard.roots.length > 1
19024
- ? `<label>into <select name="root">${onboard.roots.map((one, index) => `<option value="${index}">${escape(one)}</option>`).join("")}</select></label>`
19025
- : `<input type="hidden" name="root" value="0"><p class="meta">into ${escape(onboard.roots[0] ?? "")}</p>`,
19026
- `<button type="submit">preview</button>`,
19442
+ ? `<label>Into <select name="root">${onboard.roots.map((one, index) => `<option value="${index}">${escape(one)}</option>`).join("")}</select></label>`
19443
+ : `<input type="hidden" name="root" value="0"><p class="meta">Into ${escape(onboard.roots[0] ?? "")}</p>`,
19444
+ `<button type="submit">Preview</button>`,
19027
19445
  `</form>`,
19028
19446
  ].join("\n")
19029
19447
  : [
@@ -19035,8 +19453,8 @@ function projectsPage(chrome, recent, candidates, open, csrf, problem, _unscoped
19035
19453
  onboard.record[1].large
19036
19454
  ? `<label class="row"><input type="checkbox" name="big-ok" value="1"> this is a large repository (or its size is unknown) — clone it anyway</label>`
19037
19455
  : "",
19038
- `<label>your password, typed again <input type="password" name="token" autocomplete="current-password"></label>`,
19039
- `<div class="sticky-actions"><button type="submit">clone and open</button></div>`,
19456
+ `<label>Your password, typed again <input type="password" name="token" autocomplete="current-password"></label>`,
19457
+ `<div class="sticky-actions"><button type="submit">Clone and open</button></div>`,
19040
19458
  `</form>`,
19041
19459
  `</div>`,
19042
19460
  ].join("\n"),
@@ -19078,7 +19496,7 @@ function projectsPage(chrome, recent, candidates, open, csrf, problem, _unscoped
19078
19496
  `<div class="row">${open !== null && open === one.path
19079
19497
  ? `<a class="project-name" href="${escape(returnTo)}"><strong>${escape(one.name)}</strong></a>`
19080
19498
  : openForm(one.path, one.name, returnTo, "project-name")}`,
19081
- `<span class="right">${open !== null && open === one.path ? `<span class="badge badge-done">open now</span>` : openForm(one.path, "Open", returnTo, "button-link")}</span></div>`,
19499
+ `<span class="right">${open !== null && open === one.path ? `<span class="badge badge-done">Open now</span>` : openForm(one.path, "Open", returnTo, "button-link")}</span></div>`,
19082
19500
  `<p class="meta mono" style="overflow-wrap:anywhere;margin:.2rem 0">${escape(one.path)}</p>`,
19083
19501
  `<p class="row" style="gap:.35rem;flex-wrap:wrap">${peekChips(one.path, one.peek)}</p>`,
19084
19502
  `<p class="meta row" style="justify-content:space-between;margin-bottom:0"><span>${escape(one.note)}</span><a href="/settings/knowledge?repo=${encodeURIComponent(one.path)}" style="display:inline-flex;align-items:center;min-height:44px">Knowledge</a></p>`,
@@ -19120,13 +19538,13 @@ function projectsPage(chrome, recent, candidates, open, csrf, problem, _unscoped
19120
19538
  `<form method="post" action="/projects/open" class="card">`,
19121
19539
  `<input type="hidden" name="csrf" value="${escape(csrf)}">`,
19122
19540
  `<input type="hidden" name="return" value="${escape(returnTo)}">`,
19123
- `<label>path on this server<input type="text" name="path" placeholder="/Users/you/code/your-repo"></label>`,
19124
- `<button type="submit">open project</button>`,
19541
+ `<label>Path on this server<input type="text" name="path" placeholder="/Users/you/code/your-repo"></label>`,
19542
+ `<button type="submit">Open project</button>`,
19125
19543
  `</form></details>`,
19126
19544
  ].join("\n");
19127
19545
  const addCard = [
19128
19546
  `<div class="card project-add-card">`,
19129
- `<h2 class="project-add-title">add a project</h2>`,
19547
+ `<h2 class="project-add-title">Add a project</h2>`,
19130
19548
  `<div class="project-add-actions">`,
19131
19549
  browsable
19132
19550
  ? addAction("/projects/browse", FOLDER_PATHS, "Choose a local folder", "Browse the project folders on this machine")
@@ -19147,8 +19565,8 @@ function projectsPage(chrome, recent, candidates, open, csrf, problem, _unscoped
19147
19565
  recentItems.length === 0 && candidateItems.length === 0
19148
19566
  ? `<div class="card"><p><strong>Nothing to open yet.</strong></p><p class="meta">Add one below \u2014 opening it registers it here for next time.</p></div>`
19149
19567
  : "",
19150
- recentItems.length > 0 ? `<h2>recent</h2>${cards(recentItems)}` : "",
19151
- candidateItems.length > 0 ? `<h2>available</h2>${cards(candidateItems)}` : "",
19568
+ recentItems.length > 0 ? `<h2>Recent</h2>${cards(recentItems)}` : "",
19569
+ candidateItems.length > 0 ? `<h2>Available</h2>${cards(candidateItems)}` : "",
19152
19570
  addCard,
19153
19571
  ].join("\n"), { chrome, workspace: { view: {
19154
19572
  kind: "projects", choosing, problem, returnTo,
@@ -19186,6 +19604,15 @@ function accentHead() {
19186
19604
  }
19187
19605
  const requestContext = new AsyncLocalStorage();
19188
19606
  /** A same-site path or "/": never a scheme, a host, or a protocol-relative road. */
19607
+ /** The words a result page shows for a refusal its own form led to, by the fixed code a redirect carries. */
19608
+ const ACCEPT_ANYWAY_NEEDS_REASON = "Accepting anyway needs a reason.";
19609
+ const RESULT_REFUSALS = { reason: ACCEPT_ANYWAY_NEEDS_REASON };
19610
+ /** A same-site page address with a refusal code added, before any fragment. */
19611
+ function withRefusal(href, code) {
19612
+ const at = href.indexOf("#");
19613
+ const [path, hash] = at === -1 ? [href, ""] : [href.slice(0, at), href.slice(at)];
19614
+ return `${path}${path.includes("?") ? "&" : "?"}refused=${code}${hash}`;
19615
+ }
19189
19616
  function safeReturn(raw) {
19190
19617
  if (raw === null || raw === undefined)
19191
19618
  return "/";
@@ -19320,25 +19747,25 @@ const NAV_ICONS = {
19320
19747
  function workToolRows(scoped = false, _offersChat = false, offersCode = false) {
19321
19748
  const rows = [
19322
19749
  ...(offersCode ? [{ key: "code", href: "/code", label: "Coding sessions", hint: "Direct coding sessions and their saved results" }] : []),
19323
- { key: "inbox", href: "/inbox", label: "inbox", hint: "questions, approvals, and repairs to act on" },
19324
- { key: "board", href: "/board", label: "board", hint: "lanes by state, with the order view" },
19325
- { key: "tasks", href: "/tasks", label: "task list", hint: "everything, filterable by state" },
19326
- { key: "recipes", href: "/recipes", label: "recipes", hint: "choose, customize, and reuse a workflow" },
19327
- { key: "routines", href: "/routines", label: "routines", hint: "scheduled tracks and their firings" },
19328
- { key: "workbench", href: "/workbench", label: "portfolio", hint: "every project and live build in one place" },
19329
- { key: "ledger", href: "/ledger", label: "action ledger", hint: "who acted, what happened, and the result" },
19330
- { key: "spend", href: "/spend", label: "spend", hint: "what agent work cost, and monthly budgets" },
19750
+ { key: "inbox", href: "/inbox", label: "Inbox", hint: "questions, approvals, and repairs to act on" },
19751
+ { key: "board", href: "/board", label: "Board", hint: "lanes by state, with the order view" },
19752
+ { key: "tasks", href: "/tasks", label: "Task list", hint: "everything, filterable by state" },
19753
+ { key: "recipes", href: "/recipes", label: "Recipes", hint: "choose, customize, and reuse a workflow" },
19754
+ { key: "routines", href: "/routines", label: "Routines", hint: "scheduled tracks and their firings" },
19755
+ { key: "workbench", href: "/workbench", label: "Portfolio", hint: "every project and live build in one place" },
19756
+ { key: "ledger", href: "/ledger", label: "Action ledger", hint: "who acted, what happened, and the result" },
19757
+ { key: "spend", href: "/spend", label: "Spend", hint: "what agent work cost, and monthly budgets" },
19331
19758
  ];
19332
19759
  return scoped ? rows.filter(row => row.key !== "workbench" && row.key !== "spend") : rows;
19333
19760
  }
19334
19761
  function settingsRows(scoped = false, offersSettings = false) {
19335
19762
  const rows = [
19336
- ...(offersSettings ? [{ key: "settings", href: "/settings", label: "settings", hint: "agent defaults, alerts, credentials" }] : []),
19337
- { key: "fleet", href: "/fleet", label: "fleet", hint: "who is working, and on what" },
19338
- { key: "caps", href: "/caps", label: "requirements", hint: "tools and credentials builds need" },
19339
- { key: "people", href: "/people", label: "people", hint: "who can sign in, and what they have done" },
19340
- { key: "mode", href: "/mode", label: "operating mode", hint: "the signed posture this repository runs under" },
19341
- { key: "system", href: "/system", label: "system", hint: "workers, providers, and grants" },
19763
+ ...(offersSettings ? [{ key: "settings", href: "/settings", label: "Settings", hint: "agent defaults, alerts, credentials" }] : []),
19764
+ { key: "fleet", href: "/fleet", label: "Fleet", hint: "who is working, and on what" },
19765
+ { key: "caps", href: "/caps", label: "Requirements", hint: "tools and credentials builds need" },
19766
+ { key: "people", href: "/people", label: "People", hint: "who can sign in, and what they have done" },
19767
+ { key: "mode", href: "/mode", label: "Operating mode", hint: "the signed posture this repository runs under" },
19768
+ { key: "system", href: "/system", label: "System", hint: "workers, providers, and grants" },
19342
19769
  ];
19343
19770
  return scoped ? rows.filter(row => row.key === "people" || row.key === "settings") : rows;
19344
19771
  }
@@ -19368,10 +19795,10 @@ function queueCard(one, csrf, revision, queueRevision, workers, column) {
19368
19795
  // and the reservation owner. Money is not in this query and is not
19369
19796
  // invented here — it stays on the task page, labeled.
19370
19797
  const state = one.taken
19371
- ? `<span class="badge">being taken — keeps its claim</span>`
19798
+ ? `<span class="badge">Being taken — keeps its claim</span>`
19372
19799
  : column === "anyone"
19373
- ? `<span class="badge">queued</span>`
19374
- : `<span class="badge">reserved for ${escape(column)}</span>`;
19800
+ ? `<span class="badge">Queued</span>`
19801
+ : `<span class="badge">Reserved for ${escape(column)}</span>`;
19375
19802
  const chips = ` ${state}` +
19376
19803
  `${one.dispatch === null || one.dispatch === undefined ? "" : ` <a class="badge" href="${taskHref(one.id)}" title="${escape(one.dispatch.summary)}">${escape(dispatchHeadline(one.dispatch))}</a>`}`;
19377
19804
  const hidden = `<input type="hidden" name="csrf" value="${escape(csrf)}">` +
@@ -19388,7 +19815,7 @@ function queueCard(one, csrf, revision, queueRevision, workers, column) {
19388
19815
  `<select name="column" aria-label="reserve for">` +
19389
19816
  `<option value="anyone"${column === "anyone" ? " selected" : ""}>anyone</option>` +
19390
19817
  workers.filter(worker => !worker.retired).map(worker => `<option value="${escape(worker.name)}"${column === worker.name ? " selected" : ""}>${escape(worker.name)}</option>`).join("") +
19391
- `</select><button type="submit">move</button></form>`;
19818
+ `</select><button type="submit">Move</button></form>`;
19392
19819
  return (`<div class="card queue-card" data-task="${escape(one.id)}" data-taken="${one.taken ? "1" : "0"}"${one.dispatch === null || one.dispatch === undefined ? "" : ` data-dispatch-status="${escape(one.dispatch.code)}"`}>` +
19393
19820
  `<p class="row">${one.taken ? "" : `${GRIP_HANDLE}`}` +
19394
19821
  `<a href="${taskHref(one.id)}">${escape(one.title)}</a>${chips}</p>` +
@@ -19410,22 +19837,22 @@ function queueBody(tasks, workers, csrf, revision, queueRevision) {
19410
19837
  ? `<p class="meta lane-empty">${escape(empty)}</p>`
19411
19838
  : rows
19412
19839
  .map((one, index) => index === 0 && key === "anyone" && allWorkersBusy && !one.taken
19413
- ? queueCard(one, csrf, revision, queueRevision, workers, key).replace('</p>\n', "</p>").replace(`<p class="row meta">`, `<p class="meta">every worker has reserved work — this waits until a column empties</p><p class="row meta">`)
19840
+ ? queueCard(one, csrf, revision, queueRevision, workers, key).replace('</p>\n', "</p>").replace(`<p class="row meta">`, `<p class="meta">Every worker has reserved work — this waits until a column empties</p><p class="row meta">`)
19414
19841
  : queueCard(one, csrf, revision, queueRevision, workers, key))
19415
19842
  .join("\n")) +
19416
19843
  `</section>`;
19417
19844
  const noteForm = (worker) => worker.retired
19418
- ? `<p class="meta">this worker is retired — drag these elsewhere, or register the name again</p>`
19845
+ ? `<p class="meta">This worker is retired — drag these elsewhere, or register the name again</p>`
19419
19846
  : `<p class="meta mono">${worker.building} building in this project · unattended capacity ${worker.capacity}</p>` +
19420
19847
  `<form method="post" action="/queue/note" class="row">` +
19421
19848
  `<input type="hidden" name="csrf" value="${escape(csrf)}">` +
19422
19849
  `<input type="hidden" name="projectRevision" value="${revision}">` +
19423
19850
  `<input type="hidden" name="runner" value="${escape(worker.name)}">` +
19424
19851
  `<input type="text" name="note" value="${worker.note === null ? "" : escape(worker.note)}" data-initial="${worker.note === null ? "" : escape(worker.note)}" placeholder="what this worker is working through" aria-label="column note" maxlength="200">` +
19425
- `<button type="submit">save</button></form>` +
19426
- `<p class="meta">takes from the shared queue when this column is empty</p>`;
19852
+ `<button type="submit">Save</button></form>` +
19853
+ `<p class="meta">Takes from the shared queue when this column is empty</p>`;
19427
19854
  return (`<div class="lanes" data-queue-revision="${queueRevision}">` +
19428
- column("shared queue", "anyone", `<p class="meta">workers take from here when their column is empty — top card first</p>`, shared, "nothing waiting — every task is reserved or running") +
19855
+ column("shared queue", "anyone", `<p class="meta">Workers take from here when their column is empty — top card first</p>`, shared, "nothing waiting — every task is reserved or running") +
19429
19856
  workers
19430
19857
  .map(worker => column(worker.name + (worker.retired ? " (retired)" : ""), worker.name, noteForm(worker), columnOf(worker.name), "nothing queued — this worker will take from the shared queue"))
19431
19858
  .join("\n") +
@@ -19447,7 +19874,7 @@ function fleetBody(queued, building, runners, csrf, queueRevision, visibleRepo)
19447
19874
  const live = building.filter(one => one.runner === runner.name);
19448
19875
  const retired = runner.retiredAt !== null;
19449
19876
  const head = retired
19450
- ? `<p class="meta">this worker is retired — drag these elsewhere, or register the name again</p>`
19877
+ ? `<p class="meta">This worker is retired — drag these elsewhere, or register the name again</p>`
19451
19878
  : `<form method="post" action="/queue/note" class="row">` +
19452
19879
  `<input type="hidden" name="csrf" value="${escape(csrf)}">` +
19453
19880
  `<input type="hidden" name="from" value="fleet">` +
@@ -19464,9 +19891,9 @@ function fleetBody(queued, building, runners, csrf, queueRevision, visibleRepo)
19464
19891
  `<p class="row">${one.taken ? "" : `${GRIP_HANDLE}`}` +
19465
19892
  `<a href="${taskHref(one.id)}">${escape(one.title)}</a></p>` +
19466
19893
  `<p class="row meta"><span class="mono">${escape(one.id)}</span>${chip(one.repo)}` +
19467
- `${one.approved ? "" : ` <span class="badge">unapproved scope</span>`}` +
19468
- `${one.blockers > 0 ? ` <span class="badge">waits for ${one.blockers}</span>` : ""}` +
19469
- `${one.taken ? ` <span class="badge">being taken</span>` : ""}</p></div>`)
19894
+ `${one.approved ? "" : ` <span class="badge">Unapproved scope</span>`}` +
19895
+ `${one.blockers > 0 ? ` <span class="badge">Waits for ${one.blockers}</span>` : ""}` +
19896
+ `${one.taken ? ` <span class="badge">Being taken</span>` : ""}</p></div>`)
19470
19897
  .join("\n");
19471
19898
  const empty = live.length === 0 && own.length === 0
19472
19899
  ? `<p class="meta lane-empty">${retired ? "nothing left" : "idle — will take from the shared queue"}</p>`
@@ -19481,12 +19908,12 @@ function fleetBody(queued, building, runners, csrf, queueRevision, visibleRepo)
19481
19908
  `<p class="row">${one.taken ? "" : `${GRIP_HANDLE}`}` +
19482
19909
  `<a href="${taskHref(one.id)}">${escape(one.title)}</a></p>` +
19483
19910
  `<p class="row meta"><span class="mono">${escape(one.id)}</span>${chip(one.repo)}` +
19484
- `${one.approved ? "" : ` <span class="badge">unapproved scope</span>`}` +
19485
- `${one.blockers > 0 ? ` <span class="badge">waits for ${one.blockers}</span>` : ""}` +
19486
- `${one.taken ? ` <span class="badge">being taken</span>` : ""}</p></div>`)
19911
+ `${one.approved ? "" : ` <span class="badge">Unapproved scope</span>`}` +
19912
+ `${one.blockers > 0 ? ` <span class="badge">Waits for ${one.blockers}</span>` : ""}` +
19913
+ `${one.taken ? ` <span class="badge">Being taken</span>` : ""}</p></div>`)
19487
19914
  .join("\n");
19488
- const sharedLane = `<section class="lane queue-column" data-column="anyone"><h2>shared queue</h2>` +
19489
- `<p class="meta">any free worker takes from here, top first</p>${sharedCards}` +
19915
+ const sharedLane = `<section class="lane queue-column" data-column="anyone"><h2>Shared queue</h2>` +
19916
+ `<p class="meta">Any free worker takes from here, top first</p>${sharedCards}` +
19490
19917
  (shared.length === 0 ? `<p class="meta lane-empty">nothing waiting — every task is reserved or running</p>` : "") +
19491
19918
  `</section>`;
19492
19919
  return (`<div class="lanes" data-queue-revision="${queueRevision}">` +
@@ -19628,13 +20055,15 @@ function newTaskPage(chrome, project, csrf, projectRevision, problem, candidates
19628
20055
  * brief, but the console renders its known structure as a useful control
19629
20056
  * surface. Historical free-form plans keep their safe plain-text fallback. */
19630
20057
  const MILESTONE_STATE_WORDS = {
19631
- pending: "pending",
19632
- current: "in progress",
19633
- completed: "completed",
19634
- blocked: "blocked",
20058
+ pending: "Pending",
20059
+ current: "In progress",
20060
+ completed: "Completed",
20061
+ blocked: "Blocked",
19635
20062
  };
20063
+ /** Badges, labels and buttons read in sentence case, whatever word a record stores. */
20064
+ const sentenceCase = (word) => word.charAt(0).toUpperCase() + word.slice(1);
19636
20065
  function milestoneStateWord(state) {
19637
- return MILESTONE_STATE_WORDS[state] ?? "pending";
20066
+ return MILESTONE_STATE_WORDS[state] ?? "Pending";
19638
20067
  }
19639
20068
  /** Adaptive execution plans (v44): the live milestone projection a running
19640
20069
  * (or finished) build has reported. A shared render used by BOTH the task
@@ -19693,9 +20122,9 @@ function planRevisionLedgerHtml(ledger, taskId, csrf) {
19693
20122
  `</div>`;
19694
20123
  const history = ledger.history.length <= 1
19695
20124
  ? ""
19696
- : `<details class="plan-revision-history"><summary>revision history (${ledger.history.length})</summary>` +
20125
+ : `<details class="plan-revision-history"><summary>Revision history (${ledger.history.length})</summary>` +
19697
20126
  ledger.history
19698
- .map(one => `<p class="row"><span class="mono">rev ${one.revision}</span> <span class="badge">${escape(one.status)}</span> ` +
20127
+ .map(one => `<p class="row"><span class="mono">rev ${one.revision}</span> <span class="badge">${escape(sentenceCase(one.status))}</span> ` +
19699
20128
  `<span class="meta">${escape(one.author)} · ${whenTime(one.createdAt)}</span> — ${escape(one.reason)}</p>`)
19700
20129
  .join("\n") +
19701
20130
  `</details>`;
@@ -19814,25 +20243,24 @@ function taskBodyParts(data) {
19814
20243
  if (liveRunId === null || liveRun === undefined)
19815
20244
  return "";
19816
20245
  const minutes = Math.max(0, Math.round((data.now.getTime() - new Date(liveRun.startedAt).getTime()) / 60_000));
19817
- const head = `<p><strong>build #${liveRun.id} · ${escape(liveRun.runner)} · running ` +
20246
+ const head = `<p><strong>Build #${liveRun.id} · ${escape(liveRun.runner)} · running ` +
19818
20247
  `<time data-elapsed-since="${escape(liveRun.startedAt)}">${minutes}m</time></strong></p>`;
19819
20248
  const door = `<p class="row"><a href="/r/${liveRun.id}">full build view →</a></p>`;
19820
20249
  if (data.degraded !== undefined) {
19821
20250
  return `<div class="card attempt-live" data-live-run="${liveRun.id}">${head}` +
19822
- `<p class="meta">${data.degraded === "sensitive" ? "this page carries a password ceremony, so the live view stays on the build page" : "the live view is on the build page"}</p>${door}</div>`;
20251
+ `<p class="meta">${data.degraded === "sensitive" ? "This page carries a password ceremony, so the live view stays on the build page" : "The live view is on the build page"}</p>${door}</div>`;
19823
20252
  }
19824
- const peek = data.peekable === true
19825
- ? `<p class="meta">what is changing right now</p>` +
19826
- `<div id="run-peek"><p class="meta">watching\u2026 the first look lands within 15 seconds</p></div>` +
19827
- `<p class="meta" id="run-peek-stamp"></p>`
19828
- : `<p class="meta">the live file view is off \u2014 start serve with ${escape("--runner <name>")} naming this machine's worker, and it appears here</p>`;
19829
- const transcript = data.peekable !== true
19830
- ? ""
19831
- : liveRun.provider !== "claude"
19832
- ? `<p class="meta">the live transcript needs the claude harness for now \u2014 this build runs on ${escape(liveRun.provider)}</p>`
19833
- : `<p class="meta">what the agent is saying · display only \u2014 this is not evidence, and the machine running the agent could alter it</p>` +
19834
- `<pre id="live-transcript" class="mono" style="max-height:18rem;overflow:auto;white-space:pre-wrap"></pre>` +
19835
- `<p class="meta" id="live-transcript-state"></p>`;
20253
+ // With the live file view off there is nothing live to show here: no panel (the build's own page says why).
20254
+ if (data.peekable !== true)
20255
+ return "";
20256
+ const peek = `<p class="meta">What is changing right now</p>` +
20257
+ `<div id="run-peek"><p class="meta">Watching\u2026 the first look lands within 15 seconds</p></div>` +
20258
+ `<p class="meta" id="run-peek-stamp"></p>`;
20259
+ const transcript = liveRun.provider !== "claude"
20260
+ ? `<p class="meta">The live transcript needs the claude harness for now \u2014 this build runs on ${escape(liveRun.provider)}</p>`
20261
+ : `<p class="meta">What the agent is saying · display only \u2014 this is not evidence, and the machine running the agent could alter it</p>` +
20262
+ `<pre id="live-transcript" class="mono" style="max-height:18rem;overflow:auto;white-space:pre-wrap"></pre>` +
20263
+ `<p class="meta" id="live-transcript-state"></p>`;
19836
20264
  return `<div class="card attempt-live" data-live-run="${liveRun.id}">${head}${peek}${transcript}${door}</div>`;
19837
20265
  })();
19838
20266
  // One truthful answer to the first question on a queued task: "will this
@@ -19935,7 +20363,7 @@ function taskBodyParts(data) {
19935
20363
  `<p class="meta">Saved assessment history. Current task status and checks are shown above.</p>` +
19936
20364
  (accepted === null ? "" : `<p class="meta">Accepted with an exception by ${escape(accepted.approver)}. Check results are unchanged.${accepted.note === null ? "" : ` ${escape(accepted.note)}`}</p>`) +
19937
20365
  (proof.proofReasons.length === 0 ? "" : `<ul>${[...new Set(proof.proofReasons.map(plainReasonWords))].map(reason => `<li>${escape(reason)}</li>`).join("")}</ul>`) +
19938
- criterionMatrixHtml(proof.proofMatrix, { compact: true, runId: proof.runId, links: proof.proofMatrixLinks }) +
20366
+ criterionMatrixHtml(proof.proofMatrix, { compact: true, runId: proof.runId, links: proof.proofMatrixLinks, verdict: proof.proofVerdict }) +
19939
20367
  semanticCoverageHtml(proof.proofMatrix, proof.qualityMode ?? "default") + `</div></details>`;
19940
20368
  }
19941
20369
  // v50: the independent review's own card — attempt counts, what is
@@ -19987,7 +20415,7 @@ function taskBodyParts(data) {
19987
20415
  const proofDetails = proof.proofMatrix.length === 0 && machineNote === "" && chainHtml === ""
19988
20416
  ? ""
19989
20417
  : `<details class="dispatch-proof-details"><summary>${proof.proofMatrix.length === 0 ? "Verification details" : `${proof.proofMatrix.length} requirement${proof.proofMatrix.length === 1 ? "" : "s"}`} · View details</summary>` +
19990
- `<div class="dispatch-proof-body">${criterionMatrixHtml(proof.proofMatrix, { compact: true, runId: proof.runId, links: proof.proofMatrixLinks })}${coverageHtml}${machineNote}${chainHtml}</div></details>`;
20418
+ `<div class="dispatch-proof-body">${criterionMatrixHtml(proof.proofMatrix, { compact: true, runId: proof.runId, links: proof.proofMatrixLinks, verdict: proof.proofVerdict })}${coverageHtml}${machineNote}${chainHtml}</div></details>`;
19991
20419
  // The publication fact is separate from the evidence fact: a PR or
19992
20420
  // an observed merge is named from its record; nothing is "deployed".
19993
20421
  const publicationWords = (data.publication === null || data.publication === undefined ? "" : ` ${escape(receiptPublicationWords(publicationFactsOf(data.publication)))}`) +
@@ -20047,26 +20475,30 @@ function taskBodyParts(data) {
20047
20475
  : `<form method="post" action="${taskHref(task.id)}/steer" class="row">` +
20048
20476
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
20049
20477
  `<input type="text" name="note" placeholder="guidance for the next attempt" aria-label="steering note" style="width:100%;max-width:28rem">` +
20050
- `<button type="submit">steer</button></form>` +
20051
- `<p class="meta">lands when the next attempt starts — a running agent is not interrupted, and a note cannot widen the approved scope</p>`;
20052
- const steeringCard = steerRows === "" && steerForm === "" ? "" : `<h2>steering</h2>${steerRows}${steerForm}`;
20478
+ `<button type="submit">Steer</button></form>` +
20479
+ `<p class="meta">Lands when the next attempt starts — a running agent is not interrupted, and a note cannot widen the approved scope</p>`;
20480
+ const steeringCard = steerRows === "" && steerForm === "" ? "" : `<h2>Steering</h2>${steerRows}${steerForm}`;
20053
20481
  const holds = data.holds.length === 0
20054
20482
  ? ""
20055
- : `<h2>holds</h2>` +
20483
+ : `<h2>Holds</h2>` +
20056
20484
  data.holds
20057
20485
  .map(hold => `<p class="row">${escape(holdOwnerWords(hold.ownerKind))} — ${escape(hold.reason)}` +
20058
20486
  `${hold.until === null ? "" : ` <span class="meta">until ${whenTime(hold.until)}</span>`}</p>`)
20059
20487
  .join("\n") +
20060
- `<p class="meta">only your hold can be lifted here — waits caused by questions, incidents, or retry delays clear on their own</p>`;
20488
+ `<p class="meta">Only your hold can be lifted here — waits caused by questions, incidents, or retry delays clear on their own</p>`;
20061
20489
  const approval = approvalOf(scope);
20490
+ // A refused save from the sheet's in-place editor reopens that editor with
20491
+ // its draft; the full scope form below stays as it was.
20492
+ const inlineScopeDraft = data.scopeDraft?.has("requirement-new") === true;
20493
+ const scopeDraft = inlineScopeDraft ? undefined : data.scopeDraft;
20062
20494
  const scopeCard = scope === null
20063
- ? `<p class="meta">no scope proposed — nothing builds this until one is approved</p>`
20495
+ ? `<p class="meta">No scope proposed — nothing builds this until one is approved</p>`
20064
20496
  : [
20065
20497
  `<div class="card">`,
20066
- `<p><strong>goal</strong></p><p class="recap">${escape(scope.goal)}</p>`,
20067
- scope.outOfScope === null ? "" : `<p><strong>not this</strong></p><p class="recap">${escape(scope.outOfScope)}</p>`,
20068
- scope.touches.length === 0 ? "" : `<p class="scope-paths"><strong>touches</strong> ${scope.touches.map(one => escape(one)).join(", ")}</p>`,
20069
- `<p><strong>quality</strong> ${escape(qualityModeTitle(scope.qualityMode ?? "default"))}</p>`,
20498
+ `<p><strong>Goal</strong></p><p class="recap">${escape(scope.goal)}</p>`,
20499
+ scope.outOfScope === null ? "" : `<p><strong>Not this</strong></p><p class="recap">${escape(scope.outOfScope)}</p>`,
20500
+ scope.touches.length === 0 ? "" : `<p class="scope-paths"><strong>Touches</strong> ${scope.touches.map(one => escape(one)).join(", ")}</p>`,
20501
+ `<p><strong>Quality</strong> ${escape(qualityModeTitle(scope.qualityMode ?? "default"))}</p>`,
20070
20502
  // The fallback chain, on the card the yes reads (Layer F): the
20071
20503
  // digest binds the WHOLE chain, so every entry — credential
20072
20504
  // included — is said before anyone signs.
@@ -20074,7 +20506,7 @@ function taskBodyParts(data) {
20074
20506
  const chain = chainFromJson(scope.proposedChainJson ?? null);
20075
20507
  if (chain === null || chain.length < 2)
20076
20508
  return "";
20077
- return `<p><strong>if its subscription runs out</strong></p><p class="recap">${chain
20509
+ return `<p><strong>If its subscription runs out</strong></p><p class="recap">${chain
20078
20510
  .slice(1)
20079
20511
  .map(one => `falls back to ${escape(one.profile.provider)} (${escape(one.profile.model)}) — ${one.authMode === "api-key" ? "your API key; spend moves to that account" : "its subscription login"}`)
20080
20512
  .join("; ")}</p>`;
@@ -20092,10 +20524,10 @@ function taskBodyParts(data) {
20092
20524
  // blank — the same rule as the revision brief.
20093
20525
  const reportCard = data.report === null || data.report === undefined
20094
20526
  ? data.deliverable === "report"
20095
- ? `<div class="card"><p><strong>scout task</strong> <span class="meta">delivers a report, never a branch — a read-only session investigates the goal and its report appears here when it finishes</span></p></div>`
20527
+ ? `<div class="card"><p><strong>Scout task</strong> <span class="meta">delivers a report, never a branch — a read-only session investigates the goal and its report appears here when it finishes</span></p></div>`
20096
20528
  : ""
20097
20529
  : !data.report.ok
20098
- ? `<div class="card"><p><strong>the report</strong></p><p class="meta">${escape(data.report.problem)} · <a href="/r/${data.report.run}">run ${data.report.run}</a></p></div>`
20530
+ ? `<div class="card"><p><strong>The report</strong></p><p class="meta">${escape(data.report.problem)} · <a href="/r/${data.report.run}">run ${data.report.run}</a></p></div>`
20099
20531
  : [
20100
20532
  `<div class="card report">`,
20101
20533
  `<p><strong>${escape(data.report.report.title)}</strong> <span class="meta">the scout's report · <a href="/r/${data.report.run}">run ${data.report.run}</a></span></p>`,
@@ -20104,11 +20536,11 @@ function taskBodyParts(data) {
20104
20536
  ...(data.report.report.followUps.length === 0
20105
20537
  ? []
20106
20538
  : [
20107
- `<p><strong>follow-ups the scout proposes</strong> <span class="meta">each files as a task in this repository; its scope still needs your approval</span></p>`,
20539
+ `<p><strong>Follow-ups the scout proposes</strong> <span class="meta">each files as a task in this repository; its scope still needs your approval</span></p>`,
20108
20540
  ...data.report.report.followUps.map((one, index) => `<div class="follow-up"><p><strong>${escape(one.title)}</strong></p><p class="meta">${escape(one.goal)}</p>` +
20109
20541
  (data.csrf === "" || data.repo === null
20110
20542
  ? `<p class="meta">${data.repo === null ? "this task has no repository — file it by hand" : ""}</p>`
20111
- : `<form method="post" action="${taskHref(task.id)}/follow-up" class="inline"><input type="hidden" name="csrf" value="${escape(data.csrf)}"><input type="hidden" name="index" value="${index}"><button type="submit">file this follow-up</button></form>`) +
20543
+ : `<form method="post" action="${taskHref(task.id)}/follow-up" class="inline"><input type="hidden" name="csrf" value="${escape(data.csrf)}"><input type="hidden" name="index" value="${index}"><button type="submit">File this follow-up</button></form>`) +
20112
20544
  `</div>`),
20113
20545
  ]),
20114
20546
  `</div>`,
@@ -20140,7 +20572,7 @@ function taskBodyParts(data) {
20140
20572
  ? data.revision != null && !("problem" in data.revision)
20141
20573
  ? `<div class="card planner-status"><span class="planner-orb" aria-hidden="true"></span><p><strong>Updating the plan</strong>` +
20142
20574
  `<span class="meta">Adding your notes to the plan. You approve any change before it builds.</span></p></div>`
20143
- : `<div class="card planner-status"><span class="planner-orb" aria-hidden="true"></span><p><strong>planning requested</strong>` +
20575
+ : `<div class="card planner-status"><span class="planner-orb" aria-hidden="true"></span><p><strong>Planning requested</strong>` +
20144
20576
  `<span class="meta">${data.planAuto ? "Automatic approval is enabled for a verified plan that preserves your filed contract. Amendments and unanswered questions still pause." : "The agent is inspecting the repository and drafting the goal, acceptance criteria, and approach. It will ask only if a missing answer changes the work."}</span></p></div>`
20145
20577
  : ""
20146
20578
  : `${approval.approved ? `<details class="card planner-plan planner-plan-collapsed"><summary class="execution-plan-head">` : `<section class="card planner-plan"><div class="execution-plan-head">`}` +
@@ -20153,7 +20585,7 @@ function taskBodyParts(data) {
20153
20585
  ? ""
20154
20586
  : `<details class="plan-editor" id="plan-edit"><summary>Edit plan</summary><form method="post" action="${taskHref(task.id)}/plan-edit">` +
20155
20587
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}"><input type="hidden" name="saw-plan" value="${escape(data.planSha)}">` +
20156
- `<label>plan details <span class="meta">Keep the five headings. Approval locks this version for the build.</span>` +
20588
+ `<label>Plan details <span class="meta">Keep the five headings. Approval locks this version for the build.</span>` +
20157
20589
  `<textarea name="plan-document" rows="14">${escape(data.planDocument)}</textarea></label>` +
20158
20590
  `<button type="submit">Save plan</button></form></details>`) +
20159
20591
  `${approval.approved ? `</div></details>` : `</section>`}`;
@@ -20165,7 +20597,7 @@ function taskBodyParts(data) {
20165
20597
  const revisionCard = data.revision === null || data.revision === undefined
20166
20598
  ? ""
20167
20599
  : "problem" in data.revision
20168
- ? `<div class="card"><p><strong>revision brief</strong></p><p class="meta">${escape(data.revision.problem)}</p></div>`
20600
+ ? `<div class="card"><p><strong>Revision brief</strong></p><p class="meta">${escape(data.revision.problem)}</p></div>`
20169
20601
  : [
20170
20602
  `<div class="revision-card" data-revision-feedback>`,
20171
20603
  `<p><strong>${data.revision.kind === "ci-repair" ? "CI repair" : data.revision.kind === "criterion-repair" ? "Criterion repair" : "Revision feedback"}</strong> <span class="meta">from <a href="/r/${data.revision.sourceRun}">build #${data.revision.sourceRun}</a></span></p>`,
@@ -20193,7 +20625,7 @@ function taskBodyParts(data) {
20193
20625
  : scope.profileState === "unresolved"
20194
20626
  ? `<div class="card approve-form" id="approve"><p><strong>This task is waiting on you: its scope cannot be approved yet.</strong></p>` +
20195
20627
  profileWords(scope) +
20196
- `<p class="ceremony-road"><a class="button-link" href="#scope">edit the scope to fix it →</a></p></div>`
20628
+ `<p class="ceremony-road"><a class="button-link" href="#scope">Edit the scope to fix it →</a></p></div>`
20197
20629
  : !consentDoor.open
20198
20630
  ? consentClosedHtml(task.id, consentDoor, "task")
20199
20631
  : approvalSheetHtml({
@@ -20216,6 +20648,11 @@ function taskBodyParts(data) {
20216
20648
  editHref: data.csrf !== "" && data.planSha != null && data.planDocument !== null ? "#plan-edit" : "#scope",
20217
20649
  notNowHref: "/work",
20218
20650
  sticky: data.dispatch?.action === "approve-scope",
20651
+ earlier: { active: data.assignment?.earlierActive ?? 0, running: data.assignment?.earlierRunning ?? 0 },
20652
+ // A tournament's terms live in the full scope editor; every other plan edits in place.
20653
+ edit: data.csrf === "" || (data.raceTerms ?? null) !== null ? null
20654
+ : { action: `${taskHref(task.id)}/scope`, draft: inlineScopeDraft ? data.scopeDraft ?? null : null, problem: inlineScopeDraft ? data.problem : null,
20655
+ open: data.editPlan === true, stepsHref: data.planSha != null && data.planDocument !== null ? "#plan-edit" : null },
20219
20656
  });
20220
20657
  // The attended road (Phase 2E): beside the approval, never replacing it.
20221
20658
  // The mint button leads to the CONFIRM screen where every term renders
@@ -20228,12 +20665,12 @@ function taskBodyParts(data) {
20228
20665
  : attended.open !== null
20229
20666
  ? [
20230
20667
  `<div class="card" data-attended="${escape(attended.open.id)}">`,
20231
- `<p><strong>attended session</strong> <span class="meta">${escape(attended.open.state)}</span></p>`,
20668
+ `<p><strong>Attended session</strong> <span class="meta">${escape(attended.open.state)}</span></p>`,
20232
20669
  `<p class="meta">${attended.open.running ? "the agent is running — your open console keeps it live" : "waiting to dispatch to this machine"} · ${attended.open.turnsUsed}/${attended.open.cap} messages · $${(attended.open.spentMicrousd / 1_000_000).toFixed(2)} of $${(attended.open.budgetMicrousd / 1_000_000).toFixed(2)} · everything ends by ${whenTime(attended.open.expiresAt)}</p>`,
20233
- `<p class="meta">this page being open keeps it alive — close it and the session winds down within a minute</p>`,
20670
+ `<p class="meta">This page being open keeps it alive — close it and the session winds down within a minute</p>`,
20234
20671
  `<form method="post" action="${taskHref(task.id)}/attend-revoke" class="inline">`,
20235
20672
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
20236
- `<button type="submit">revoke — stop the session</button>`,
20673
+ `<button type="submit">Revoke — stop the session</button>`,
20237
20674
  `</form>`,
20238
20675
  `</div>`,
20239
20676
  ].join("\n")
@@ -20241,36 +20678,36 @@ function taskBodyParts(data) {
20241
20678
  ? [
20242
20679
  `<form method="post" action="${taskHref(task.id)}/attend-preview" class="card">`,
20243
20680
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
20244
- `<p><strong>or run it once while you watch</strong></p>`,
20245
- `<p class="meta">no approval filed: one attempt, on this machine, only while this page is open. You will read every term before ${attended.mint?.quick === true ? "you confirm it" : "your password signs it"}.</p>`,
20681
+ `<p><strong>Or run it once while you watch</strong></p>`,
20682
+ `<p class="meta">No approval filed: one attempt, on this machine, only while this page is open. You will read every term before ${attended.mint?.quick === true ? "you confirm it" : "your password signs it"}.</p>`,
20246
20683
  attended.mint === undefined || attended.mint.models.length <= 1
20247
20684
  ? ""
20248
- : `<label>model <span class="meta">(watched sessions run claude only)</span><select name="model">` +
20685
+ : `<label>Model <span class="meta">(watched sessions run claude only)</span><select name="model">` +
20249
20686
  attended.mint.models
20250
20687
  .map(model => `<option value="${escape(model)}"${model === attended.mint?.pinnedModel ? " selected" : ""}>${escape(model)}</option>`)
20251
20688
  .join("") +
20252
20689
  `</select></label>`,
20253
20690
  attended.mint === undefined
20254
20691
  ? ""
20255
- : `<label>permissions<select name="posture">` +
20692
+ : `<label>Permissions<select name="posture">` +
20256
20693
  `<option value="auto"${attended.mint.posture === "auto" ? " selected" : ""}>safe unattended — routine project commands and edits proceed</option>` +
20257
20694
  `<option value="acceptEdits"${attended.mint.posture === "acceptEdits" ? " selected" : ""}>legacy acceptEdits — commands that ask are denied unattended</option>` +
20258
20695
  `<option value="bypassPermissions"${attended.mint.posture === "bypassPermissions" ? " selected" : ""}>full permissions — nothing asks</option>` +
20259
20696
  `</select></label>`,
20260
- `<button type="submit">read the terms</button>`,
20697
+ `<button type="submit">Read the terms</button>`,
20261
20698
  `</form>`,
20262
20699
  ].join("\n")
20263
20700
  : "";
20264
20701
  const scopeForm = [
20265
- `<details${scope === null || data.scopeDraft !== undefined ? " open" : ""}><summary>${scope === null ? "write the scope" : "edit the scope"}${approval.approved ? " (editing voids the approval)" : ""}</summary>`,
20702
+ `<details${scope === null || scopeDraft !== undefined ? " open" : ""}><summary>${scope === null ? "Write the scope" : "Edit the scope"}${approval.approved ? " (editing voids the approval)" : ""}</summary>`,
20266
20703
  `<form method="post" action="${taskHref(task.id)}/scope" class="scope-editor">`,
20267
- data.scopeDraft === undefined || data.problem === null ? "" : `<div class="problem" role="alert" id="scope-error">${escape(data.problem)}</div>`,
20704
+ scopeDraft === undefined || data.problem === null ? "" : `<div class="problem" role="alert" id="scope-error">${escape(data.problem)}</div>`,
20268
20705
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
20269
- `<input type="hidden" name="sawDigest" value="${escape(data.scopeDraft?.get("sawDigest") ?? scope?.digest ?? "")}">`,
20270
- `<label>goal<textarea name="goal" rows="3"${data.scopeDraft === undefined ? "" : ' autofocus aria-describedby="scope-error"'}>${escape(data.scopeDraft?.get("goal") ?? scope?.goal ?? "")}</textarea></label>`,
20271
- `<label>not this<textarea name="not" rows="2">${escape(data.scopeDraft?.get("not") ?? scope?.outOfScope ?? "")}</textarea></label>`,
20272
- `<label>touches <span class="meta">(one per line)</span><textarea name="touches" rows="2">${escape(data.scopeDraft?.get("touches") ?? (scope?.touches ?? []).join("\n"))}</textarea></label>`,
20273
- `<label>acceptance <span class="meta">(required — one criterion per line: <code>statement | evidence,kinds | how</code>; evidence kinds are check, screenshot, changed-path, manual-review; id is optional and auto-numbered)</span><textarea name="acceptance" rows="3" placeholder="The button opens the settings panel | screenshot">${escape(data.scopeDraft?.get("acceptance") ?? acceptanceToLines(scope?.acceptance ?? []).join("\n"))}</textarea></label>`,
20706
+ `<input type="hidden" name="sawDigest" value="${escape(scopeDraft?.get("sawDigest") ?? scope?.digest ?? "")}">`,
20707
+ `<label>Goal<textarea name="goal" rows="3"${scopeDraft === undefined ? "" : ' autofocus aria-describedby="scope-error"'}>${escape(scopeDraft?.get("goal") ?? scope?.goal ?? "")}</textarea></label>`,
20708
+ `<label>Not this<textarea name="not" rows="2">${escape(scopeDraft?.get("not") ?? scope?.outOfScope ?? "")}</textarea></label>`,
20709
+ `<label>Touches <span class="meta">(one per line)</span><textarea name="touches" rows="2">${escape(scopeDraft?.get("touches") ?? (scope?.touches ?? []).join("\n"))}</textarea></label>`,
20710
+ `<label>Acceptance <span class="meta">(required — one criterion per line: <code>statement | evidence,kinds | how</code>; evidence kinds are check, screenshot, changed-path, manual-review; id is optional and auto-numbered)</span><textarea name="acceptance" rows="3" placeholder="The button opens the settings panel | screenshot">${escape(scopeDraft?.get("acceptance") ?? acceptanceToLines(scope?.acceptance ?? []).join("\n"))}</textarea></label>`,
20274
20711
  (() => {
20275
20712
  const defaults = data.spendDefaults ?? null;
20276
20713
  const budgetPrefill = scope?.budgetMicrousd != null
@@ -20278,8 +20715,8 @@ function taskBodyParts(data) {
20278
20715
  : defaults?.buildPerRunMicrousd != null
20279
20716
  ? (defaults.buildPerRunMicrousd / 1_000_000).toFixed(2)
20280
20717
  : "";
20281
- return `<label>agent-reported usage cap <span class="meta">(optional — leave blank for uncapped subscription work)</span>` +
20282
- `<input type="number" name="budget-usd" step="0.01" min="0.01" value="${escape(data.scopeDraft?.get("budget-usd") ?? budgetPrefill)}" placeholder="no cap"></label>` +
20718
+ return `<label>Agent-reported usage cap <span class="meta">(optional — leave blank for uncapped subscription work)</span>` +
20719
+ `<input type="number" name="budget-usd" step="0.01" min="0.01" value="${escape(scopeDraft?.get("budget-usd") ?? budgetPrefill)}" placeholder="no cap"></label>` +
20283
20720
  `<p class="meta">Claude expresses this limiter in API-equivalent dollars even on a membership. It does not switch the run to API billing.</p>`;
20284
20721
  })(),
20285
20722
  (() => {
@@ -20290,14 +20727,14 @@ function taskBodyParts(data) {
20290
20727
  : scope?.profile?.provider === "codex" || scope?.profile?.provider === "openrouter"
20291
20728
  ? scope.profile.sandboxMode === "danger-full-access" ? "bypassPermissions" : "auto"
20292
20729
  : null;
20293
- const selected = data.scopeDraft?.get("permission-mode") === "bypassPermissions" ? "bypassPermissions" : data.scopeDraft?.get("permission-mode") === "auto" ? "auto" : data.permissionMode ?? profileMode ?? data.permissionDefault ?? "auto";
20294
- return `<fieldset class="permission-field"><legend>agent permissions</legend>` +
20730
+ const selected = scopeDraft?.get("permission-mode") === "bypassPermissions" ? "bypassPermissions" : scopeDraft?.get("permission-mode") === "auto" ? "auto" : data.permissionMode ?? profileMode ?? data.permissionDefault ?? "auto";
20731
+ return `<fieldset class="permission-field"><legend>Agent permissions</legend>` +
20295
20732
  permissionModeChoices("permission-mode", selected) +
20296
20733
  `<p class="meta permission-note">This task’s choice is sealed into its scope. Full access prevents permission prompts or sandbox limits from pausing supported unattended agents.</p></fieldset>`;
20297
20734
  })(),
20298
20735
  (() => {
20299
- const selected = data.scopeDraft?.get("quality-mode") === "strict" ? "strict" : data.scopeDraft?.get("quality-mode") === "default" ? "default" : scope?.qualityMode ?? data.qualityMode ?? data.qualityDefault ?? "default";
20300
- return `<fieldset class="permission-field"><legend>quality</legend>` +
20736
+ const selected = scopeDraft?.get("quality-mode") === "strict" ? "strict" : scopeDraft?.get("quality-mode") === "default" ? "default" : scope?.qualityMode ?? data.qualityMode ?? data.qualityDefault ?? "default";
20737
+ return `<fieldset class="permission-field"><legend>Quality</legend>` +
20301
20738
  qualityModeChoices("quality-mode", selected) +
20302
20739
  `<p class="meta permission-note">This choice is signed into the scope. Inspect the saved work and actual check results when it is ready. Publication and deployment need their own authorization.</p></fieldset>`;
20303
20740
  })(),
@@ -20308,8 +20745,8 @@ function taskBodyParts(data) {
20308
20745
  // approval above restates and covers both.
20309
20746
  const defaults = data.spendDefaults ?? null;
20310
20747
  const terms = data.raceTerms ?? null;
20311
- const selectedCount = data.scopeDraft === undefined ? terms !== null ? terms.n : defaults?.raceAgents ?? 0 : Number(data.scopeDraft.get("race-count") ?? "0");
20312
- const selectedModel = data.scopeDraft?.get("race-model") ?? terms?.agents[0]?.model ?? "claude-sonnet-5";
20748
+ const selectedCount = scopeDraft === undefined ? terms !== null ? terms.n : defaults?.raceAgents ?? 0 : Number(scopeDraft.get("race-count") ?? "0");
20749
+ const selectedModel = scopeDraft?.get("race-model") ?? terms?.agents[0]?.model ?? "claude-sonnet-5";
20313
20750
  const perPrefill = terms !== null
20314
20751
  ? (terms.perAgentBudgetMicrousd / 1_000_000).toFixed(2)
20315
20752
  : defaults?.racePerAgentMicrousd != null
@@ -20321,31 +20758,31 @@ function taskBodyParts(data) {
20321
20758
  ? (defaults.raceTotalMicrousd / 1_000_000).toFixed(2)
20322
20759
  : "";
20323
20760
  return [
20324
- `<details class="more-agents"${selectedCount > 0 ? " open" : ""}><summary>more than one agent (optional)</summary>`,
20325
- `<label>how many agents compete <span class="meta">(a tournament builds the task independently N times — you compare and pick one)</span>` +
20761
+ `<details class="more-agents"${selectedCount > 0 ? " open" : ""}><summary>More than one agent (optional)</summary>`,
20762
+ `<label>How many agents compete <span class="meta">(a tournament builds the task independently N times — you compare and pick one)</span>` +
20326
20763
  `<select name="race-count">` +
20327
20764
  `<option value=""${selectedCount === 0 ? " selected" : ""}>one agent — no tournament</option>` +
20328
20765
  [2, 3, 4].map(count => `<option value="${count}"${selectedCount === count ? " selected" : ""}>${count} agents</option>`).join("") +
20329
20766
  `</select></label>`,
20330
- `<label>competing model <select name="race-model">` +
20767
+ `<label>Competing model <select name="race-model">` +
20331
20768
  PRICED_BUILD_MODELS.map(model => `<option value="${escape(model)}"${model === selectedModel ? " selected" : ""}>${escape(model)}</option>`).join("") +
20332
20769
  `</select></label>`,
20333
- `<label>each competing agent may spend ($)<input type="number" name="race-per-usd" step="0.01" min="0.01" value="${escape(data.scopeDraft?.get("race-per-usd") ?? perPrefill)}"></label>`,
20334
- `<label>the whole tournament may spend ($)<input type="number" name="race-total-usd" step="0.01" min="0.01" value="${escape(data.scopeDraft?.get("race-total-usd") ?? totalPrefill)}"></label>`,
20770
+ `<label>Each competing agent may spend ($)<input type="number" name="race-per-usd" step="0.01" min="0.01" value="${escape(scopeDraft?.get("race-per-usd") ?? perPrefill)}"></label>`,
20771
+ `<label>The whole tournament may spend ($)<input type="number" name="race-total-usd" step="0.01" min="0.01" value="${escape(scopeDraft?.get("race-total-usd") ?? totalPrefill)}"></label>`,
20335
20772
  // The comparison lanes (Phase 3 slice B): 2-4 rows, any registered
20336
20773
  // provider, exact model required — no dollar fields exist. Blank
20337
20774
  // rows are unused; filling any row files a comparison INSTEAD of a
20338
20775
  // tournament, and mixing the two refuses in words.
20339
20776
  `<p class="meta" style="margin-top:.75rem">or compare different agents side by side — no dollar caps; each agent's clock is its bound, and spend lands measured only where the harness reports dollars:</p>`,
20340
- ...[1, 2, 3, 4].map(lane => `<div class="row"><label>agent ${lane} <select name="compare-provider-${lane}">` +
20777
+ ...[1, 2, 3, 4].map(lane => `<div class="row"><label>Agent ${lane} <select name="compare-provider-${lane}">` +
20341
20778
  `<option value="">—</option>` +
20342
- PROVIDER_IDS.map(provider => `<option value="${escape(provider)}"${data.scopeDraft?.get(`compare-provider-${lane}`) === provider ? " selected" : ""}>${escape(provider)}</option>`).join("") +
20779
+ PROVIDER_IDS.map(provider => `<option value="${escape(provider)}"${scopeDraft?.get(`compare-provider-${lane}`) === provider ? " selected" : ""}>${escape(provider)}</option>`).join("") +
20343
20780
  `</select></label>` +
20344
- `<label>its exact model<input type="text" name="compare-model-${lane}" value="${escape(data.scopeDraft?.get(`compare-model-${lane}`) ?? "")}" placeholder="e.g. gemini-2.5-pro"></label></div>`),
20781
+ `<label>Its exact model<input type="text" name="compare-model-${lane}" value="${escape(scopeDraft?.get(`compare-model-${lane}`) ?? "")}" placeholder="e.g. gemini-2.5-pro"></label></div>`),
20345
20782
  `</details>`,
20346
20783
  ].join("\n");
20347
20784
  })(),
20348
- `<button type="submit">save scope</button>`,
20785
+ `<button type="submit">Save scope</button>`,
20349
20786
  `</form></details>`,
20350
20787
  ].join("\n");
20351
20788
  // 41_237 → "41k": token counts read at a glance; exactness lives on the run page.
@@ -20367,7 +20804,7 @@ function taskBodyParts(data) {
20367
20804
  // duration · tokens · measured-or-unmeasured cost.
20368
20805
  const runs = data.runs.length === 0
20369
20806
  ? ""
20370
- : `<h2>attempts</h2>` +
20807
+ : `<h2>Attempts</h2>` +
20371
20808
  data.runs
20372
20809
  .map(run => {
20373
20810
  const bits = [
@@ -20560,20 +20997,20 @@ function taskBodyParts(data) {
20560
20997
  })();
20561
20998
  const decisions = data.decisions.length === 0
20562
20999
  ? ""
20563
- : `<h2>decisions</h2>` +
21000
+ : `<h2>Decisions</h2>` +
20564
21001
  data.decisions
20565
21002
  .map(decision => `<p class="row"><a href="/d/${decision.id}">${escape(decision.question)}</a> ` +
20566
21003
  `<span class="meta">${escape(decision.state)}${isOverdue(decision, data.now) ? " · overdue" : ""}</span></p>`)
20567
21004
  .join("\n");
20568
21005
  const incidents = data.incidents.length === 0
20569
21006
  ? ""
20570
- : `<h2>incidents</h2>` +
21007
+ : `<h2>Incidents</h2>` +
20571
21008
  data.incidents
20572
21009
  .map(one => one.resolvedAt === null
20573
21010
  ? `<p class="row">${escape(incidentWords(one.kind))} ` +
20574
21011
  `<form method="post" action="/i/${one.id}/resolve" class="inline">` +
20575
21012
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
20576
- `<button type="submit">resolve</button></form></p>`
21013
+ `<button type="submit">Resolve</button></form></p>`
20577
21014
  : `<p class="row meta">${escape(incidentWords(one.kind))} — resolved by ${escape(one.resolvedBy ?? "?")}</p>`)
20578
21015
  .join("\n");
20579
21016
  const stalled = task.state === "failed" || data.incidents.some(one => one.resolvedAt === null);
@@ -20585,11 +21022,11 @@ function taskBodyParts(data) {
20585
21022
  const waitsFor = data.waitsFor ?? [];
20586
21023
  const candidates = (data.waitCandidates ?? []).filter(one => !waitsFor.some(existing => existing.id === one.id));
20587
21024
  const waitRows = waitsFor
20588
- .map(one => `<p class="row">${one.admitted ? `<a href="${taskHref(one.id)}" class="mono">${escape(one.id)}</a>` : `<span class="mono">${escape(one.id)}</span>`}${one.state === null ? "" : ` <span class="badge badge-${escape(one.state)}">${escape(one.state)}</span>`}` +
21025
+ .map(one => `<p class="row">${one.admitted ? `<a href="${taskHref(one.id)}" class="mono">${escape(one.id)}</a>` : `<span class="mono">${escape(one.id)}</span>`}${one.state === null ? "" : ` <span class="badge badge-${escape(one.state)}">${escape(sentenceCase(one.state))}</span>`}` +
20589
21026
  `<form method="post" action="${taskHref(task.id)}/unblock" class="inline">` +
20590
21027
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
20591
21028
  `<input type="hidden" name="on" value="${escape(one.id)}">` +
20592
- `<button type="submit">don't wait for this</button></form></p>`)
21029
+ `<button type="submit">Don't wait for this</button></form></p>`)
20593
21030
  .join("\n");
20594
21031
  const waitAdd = data.csrf === "" || candidates.length === 0
20595
21032
  ? ""
@@ -20598,11 +21035,11 @@ function taskBodyParts(data) {
20598
21035
  `<select name="on" aria-label="task to wait for">` +
20599
21036
  candidates.map(one => `<option value="${escape(one.id)}">${escape(one.id)} — ${escape(one.title)}</option>`).join("") +
20600
21037
  `</select>` +
20601
- `<button type="submit">wait for this task</button>` +
21038
+ `<button type="submit">Wait for this task</button>` +
20602
21039
  `<span class="meta"> — this task starts only after it finishes</span></form>`;
20603
21040
  const waitsForCard = waitRows === "" && waitAdd === ""
20604
21041
  ? ""
20605
- : `<h2>waits for</h2>${waitRows === "" ? `<p class="meta">nothing — it starts when a worker is free</p>` : waitRows}${waitAdd}`;
21042
+ : `<h2>Waits for</h2>${waitRows === "" ? `<p class="meta">Nothing — it starts when a worker is free</p>` : waitRows}${waitAdd}`;
20606
21043
  // The acts bar (task page pass): every verb in one row under the title,
20607
21044
  // the one that resolves this task's state first and primary — retry on a
20608
21045
  // stalled task, build-next in the queue, plan-first with no scope. The
@@ -20664,9 +21101,10 @@ function taskBodyParts(data) {
20664
21101
  const section = (title, html, open, count, id = title.replace(/\s+/g, "-")) => {
20665
21102
  if (html === "")
20666
21103
  return "";
20667
- const inner = html.replace(`<h2>${title}</h2>`, "");
20668
- sectionParts.push({ id, title: title[0].toUpperCase() + title.slice(1), html: inner, open, count: count ?? null });
20669
- return `<details class="section" id="${id}"${open ? " open" : ""}><summary><h2>${title}${count === undefined ? "" : ` <span class="lane-count">${count}</span>`}</h2></summary>` + inner + `</details>`;
21104
+ const heading = sentenceCase(title);
21105
+ const inner = html.replace(`<h2>${title}</h2>`, "").replace(`<h2>${heading}</h2>`, "");
21106
+ sectionParts.push({ id, title: heading, html: inner, open, count: count ?? null });
21107
+ return `<details class="section" id="${id}"${open ? " open" : ""}><summary><h2>${heading}${count === undefined ? "" : ` <span class="lane-count">${count}</span>`}</h2></summary>` + inner + `</details>`;
20670
21108
  };
20671
21109
  const receiptLeads = data.assignment == null && task.state === "done" && data.completion?.receipt != null;
20672
21110
  // Exact identity stays available in Task options; failures stay in the
@@ -20678,7 +21116,7 @@ function taskBodyParts(data) {
20678
21116
  ? ` · filed by <span class="mono">${escape(data.coordinator.label)}</span>${data.coordinator.filedAgo === null ? "" : ` ${escape(data.coordinator.filedAgo)}`}`
20679
21117
  : data.filedVia === null || data.filedVia === undefined
20680
21118
  ? ""
20681
- : ` · filed via ${escape(data.filedVia)}`}${data.deliverable === "report" ? ` · <span class="badge">scout</span>` : ""}</p>`;
21119
+ : ` · filed via ${escape(data.filedVia)}`}${data.deliverable === "report" ? ` · <span class="badge">Scout</span>` : ""}</p>`;
20682
21120
  // A pull request that can be merged (or opened) owns the one primary action; the result stays in its section.
20683
21121
  const pullRequestActs = data.csrf !== "" && (data.pullRequest?.view?.canMerge === true || (data.pullRequest?.view == null && data.pullRequest?.offer != null));
20684
21122
  const pullRequestFact = data.pullRequest?.fact != null && data.pullRequest.view?.runId === data.assignment?.receipt?.runId ? data.pullRequest.fact : undefined;
@@ -20719,12 +21157,12 @@ function taskBodyParts(data) {
20719
21157
  : "gone from the tracker";
20720
21158
  const reopenable = mirror.remoteState === "open" && !mirror.dispatchOk && mirror.closeGeneration !== null &&
20721
21159
  mirror.syncGeneration > mirror.closeGeneration && ["cancelled", "failed", "queued"].includes(task.state) && data.csrf !== "";
20722
- return (`<div class="card"><p><strong>external work</strong> <span class="meta">${link} · ${escape(state)}</span></p>` +
21160
+ return (`<div class="card"><p><strong>External work</strong> <span class="meta">${link} · ${escape(state)}</span></p>` +
20723
21161
  (reopenable
20724
21162
  ? `<form method="post" action="${taskHref(task.id)}/reopen" class="row">` +
20725
21163
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
20726
21164
  `<input type="password" name="token" placeholder="your password" aria-label="your password" autocomplete="current-password">` +
20727
- `<button type="submit">reopen — the approved scope stands</button></form>`
21165
+ `<button type="submit">Reopen — the approved scope stands</button></form>`
20728
21166
  : "") +
20729
21167
  `</div>`);
20730
21168
  })();
@@ -20738,7 +21176,7 @@ function taskBodyParts(data) {
20738
21176
  // is as fenced as the builder on a coordinator filing, so "plan
20739
21177
  // first" would recommend a road that refuses.
20740
21178
  ? `<div class="card"><p><strong>This task is waiting on you: an agent filed it, and it has no scope.</strong></p>` +
20741
- `<p class="meta">filed by <span class="mono">${escape(data.coordinator.label)}</span> — nothing plans, claims, or runs until you write a scope below and sign it. Your signature runs their request.</p></div>`
21179
+ `<p class="meta">Filed by <span class="mono">${escape(data.coordinator.label)}</span> — nothing plans, claims, or runs until you write a scope below and sign it. Your signature runs their request.</p></div>`
20742
21180
  : `<div class="card task-scope-needed"><p><strong>No approved scope yet</strong></p>` +
20743
21181
  `<p class="meta"><strong>Plan first</strong> drafts it from the repository, or <a href="#scope">write it yourself</a>.</p></div>`
20744
21182
  : "";
@@ -20773,7 +21211,7 @@ function taskBodyParts(data) {
20773
21211
  attemptPanel,
20774
21212
  data.coordinatorProposals == null || data.coordinatorProposals.rows.length === 0
20775
21213
  ? ""
20776
- : section("proposals", `<h2>proposed by coordinators</h2>` +
21214
+ : section("proposals", `<h2>Proposed by coordinators</h2>` +
20777
21215
  coordinatorProposalsSection(data.coordinatorProposals.rows, data.coordinatorProposals.decisions, data.csrf, data.coordinatorProposals.now, false).replace(/<form method="post" action="\/proposals\/(\d+)\/(confirm|dismiss)" class="inline">/g, (_m, id, verb) => `<form method="post" action="/proposals/${id}/${verb}" class="inline"><input type="hidden" name="return" value="${escape(taskHref(data.task.id))}">`), true, data.coordinatorProposals.rows.length),
20778
21216
  section("decisions", decisions, true, data.decisions.length),
20779
21217
  section("incidents", incidents, true, data.incidents.length),
@@ -20785,13 +21223,13 @@ function taskBodyParts(data) {
20785
21223
  ? ` · CI ${data.publication.lastCheckState} at last observation`
20786
21224
  : " · no checks observed"}</span></p>`,
20787
21225
  section("report", reportCard, true),
20788
- data.assignment == null ? section("attempts", runs, true, data.runs.length) : section("Build activity", runs.replace("<h2>attempts</h2>", ""), false, data.runs.length, "attempts"),
21226
+ data.assignment == null ? section("attempts", runs, true, data.runs.length) : section("Build activity", runs.replace("<h2>Attempts</h2>", ""), false, data.runs.length, "attempts"),
20789
21227
  section("usage", spendCard, false),
20790
21228
  section("steering", steeringCard, (data.steering ?? []).length > 0, (data.steering ?? []).length),
20791
21229
  section("scope",
20792
21230
  // The recipe road rides with the scope it reuses (UI polish
20793
21231
  // 2026-09-13), off the title-to-action path.
20794
- ["<h2>scope</h2>", scopeCard, data.repo !== null && data.scope !== null ? `<p class="meta"><a href="/recipes/from-task?task=${encodeURIComponent(task.id)}">Reuse this scope as a recipe →</a></p>` : "", agentsCardHtml(task.id, data.route, data.csrf, data.canEditRoute === true), revisionInApproval ? "" : revisionCard, data.completion != null ? "" : repairChainHtml(data.repairChain ?? null), attendedCard, scopeForm].join("\n"), data.scopeDraft !== undefined || (data.plan !== "requested" && approveForm === "" && !(scope === null && canPlan))),
21232
+ ["<h2>Scope</h2>", scopeCard, data.repo !== null && data.scope !== null ? `<p class="meta"><a href="/recipes/from-task?task=${encodeURIComponent(task.id)}">Reuse this scope as a recipe →</a></p>` : "", agentsCardHtml(task.id, data.route, data.csrf, data.canEditRoute === true), revisionInApproval ? "" : revisionCard, data.completion != null ? "" : repairChainHtml(data.repairChain ?? null), attendedCard, scopeForm].join("\n"), scopeDraft !== undefined || (data.plan !== "requested" && approveForm === "" && !(scope === null && canPlan))),
20795
21233
  dependencyChoiceNeeded ? "" : section("waits for", waitsForCard, (data.waitsFor ?? []).length > 0, (data.waitsFor ?? []).length),
20796
21234
  section("holds", holds, true, data.holds.length),
20797
21235
  cancelAct,
@@ -20811,7 +21249,19 @@ function taskBodyParts(data) {
20811
21249
  thread.push({ key: "filed", at: rootFiled, kind: "filed", who: "person", author: filedBy ?? "", title: "Filed the task",
20812
21250
  text: (() => { const goal = data.family?.root.goal ?? scope?.goal ?? null; return goal === null ? null : oneLineOf(goal, 280); })(), link: null, html: "", more: null });
20813
21251
  const newestBuilt = familyRuns.filter(run => run.outcome === "built" && run.role !== "planner" && run.role !== "reviewer").sort((a, b) => b.id - a.id)[0];
21252
+ // Failed: the card says what went wrong in one line, and its one act is Retry itself (the same requeue), never a link to
21253
+ // this page; someone who can't retry here (a viewer, a live claim) gets no act at all.
21254
+ const statusCard = data.assignment != null && assignmentOptions !== null ? assignmentCardOf(data.assignment, assignmentOptions) : null;
21255
+ // A live build says which step it is on, or which step it is stuck on and why.
21256
+ const building = statusCard?.status.headline === "Building" && liveRun !== undefined && liveRun.role !== "planner" && liveRun.role !== "reviewer";
21257
+ // While a build runs, the attempts that stopped before it fold into one quiet line under its Building card. Only
21258
+ // there: with any other card (or none) they stay in the thread, never vanish.
21259
+ const earlierStopped = !building ? []
21260
+ : familyRuns.filter(run => run.id < liveRun.id && (run.role === "builder" || run.role === "scout") && run.outcome !== "built" && run.outcome !== "no-change" && run.id !== liveHistoryRunId);
21261
+ const foldedAttempts = new Set(earlierStopped.map(run => run.id));
20814
21262
  for (const run of familyRuns) {
21263
+ if (foldedAttempts.has(run.id))
21264
+ continue;
20815
21265
  const live = run.id === liveHistoryRunId || (run.outcome === null && run.id === liveRunId);
20816
21266
  const noun = runNoun(run);
20817
21267
  const href = { label: `${noun[0].toUpperCase()}${noun.slice(1)} #${run.id}`, href: `/r/${run.id}` };
@@ -20837,7 +21287,7 @@ function taskBodyParts(data) {
20837
21287
  thread.push({ key: `run-${run.id}`, at: run.finishedAt ?? run.startedAt, kind: "result", who: "agent", author: run.runner,
20838
21288
  title: `${title}${revision}`, text: run.handoff === null ? (run.outcome === "failed" ? failedAttemptSentence(run.reason) : run.reason === null || run.outcome === "built" ? null : reasonWords(run.reason)) : oneLineOf(run.handoff, 600),
20839
21289
  link: href, html: "",
20840
- more: previousResult !== null && run.id === newestBuilt?.id && run.taskId === task.id ? { summary: "Result details", html: previousResult.html } : null });
21290
+ more: previousResult !== null && run.id === newestBuilt?.id && run.taskId === task.id ? { summary: STATUS_MORE, html: previousResult.html } : null });
20841
21291
  }
20842
21292
  // What the agent is working through now, and how its plan changed.
20843
21293
  const liveAt = liveRun?.startedAt ?? task.updatedAt;
@@ -20853,7 +21303,7 @@ function taskBodyParts(data) {
20853
21303
  const feedback = data.revision;
20854
21304
  thread.push({ key: "revision", at: task.createdAt, kind: "reply", who: "person", author: feedback.comments[0]?.author ?? "You",
20855
21305
  title: feedback.kind === "ci-repair" ? "Asked for a CI repair" : feedback.kind === "criterion-repair" ? "Asked for a repair" : "Asked for changes",
20856
- text: feedback.comments.map(one => `${one.path === null ? "" : `${one.path}${one.line === null ? "" : `:${one.line}`} — `}${one.note}`).join("\n"),
21306
+ text: feedback.comments.map(one => `${one.path === null ? "" : `${one.path}${one.line === null ? "" : `:${one.line}`} — `}${one.note.trim()}`).filter(one => one !== "").join("\n") || null,
20857
21307
  link: { label: `From build #${feedback.sourceRun}`, href: `/r/${feedback.sourceRun}` }, html: "", more: null });
20858
21308
  }
20859
21309
  for (const decision of data.decisions) {
@@ -20930,9 +21380,6 @@ function taskBodyParts(data) {
20930
21380
  // needs a person, then facts, then folds; the mechanics under Manage.
20931
21381
  const MANAGE = new Set(["steering", "waits-for", "holds"]);
20932
21382
  const finished = task.state === "done" || task.state === "cancelled";
20933
- // Failed: the card says what went wrong in one line, and its one act is Retry itself (the same requeue), never a link to
20934
- // this page; someone who can't retry here (a viewer, a live claim) gets no act at all.
20935
- const statusCard = data.assignment != null && assignmentOptions !== null ? assignmentCardOf(data.assignment, assignmentOptions) : null;
20936
21383
  const failedCard = statusCard?.status.headline === "Failed";
20937
21384
  // A link to this very page, with no section to open, goes nowhere: a card never offers one.
20938
21385
  const bareSelfLink = (href) => {
@@ -20945,7 +21392,33 @@ function taskBodyParts(data) {
20945
21392
  // Build again, in place, names its form after the card's own act.
20946
21393
  const rebuild = data.csrf !== "" && data.assignment?.primaryAction?.code === "retry-task" && data.assignment.need != null && "key" in data.assignment.need && data.assignment.need.key === "rebuild"
20947
21394
  ? { action: `${taskHref(data.assignment.rootId)}/requeue` } : null;
20948
- const retry = failedCard && task.state === "failed" && !data.claimed && !stopControlsActive && data.csrf !== "" ? { action: `${taskHref(task.id)}/requeue` } : null;
21395
+ const retry = failedCard && task.state === "failed" && !data.claimed && !stopControlsActive && data.csrf !== ""
21396
+ ? { action: `${taskHref(task.id)}/requeue`, ...(data.failure == null ? {} : { note: retryNoteOf(data.failure.suggestion) }) } : null;
21397
+ // Run checks happens in place: a status row's Run checks posts here and comes back; with nothing to run, the row offers nothing.
21398
+ const runChecks = data.runChecks ?? null;
21399
+ // A row's link to a result in Chat opens that result's own page instead: Chat would only lead back here.
21400
+ const resultPageOf = (href) => {
21401
+ const chat = href === null ? null : /^\/chat\?task=([A-Za-z0-9._~%-]{1,200})&result=([0-9]{1,15})(?:&tab=(changes|checks))?(#[A-Za-z0-9_-]{1,80})?$/.exec(href);
21402
+ return chat === null ? href : `/review?result=${chat[1]}&run=${chat[2]}${chat[3] === undefined ? "" : `&tab=${chat[3]}`}${chat[4] ?? ""}`;
21403
+ };
21404
+ const inPlaceChecks = (card) => {
21405
+ const status = data.demo === true ? demoChecksOf(card.status) : card.status;
21406
+ return { ...card, status: { ...status, details: status.details.map(one => one.action?.href?.endsWith("#follow-ups") === true ? { ...one, action: runChecks === null ? null : { label: one.action.label, href: runChecks.action } }
21407
+ : { ...one, href: resultPageOf(one.href), action: one.action === null ? null : { ...one.action, href: resultPageOf(one.action.href) } }) } };
21408
+ };
21409
+ const steps = building ? buildProgressOf(data.milestoneProgress ?? null) : null;
21410
+ const progress = steps === null ? null : { line: steps.line,
21411
+ stuck: steps.stuck === null ? null : { ...steps.stuck, action: data.csrf === "" ? null : { label: "Send the agent a note", href: "#steering" } } };
21412
+ // The Building card carries Stop for the build it describes (the same form), in place of a second card below it.
21413
+ const stop = building && data.control?.kind === "stop" && data.control.run === liveRun.id && data.csrf !== ""
21414
+ ? { action: `${taskHref(task.id)}/stop`, run: data.control.run } : null;
21415
+ // The Building card keeps its way to the build's own record (/r/<id>), whatever act it shows.
21416
+ const record = building ? { label: `Build #${liveRun.id} record`, href: `/r/${liveRun.id}` } : null;
21417
+ const earlier = earlierStopped.length === 0 ? null : {
21418
+ summary: earlierAttemptsWords(earlierStopped.length),
21419
+ attempts: [...earlierStopped].sort((a, b) => a.id - b.id).map(run => ({ label: `${runNoun(run)[0].toUpperCase()}${runNoun(run).slice(1)} #${run.id}`, href: `/r/${run.id}`,
21420
+ text: run.outcome === null ? "Never finished." : run.outcome === "failed" ? failedAttemptSentence(run.reason) : run.reason === null ? null : (words => `${words.charAt(0).toUpperCase()}${words.slice(1)}.`)(reasonWords(run.reason)) })),
21421
+ };
20949
21422
  const view = {
20950
21423
  kind: "task",
20951
21424
  id: task.id,
@@ -20957,18 +21430,24 @@ function taskBodyParts(data) {
20957
21430
  { label: "Ask", href: taskChatHref(data.rootId ?? task.id), active: false },
20958
21431
  ],
20959
21432
  version: data.versionLabel == null ? null : { label: data.versionLabel, current: { label: "Current work", href: taskHref(data.rootId ?? task.id) } },
20960
- status: statusCard === null ? null : failedCard ? { ...statusCard, action: null, status: { ...statusCard.status, sentence: data.failure?.line ?? statusCard.status.sentence } }
20961
- : statusCard.action !== null && rebuild === null && bareSelfLink(statusCard.action.href) ? { ...statusCard, action: null } : statusCard,
21433
+ status: statusCard === null ? null : inPlaceChecks(failedCard ? { ...statusCard, action: null, status: { ...statusCard.status, sentence: data.failure?.line ?? statusCard.status.sentence } }
21434
+ : progress !== null || record !== null ? { ...statusCard, action: statusCard.action?.href === record?.href ? null : statusCard.action, status: { ...statusCard.status, sentence: progress?.line ?? statusCard.status.sentence } }
21435
+ : statusCard.action !== null && rebuild === null && bareSelfLink(statusCard.action.href) ? { ...statusCard, action: null } : statusCard),
20962
21436
  statusHtml,
20963
- failure: failedCard ? data.failure ?? null : null,
21437
+ failure: failedCard && data.failure != null ? { line: data.failure.line, evidence: data.failure.evidence, suggestion: data.failure.suggestion, link: data.failure.link } : null,
20964
21438
  retry,
21439
+ runChecks,
21440
+ progress,
21441
+ stop,
21442
+ record,
21443
+ earlier,
20965
21444
  approval: approvalHtml,
20966
21445
  confirmStopped: data.csrf !== "" && data.assignment?.primaryAction?.code === "confirm-stopped" && data.assignment.primaryAction.target.runId !== null
20967
21446
  ? { action: `${taskHref(data.assignment.rootId)}/confirm-stopped`, run: data.assignment.primaryAction.target.runId, checked: needsCheck(data.assignment) } : null,
20968
21447
  rebuild,
20969
21448
  // The plan, progress and plan changes are thread entries now; the rest still needs a person here.
20970
21449
  lead: [
20971
- { key: "history", html: data.history ?? "" }, { key: "control", html: controlHtml }, { key: "problem", html: problemHtml },
21450
+ { key: "history", html: data.history ?? "" }, { key: "control", html: stop === null ? controlHtml : taskControlDetailsHtml(data.control, task.id, data.csrf, "task", false, true) }, { key: "problem", html: problemHtml },
20972
21451
  { key: "pull-request", html: pullRequestCard },
20973
21452
  { key: "needs-scope", html: needsScopeCard },
20974
21453
  { key: "mirror", html: mirrorCard }, { key: "contest", html: contestCard }, { key: "attempt", html: attemptPanel },
@@ -20979,7 +21458,7 @@ function taskBodyParts(data) {
20979
21458
  sections: [
20980
21459
  ...(previousResult === null || thread.some(one => one.more !== null) ? [] : [{ id: "result", title: previousResult.title, html: previousResult.html, open: false, count: null }]),
20981
21460
  // Questions are in the thread; the scope and the ledgers fold here until asked.
20982
- ...sectionParts.filter(one => !MANAGE.has(one.id) && one.id !== "decisions").map(one => one.id === "scope" && data.scopeDraft === undefined && (finished || approval.approved) ? { ...one, open: false } : one),
21461
+ ...sectionParts.filter(one => !MANAGE.has(one.id) && one.id !== "decisions").map(one => one.id === "scope" && scopeDraft === undefined && (finished || approval.approved) ? { ...one, open: false } : one),
20983
21462
  ],
20984
21463
  manage: [
20985
21464
  ...sectionParts.filter(one => MANAGE.has(one.id)),
@@ -21384,12 +21863,13 @@ function reviewCockpitPage(chrome, data) {
21384
21863
  capped: queue.length >= data.queueCap ? data.queueCap : null,
21385
21864
  missing: data.missing === null ? null : `No completed task ${data.missing} is in view here — it may not be finished, or it is outside this console's projects.`,
21386
21865
  beyond: data.beyondQueue && selected !== null,
21387
- selected: detailParts === null ? null : detailParts.selected,
21866
+ selected: detailParts === null ? null : data.refusal == null ? detailParts.selected : { ...detailParts.selected, problem: data.refusal },
21388
21867
  };
21389
21868
  return screen("review", [
21390
21869
  selected === null ? `<h1>Results</h1>` : "",
21391
21870
  missingNote,
21392
21871
  beyondNote,
21872
+ data.refusal == null || selected === null ? "" : `<p class="problem" data-result-refusal>${escape(data.refusal)}</p>`,
21393
21873
  `<div class="cockpit">${queuePane}${detail}</div>`,
21394
21874
  ].join("\n"), {
21395
21875
  chrome,
@@ -21425,26 +21905,27 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
21425
21905
  const intent = view.intent;
21426
21906
  let intentView = null;
21427
21907
  if (intent === null) {
21428
- parts.push(`<section class="card cockpit-section" data-cockpit-section="intent"><h3>approved scope</h3><p class="meta">No scope was filed for this task, so there is no approved goal or boundary to review.</p></section>`);
21908
+ parts.push(`<section class="card cockpit-section" data-cockpit-section="intent"><h3>Approved scope</h3><p class="meta">No scope was filed for this task, so there is no approved goal or boundary to review.</p></section>`);
21429
21909
  }
21430
21910
  else {
21911
+ // The time itself goes to the page as a stamp: the one formatter words it in the viewer's zone.
21431
21912
  const approvalWords = intent.approval.approved
21432
- ? `approved by ${intent.approvedBy ?? "an operator"} · ${when(intent.approval.at)}`
21913
+ ? `Approved by ${intent.approvedBy ?? "an operator"}`
21433
21914
  : intent.approval.reason === "changed"
21434
- ? "the scope changed after approval — the words below are the current text, not the signed one"
21435
- : "never approved — the result was built without a signed scope";
21915
+ ? "The scope changed after approval. The words below are the current text, not the signed one."
21916
+ : "Never approved. The result was built without a signed scope.";
21436
21917
  const approval = intent.approval.approved
21437
- ? `approved by ${escape(intent.approvedBy ?? "an operator")} · ${whenTime(intent.approval.at)}`
21438
- : approvalWords;
21439
- const body = `<p class="recap" style="margin-top:.25rem"><strong>goal</strong> ${escape(intent.goal)}</p>` +
21440
- (intent.outOfScope === null ? `<p class="meta">no boundary was stated</p>` : `<p class="recap"><strong>not this</strong> ${escape(intent.outOfScope)}</p>`) +
21918
+ ? `Approved by ${escape(intent.approvedBy ?? "an operator")} · ${whenTime(intent.approval.at)}`
21919
+ : escape(approvalWords);
21920
+ const body = `<p class="recap" style="margin-top:.25rem"><strong>Goal</strong> ${escape(intent.goal)}</p>` +
21921
+ (intent.outOfScope === null ? `<p class="meta">No boundary was stated</p>` : `<p class="recap"><strong>Not this</strong> ${escape(intent.outOfScope)}</p>`) +
21441
21922
  (intent.touches.length === 0
21442
- ? `<p class="meta">no expected paths were signed — every changed file reads as in bounds</p>`
21443
- : `<p class="row"><span class="meta">expected to touch</span> ${intent.touches.map(one => `<span class="mono">${escape(one)}</span>`).join(" ")}</p>`) +
21923
+ ? `<p class="meta">No expected paths were signed — every changed file reads as in bounds</p>`
21924
+ : `<p class="row"><span class="meta">Expected to touch</span> ${intent.touches.map(one => `<span class="mono">${escape(one)}</span>`).join(" ")}</p>`) +
21444
21925
  (view.plan === null
21445
21926
  ? ""
21446
- : `<p class="meta">plan revision ${view.plan.revision} · <span class="mono" title="${escape(view.plan.sha256)}">${escape(view.plan.sha256.slice(0, 12))}…</span>${view.plan.approach === null ? "" : ` — ${escape(view.plan.approach)}`}</p>`);
21447
- intentView = { approval: approvalWords, html: body };
21927
+ : `<p class="meta">Plan revision ${view.plan.revision} · <span class="mono" title="${escape(view.plan.sha256)}">${escape(view.plan.sha256.slice(0, 12))}…</span>${view.plan.approach === null ? "" : ` — ${escape(view.plan.approach)}`}</p>`);
21928
+ intentView = { approval: approvalWords, approvedAt: intent.approval.approved ? intent.approval.at ?? null : null, html: body };
21448
21929
  parts.push(`<details class="card cockpit-section cockpit-disclosure" data-cockpit-section="intent"><summary><h3>Approved scope<small>What this build was asked to do · ${approval}</small></h3><span class="cockpit-disclosure-action">View</span></summary>` +
21449
21930
  `<div class="cockpit-disclosure-body">` + body + `</div></details>`);
21450
21931
  }
@@ -21466,7 +21947,7 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
21466
21947
  ] },
21467
21948
  };
21468
21949
  if (run === null || view.detail === null) {
21469
- parts.push(`<section class="card cockpit-section" id="verification" data-cockpit-section="proof"><h3>verification</h3>` +
21950
+ parts.push(`<section class="card cockpit-section" id="verification" data-cockpit-section="proof"><h3>Verification</h3>` +
21470
21951
  `<p class="meta">This task has no finished build record, so there are no captured changes or checks to review.</p>` +
21471
21952
  `<p class="row"><a href="${taskHref(view.taskId)}">Open the task →</a></p></section>`);
21472
21953
  return { html: `<section class="cockpit-detail">${parts.join("\n")}</section>`, selected: { ...selected, noRun: "This task has no finished build record, so there are no captured changes or checks to review." } };
@@ -21499,24 +21980,31 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
21499
21980
  });
21500
21981
  const contest = view.contest === null ? "" : `<p class="row"><a href="/contest/${view.contest.id}">${view.contest.state === "pick-wait" ? `compare the ${contestNoun(view.contest.kind)} and pick →` : `the ${contestNoun(view.contest.kind)} (${escape(view.contest.state)}) →`}</a></p>`;
21501
21982
  parts.push(`<div id="verification" data-cockpit-section="result">` + panel.html + contest + `</div>`);
21983
+ const blocked = cantAcceptYetOf(proof?.verdict ?? null, proof?.reasons ?? [], accepted);
21984
+ const youCheck = panel.panel.youCheck;
21985
+ // Accept and finish posts the completion; when an acceptance is owed (the person's own checks, or a reason a
21986
+ // report that doesn't match its changes asks for), the same request records it first.
21987
+ const owed = accepted ? null : youCheck?.accept != null ? { note: null } : panel.panel.need?.accept != null ? { note: panel.panel.need.accept.note }
21988
+ : blocked === ACCEPT_NEEDS_REASON ? { note: "Why is this safe to accept?" } : null;
21502
21989
  const complete = assignment?.state === "ready-to-check" && assignment.receipt !== null && canRetryReview && csrf !== ""
21503
- ? { action: `${taskHref(view.taskId)}/complete`, receipt: assignment.receipt.digest, run: run.id } : null;
21990
+ ? { action: `${taskHref(view.taskId)}/complete`, receipt: assignment.receipt.digest, run: run.id, accept: owed } : null;
21504
21991
  if (complete !== null)
21505
- parts.push(completionForm(view.taskId, run.id, complete.receipt, csrf, view.detail?.pullRequestTo ?? null));
21506
- // The one decision, after the evidence: Accept only when every requirement is met and the checks passed.
21507
- const acceptsHere = complete !== null || panel.panel.need?.accept != null;
21992
+ parts.push(completionForm(view.taskId, run.id, complete.receipt, csrf, view.detail?.pullRequestTo ?? null, owed));
21993
+ // The one decision, after the evidence: Accept and finish only when every requirement is met and the checks passed.
21994
+ const acceptsHere = complete !== null || panel.panel.need?.accept != null || youCheck?.accept != null;
21508
21995
  const matrix = proof === null || proof.proofProblem !== null ? [] : proof.matrix;
21509
- const blocked = cantAcceptYetOf(proof?.verdict ?? null, proof?.reasons ?? [], accepted);
21510
- const words = !acceptsHere ? null : { sentence: RESULT_DECISION_SENTENCE, ...acceptWordsOf({
21511
- checks: checks === undefined ? null : checks.running != null ? "running" : checks.level === "off" && checks.status !== "passed" ? "off" : checks.status,
21512
- unmet: matrix.filter(row => row.state !== "pass" && row.state !== "manual-review").length,
21513
- yours: complete === null || accepted ? 0 : panel.panel.youCheck?.lines.length ?? 0,
21514
- action: complete !== null ? "complete" : "accept",
21515
- publishing: view.detail?.publishing ?? "other",
21516
- proof: proof !== null && proof.proof !== null && proof.proofProblem === null,
21517
- }) };
21518
- // Refuted: plain Accept stays as allowed, in outline, and the one line before the acts (acts.line) says why.
21519
- const decision = words === null || blocked === null ? words : { ...words, label: "Accept", ready: false };
21996
+ const unanswered = accepted || youCheck == null ? [] : youCheck.items.length > 0 ? youCheck.items.map(one => one.statement === "" ? one.words : one.statement) : youCheck.lines;
21997
+ const base = !acceptsHere ? null : acceptWordsOf({
21998
+ checks: checks === undefined ? null : checks.running != null ? "running" : checks.level === "off" && checks.status !== "passed" ? "off" : checks.status,
21999
+ unmet: matrix.filter(row => row.state !== "pass" && row.state !== "manual-review").length,
22000
+ action: complete !== null ? "complete" : "accept",
22001
+ publishing: view.detail?.publishing ?? "other",
22002
+ proof: proof !== null && proof.proof !== null && proof.proofProblem === null,
22003
+ });
22004
+ // Refuted: plain Accept and finish stays as allowed, in outline, and the one line before the acts (acts.line) says why.
22005
+ const settled = base === null || blocked === null ? base : { ...base, label: "Accept and finish", ready: false };
22006
+ const decision = settled === null ? null : { sentence: RESULT_DECISION_SENTENCE, ...settled, ...acceptWithChecksOf(settled, { unanswered, notRight: [] }),
22007
+ base: { label: settled.label, ready: settled.ready, why: settled.why } };
21520
22008
  // 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.
21521
22009
  const followUps = view.detail.followUps ?? null;
21522
22010
  const checksRunning = checks?.running != null || (followUps?.checks.some(one => one.state === "waiting" || one.state === "running") ?? false);
@@ -21526,18 +22014,27 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
21526
22014
  ? { action: `/r/${run.id}/checks`, level: followUps.full ? "full" : "quick", returnTo: `${here}&run=${run.id}&tab=checks` } : null;
21527
22015
  const need = panel.panel.need;
21528
22016
  const nextKind = selected.next?.kind;
21529
- const acts = resultActsOf({
21530
- accept: decision === null ? null : { ready: decision.ready },
21531
- runChecks: runChecks !== null,
21532
- checksRunning,
21533
- blocked,
21534
- canRequest: panel.panel.canRequest && panel.panel.request !== null,
21535
- need: need === null || need.accept != null ? null : need.rebuild != null ? "rebuild" : need.confirm !== null ? "confirm-stopped" : null,
21536
- next: nextKind === "revise" || nextKind === "draft-repair" ? nextKind : null,
21537
- });
22017
+ // A failed build: what went wrong is the card, Retry the act; a link to this same page goes nowhere, so it isn't one.
22018
+ const failure = view.failure == null ? null : { line: view.failure.line, evidence: view.failure.evidence, suggestion: view.failure.suggestion, retry: view.failure.retry,
22019
+ link: view.failure.link === null || (view.failure.link.href.startsWith(`${here}&run=${run.id}`) && !view.failure.link.href.includes("#")) ? null : view.failure.link,
22020
+ ...(view.failure.acceptAnyway == null ? {} : { acceptAnyway: { ...view.failure.acceptAnyway, returnTo: `${here}&run=${run.id}` } }) };
22021
+ const actFacts = failure !== null
22022
+ ? { accept: null, runChecks: runChecks !== null, checksRunning, blocked: null, canRequest: false, need: null, next: null, failed: { retry: failure.retry !== null, acceptAnyway: failure.acceptAnyway !== undefined } }
22023
+ : {
22024
+ accept: settled === null ? null : { ready: settled.ready },
22025
+ runChecks: runChecks !== null,
22026
+ checksRunning,
22027
+ blocked,
22028
+ canRequest: panel.panel.canRequest && panel.panel.request !== null,
22029
+ need: need === null || need.accept != null ? null : need.rebuild != null ? "rebuild" : need.confirm !== null ? "confirm-stopped" : null,
22030
+ next: nextKind === "revise" || nextKind === "draft-repair" ? nextKind : null,
22031
+ unanswered: settled === null ? 0 : unanswered.length,
22032
+ notRight: 0,
22033
+ };
22034
+ const acts = resultActsOf(actFacts);
21538
22035
  // A refuted result's card lists every recorded disagreement, each tied to its lines; when the report itself doesn't
21539
22036
  // match the changes (not a failed check), that is the headline.
21540
- const mismatch = proof === null || accepted || proof.verdict !== "refuted" ? null : (() => {
22037
+ const mismatch = failure !== null || proof === null || accepted || proof.verdict !== "refuted" ? null : (() => {
21541
22038
  const patch = view.detail?.terminal?.patch ?? null;
21542
22039
  const files = patch === null || "problem" in patch ? [] : parseReviewDiff(patch.text).files;
21543
22040
  // Each file's first change: the lines it added (else every line it shows), and where that change starts (its link target).
@@ -21570,25 +22067,51 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
21570
22067
  };
21571
22068
  })();
21572
22069
  if (view.notes.length > 0) {
21573
- parts.push(`<section class="card cockpit-section" data-cockpit-section="notes"><h3>operator notes</h3>` +
22070
+ parts.push(`<section class="card cockpit-section" data-cockpit-section="notes"><h3>Operator notes</h3>` +
21574
22071
  view.notes.map(one => `<p class="row"><span class="meta">${escape(one.author)} · ${whenTime(one.createdAt)}</span> ${escape(one.note)}</p>`).join("\n") +
21575
22072
  `</section>`);
21576
22073
  }
21577
22074
  return {
21578
22075
  html: `<section class="cockpit-detail">${parts.join("\n")}</section>`,
21579
22076
  selected: {
21580
- ...selected, complete, decision, panel: panel.panel, contest, acts, runChecks, mismatch,
22077
+ ...selected, complete: failure === null ? complete : null, decision: failure === null ? decision : null, contest, acts, actFacts, runChecks, mismatch, failure,
22078
+ // A failed build reads Failed, whatever its task has done since; its outcome is what went wrong.
22079
+ // Run checks is the decision's own act on this page; a status row never sends the person to Chat for it.
22080
+ panel: (() => {
22081
+ // A Chat link to this same result would lead back here: it isn't one.
22082
+ const elsewhere = (href) => href !== null && !href.endsWith("#follow-ups") && !new RegExp(`^/chat\\?task=[^&]+&result=${run.id}(?:&|$)`).test(href);
22083
+ const shown = panel.panel.status === null ? null : view.demo === true ? demoChecksOf(panel.panel.status) : panel.panel.status;
22084
+ const missed = proof === null || proof.proofProblem !== null ? 0 : requirementsOf(proof.matrix)?.missed ?? 0;
22085
+ const status = shown === null ? null : { ...shown, details: shown.details.map(one => ({ ...one,
22086
+ href: elsewhere(one.href) ? one.href : null, action: one.action !== null && elsewhere(one.action.href) ? one.action : null })) };
22087
+ return failure === null ? { ...panel.panel, status }
22088
+ : { ...panel.panel, outcome: failure.line, need: null, youCheck: null, status: status === null ? null : { ...status, headline: "Failed", tone: "danger", sentence: failure.line, need: null,
22089
+ // Read as Failed, the Requirements row counts what it missed, as the task's card does.
22090
+ details: status.details.map(one => one.key !== "requirements" || missed === 0 ? one : { ...one, text: `${missed} missed`, mark: "failed", action: null }) },
22091
+ // What it missed is said once, in plain words above; the recorded wording stays under Details.
22092
+ attention: panel.panel.attention.filter(one => !(proof?.reasons ?? []).includes(one) && !(proof?.matrix ?? []).some(row => row.detail.includes(one))) };
22093
+ })(),
22094
+ ...(failure === null ? {} : { status: { label: "Failed", tone: "problem", token: "failed" } }),
21581
22095
  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}` },
21582
22096
  },
21583
22097
  };
21584
22098
  }
21585
- function completionForm(taskId, runId, digest, csrf, pullRequestTo = null) {
21586
- const hidden = `<input type="hidden" name="csrf" value="${escape(csrf)}"><input type="hidden" name="receipt" value="${escape(digest)}"><input type="hidden" name="run" value="${runId}">`;
22099
+ /** What Accept and finish owes before it completes, as the form asks for it: the server's own rule
22100
+ * (owedAcceptance), so the form asks for exactly what the completion requires. */
22101
+ function owedAcceptanceOf(receipt) {
22102
+ const owed = owedAcceptance(receipt);
22103
+ return owed === null ? null : owed === "person-check" ? { note: null } : { note: "Why is this safe to accept?" };
22104
+ }
22105
+ function completionForm(taskId, runId, digest, csrf, pullRequestTo = null, accept = null) {
22106
+ // Accept and finish, in one request: with an acceptance owed (the person's own checks, or an exception and
22107
+ // its reason, asked for right here), the same post records it first.
22108
+ const hidden = `<input type="hidden" name="csrf" value="${escape(csrf)}"><input type="hidden" name="receipt" value="${escape(digest)}"><input type="hidden" name="run" value="${runId}">${accept === null ? "" : `<input type="hidden" name="accept" value="1">`}` +
22109
+ (accept?.note == null ? "" : `<label class="meta" for="complete-reason-${runId}">${escape(ACCEPT_NEEDS_REASON)}</label><input type="text" id="complete-reason-${runId}" name="note" maxlength="500" required placeholder="${escape(accept.note)}">`);
21587
22110
  if (pullRequestTo !== null) {
21588
- // With pull requests set up: the PR is the primary road, a bare completion the quiet one beside it.
21589
- return `<form method="post" action="${taskHref(taskId)}/complete" class="card result-complete">${hidden}<p class="meta">After inspecting the work, mark this result complete. A pull request opens on ${escape(pullRequestTo)} from this exact commit.</p><div class="result-complete-actions"><button type="submit" name="publish" value="1" style="min-height:44px">Complete and open a pull request</button><button type="submit" class="secondary" style="min-height:44px">Complete only</button></div></form>`;
22111
+ // With pull requests set up: the PR is the primary road, a bare finish the quiet one beside it.
22112
+ return `<form method="post" action="${taskHref(taskId)}/complete" class="card result-complete">${hidden}<p class="meta">Finishes the task. A pull request opens on ${escape(pullRequestTo)} from this exact commit when you ask for one.</p><div class="result-complete-actions"><button type="submit" name="publish" value="1" style="min-height:44px">Complete and open a pull request</button><button type="submit" class="secondary" style="min-height:44px">Accept and finish</button></div></form>`;
21590
22113
  }
21591
- return `<form method="post" action="${taskHref(taskId)}/complete" class="card result-complete">${hidden}<p class="meta">After inspecting the work, mark this result complete. Checks stay unchanged; nothing is published or deployed.</p><button type="submit" style="min-height:44px">Mark complete</button></form>`;
22114
+ return `<form method="post" action="${taskHref(taskId)}/complete" class="card result-complete">${hidden}<p class="meta">Finishes the task. Checks stay unchanged; nothing is published or deployed.</p><button type="submit" style="min-height:44px">Accept and finish</button></form>`;
21592
22115
  }
21593
22116
  /** The pull request, in one card: its state and link, one line of detail, and the one action it needs — Merge
21594
22117
  * behind the password when checks passed, or Open a pull request for a result completed without one. */
@@ -21726,13 +22249,13 @@ function shortDigest(digest) {
21726
22249
  * orphaned run keeps saying what actually became of it. */
21727
22250
  function runOutcomeBadge(run, live) {
21728
22251
  if (!live && run.role === "planner" && run.reason === "plan-drafted") {
21729
- return `<span class="badge">planned</span>`;
22252
+ return `<span class="badge">Planned</span>`;
21730
22253
  }
21731
22254
  if (!live && run.reason === "interrupted")
21732
- return `<span class="badge">interrupted</span>`;
22255
+ return `<span class="badge">Interrupted</span>`;
21733
22256
  return live
21734
- ? `<span class="badge badge-running">running</span>`
21735
- : `<span class="badge badge-${escape(run.outcome ?? "cut")}">${escape(run.outcome ?? "never finished")}</span>`;
22257
+ ? `<span class="badge badge-running">Running</span>`
22258
+ : `<span class="badge badge-${escape(run.outcome ?? "cut")}">${escape(sentenceCase(run.outcome ?? "never finished"))}</span>`;
21736
22259
  }
21737
22260
  const runNoun = (run) => run.role === "planner" ? "plan" : run.role === "reviewer" ? "review" : run.role === "scout" ? "report" : "build";
21738
22261
  function runsPage(chrome, rows, liveIds, nextCursor, verdicts = new Map(), accepted = new Set()) {
@@ -21747,7 +22270,7 @@ function runsPage(chrome, rows, liveIds, nextCursor, verdicts = new Map(), accep
21747
22270
  return (`<p class="row"><a href="/r/${run.id}" class="mono">#${run.id}</a> ` +
21748
22271
  `<a href="${taskHref(run.taskId)}" class="mono">${escape(run.taskId)}</a> ` +
21749
22272
  runOutcomeBadge(run, liveIds.has(run.id)) +
21750
- `${run.qualityMode === "strict" ? ` <span class="badge">strict review</span>` : ""}` +
22273
+ `${run.qualityMode === "strict" ? ` <span class="badge">Strict review</span>` : ""}` +
21751
22274
  needsVerification +
21752
22275
  criterionMatrixSummary(verdicts.get(run.id)?.matrix ?? []) +
21753
22276
  `${run.provider === "claude" ? "" : ` <span class="meta mono">${escape(run.provider)}</span>`}` +
@@ -21756,7 +22279,7 @@ function runsPage(chrome, rows, liveIds, nextCursor, verdicts = new Map(), accep
21756
22279
  })
21757
22280
  .join("\n");
21758
22281
  const older = nextCursor === null ? "" : `<p><a href="/runs?before=${nextCursor}">older →</a></p>`;
21759
- return screen("builds", [`<h1>builds <a class="badge" href="/peek">peek at the live ones \u2192</a></h1>`, buildsViews("builds"), `<p class="hint">one build = one attempt by an agent to complete a task, on its own branch</p>`, list, older].join("\n"), { chrome });
22282
+ return screen("builds", [`<h1>Builds <a class="badge" href="/peek">Peek at the live ones \u2192</a></h1>`, buildsViews("builds"), `<p class="hint">one build = one attempt by an agent to complete a task, on its own branch</p>`, list, older].join("\n"), { chrome });
21760
22283
  }
21761
22284
  function structuredHandoffView(artifacts, root) {
21762
22285
  const artifact = [...artifacts].reverse().find(one => one.kind === "handoff");
@@ -22111,7 +22634,7 @@ function reviewDiffHtml(patch, stat, runId, commentable, anchors = new Map()) {
22111
22634
  const parsed = parseReviewDiff(patch.text);
22112
22635
  const structured = parsed.files.some(file => file.hunks.length > 0);
22113
22636
  if (!structured) {
22114
- return (`<details><summary>the patch${patch.truncated ? " (TRUNCATED — the raw record says how much was cut)" : ""}</summary>` +
22637
+ return (`<details><summary>The patch${patch.truncated ? " (TRUNCATED — the raw record says how much was cut)" : ""}</summary>` +
22115
22638
  `<pre class="mono" style="overflow-x:auto">${escape(patch.text)}</pre></details>` +
22116
22639
  storedDownloadLink(runId, patch.artifactId, "diff", patch.truncated));
22117
22640
  }
@@ -22185,12 +22708,12 @@ function storedDownloadLink(runId, artifactId, what, truncated) {
22185
22708
  * page's prefill script reads — a real button, keyboard-reachable, separate
22186
22709
  * from the link so the two never fight over one click (finding 4). */
22187
22710
  function terminalDiffCard(view, runId, editor = null, commentable = false) {
22188
- const parts = ["<h2>what changed</h2>"];
22711
+ const parts = ["<h2>What changed</h2>"];
22189
22712
  if (view.stat === null) {
22190
- parts.push(`<p class="meta">no change summary was captured for this build</p>`);
22713
+ parts.push(`<p class="meta">No change summary was captured for this build</p>`);
22191
22714
  }
22192
22715
  else if ("problem" in view.stat) {
22193
- parts.push(`<p class="meta">stat: ${escape(view.stat.problem)}</p>`);
22716
+ parts.push(`<p class="meta">Stat: ${escape(view.stat.problem)}</p>`);
22194
22717
  }
22195
22718
  else {
22196
22719
  const s = view.stat;
@@ -22212,23 +22735,23 @@ function terminalDiffCard(view, runId, editor = null, commentable = false) {
22212
22735
  .slice(0, 40)
22213
22736
  .map(file => `<p class="row mono">${fileName(file.path)}${file.renamedFrom === undefined ? "" : ` (was ${escape(file.renamedFrom)})`} ` +
22214
22737
  `<span class="meta">${file.additions === null || file.deletions === null ? "binary" : `+${file.additions} −${file.deletions}`}</span>` +
22215
- `${commentable ? ` <button type="button" class="pick-file" data-path="${escape(file.path)}">comment</button>` : ""}</p>`)
22738
+ `${commentable ? ` <button type="button" class="pick-file" data-path="${escape(file.path)}">Comment</button>` : ""}</p>`)
22216
22739
  .join("\n") +
22217
22740
  (s.files.length > 40 ? `<p class="meta">…and ${s.files.length - 40} more file(s)</p>` : "") +
22218
22741
  `</div>`);
22219
22742
  if (editor !== null) {
22220
- parts.push(`<p class="meta">file links open in VS Code on THIS device — if the build's worktree is gone, a link opens nothing</p>`);
22743
+ parts.push(`<p class="meta">File links open in VS Code on THIS device — if the build's worktree is gone, a link opens nothing</p>`);
22221
22744
  }
22222
22745
  }
22223
22746
  }
22224
22747
  if (view.patch === null) {
22225
- parts.push(`<p class="meta">the final diff was not captured for this build</p>`);
22748
+ parts.push(`<p class="meta">The final diff was not captured for this build</p>`);
22226
22749
  }
22227
22750
  else if ("problem" in view.patch) {
22228
- parts.push(`<p class="meta">patch: ${escape(view.patch.problem)}</p>`);
22751
+ parts.push(`<p class="meta">Patch: ${escape(view.patch.problem)}</p>`);
22229
22752
  }
22230
22753
  else if (view.patch.text.trim() === "") {
22231
- parts.push(`<p class="meta">empty diff — captured successfully, nothing changed</p>`);
22754
+ parts.push(`<p class="meta">Empty diff — captured successfully, nothing changed</p>`);
22232
22755
  }
22233
22756
  else {
22234
22757
  parts.push(reviewDiffHtml(view.patch, view.stat, runId, commentable));
@@ -22247,7 +22770,7 @@ function evidenceBundleCard(view, run, publication = null, review = null) {
22247
22770
  if (view === null)
22248
22771
  return "";
22249
22772
  const runId = run.id;
22250
- const parts = ["<h2>verification and evidence</h2>"];
22773
+ const parts = ["<h2>Verification and evidence</h2>"];
22251
22774
  if (view.verdict !== null) {
22252
22775
  // The same status line every other surface renders for this run
22253
22776
  // (workspace package 1), beside the verdict's own explanation.
@@ -22257,7 +22780,7 @@ function evidenceBundleCard(view, run, publication = null, review = null) {
22257
22780
  if (view.accepted !== null) {
22258
22781
  parts.push(`<p class="meta">Accepted with an exception by <span class="mono">${escape(view.accepted.by)}</span> · ${whenTime(view.accepted.at)}${view.accepted.note === null ? "" : ` — ${escape(view.accepted.note)}`}</p>`);
22259
22782
  }
22260
- parts.push(criterionMatrixHtml(view.matrix, { runId, links: view.matrixLinks }));
22783
+ parts.push(criterionMatrixHtml(view.matrix, { runId, links: view.matrixLinks, verdict: view.verdict }));
22261
22784
  parts.push(semanticCoverageHtml(view.matrix, view.qualityMode));
22262
22785
  if (reviewConflict(view.matrix, view.machineVerdict, view.verdict)) {
22263
22786
  parts.push(`<p class="meta">An independent review found conflicting evidence.</p>`);
@@ -22268,25 +22791,25 @@ function evidenceBundleCard(view, run, publication = null, review = null) {
22268
22791
  }
22269
22792
  else if (view.proof !== null) {
22270
22793
  if (view.proof.criteria.length > 0) {
22271
- parts.push(`<div class="result-section"><strong>acceptance criteria</strong><ul>` +
22794
+ parts.push(`<div class="result-section"><strong>Acceptance criteria</strong><ul>` +
22272
22795
  view.proof.criteria
22273
- .map(one => `<li><span class="badge${one.verdict === "met" ? " badge-done" : one.verdict === "not-met" ? " badge-failed" : ""}">${escape(one.verdict)}</span> ${escape(one.statement)} <span class="meta">— ${escape(one.how)}</span></li>`)
22796
+ .map(one => `<li><span class="badge${one.verdict === "met" ? " badge-done" : one.verdict === "not-met" ? " badge-failed" : ""}">${escape(sentenceCase(one.verdict))}</span> ${escape(one.statement)} <span class="meta">— ${escape(one.how)}</span></li>`)
22274
22797
  .join("") +
22275
22798
  `</ul></div>`);
22276
22799
  }
22277
22800
  if (view.proof.checks.length > 0) {
22278
- parts.push(`<div class="result-section"><strong>checks reported by the agent</strong><ul>` +
22801
+ parts.push(`<div class="result-section"><strong>Checks reported by the agent</strong><ul>` +
22279
22802
  view.proof.checks.map(one => `<li><span class="mono">${escape(one.command)}</span> <span class="meta">(exit ${one.exitCode}) — ${escape(one.summary)}</span></li>`).join("") +
22280
22803
  `</ul></div>`);
22281
22804
  }
22282
22805
  if (view.proof.caveats.length > 0) {
22283
- parts.push(`<div class="result-section"><strong>caveats</strong><ul>${view.proof.caveats.map(one => `<li>${escape(one)}</li>`).join("")}</ul></div>`);
22806
+ parts.push(`<div class="result-section"><strong>Caveats</strong><ul>${view.proof.caveats.map(one => `<li>${escape(one)}</li>`).join("")}</ul></div>`);
22284
22807
  }
22285
22808
  }
22286
22809
  if (view.checkLog !== null)
22287
22810
  parts.push(checkLogHtml(view.checkLog, runId, ""));
22288
22811
  if (view.screenshots.length > 0) {
22289
- parts.push(`<div class="result-section"><strong>screenshots</strong>` +
22812
+ parts.push(`<div class="result-section"><strong>Screenshots</strong>` +
22290
22813
  view.screenshots
22291
22814
  .map(shot => `<p class="row"><a href="/r/${runId}/evidence/${shot.artifactId}">` +
22292
22815
  `<img src="/r/${runId}/evidence/${shot.artifactId}" alt="${escape(shot.caption)}" style="max-width:12rem;max-height:9rem;border-radius:var(--radius);border:1px solid var(--border)"></a> ` +
@@ -22403,7 +22926,7 @@ function completionReceiptCard(view, taskId, place, standing = receiptStatusOf(v
22403
22926
  (view.publication?.state === "failed" ? `<p class="receipt-note">${statusIconSvg("note")} Pull request couldn't open. The commit is safe locally.</p>` : "") +
22404
22927
  caveats +
22405
22928
  coverage +
22406
- `<details class="receipt-details"><summary>Result details</summary>` +
22929
+ `<details class="receipt-details"><summary>${STATUS_MORE}</summary>` +
22407
22930
  (view.summary !== null && view.summary !== conciseOutcomeOf(view.summary) ? `<p class="recap">${escape(view.summary)}</p>` : "") +
22408
22931
  `<p class="receipt-publication" data-receipt-publication="${escape(facts.publicationState)}">${escape(facts.publicationWords)}</p>` +
22409
22932
  // A review in flight is the receipt's primary status too (its chip
@@ -22534,9 +23057,10 @@ function resultPanelParts(detail, o) {
22534
23057
  items: personCheckItems(proof, patchOk ? patch.text : null, shown, runId),
22535
23058
  accept: acceptable && need?.accept == null ? { action: `${taskHref(detail.taskId)}/accept-proof`, run: run.id, returnTo: o.returnTo } : null,
22536
23059
  };
23060
+ const youCheckForm = youCheck?.accept != null && o.finishes !== true ? youCheck.accept : null;
22537
23061
  const youCheckHtml = youCheck === null ? "" :
22538
23062
  `<div class="result-you-check" data-result-you-check="${youCheck.lines.length}"><ul>${youCheck.lines.map(one => `<li>${escape(one)}</li>`).join("")}</ul>` +
22539
- (youCheck.accept === null ? "" : `<form method="post" action="${escape(youCheck.accept.action)}"><input type="hidden" name="csrf" value="${escape(o.csrf)}"><input type="hidden" name="run" value="${youCheck.accept.run}"><input type="hidden" name="return" value="${escape(youCheck.accept.returnTo)}"><button type="submit" data-accept-result>Accept</button></form>`) +
23063
+ (youCheckForm === null ? "" : `<form method="post" action="${escape(youCheckForm.action)}"><input type="hidden" name="csrf" value="${escape(o.csrf)}"><input type="hidden" name="run" value="${youCheckForm.run}"><input type="hidden" name="return" value="${escape(youCheckForm.returnTo)}"><button type="submit" data-accept-result>Accept</button></form>`) +
22540
23064
  `</div>`;
22541
23065
  const attentionHtml = attention.length === 0
22542
23066
  ? ""
@@ -22663,9 +23187,9 @@ function resultPanelParts(detail, o) {
22663
23187
  detail.files
22664
23188
  .map(file => {
22665
23189
  const counts = file.additions === null || file.deletions === null ? "binary" : `+${file.additions} −${file.deletions}`;
22666
- const flags = (file.outsideTouches ? ` <span class="badge badge-failed" data-outside-touches="1">outside touches</span>` : "") + (file.cited ? ` <span class="badge badge-done">cited</span>` : "");
23190
+ const flags = (file.outsideTouches ? ` <span class="badge badge-failed" data-outside-touches="1">Outside touches</span>` : "") + (file.cited ? ` <span class="badge badge-done">Cited</span>` : "");
22667
23191
  const why = file.priority.reasons.length === 0 ? "" : ` <span class="meta">— ${file.priority.reasons.map(escape).join("; ")}</span>`;
22668
- return `<li data-file-priority="${file.priority.band}">${fileName(file)}${file.renamedFrom === null ? "" : ` <span class="meta">(was ${escape(file.renamedFrom)})</span>`} <span class="meta">${counts}</span>${flags}${detail.canAnnotate ? ` <button type="button" class="pick-file" data-path="${escape(file.path)}">comment</button>` : ""}${why}</li>`;
23192
+ return `<li data-file-priority="${file.priority.band}">${fileName(file)}${file.renamedFrom === null ? "" : ` <span class="meta">(was ${escape(file.renamedFrom)})</span>`} <span class="meta">${counts}</span>${flags}${detail.canAnnotate ? ` <button type="button" class="pick-file" data-path="${escape(file.path)}">Comment</button>` : ""}${why}</li>`;
22669
23193
  })
22670
23194
  .join("") +
22671
23195
  `</ol>` +
@@ -22706,14 +23230,14 @@ function resultPanelParts(detail, o) {
22706
23230
  checkParts.push(detail.signedCriteria > 0 ? `<p class="meta">The approved scope has ${detail.signedCriteria} requirement${detail.signedCriteria === 1 ? "" : "s"}, but this build has no requirement-by-requirement verification.</p>` : `<p class="meta">This scope signed no acceptance checks.</p>`);
22707
23231
  }
22708
23232
  else {
22709
- checkParts.push(criterionMatrixHtml(proof.matrix, { runId, links: proof.matrixLinks, fileAnchors: detail.fileAnchors }));
23233
+ checkParts.push(criterionMatrixHtml(proof.matrix, { runId, links: proof.matrixLinks, fileAnchors: detail.fileAnchors, verdict: proof.verdict }));
22710
23234
  }
22711
23235
  checkParts.push(semanticCoverageHtml(proof.matrix, proof.qualityMode));
22712
23236
  if (reviewConflict(proof.matrix, proof.machineVerdict, proof.verdict))
22713
23237
  checkParts.push(`<p class="meta">An independent review found conflicting evidence.</p>`);
22714
23238
  checkParts.push(repairChainHtml(proof.repairChain));
22715
23239
  if (detail.reviewerFindings.length > 0) {
22716
- checkParts.push(`<div class="result-section" data-cockpit-source="reviewer"><strong>independent review</strong><ul>` +
23240
+ checkParts.push(`<div class="result-section" data-cockpit-source="reviewer"><strong>Independent review</strong><ul>` +
22717
23241
  reviewerNotes +
22718
23242
  `</ul></div>`);
22719
23243
  }
@@ -22742,7 +23266,7 @@ function resultPanelParts(detail, o) {
22742
23266
  // The handoff's own account of its checks — the agent's words, labeled
22743
23267
  // as such, whether or not a proof exists.
22744
23268
  if (!directAssessment && handoff !== null && handoff.verification.length > 0)
22745
- checkParts.push(`<div class="result-section" data-cockpit-source="agent-words"><strong>the agent's own account</strong><ul>${handoff.verification.map(one => `<li>${escape(one)}</li>`).join("")}</ul></div>`);
23269
+ checkParts.push(`<div class="result-section" data-cockpit-source="agent-words"><strong>The agent's own account</strong><ul>${handoff.verification.map(one => `<li>${escape(one)}</li>`).join("")}</ul></div>`);
22746
23270
  if (o.extraChecks !== undefined)
22747
23271
  checkParts.push(o.extraChecks);
22748
23272
  // ---- request changes: both feedback styles, one sealed road ------------
@@ -22787,8 +23311,8 @@ function resultPanelParts(detail, o) {
22787
23311
  `<textarea name="note" rows="2" maxlength="${LIMITS.note}" placeholder="Describe the change…" aria-label="review comment" aria-describedby="comment-note-limit"${o.noted ? " autofocus" : ""}></textarea>` +
22788
23312
  `<span class="meta diff-comment-limit" id="comment-note-limit">up to ${LIMITS.note} characters</span>` +
22789
23313
  `<details class="result-pin"><summary>Attach to a file or line</summary>` +
22790
- `<div class="diff-comment-target"><label>file<input type="text" name="path" placeholder="src/…" aria-label="file" class="mono"></label>` +
22791
- `<label>line<input type="text" name="line" placeholder="—" aria-label="line" inputmode="numeric"></label></div></details>` +
23314
+ `<div class="diff-comment-target"><label>File<input type="text" name="path" placeholder="src/…" aria-label="file" class="mono"></label>` +
23315
+ `<label>Line<input type="text" name="line" placeholder="—" aria-label="line" inputmode="numeric"></label></div></details>` +
22792
23316
  `<div class="result-feedback-actions"><button type="submit" name="intent" value="revise" data-request-changes>Request changes</button><button type="submit" name="intent" value="note" class="quiet" data-save-feedback>Save for later</button></div></form></details>`);
22793
23317
  }
22794
23318
  // ---- the panel ---------------------------------------------------------
@@ -22830,6 +23354,8 @@ function resultPanelParts(detail, o) {
22830
23354
  reviewHistory: REVIEW_TOKENS.has(status.token) ? status.detail : null,
22831
23355
  attention: attention.filter(one => !shortenedMaterialReason(one)).map(plainReasonWords),
22832
23356
  youCheck,
23357
+ // The same counts as the Requirements row (a refuted report verifies none of them, and says Unverified).
23358
+ requirements: proof === null || proof.verdict === "refuted" ? null : requirementsOf(proof.matrix),
22833
23359
  limits: attention.filter(one => shortenedMaterialReason(one)),
22834
23360
  tabs: RESULT_TABS.map(tab => ({ key: tab.key, label: tab.key === "checks" && proof?.matrix.some(row => row.assessment !== undefined) ? "Requirements" : tab.label, count: tabCounts[tab.key], href: o.hrefFor(tab.key), active: tab.key === o.tab })),
22835
23361
  views: [{ key: "summary", html: summaryParts.join("\n") }, { key: "changes", html: changeParts.join("\n") }, { key: "checks", html: checkParts.join("\n") }],
@@ -23024,9 +23550,9 @@ function runPage(chrome, run, taskId, artifacts, terminal = null, notes = [], cs
23024
23550
  : " · the agent is working on this";
23025
23551
  const conversation = heldTurns === null
23026
23552
  ? ""
23027
- : `<h2>conversation</h2>` +
23553
+ : `<h2>Conversation</h2>` +
23028
23554
  (heldTurns.turns.length === 0
23029
- ? `<p class="meta">nothing said yet</p>`
23555
+ ? `<p class="meta">Nothing said yet</p>`
23030
23556
  : heldTurns.turns
23031
23557
  .map(turn => `<p class="row"><span class="meta">${turn.sourceKind === "operator"
23032
23558
  ? escape(turn.author ?? "operator")
@@ -23040,11 +23566,11 @@ function runPage(chrome, run, taskId, artifacts, terminal = null, notes = [], cs
23040
23566
  ? `<form method="post" action="/r/${run.id}/turn" class="row">` +
23041
23567
  `<input type="hidden" name="csrf" value="${escape(csrf)}">` +
23042
23568
  `<input type="text" name="text" maxlength="500" placeholder="say something to the agent — it reads this as its next instruction, inside the approved scope" aria-label="turn" style="width:100%;max-width:34rem">` +
23043
- `<button type="submit">send</button></form>` +
23569
+ `<button type="submit">Send</button></form>` +
23044
23570
  `<p class="meta">${escape(heldTurns.state)} · ${heldTurns.turns.length}/${heldTurns.cap} messages · a waiting question must be answered before free-form messages</p>`
23045
23571
  : heldTurns.open
23046
23572
  ? ""
23047
- : `<p class="meta">the session has ended — the record above is complete</p>`);
23573
+ : `<p class="meta">The session has ended — the record above is complete</p>`);
23048
23574
  // The live peek region (A2): the poller fills it only on a serve that
23049
23575
  // asserted its runner. Without the assertion the section still appears
23050
23576
  // for a running build and says honestly why it is empty \u2014 a page that
@@ -23053,24 +23579,24 @@ function runPage(chrome, run, taskId, artifacts, terminal = null, notes = [], cs
23053
23579
  const peek = !running
23054
23580
  ? ""
23055
23581
  : peekable
23056
- ? `<h2>what is changing right now</h2>` +
23057
- `<div id="run-peek"><p class="meta">watching\u2026 the first look lands within 15 seconds</p></div>` +
23582
+ ? `<h2>What is changing right now</h2>` +
23583
+ `<div id="run-peek"><p class="meta">Watching\u2026 the first look lands within 15 seconds</p></div>` +
23058
23584
  `<p class="meta" id="run-peek-stamp"></p>`
23059
- : `<h2>what is changing right now</h2>` +
23060
- `<p class="meta">the live file view is off \u2014 start serve with ${escape("--runner <name>")} naming this machine's worker, and it appears here</p>`;
23585
+ : `<h2>What is changing right now</h2>` +
23586
+ `<p class="meta">The live file view is off \u2014 start serve with ${escape("--runner <name>")} naming this machine's worker, and it appears here</p>`;
23061
23587
  // The live transcript (arc 1): the agent's own words, streamed to a file
23062
23588
  // beside the run and polled as raw text. Honesty stated on the surface:
23063
23589
  // this is display only, and the machine running the agent could alter it.
23064
23590
  const transcript = !running
23065
23591
  ? ""
23066
23592
  : !peekable
23067
- ? `<h2>what the agent is saying</h2>` +
23068
- `<p class="meta">the live transcript is off \u2014 start serve with ${escape("--runner <name>")} naming this machine's worker, and it appears here</p>`
23593
+ ? `<h2>What the agent is saying</h2>` +
23594
+ `<p class="meta">The live transcript is off \u2014 start serve with ${escape("--runner <name>")} naming this machine's worker, and it appears here</p>`
23069
23595
  : run.provider !== "claude"
23070
- ? `<h2>what the agent is saying</h2>` +
23071
- `<p class="meta">the live transcript needs the claude harness for now \u2014 this build runs on ${escape(run.provider)}</p>`
23072
- : `<h2>what the agent is saying</h2>` +
23073
- `<p class="meta">display only \u2014 this is not evidence, and the machine running the agent could alter it</p>` +
23596
+ ? `<h2>What the agent is saying</h2>` +
23597
+ `<p class="meta">The live transcript needs the claude harness for now \u2014 this build runs on ${escape(run.provider)}</p>`
23598
+ : `<h2>What the agent is saying</h2>` +
23599
+ `<p class="meta">Display only \u2014 this is not evidence, and the machine running the agent could alter it</p>` +
23074
23600
  `<pre id="live-transcript" class="mono" style="max-height:24rem;overflow:auto;white-space:pre-wrap"></pre>` +
23075
23601
  `<p class="meta" id="live-transcript-state"></p>`;
23076
23602
  const resultSummary = structuredHandoff?.conclusion ?? run.handoff;
@@ -23079,7 +23605,7 @@ function runPage(chrome, run, taskId, artifacts, terminal = null, notes = [], cs
23079
23605
  : `<div class="result-section"><strong>${escape(label)}</strong><ul>${items.map(one => `<li>${escape(one)}</li>`).join("")}</ul></div>`;
23080
23606
  const handoff = resultSummary === null
23081
23607
  ? ""
23082
- : `<section class="card result-card"><h2>result</h2><p class="recap">${escape(resultSummary)}</p>` +
23608
+ : `<section class="card result-card"><h2>Result</h2><p class="recap">${escape(resultSummary)}</p>` +
23083
23609
  (structuredHandoff === null
23084
23610
  ? ""
23085
23611
  : resultList("completed", structuredHandoff.changes) +
@@ -23115,9 +23641,9 @@ function runPage(chrome, run, taskId, artifacts, terminal = null, notes = [], cs
23115
23641
  ? ""
23116
23642
  : `<form method="post" action="/r/${run.id}/comment" class="card diff-comment-form" id="comment-form">` +
23117
23643
  `<input type="hidden" name="csrf" value="${escape(csrf)}">` +
23118
- `<div class="diff-comment-target"><label>file<input type="text" name="path" placeholder="select a line above" aria-label="file" class="mono"></label>` +
23119
- `<label>line<input type="text" name="line" placeholder="—" aria-label="line" inputmode="numeric"></label></div>` +
23120
- `<label>change requested<textarea name="note" rows="2" maxlength="${LIMITS.note}" placeholder="Explain what should change and why" aria-label="review comment" aria-describedby="comment-note-limit"${noted ? " autofocus" : ""}></textarea></label>` +
23644
+ `<div class="diff-comment-target"><label>File<input type="text" name="path" placeholder="select a line above" aria-label="file" class="mono"></label>` +
23645
+ `<label>Line<input type="text" name="line" placeholder="—" aria-label="line" inputmode="numeric"></label></div>` +
23646
+ `<label>Change requested<textarea name="note" rows="2" maxlength="${LIMITS.note}" placeholder="Explain what should change and why" aria-label="review comment" aria-describedby="comment-note-limit"${noted ? " autofocus" : ""}></textarea></label>` +
23121
23647
  `<span class="meta diff-comment-limit" id="comment-note-limit">up to ${LIMITS.note} characters</span>` +
23122
23648
  `<button type="submit">Add annotation</button></form>`;
23123
23649
  // The device-side half of the editor-link activation (arc 6, finding 1):
@@ -23145,7 +23671,7 @@ function runPage(chrome, run, taskId, artifacts, terminal = null, notes = [], cs
23145
23671
  : `<div class="card"><p><strong>CI is failing on PR #${ciRepair.pr}</strong> <span class="meta">observed by the episode watcher</span></p>` +
23146
23672
  `<form method="post" action="/r/${run.id}/draft-repair">` +
23147
23673
  `<input type="hidden" name="csrf" value="${escape(csrf)}">` +
23148
- `<button type="submit">draft a repair task</button>` +
23674
+ `<button type="submit">Draft a repair task</button>` +
23149
23675
  `<span class="meta"> — one unapproved task; you approve its scope before anything builds</span></form></div>`;
23150
23676
  const reviewCard = (commentRows === "" && commentForm === "" ? "" : `<h2 id="review">Review and revise</h2>` +
23151
23677
  `<p class="meta">Annotate the diff above. When the batch is ready, create one scoped revision task from it.</p>` +
@@ -23161,8 +23687,8 @@ function runPage(chrome, run, taskId, artifacts, terminal = null, notes = [], cs
23161
23687
  : `<form method="post" action="/r/${run.id}/note" class="row">` +
23162
23688
  `<input type="hidden" name="csrf" value="${escape(csrf)}">` +
23163
23689
  `<input type="text" name="note" placeholder="a note for whoever reads this run next" aria-label="run note" style="width:100%;max-width:28rem">` +
23164
- `<button type="submit">add note</button></form>`;
23165
- const notesCard = noteRows === "" && noteForm === "" ? "" : `<h2>operator notes</h2>${noteRows}${noteForm}`;
23690
+ `<button type="submit">Add note</button></form>`;
23691
+ const notesCard = noteRows === "" && noteForm === "" ? "" : `<h2>Operator notes</h2>${noteRows}${noteForm}`;
23166
23692
  // Continuation (Phase 2E, A4): a finished attempt offers a watched
23167
23693
  // follow-up — the text you type here enters the SIGNED terms on the
23168
23694
  // confirm screen; nothing runs until your password agrees to exactly it.
@@ -23302,7 +23828,7 @@ function capsPage(chrome, caps, gaps, repo, now) {
23302
23828
  if (caps === null) {
23303
23829
  return screen("requirements", [
23304
23830
  `<h1>Requirements</h1>`,
23305
- `<p class="meta">open a project to see its requirements — <a href="/projects">projects</a></p>`,
23831
+ `<p class="meta">Open a project to see its requirements — <a href="/projects">projects</a></p>`,
23306
23832
  ].join("\n"), { chrome });
23307
23833
  }
23308
23834
  const list = caps.length === 0
@@ -23312,7 +23838,7 @@ function capsPage(chrome, caps, gaps, repo, now) {
23312
23838
  `${escape(describeCapability(capability, now ?? new Date()))}</p>`)
23313
23839
  .join("\n");
23314
23840
  const blocked = gaps.length === 0
23315
- ? `<p class="meta">no gaps — everything recorded is verified</p>`
23841
+ ? `<p class="meta">No gaps — everything recorded is verified</p>`
23316
23842
  : gaps
23317
23843
  .map(gap => `<div class="card"><p>${escape(gap.key)} — ${escape(gap.state)}</p>` +
23318
23844
  `<p class="meta">${gap.unblocks.length > 0
@@ -23320,16 +23846,16 @@ function capsPage(chrome, caps, gaps, repo, now) {
23320
23846
  : gap.alsoBlocks.length > 0
23321
23847
  ? `part of what holds: ${gap.alsoBlocks.map(one => escape(one)).join(", ")}`
23322
23848
  : "nothing queued needs it yet"}</p>` +
23323
- `<p class="meta">verify: ${escape(gap.verify)}</p>` +
23849
+ `<p class="meta">Verify: ${escape(gap.verify)}</p>` +
23324
23850
  `<p class="meta">${escape(gap.instructions)}</p></div>`)
23325
23851
  .join("\n");
23326
23852
  return screen("requirements", [
23327
23853
  `<h1>Requirements</h1>`,
23328
23854
  `<p class="hint">tools and credentials builds need — each is probed on the worker before any build spends money; values never leave your machine</p>`,
23329
23855
  list,
23330
- `<h2>missing, ranked by what filling them frees</h2>`,
23856
+ `<h2>Missing, ranked by what filling them frees</h2>`,
23331
23857
  blocked,
23332
- `<p class="meta">read-only here: checks are shell commands you wrote, and a web button that runs shell would need its own security review</p>`,
23858
+ `<p class="meta">Read-only here: checks are shell commands you wrote, and a web button that runs shell would need its own security review</p>`,
23333
23859
  ].join("\n"), { chrome });
23334
23860
  }
23335
23861
  /** v98: how the bot's messages reach Toolroll, in words: pushed to the public address, or asked for. */
@@ -23358,7 +23884,7 @@ function settingsPage(chrome, existing, hasEnv, csrf, problem, messaging = null,
23358
23884
  `<input type="hidden" name="csrf" value="${escape(csrf)}">` +
23359
23885
  permissionModeChoices("permission-mode", permissionDefault.mode) +
23360
23886
  `<button type="submit">Save default</button></form>`
23361
- : `<div class="card"><p><strong>${permissionDefault.mode === "bypassPermissions" ? "Full access" : "Auto"}</strong></p><p class="meta">an approver can change this default</p></div>`,
23887
+ : `<div class="card"><p><strong>${permissionDefault.mode === "bypassPermissions" ? "Full access" : "Auto"}</strong></p><p class="meta">An approver can change this default</p></div>`,
23362
23888
  permissionDefault.updatedAt === null
23363
23889
  ? ""
23364
23890
  : `<p class="meta settings-changed">Changed ${whenTime(permissionDefault.updatedAt)}${permissionDefault.updatedBy === null ? "" : ` by ${escape(permissionDefault.updatedBy)}`}</p>`,
@@ -23372,7 +23898,7 @@ function settingsPage(chrome, existing, hasEnv, csrf, problem, messaging = null,
23372
23898
  `<input type="hidden" name="csrf" value="${escape(csrf)}">` +
23373
23899
  qualityModeChoices("quality-mode", qualityDefault.mode) +
23374
23900
  `<button type="submit">Save default</button></form>`
23375
- : `<div class="card"><p><strong>${escape(qualityModeTitle(qualityDefault.mode))}</strong></p><p class="meta">an approver can change this default</p></div>`,
23901
+ : `<div class="card"><p><strong>${escape(qualityModeTitle(qualityDefault.mode))}</strong></p><p class="meta">An approver can change this default</p></div>`,
23376
23902
  qualityDefault.updatedAt === null
23377
23903
  ? ""
23378
23904
  : `<p class="meta settings-changed">Changed ${whenTime(qualityDefault.updatedAt)}${qualityDefault.updatedBy === null ? "" : ` by ${escape(qualityDefault.updatedBy)}`}</p>`,
@@ -23762,18 +24288,18 @@ function nextPage(chrome, data) {
23762
24288
  `<p>${escape(item.approval.title)}</p>` +
23763
24289
  (data.planDocument === null
23764
24290
  ? ""
23765
- : `<div class="card"><p><strong>the plan</strong> <span class="meta">drafted by a planning session</span></p>${executionPlanHtml(data.planDocument, true)}${planContractHtml(data.planContract ?? null, "ceremony")}</div>`) +
24291
+ : `<div class="card"><p><strong>The plan</strong> <span class="meta">drafted by a planning session</span></p>${executionPlanHtml(data.planDocument, true)}${planContractHtml(data.planContract ?? null, "ceremony")}</div>`) +
23766
24292
  `<form method="post" action="${taskHref(item.approval.taskId)}/approve" class="card approve-form">` +
23767
24293
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
23768
24294
  `<input type="hidden" name="nonce" value="${escape(data.nonce)}">` +
23769
24295
  `<input type="hidden" name="digest" value="${escape(data.approvalDigest ?? item.approval.digest)}">` +
23770
24296
  `<input type="hidden" name="return" value="next">` +
23771
- `<p><strong>approve exactly this:</strong></p>` +
23772
- (data.deliverable === "report" ? `<p class="meta"><span class="badge">scout</span> a read-only session investigates this goal and delivers a report — no branch, nothing changes in the repository</p>` : "") +
24297
+ `<p><strong>Approve exactly this:</strong></p>` +
24298
+ (data.deliverable === "report" ? `<p class="meta"><span class="badge">Scout</span> a read-only session investigates this goal and delivers a report — no branch, nothing changes in the repository</p>` : "") +
23773
24299
  (scope === null || scope.profileState !== "unresolved" ? "" : profileWords(scope)) +
23774
- `<p class="meta">goal</p><p class="recap" style="margin-top:0">${escape(scope?.goal ?? item.approval.goal)}</p>` +
23775
- `<p class="meta">not this</p><p class="recap" style="margin-top:0">${scope?.outOfScope == null ? "<em>no exclusions</em>" : escape(scope.outOfScope)}</p>` +
23776
- `<p class="meta">touches · ${scope === null || scope.touches.length === 0 ? "anything" : scope.touches.map(one => escape(one)).join(", ")}</p>` +
24300
+ `<p class="meta">Goal</p><p class="recap" style="margin-top:0">${escape(scope?.goal ?? item.approval.goal)}</p>` +
24301
+ `<p class="meta">Not this</p><p class="recap" style="margin-top:0">${scope?.outOfScope == null ? "<em>no exclusions</em>" : escape(scope.outOfScope)}</p>` +
24302
+ `<p class="meta">Touches · ${scope === null || scope.touches.length === 0 ? "anything" : scope.touches.map(one => escape(one)).join(", ")}</p>` +
23777
24303
  (scope === null ? "" : acceptanceCeremonyHtml(scope.acceptance)) +
23778
24304
  (data.raceTerms === null
23779
24305
  ? ""
@@ -23783,8 +24309,8 @@ function nextPage(chrome, data) {
23783
24309
  // tap away, never in the way.
23784
24310
  agentsCeremonyHtml(data.route) +
23785
24311
  (scope === null ? "" : runtimeDetailsHtml(scope)) +
23786
- `<label>your password, typed again — a signed-in session alone cannot agree to work<input type="password" name="token" autocomplete="current-password"></label>` +
23787
- `<div class="sticky-actions"><button type="submit">approve this scope</button></div>` +
24312
+ `<label>Your password, typed again — a signed-in session alone cannot agree to work<input type="password" name="token" autocomplete="current-password"></label>` +
24313
+ `<div class="sticky-actions"><button type="submit">Approve this scope</button></div>` +
23788
24314
  `</form>` +
23789
24315
  `<p class="meta"><a href="${taskHref(item.approval.taskId)}">open the full task</a> to edit the scope first</p>`;
23790
24316
  }
@@ -23792,22 +24318,22 @@ function nextPage(chrome, data) {
23792
24318
  card =
23793
24319
  `<h1>${escape(item.stalled.taskId)}</h1>` +
23794
24320
  `<p>${escape(item.stalled.title)}</p>` +
23795
- `<p class="meta">stopped — ${item.stalled.incidentCount} incident(s)${item.stalled.strikes > 0 ? ` after ${item.stalled.strikes} attempt(s)` : ""}</p>` +
24321
+ `<p class="meta">Stopped — ${item.stalled.incidentCount} incident(s)${item.stalled.strikes > 0 ? ` after ${item.stalled.strikes} attempt(s)` : ""}</p>` +
23796
24322
  `<form method="post" action="${taskHref(item.stalled.taskId)}/requeue" class="card">` +
23797
24323
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
23798
24324
  `<input type="hidden" name="return" value="next">` +
23799
- `<p class="meta">requeue resolves the incidents, clears the failed attempts, and puts it back in line</p>` +
23800
- `<button type="submit">retry this work</button>` +
24325
+ `<p class="meta">Requeue resolves the incidents, clears the failed attempts, and puts it back in line</p>` +
24326
+ `<button type="submit">Retry this work</button>` +
23801
24327
  `</form>` +
23802
24328
  `<p class="meta"><a href="${taskHref(item.stalled.taskId)}">open the full task</a> to read the runs first</p>`;
23803
24329
  }
23804
24330
  else {
23805
24331
  const { gap } = item;
23806
24332
  card =
23807
- `<h1>supply ${escape(gap.key)}</h1>` +
24333
+ `<h1>Supply ${escape(gap.key)}</h1>` +
23808
24334
  `<p class="meta">${escape(gap.state)}</p>` +
23809
24335
  `<p>Filling this starts ${gap.unblocks.length} task(s): ${gap.unblocks.map(one => `<span class="mono">${escape(one)}</span>`).join(", ")}</p>` +
23810
- `<div class="card"><p class="meta">prove it filled from the terminal:</p><pre class="recap">${escape(gap.verify)}</pre></div>`;
24336
+ `<div class="card"><p class="meta">Prove it filled from the terminal:</p><pre class="recap">${escape(gap.verify)}</pre></div>`;
23811
24337
  }
23812
24338
  return screen("next", [header, card].join("\n"), { chrome });
23813
24339
  }
@@ -23828,9 +24354,9 @@ function decisionOptionForms(decision, csrf, returnTo) {
23828
24354
  `<input type="hidden" name="choice" value="${escape(option.id)}">`,
23829
24355
  ...(returnTo === null ? [] : [`<input type="hidden" name="return" value="${escape(returnTo)}">`]),
23830
24356
  ...(option.reversible ? [] : [`<input type="hidden" name="confirm" value="yes">`]),
23831
- recommended ? `<p class="meta" style="margin:0 0 .375rem"><span class="badge">recommended</span></p>` : "",
24357
+ recommended ? `<p class="meta" style="margin:0 0 .375rem"><span class="badge">Recommended</span></p>` : "",
23832
24358
  `<p class="consequence">${escape(option.consequence)}</p>`,
23833
- `<button type="submit">${escape(option.label)}${option.reversible ? "" : ` <span class="badge badge-overdue">irreversible</span>`}</button>`,
24359
+ `<button type="submit">${escape(option.label)}${option.reversible ? "" : ` <span class="badge badge-overdue">Irreversible</span>`}</button>`,
23834
24360
  `<input type="text" name="note" placeholder="optional note — travels with this answer" aria-label="optional note">`,
23835
24361
  `</form>`,
23836
24362
  ].join("\n");
@@ -23848,14 +24374,14 @@ function decisionPage(chrome, decision, taskId, artifacts, who, now, returnTo =
23848
24374
  : "";
23849
24375
  const evidence = artifacts.length === 0
23850
24376
  ? ""
23851
- : `<div class="evidence"><strong>evidence</strong>` +
24377
+ : `<div class="evidence"><strong>Evidence</strong>` +
23852
24378
  artifacts
23853
24379
  .map(artifact => `<a href="/d/${decision.id}/evidence/${artifact.id}">${escape(evidenceWords(artifact.kind))}` +
23854
24380
  `${artifact.truncated ? " (truncated)" : ""} · ${artifact.bytesStored} bytes</a>`)
23855
24381
  .join("\n") +
23856
24382
  "</div>";
23857
24383
  return screen(`decide \u00b7 ${taskId}`, [
23858
- `<h1>${escape(taskId)} <span class="badge badge-${escape(decision.state)}">${escape(decision.state)}</span>${isOverdue(decision, now) ? ` <span class="badge badge-overdue">overdue</span>` : ""}${decision.deadline === null ? "" : ` <span class="meta">deadline ${escape(decision.deadline)}</span>`}</h1>`,
24384
+ `<h1>${escape(taskId)} <span class="badge badge-${escape(decision.state)}">${escape(decision.state)}</span>${isOverdue(decision, now) ? ` <span class="badge badge-overdue">Overdue</span>` : ""}${decision.deadline === null ? "" : ` <span class="meta">deadline ${escape(decision.deadline)}</span>`}</h1>`,
23859
24385
  `<div class="recap">${escape(decision.recap)}</div>`,
23860
24386
  `<div class="question">${escape(decision.question)}</div>`,
23861
24387
  decision.state === "answered" ? answered : options,