toolroll 0.9.13 → 0.9.15

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 (43) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/assignment-ui.d.ts +1 -0
  3. package/dist/assignment-ui.js +8 -5
  4. package/dist/assignment.d.ts +32 -0
  5. package/dist/assignment.js +81 -7
  6. package/dist/browser/workspace.css +1 -1
  7. package/dist/browser/workspace.js +38 -38
  8. package/dist/browser-crew.d.ts +2 -1
  9. package/dist/browser-crew.js +4 -2
  10. package/dist/browser-workspace.d.ts +109 -11
  11. package/dist/chat-actions.d.ts +1 -1
  12. package/dist/chat-actions.js +6 -4
  13. package/dist/chat-delivery-state.d.ts +1 -1
  14. package/dist/chat-voice.d.ts +3 -3
  15. package/dist/chat-voice.js +3 -3
  16. package/dist/demo.js +82 -2
  17. package/dist/disclosure.d.ts +6 -0
  18. package/dist/disclosure.js +13 -0
  19. package/dist/dispatch.js +7 -2
  20. package/dist/lead-status.js +2 -2
  21. package/dist/needs-you.d.ts +72 -0
  22. package/dist/needs-you.js +193 -1
  23. package/dist/result-acts.d.ts +51 -5
  24. package/dist/result-acts.js +36 -3
  25. package/dist/result-review.d.ts +4 -3
  26. package/dist/result-review.js +7 -3
  27. package/dist/serve.d.ts +3 -0
  28. package/dist/serve.js +1235 -711
  29. package/dist/slack-chat.js +1 -1
  30. package/dist/task-status.d.ts +24 -3
  31. package/dist/task-status.js +48 -17
  32. package/dist/telegram-progress.js +2 -2
  33. package/dist/test-temp.js +1 -1
  34. package/dist/transitions-recipes.d.ts +1 -1
  35. package/dist/transitions-recipes.js +5 -9
  36. package/dist/when-html.d.ts +19 -2
  37. package/dist/when-html.js +78 -11
  38. package/dist/work-index.d.ts +3 -1
  39. package/dist/work-index.js +18 -5
  40. package/dist/workspace-motion.js +3 -3
  41. package/dist/workspace-ui.d.ts +86 -6
  42. package/dist/workspace-ui.js +143 -17
  43. package/package.json +1 -1
package/dist/serve.js CHANGED
@@ -56,7 +56,7 @@ import { knowledgeHtml, knowledgeContextHtml, KNOWLEDGE_CSS, decisionsHtml, memo
56
56
  import { listDecisions, recordDecision, retireDecision, searchMemory } from "./project-memory.js";
57
57
  import { decideProposal, listProposals, memoryStatus } from "./memory-pass.js";
58
58
  import { learningHtml } from "./workspace-ui.js";
59
- import { resultActsOf } from "./result-acts.js";
59
+ import { acceptWithChecksOf, resultActsOf } from "./result-acts.js";
60
60
  import { createSessionEndpoint } from './session-server.js';
61
61
  import { handleTeamHttp } from './team-http.js';
62
62
  import { teamWorkspaceHtml } from './team-ui.js';
@@ -65,7 +65,7 @@ import { prepareWorkspaceRevision, WorkspaceValidatorCache } from "./workspace-r
65
65
  import { workIndexPage, workCountsByProject, WorkIndexCursorError, WORK_INDEX_MAX_LIMIT } from "./work-index.js";
66
66
  import { leadActivity } from "./lead-voice.js";
67
67
  import { openWorkDecisionOf } from "./work-summary.js";
68
- import { assignmentOf, checkAssignmentAsOperator } from './assignment.js';
68
+ import { acceptAndCompleteAsOperator, assignmentOf, checkAssignmentAsOperator, personCheckPending } from './assignment.js';
69
69
  import { demoChatHtml, demoThreadHtml, DEMO_CHAT_SCRIPT } from "./demo-chat.js";
70
70
  import { pullRequestSettingsHtml, PULL_REQUEST_SETTINGS_CSS } from './pull-request-ui.js';
71
71
  import { checkSettingsHtml, CHECK_SETTINGS_CSS } from './check-levels-ui.js';
@@ -74,8 +74,8 @@ import { checkPublishing, completeAndOpenPullRequest, mergeAsPerson, pullRequest
74
74
  import { leadBriefHtml, LEAD_CONTEXT_CSS } from './lead-context.js';
75
75
  import { assignmentCatchUp } from './assignment-brief.js';
76
76
  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 } from './needs-you.js';
77
+ import { assignmentStageOf, demoChecksOf, requirementWordOf, STATUS_MORE, pullRequestFactOf, requirementsOf, stageOfCode, stageOfDispatch, statusDetailsHtml, statusIconSvg, statusWhyHtml, taskStatusOf, TASK_STATUS_CSS } from './task-status.js';
78
+ import { ASKS, ASK_LABEL, GENERAL_SUGGESTION, NEEDS, NO_REASON_RECORDED, failedAttemptSentence, retryNoteOf, failingCheckSuggestion, isInternalErrorReason, latestFinishedAttempt, missedRequirementLine, missedRequirementSuggestion, runReasonWords, stopSuggestionOf } from './needs-you.js';
79
79
  import { assignmentPresentationOf, historicalAssessmentReason, shortenedMaterialReason } from './assignment-presentation.js';
80
80
  import { ledgerBody } from "./ledger-view.js";
81
81
  import { EVIDENCE_PACK_CSS, evidencePack, evidencePackHtml, exportDay, ledgerExportChunks } from "./evidence-pack.js";
@@ -86,6 +86,7 @@ import { createResultRevision, requestResultChanges } from "./result-actions.js"
86
86
  import { CHAT_CONTROLS, chatControlHref, chatResultHref as sharedResultHref, isChatControl } from "./chat-controls.js";
87
87
  import { CHAT_TASK_ACTIONS, isChatTaskAction } from "./chat-task-actions.js";
88
88
  import { WORKSPACE_MOTION_CSS, WORKSPACE_MOTION_SCRIPT } from "./workspace-motion.js";
89
+ import { DISCLOSURE_CSS } from "./disclosure.js";
89
90
  import { styleAsset } from "./style-asset.js";
90
91
  import { MOBILE_VIEWPORT_SCRIPT } from "./mobile-viewport.js";
91
92
  import { authorizePlanUnderMode, applyModeToNewFiling, planAutoPending } from "./plan-auto.js";
@@ -164,7 +165,7 @@ import { starterRecipes, savedRecipes, findRecipe, importRecipe, exportRecipe, c
164
165
  import { recipeFromForm, recipeLibraryHtml, recipeEditorHtml, workflowPreviewHtml, recipeScript, RECIPE_CSS, recipeDefinitionPreviewHtml, recipeRunHtml, recipeAnswersFromForm } from "./recipe-ui.js";
165
166
  import { EVIDENCE_CAPS, readVerifiedArtifact, readVerifiedReport, readVerifiedProofForRun, storeEvidence, writeEvidenceFile, scanForSecrets } from "./evidence.js";
166
167
  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, parseWorkView, resultStatusOf, resultHeadlineOf, receiptHeadingOf, receiptPublicationWords, reviewFactsOf, workStatusOf, primaryDestinationOf, needsPerson, dispatchActionLabel, } from "./workspace-ui.js";
168
+ 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
169
  import { PRICED_BUILD_MODELS } from "./pricing.js";
169
170
  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
171
  import { parseReport } from "./scout-report.js";
@@ -240,7 +241,7 @@ import { parseProjectConcurrency, PROJECT_CONCURRENCY_DEFAULT, projectConcurrenc
240
241
  import { buildReviewOf, findingWords } from "./review-switch.js";
241
242
  import { deleteProject, holdingsWords, projectHoldings, projectRunning } from "./project-delete.js";
242
243
  import { POLICY_CSS, policyHtml } from "./policy-ui.js";
243
- import { checkPolicy, parseList, policyParts } from "./policy.js";
244
+ import { checkPolicy, levelOfProfile, parseList, policyParts } from "./policy.js";
244
245
  import { LIMITS_CSS, limitsHtml, limitsView } from "./limits-ui.js";
245
246
  import { budgetHoldWords, budgetLabel, budgetStates, monthNamed, monthOf, spendItems, teammateNames as teammateNamesOf, usd as spendUsd } from "./spend.js";
246
247
  import { targetOf } from "./monitoring.js";
@@ -2104,6 +2105,30 @@ export function createDecisionServer(options) {
2104
2105
  return refuse(response, who, 400, 'This task page has expired. Open the first page.', '/work');
2105
2106
  throw error;
2106
2107
  }
2108
+ // A failed row says what its latest attempt missed, as its task page does (a stop reason already reads so), from
2109
+ // 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
2110
+ // stuck on, from that live attempt's own progress only, never a stopped attempt's; the step's words come from its
2111
+ // current plan (one saved file, for live rows only), and without it the row names the step by number.
2112
+ work = { ...work, items: work.items.map((item) => {
2113
+ if (item.liveRunId !== null) {
2114
+ const ref = store.lookupRef(item.activeTaskId);
2115
+ const recorded = ref === null ? null : store.latestCheckpointForRun(item.liveRunId);
2116
+ if (ref === null || recorded === null || recorded.taskRef !== ref.id)
2117
+ return item;
2118
+ const plan = store.currentPlanRevision(ref.id);
2119
+ const parsed = plan === null ? null : (() => { const doc = revisionDocOf(plan); return doc === null ? null : parseExecutionPlanDocument(doc.document); })();
2120
+ const words = new Map(parsed?.ok === true ? milestonesOf(parsed.document).map(one => [one.id, one.description]) : []);
2121
+ const steps = buildProgressOf(recorded.snapshot.milestones.map(one => ({ description: words.get(one.id) ?? null, state: one.state, note: one.note ?? null })));
2122
+ return steps === null ? item : { ...item, progress: steps.line };
2123
+ }
2124
+ if (item.state !== "failed")
2125
+ return item;
2126
+ const family = familyOf(item.activeTaskId);
2127
+ if (family === null || family.problem !== null)
2128
+ return item;
2129
+ const failure = failureOf(family.versions.flatMap(version => store.runsFor(version.refId)), null, false);
2130
+ return failure.kind === "reason" ? item : { ...item, status: { ...item.status, detail: failure.line } };
2131
+ }) };
2107
2132
  const page = workPage(chromeFor(project, "work", undefined, rollup ? "all" : undefined), {
2108
2133
  view,
2109
2134
  ...(url.searchParams.has('project') && project !== null ? { projectFilter: project } : {}),
@@ -2234,7 +2259,7 @@ export function createDecisionServer(options) {
2234
2259
  if (selected !== null) {
2235
2260
  const view = taskViewData(selected, who, null);
2236
2261
  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>`
2262
+ ? `<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
2263
  : `<p class="workbench-mobile-back"><a href="/workbench">← all work</a></p>${taskBody({ ...view, degraded: "pane" })}`;
2239
2264
  }
2240
2265
  return sendScreen(response, 200, screen("portfolio", detail, {
@@ -2389,10 +2414,15 @@ export function createDecisionServer(options) {
2389
2414
  // The ranked queue itself stays bounded and says so.
2390
2415
  const beyond = wantedId === null || inQueue !== null ? null : completedRowFor(wantedId, project);
2391
2416
  const beyondRow = beyond === null ? null : resultRow(beyond);
2392
- const chosen = inQueue ?? (beyondRow === null ? null : { ...beyondRow, priority: reviewPriorityOf(beyondRow) });
2417
+ const completedChoice = inQueue ?? (beyondRow === null ? null : { ...beyondRow, priority: reviewPriorityOf(beyondRow) });
2418
+ const expectedRun = url.searchParams.get("run");
2419
+ const namedRun = expectedRun !== null && /^[1-9]\d*$/.test(expectedRun) ? Number(expectedRun) : null;
2420
+ // A build that failed (or stopped) has its own result page too: the run named, or a failed task's latest attempt.
2421
+ const attempt = wantedId === null || (completedChoice !== null && (namedRun === null || completedChoice.runId === namedRun)) ? null : attemptRowFor(wantedId, namedRun, project);
2422
+ const attemptRow = attempt === null ? null : resultRow(attempt);
2423
+ const chosen = attemptRow !== null ? { ...attemptRow, priority: reviewPriorityOf(attemptRow) } : completedChoice;
2393
2424
  const selectedRow = wantedId === null ? ranked[0] ?? null : chosen;
2394
2425
  const csrf = who.via === "cookie" ? who.session.csrf : "";
2395
- const expectedRun = url.searchParams.get("run");
2396
2426
  // An exact link to a result this person can't see (or that never existed) reads the same either way.
2397
2427
  if (expectedRun !== null && wantedId !== null && chosen === null)
2398
2428
  return refuse(response, who, 404, "No such result in your projects.", "/work");
@@ -2400,11 +2430,34 @@ export function createDecisionServer(options) {
2400
2430
  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
2431
  }
2402
2432
  const selected = selectedRow === null ? null : reviewCockpitViewOf(selectedRow, who, now);
2433
+ // A build that delivered nothing reads Failed here, and so does a failed task's delivered result: what went wrong
2434
+ // is the card and Retry the ink act. That result may still be accepted, but only in outline, as Accept anyway, with
2435
+ // a reason.
2436
+ const delivered = selected?.run?.outcome === "built" || selected?.run?.outcome === "no-change";
2437
+ const failedTask = selected === null ? null : store.getTask(selected.taskId);
2438
+ if (selected !== null && attemptRow !== null && selected.run !== null && (!delivered || failedTask?.state === "failed")) {
2439
+ const attemptRun = store.getRun(selected.run.id);
2440
+ const runs = attemptRun === null ? [] : store.runsFor(attemptRun.taskRef);
2441
+ const latest = latestFinishedAttempt(runs);
2442
+ // Retry from here only when the task failed, nothing holds it, and this is its latest attempt or a result it delivered.
2443
+ const retry = failedTask?.state === "failed" && (latest?.id === selected.run.id || delivered) && csrf !== "" && who.role === "approver" && store.currentLiveLease(attemptRun.taskRef, now) === null
2444
+ ? { action: `${taskHref(selected.taskId)}/requeue` } : null;
2445
+ // Accept anyway: the same acceptance this task's latest result takes, never for an older one or one already accepted.
2446
+ const family = delivered ? familyOf(selected.taskId) : null;
2447
+ const acceptAnyway = delivered && csrf !== "" && who.role === "approver" && family?.current.id === selected.taskId && family.problem === null &&
2448
+ runs.find(runIsTaskResult)?.id === selected.run.id && store.proofAcceptance(selected.run.id) === null
2449
+ ? { action: `${taskHref(selected.taskId)}/accept-proof`, run: selected.run.id } : null;
2450
+ if (attemptRun !== null) {
2451
+ // A delivered result didn't fail itself: the task's failure is its latest attempt's.
2452
+ const failure = delivered ? failureOf(runs, null) : explainAttempt(attemptRun, null);
2453
+ selected.failure = { ...failure, retry: retry === null ? null : { ...retry, note: retryNoteOf(failure.suggestion) }, ...(acceptAnyway === null ? {} : { acceptAnyway }) };
2454
+ }
2455
+ }
2403
2456
  const reviewPage = reviewCockpitPage(chromeFor(wantedId === null ? project : chosenProject, "runs"), {
2404
2457
  queue: ranked,
2405
2458
  queueCap: REVIEW_QUEUE_CAP,
2406
2459
  selected,
2407
- beyondQueue: beyond !== null,
2460
+ beyondQueue: beyond !== null && attemptRow === null,
2408
2461
  // A deep link to a result this console cannot show — not done,
2409
2462
  // not admitted, or never existed — says so in one sentence and
2410
2463
  // shows the top of the queue; the three cases read identically.
@@ -2547,7 +2600,7 @@ export function createDecisionServer(options) {
2547
2600
  const csrf = who.via === "cookie" ? who.session.csrf : "";
2548
2601
  const hasGrant = store.hasMergeCapableGrant(project, now);
2549
2602
  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>`
2603
+ ? `<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
2604
  : [
2552
2605
  `<div class="card">`,
2553
2606
  `<h2 style="margin-top:0">${escape(live.name)} <span class="meta">signed by ${personChip(live.signedBy)}</span></h2>`,
@@ -2557,7 +2610,7 @@ export function createDecisionServer(options) {
2557
2610
  who.role === "approver"
2558
2611
  ? `<form method="post" action="/mode/revoke" class="row">` +
2559
2612
  `<input type="hidden" name="csrf" value="${escape(csrf)}">` +
2560
- `<button type="submit">end this mode now</button>` +
2613
+ `<button type="submit">End this mode now</button>` +
2561
2614
  `<span class="meta">one click — every act it covered falls back to its own ceremony</span></form>`
2562
2615
  : "",
2563
2616
  `</div>`,
@@ -2569,31 +2622,31 @@ export function createDecisionServer(options) {
2569
2622
  `<h2 style="margin-top:0">${live === null ? "sign a mode" : "replace it — a renewal is a new signature"}</h2>`,
2570
2623
  `<form method="post" action="/mode/confirm">`,
2571
2624
  `<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">` +
2625
+ `<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>`,
2626
+ `<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>`,
2627
+ `<label>Merges<select name="publication">` +
2575
2628
  `<option value="notify">wait for me — even under a merge grant</option>` +
2576
2629
  (hasGrant ? `<option value="automerge">merge themselves when CI is green on the exact commit</option>` : "") +
2577
2630
  `</select></label>` +
2578
2631
  (hasGrant ? "" : `<span class="meta">self-merging needs a merge-capable publication grant first</span>`),
2579
- `<label>my filings<select name="auto-approve">` +
2632
+ `<label>My filings<select name="auto-approve">` +
2580
2633
  `<option value="">the preset's default (standard: wait for approval; hands-off: auto-approve)</option>` +
2581
2634
  `<option value="1">approve the moment I file them</option>` +
2582
2635
  `<option value="0">wait for their own approval</option>` +
2583
2636
  `</select></label>`,
2584
- `<label>planner approval<select name="plan-auto">` +
2637
+ `<label>Planner approval<select name="plan-auto">` +
2585
2638
  `<option value="0">wait for my approval</option>` +
2586
2639
  `<option value="1">auto-approve plans that preserve my filed contract</option>` +
2587
2640
  `</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">` +
2641
+ `<label>If a subscription runs out<select name="allow-paid-fallback">` +
2589
2642
  `<option value="">never switch to a paid API key on its own (the default, every preset)</option>` +
2590
2643
  `<option value="1">allow the approved fallback — spend moves to that account</option>` +
2591
2644
  `</select></label>`,
2592
- `<button type="submit">read the full terms</button>`,
2645
+ `<button type="submit">Read the full terms</button>`,
2593
2646
  `</form>`,
2594
2647
  `</div>`,
2595
2648
  ].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") }));
2649
+ 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
2650
  }
2598
2651
  if (url.pathname === "/people") {
2599
2652
  // Approvers see everyone (D7's ceiling: every fact already passed
@@ -2635,13 +2688,13 @@ export function createDecisionServer(options) {
2635
2688
  ? ""
2636
2689
  : `<p class="row">watching now: ${attended.map(session => `<a href="/t/${escape(session.taskId)}">${escape(session.taskId)}</a>`).join(", ")}</p>`,
2637
2690
  acts.length === 0
2638
- ? `<p class="meta">no recorded acts yet</p>`
2691
+ ? `<p class="meta">No recorded acts yet</p>`
2639
2692
  : 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
2693
  approverView && one.revokedAt === null && one.name !== who.name
2641
2694
  ? `<form method="post" action="/people/revoke" class="row">` +
2642
2695
  `<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>` +
2696
+ `<label>Your password<input type="password" name="token" autocomplete="current-password"></label>` +
2697
+ `<button type="submit">Remove ${escape(one.name)}'s sign-in</button>` +
2645
2698
  `<span class="meta">ends their access and everything it signed \u2014 history stays</span></form>`
2646
2699
  : "",
2647
2700
  `</div>`,
@@ -2651,20 +2704,20 @@ export function createDecisionServer(options) {
2651
2704
  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
2705
  ` <form method="post" action="/people/invite-revoke" style="display:inline">` +
2653
2706
  `<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>`);
2707
+ `<label>Password <input type="password" name="token" autocomplete="current-password" style="width:8rem"></label>` +
2708
+ `<button type="submit">Cancel it</button></form></p>`);
2656
2709
  const inviteCard = !approverView
2657
2710
  ? ""
2658
2711
  : [
2659
2712
  `<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"),
2713
+ `<h2 style="margin-top:0">Invite someone</h2>`,
2714
+ inviteRows.length === 0 ? `<p class="meta">No open invites</p>` : inviteRows.join("\n"),
2662
2715
  `<form method="post" action="/people/invite" class="row">`,
2663
2716
  `<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>`,
2717
+ `<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
2718
  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>`,
2719
+ `<label>Your password<input type="password" name="token" autocomplete="current-password"></label>`,
2720
+ `<button type="submit">Make an invite link</button>`,
2668
2721
  `<span class="meta">single-use, expires in 72 hours</span>`,
2669
2722
  `</form>`,
2670
2723
  `</div>`,
@@ -2786,7 +2839,7 @@ export function createDecisionServer(options) {
2786
2839
  `<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
2840
  `<span class="right meta mono">${escape(run.runner)} \u00b7 ${escape(run.role)} \u00b7 ${escape(stage)} \u00b7 ${escape(relativeAge(run.startedAt, now))}</span></p>`,
2788
2841
  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>`
2842
+ ? `<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
2843
  : `<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
2844
  `</section>`,
2792
2845
  ].join("\n");
@@ -2799,7 +2852,7 @@ export function createDecisionServer(options) {
2799
2852
  return sendScreen(response, 200, screen("peek", [
2800
2853
  `<h1>Peek</h1>`,
2801
2854
  panes.length === 0
2802
- ? `<p class="meta">no agent is working right now \u2014 this page follows them the moment one starts</p>`
2855
+ ? `<p class="meta">No agent is working right now \u2014 this page follows them the moment one starts</p>`
2803
2856
  : panes.join("\n"),
2804
2857
  ].join("\n"), { chrome: chromeFor(project, "runs"), functional: { script, fetches: true } }));
2805
2858
  }
@@ -2823,19 +2876,19 @@ export function createDecisionServer(options) {
2823
2876
  ...(said === null ? [] : [`<p class="meta">${escape(said)}</p>`]),
2824
2877
  `<div id="fleet-region">${body}</div>`,
2825
2878
  `<p class="meta" id="fleet-region-stamp"></p>`,
2826
- `<h2>register a worker</h2>`,
2879
+ `<h2>Register a worker</h2>`,
2827
2880
  `<form method="post" action="/fleet/runner/register" class="card">` +
2828
2881
  `<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>` +
2882
+ `<label>Name<input type="text" name="name" placeholder="builder-2" maxlength="60" required></label>` +
2883
+ `<label>Capacity<input type="text" name="capacity" inputmode="numeric" value="1" aria-label="capacity"></label>` +
2884
+ `<label>Your password, typed again<input type="password" name="token" autocomplete="current-password" required></label>` +
2885
+ `<button type="submit">Register — its token is shown once</button></form>`,
2886
+ `<details class="arm-danger"><summary>Retire a worker</summary>` +
2834
2887
  `<form method="post" action="/fleet/runner/retire" class="card">` +
2835
2888
  `<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>`,
2889
+ `<label>Which worker<input type="text" name="name" placeholder="builder-1" required></label>` +
2890
+ `<label>Your password, typed again<input type="password" name="token" autocomplete="current-password" required></label>` +
2891
+ `<button type="submit" class="danger">Retire it</button></form></details>`,
2839
2892
  ].join("\n"),
2840
2893
  // The password fields make this screen sensitive: sendScreen strips
2841
2894
  // the chrome additions and keeps the reorder poller — the named
@@ -2863,7 +2916,7 @@ export function createDecisionServer(options) {
2863
2916
  return refuse(response, who, 404, "That version is not available for this task.", taskHref(family.root.id));
2864
2917
  if (task.taskId !== family.root.id)
2865
2918
  return redirect(response, `${taskHref(family.root.id)}?version=${encodeURIComponent(task.taskId)}`);
2866
- return taskScreen(response, who, version ?? family.current.id, null, 200);
2919
+ return taskScreen(response, who, version ?? family.current.id, null, 200, undefined, undefined, url.searchParams.get("edit") === "plan");
2867
2920
  }
2868
2921
  // v103: a task's evidence pack, as a printable page or JSON (the whole family, sealed ledger entries included).
2869
2922
  const evidence = matchTaskPath(url.pathname, "/evidence$");
@@ -3034,6 +3087,11 @@ export function createDecisionServer(options) {
3034
3087
  const tab = parseResultTab(url.searchParams.get("tab"));
3035
3088
  return redirect(response, `${reviewHref(taskId, found.id)}${tab === "summary" ? "" : `&tab=${tab}`}`);
3036
3089
  }
3090
+ // A build that failed or stopped opens on its own result page too: its changes, its checks and what went wrong.
3091
+ if (url.searchParams.get("record") === null && !running && (found.role === "builder" || found.role === "scout") && attemptRowFor(taskId, found.id, null) !== null) {
3092
+ const tab = parseResultTab(url.searchParams.get("tab"));
3093
+ return redirect(response, `${reviewHref(taskId, found.id)}${tab === "summary" ? "" : `&tab=${tab}`}`);
3094
+ }
3037
3095
  // Pollers exist only for a LIVE run — an orphaned null-outcome run
3038
3096
  // would otherwise be refetched forever (finding 15). The nonce is
3039
3097
  // sendScreen's business now.
@@ -3533,6 +3591,8 @@ export function createDecisionServer(options) {
3533
3591
  })();
3534
3592
  const roomId = url.searchParams.get('conversation');
3535
3593
  const resultLink = (href) => roomId ? href + '&conversation=' + encodeURIComponent(roomId) : href;
3594
+ // Accept and finish is here: the one act that accepts the person's own checks and finishes the task.
3595
+ const finishes = resultRun !== null && who.role === 'approver' && focusTask?.assignment?.state === 'ready-to-check' && focusTask.assignment.receipt?.runId === resultRun.id;
3536
3596
  const resultPanel = resultRun === null
3537
3597
  ? null
3538
3598
  : resultPanelHtml(resultDetailOf(resultRun, who, now), {
@@ -3545,9 +3605,9 @@ export function createDecisionServer(options) {
3545
3605
  hrefFor: one => resultLink(chatResultHref(focusTask?.id ?? "", resultRun.id, one)),
3546
3606
  returnTo: resultLink(chatResultHref(focusTask?.id ?? "", resultRun.id)),
3547
3607
  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)) : '');
3608
+ finishes,
3609
+ }) + (finishes && focusTask?.assignment?.receipt != null
3610
+ ? completionForm(focusTask.assignment.receipt.taskId, resultRun.id, focusTask.assignment.receipt.digest, who.session.csrf, pullRequestTargetOf(resultRun.id), owedAcceptanceOf(focusTask.assignment.receipt)) : '');
3551
3611
  const focusProblem = requestedTask !== null && focusTask === null
3552
3612
  ? "That task is not available in this workspace."
3553
3613
  : requestedResult !== null && focusTask !== null && resultRun === null
@@ -5317,7 +5377,7 @@ export function createDecisionServer(options) {
5317
5377
  .map(task => `<a class="item${task.id === currentId ? " current" : ""}" href="${taskHref(task.id)}">` +
5318
5378
  `<span class="t">${escape(task.title)}</span></a>`)
5319
5379
  .join("\n");
5320
- return `<h2>tasks</h2>\n${items === "" ? `<p class="meta">none yet</p>` : items}`;
5380
+ return `<h2>Tasks</h2>\n${items === "" ? `<p class="meta">None yet</p>` : items}`;
5321
5381
  }
5322
5382
  /**
5323
5383
  * The one liveness fact (round-4 findings 14/15): a run is being built
@@ -5350,7 +5410,7 @@ export function createDecisionServer(options) {
5350
5410
  `<span class="m">${runOutcomeBadge(run, live.has(run.id))}` +
5351
5411
  `<span class="mono">${whenTime(run.startedAt)}</span></span></a>`)
5352
5412
  .join("\n");
5353
- return `<h2>builds</h2>\n${items === "" ? `<p class="meta">none yet</p>` : items}`;
5413
+ return `<h2>Builds</h2>\n${items === "" ? `<p class="meta">None yet</p>` : items}`;
5354
5414
  }
5355
5415
  /**
5356
5416
  * The opener: every project inside the ceiling, most recently opened
@@ -5834,6 +5894,8 @@ export function createDecisionServer(options) {
5834
5894
  // available, computed fresh only when this call has no `who`.
5835
5895
  planRevisions,
5836
5896
  milestoneProgress: view?.milestoneProgress ?? progressOf(ref.id, planRevisions?.current?.document ?? null),
5897
+ // What a failed task missed, said as the task page says it.
5898
+ failure: task.state === "failed" ? failureOf(family.versions.flatMap(version => store.runsFor(version.refId)), view?.assignment ?? assignmentOf(store, task.id, now, workAccess(), evidenceRoot)) : null,
5837
5899
  claimed: view?.claimed ?? store.hasLiveClaim(ref.id, now),
5838
5900
  liveRun: live === null ? null : { id: live.id, runner: live.runner, startedAt: live.startedAt, phase: live.phase },
5839
5901
  checkProgress: view?.checkProgress ?? (live === null && latest === null ? null : store.checkProgress((live ?? latest).id)),
@@ -5895,7 +5957,69 @@ export function createDecisionServer(options) {
5895
5957
  return null;
5896
5958
  return { taskId: rootId, view: null, offer: { taskId: receipt.taskId, runId: receipt.runId, digest: receipt.digest }, target };
5897
5959
  }
5898
- function taskScreen(response, who, taskId, problem, status, scopeDraft, cancelDraft) {
5960
+ /** What went wrong with the latest finished attempt (whatever its outcome, never an older failure). */
5961
+ function failureOf(runs, assignment, readLog = true) {
5962
+ const last = latestFinishedAttempt(runs);
5963
+ if (last === null)
5964
+ return { kind: "reason", line: NO_REASON_RECORDED, evidence: null, suggestion: GENERAL_SUGGESTION, link: null };
5965
+ const full = store.getRun(last.id);
5966
+ return full === null ? { kind: "reason", line: failedAttemptSentence(last.reason), evidence: null, suggestion: stopSuggestionOf(last.reason), link: null }
5967
+ : explainAttempt(full, assignment?.receipt?.runId === last.id ? assignment.receipt : null, readLog);
5968
+ }
5969
+ /** One finished attempt's failure in plain words: a failing check's last error line (linked to that line in its
5970
+ * saved log), else the first signed requirement it missed with the evidence line behind it, else its recorded reason
5971
+ * in plain words (or that none was recorded). Each comes with one suggestion of what to change, and a link to the
5972
+ * attempt's own result page, where its changes, checks and failure are; machine output reads as an internal error,
5973
+ * its detail behind a link to that line of the attempt's record. `readLog` false (a list's rows) reads no saved
5974
+ * check log: the requirement or the reason, from the database alone. */
5975
+ function explainAttempt(run, receipt, readLog = true) {
5976
+ const taskId = store.externalIdFor(run.taskRef) ?? "";
5977
+ const page = `${reviewHref(taskId)}&run=${run.id}`;
5978
+ const see = taskId === "" ? null : { label: `See build #${run.id}`, href: page };
5979
+ const artifacts = store.artifactsFor(run.id);
5980
+ const logArtifact = !readLog ? null : receipt?.checks.status === "failed" && receipt.checks.logArtifactId !== null
5981
+ ? artifacts.find(one => one.id === receipt.checks.logArtifactId) ?? null
5982
+ : [...artifacts].reverse().find(one => one.kind === "check-log") ?? null;
5983
+ if (logArtifact !== null) {
5984
+ let read = null;
5985
+ try {
5986
+ read = readVerifiedArtifact(evidenceRoot, logArtifact);
5987
+ }
5988
+ catch {
5989
+ read = null;
5990
+ }
5991
+ const text = read !== null && read.ok ? read.content.toString("utf8") : null;
5992
+ const exit = text === null ? null : /^\(exit (-?[0-9]+)\)$/m.exec(text);
5993
+ const failedCheck = receipt?.checks.status === "failed" || (exit !== null && exit !== undefined && Number(exit[1]) !== 0);
5994
+ const found = text !== null && failedCheck ? lastErrorLineOf(text) : null;
5995
+ if (found !== null) {
5996
+ const line = oneLineOf(found.text, 140);
5997
+ return { kind: "check", line: `The check failed: ${line}`, evidence: null, suggestion: failingCheckSuggestion(line),
5998
+ link: taskId === "" ? null : { label: `Check output, line ${found.line}`, href: `${page}&tab=checks#check-log-L${found.line}` } };
5999
+ }
6000
+ }
6001
+ const verdict = store.proofVerdictFor(run.id);
6002
+ const missed = verdict === null ? null : missedRequirementOf(verdict.matrix);
6003
+ if (missed !== null) {
6004
+ return { kind: "requirement", line: missedRequirementLine(missed.statement), evidence: missed.evidence === null ? null : oneLineOf(missed.evidence, 240),
6005
+ suggestion: missedRequirementSuggestion(missed.statement), link: see };
6006
+ }
6007
+ const internal = isInternalErrorReason(run.reason);
6008
+ return { kind: "reason", line: failedAttemptSentence(run.reason), evidence: null, suggestion: stopSuggestionOf(run.reason),
6009
+ link: internal ? { label: "The recorded error", href: `/r/${run.id}?record=1#run-reason-detail` } : see };
6010
+ }
6011
+ /** Run checks in place on a result's commit: an approver's browser session, outside the demo, when the project has a
6012
+ * check and none is waiting or running on it. Comes back to `returnTo`. */
6013
+ function runChecksHere(runId, who, returnTo) {
6014
+ if (runId === null || who.via !== "cookie" || who.role !== "approver" || store.isDemo())
6015
+ return null;
6016
+ const run = store.getRun(runId);
6017
+ const followUps = run === null ? null : followUpsFor(store, evidenceRoot, run, clock());
6018
+ if (followUps === null || !(followUps.quick || followUps.full) || followUps.checks.some(one => one.state === "waiting" || one.state === "running"))
6019
+ return null;
6020
+ return { action: `/r/${runId}/checks`, level: followUps.full ? "full" : "quick", returnTo };
6021
+ }
6022
+ function taskScreen(response, who, taskId, problem, status, scopeDraft, cancelDraft, editPlan = false) {
5899
6023
  const admittedFamily = familyOf(taskId);
5900
6024
  if (admittedFamily?.problem != null)
5901
6025
  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") }));
@@ -5910,6 +6034,8 @@ export function createDecisionServer(options) {
5910
6034
  presentedData.scopeDraft = scopeDraft;
5911
6035
  if (cancelDraft !== undefined)
5912
6036
  presentedData.cancelDraft = cancelDraft;
6037
+ if (editPlan)
6038
+ presentedData.editPlan = true;
5913
6039
  presentedData.pullRequest = taskPullRequestOf(taskId, who);
5914
6040
  // The thread reads the whole family: the original, its revisions, and every attempt across them.
5915
6041
  if (family !== null && family.problem === null) {
@@ -5917,6 +6043,10 @@ export function createDecisionServer(options) {
5917
6043
  versions: family.versions.map(one => ({ id: one.id, title: one.title, state: one.state })),
5918
6044
  runs: family.versions.flatMap(version => version.id === data.task.id ? data.runs.map(run => ({ ...run, taskId: version.id })) : store.runsFor(version.refId).map(run => ({ ...run, taskId: version.id }))) };
5919
6045
  }
6046
+ // What went wrong reads the family's latest finished attempt, as the Tasks row does.
6047
+ presentedData.failure = failureOf(presentedData.family?.runs ?? data.runs, data.assignment ?? null);
6048
+ presentedData.runChecks = runChecksHere(data.assignment?.receipt?.runId ?? null, who, taskHref(taskId));
6049
+ presentedData.demo = store.isDemo();
5920
6050
  const paneProject = restricted() ? store.lookupRef(taskId)?.repo ?? null : who.via === "cookie" ? who.session.project : null;
5921
6051
  const page = taskPage(paneProject === null && !unscopedMode
5922
6052
  ? chromeFor(paneProject, "tasks")
@@ -6097,7 +6227,7 @@ export function createDecisionServer(options) {
6097
6227
  return peekSay(seen.reason);
6098
6228
  const stamp = clock().toISOString().slice(11, 19);
6099
6229
  const parts = [
6100
- `<p class="meta">best-effort look at ${escape(stamp)} UTC — files can change mid-read</p>`,
6230
+ `<p class="meta">Best-effort look at ${escape(stamp)} UTC — files can change mid-read</p>`,
6101
6231
  ];
6102
6232
  const changed = seen.rows.filter(one => one.kind === "changed");
6103
6233
  const deleted = seen.rows.filter(one => one.kind === "deleted");
@@ -6122,7 +6252,7 @@ export function createDecisionServer(options) {
6122
6252
  for (const row of deleted)
6123
6253
  parts.push(line(row, "−"));
6124
6254
  if (fresh.total > 0) {
6125
- parts.push(`<p class="meta">new files · ${fresh.total}</p>`);
6255
+ parts.push(`<p class="meta">New files · ${fresh.total}</p>`);
6126
6256
  for (const row of fresh.rows) {
6127
6257
  parts.push(row.collapsed
6128
6258
  ? `<p class="row mono">+ ${peekName(row.label)} <span class="meta">collapsed names — ${row.count} files</span></p>`
@@ -6133,7 +6263,7 @@ export function createDecisionServer(options) {
6133
6263
  }
6134
6264
  }
6135
6265
  if (unchecked.length > 0) {
6136
- parts.push(`<p class="meta">not verified this look — absence above does not mean unchanged:</p>`);
6266
+ parts.push(`<p class="meta">Not verified this look — absence above does not mean unchanged:</p>`);
6137
6267
  for (const row of unchecked)
6138
6268
  parts.push(line(row, "?"));
6139
6269
  }
@@ -6144,7 +6274,7 @@ export function createDecisionServer(options) {
6144
6274
  // The byte cap is enforced AFTER escaping (finding 32): an oversize
6145
6275
  // rendering is replaced whole by its exact counts.
6146
6276
  fragment =
6147
- `<p class="meta">best-effort look at ${escape(stamp)} UTC</p>` +
6277
+ `<p class="meta">Best-effort look at ${escape(stamp)} UTC</p>` +
6148
6278
  `<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>`;
6149
6279
  }
6150
6280
  peekCache.set(key, { fragment, at: Date.now() });
@@ -8575,7 +8705,7 @@ export function createDecisionServer(options) {
8575
8705
  // THE CEREMONY (M1): every resolved term in words, and the password
8576
8706
  // signs exactly this digest — a drifted form refuses at /mode/sign.
8577
8707
  const nonce = mintApprovalNonce(who.name, "mode-sign", `${project}:${digest}`);
8578
- const bodyHtml = `<h1 style="overflow-wrap:anywhere">sign the ${escape(name)} mode for ${escape(project)}</h1>` +
8708
+ const bodyHtml = `<h1 style="overflow-wrap:anywhere">Sign the ${escape(name)} mode for ${escape(project)}</h1>` +
8579
8709
  `<form method="post" action="/mode/sign" class="card approve-form">` +
8580
8710
  `<input type="hidden" name="csrf" value="${escape(who.session.csrf)}">` +
8581
8711
  `<input type="hidden" name="nonce" value="${escape(nonce)}">` +
@@ -8584,12 +8714,12 @@ export function createDecisionServer(options) {
8584
8714
  .map(field => `<input type="hidden" name="${field}" value="${escape(body.get(field) ?? "")}">`)
8585
8715
  .join("") +
8586
8716
  `<input type="hidden" name="expiry" value="${escape(expiry)}">` +
8587
- `<p><strong>your password signs exactly this:</strong></p>` +
8717
+ `<p><strong>Your password signs exactly this:</strong></p>` +
8588
8718
  modeWords(terms)
8589
8719
  .map(words => `<p class="recap" style="margin-top:.4rem">${escape(words)}</p>`)
8590
8720
  .join("") +
8591
- `<label>your password, typed again<input type="password" name="token" autocomplete="current-password"></label>` +
8592
- `<div class="sticky-actions"><button type="submit">sign it</button></div>` +
8721
+ `<label>Your password, typed again<input type="password" name="token" autocomplete="current-password"></label>` +
8722
+ `<div class="sticky-actions"><button type="submit">Sign it</button></div>` +
8593
8723
  `</form>` +
8594
8724
  `<p class="meta"><a href="/mode">back — sign nothing</a></p>`;
8595
8725
  return sendScreen(response, 200, screen("mode", bodyHtml, { chrome: chromeFor(project, "mode") }));
@@ -9907,7 +10037,9 @@ export function createDecisionServer(options) {
9907
10037
  const found = store.getRun(id);
9908
10038
  if (found === null || !visible(taskRepoOf(found.taskRef)))
9909
10039
  return refuse(response, who, 404, "no such run");
9910
- const back = resultReturnTarget(body.get("return"), id);
10040
+ // The task page that offered Run checks in place is a return target too: this run's own task, or its family's root.
10041
+ const runTask = store.externalIdFor(found.taskRef);
10042
+ const back = resultReturnTarget(body.get("return"), id, runTask === null ? [] : [runTask, familyOf(runTask)?.root.id ?? runTask]);
9911
10043
  if (who.via !== "cookie")
9912
10044
  return refuse(response, who, 403, "Sign in with a browser session to do this.", back);
9913
10045
  if (followUp[2] === "checks") {
@@ -9919,7 +10051,8 @@ export function createDecisionServer(options) {
9919
10051
  const asked = requestFollowUpChecks(store, { runId: id, level, actor: who.name }, now);
9920
10052
  if (!asked.ok)
9921
10053
  return refuse(response, who, 409, asked.message, back);
9922
- return redirect(response, `${back.split("#")[0]}#follow-ups`);
10054
+ // Back where it was asked: the task page's status row says the checks are running; a result page shows them under #follow-ups.
10055
+ return redirect(response, back.startsWith("/t/") ? back : `${back.split("#")[0]}#follow-ups`);
9923
10056
  }
9924
10057
  const filed = fileAddTestsTask(store, evidenceRoot, { runId: id, actor: who.name, filedVia: "console", ...(admissionList() === null ? {} : { admittedRepos: admissionList() }) }, now);
9925
10058
  if (!filed.ok)
@@ -10133,7 +10266,7 @@ export function createDecisionServer(options) {
10133
10266
  const quickMode = quickRef?.repo == null ? null : store.activeMode(quickRef.repo, clock());
10134
10267
  const quickTerms = quickMode === null ? null : modeTermsFromJson(quickMode.termsJson);
10135
10268
  const quick = quickTerms?.quickMint === true && quickMode !== null && quickMode.signedBy === who.name;
10136
- const body = `<h1>run ${escape(terms.taskId)} once, while you watch</h1>` +
10269
+ const body = `<h1>Run ${escape(terms.taskId)} once, while you watch</h1>` +
10137
10270
  `<form method="post" action="${taskHref(terms.taskId)}/attend" class="card approve-form">` +
10138
10271
  `<input type="hidden" name="csrf" value="${escape(csrf)}">` +
10139
10272
  `<input type="hidden" name="nonce" value="${escape(nonce)}">` +
@@ -10146,10 +10279,10 @@ export function createDecisionServer(options) {
10146
10279
  (inputs.posture === undefined ? "" : `<input type="hidden" name="posture" value="${escape(inputs.posture)}">`) +
10147
10280
  (terms.parentRun == null ? "" : `<input type="hidden" name="parent" value="${terms.parentRun}">`) +
10148
10281
  (terms.followup == null ? "" : `<input type="hidden" name="followup" value="${escape(terms.followup)}">`) +
10149
- `<p><strong>your password signs exactly this:</strong></p>` +
10150
- `<p class="meta">goal</p><p class="recap" style="margin-top:0">${escape(scope?.goal ?? "")}</p>` +
10151
- `<p class="meta">not this</p><p class="recap" style="margin-top:0">${scope?.outOfScope == null ? "<em>no exclusions</em>" : escape(scope.outOfScope)}</p>` +
10152
- `<p class="meta">touches · ${scope === null || scope.touches.length === 0 ? "anything" : scope.touches.map(one => escape(one)).join(", ")}</p>` +
10282
+ `<p><strong>Your password signs exactly this:</strong></p>` +
10283
+ `<p class="meta">Goal</p><p class="recap" style="margin-top:0">${escape(scope?.goal ?? "")}</p>` +
10284
+ `<p class="meta">Not this</p><p class="recap" style="margin-top:0">${scope?.outOfScope == null ? "<em>no exclusions</em>" : escape(scope.outOfScope)}</p>` +
10285
+ `<p class="meta">Touches · ${scope === null || scope.touches.length === 0 ? "anything" : scope.touches.map(one => escape(one)).join(", ")}</p>` +
10153
10286
  (() => {
10154
10287
  const profile = JSON.parse(terms.profileJson).profile;
10155
10288
  const posture = profile?.permissionArgv === "bypassPermissions"
@@ -10157,21 +10290,21 @@ export function createDecisionServer(options) {
10157
10290
  : profile?.permissionArgv === "auto"
10158
10291
  ? "safe unattended permissions — routine project commands and edits proceed; risky acts stop"
10159
10292
  : "legacy acceptEdits — edits proceed, commands that ask are denied unattended";
10160
- return `<p class="meta">runs on</p><p class="recap" style="margin-top:0">claude · ${escape(String(profile?.model ?? ""))} — ${posture}</p>`;
10293
+ return `<p class="meta">Runs on</p><p class="recap" style="margin-top:0">claude · ${escape(String(profile?.model ?? ""))} — ${posture}</p>`;
10161
10294
  })() +
10162
10295
  (terms.parentRun == null
10163
10296
  ? ""
10164
- : `<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>` +
10165
- `<p class="meta">the follow-up — this is the instruction</p><p class="recap" style="margin-top:0">${escape(terms.followup ?? "")}</p>`) +
10166
- `<p class="meta">repository · head</p><p class="recap mono" style="margin-top:0">${escape(terms.repo)} @ ${escape(terms.head.slice(0, 12))}</p>` +
10167
- `<p class="meta">worker</p><p class="recap" style="margin-top:0">${escape(terms.runner)} (this machine)</p>` +
10168
- `<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>` +
10169
- `<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>` +
10170
- `<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>` +
10297
+ : `<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>` +
10298
+ `<p class="meta">The follow-up — this is the instruction</p><p class="recap" style="margin-top:0">${escape(terms.followup ?? "")}</p>`) +
10299
+ `<p class="meta">Repository · head</p><p class="recap mono" style="margin-top:0">${escape(terms.repo)} @ ${escape(terms.head.slice(0, 12))}</p>` +
10300
+ `<p class="meta">Worker</p><p class="recap" style="margin-top:0">${escape(terms.runner)} (this machine)</p>` +
10301
+ `<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>` +
10302
+ `<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>` +
10303
+ `<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>` +
10171
10304
  (quick
10172
- ? `<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>`
10173
- : `<label>your password, typed again — a signed-in session alone cannot authorize work<input type="password" name="token" autocomplete="current-password"></label>`) +
10174
- `<div class="sticky-actions"><button type="submit">run it while I watch</button></div>` +
10305
+ ? `<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>`
10306
+ : `<label>Your password, typed again — a signed-in session alone cannot authorize work<input type="password" name="token" autocomplete="current-password"></label>`) +
10307
+ `<div class="sticky-actions"><button type="submit">Run it while I watch</button></div>` +
10175
10308
  `</form>` +
10176
10309
  `<p class="meta"><a href="${taskHref(terms.taskId)}">back to the task</a></p>`;
10177
10310
  // The chrome binds to the AUTHORITY repository (surfaces round 1,
@@ -10446,10 +10579,21 @@ export function createDecisionServer(options) {
10446
10579
  return redirect(response, taskHref(taskId));
10447
10580
  }
10448
10581
  case "requeue": {
10582
+ // Retry may carry a note for the next attempt: the same steering note, checked before anything changes.
10583
+ const note = (body.get("note") ?? "").trim();
10584
+ if (note !== "" && who.via !== "cookie")
10585
+ return refuse(response, who, 403, "steering is a browser session's act");
10586
+ if (note !== "" && !validateNote(note).ok)
10587
+ return taskScreen(response, who, taskId, "that note will not store, so nothing was retried", 400);
10449
10588
  const requeued = store.requeueTask(taskId, who.name, now);
10450
10589
  if (!requeued.ok) {
10451
10590
  return taskScreen(response, who, taskId, `not requeued: ${requeued.reason}`, 409);
10452
10591
  }
10592
+ if (note !== "") {
10593
+ const filed = store.fileSteerNote(taskId, verifiedAuthor(who.name), note, now);
10594
+ if (!filed.ok)
10595
+ return taskScreen(response, who, taskId, "Retried. The note wasn't saved; add it under Steering.", 409);
10596
+ }
10453
10597
  // Allow-listed return only — never an arbitrary URL from the form.
10454
10598
  return redirect(response, body.get("return") === "inbox" ? "/inbox" : body.get("return") === "next" ? "/next" : taskHref(taskId));
10455
10599
  }
@@ -10708,6 +10852,13 @@ export function createDecisionServer(options) {
10708
10852
  if (budgetUsd !== null && (!Number.isFinite(budgetUsd) || budgetUsd <= 0)) {
10709
10853
  return taskScreen(response, who, taskId, "the dollar cap is a positive amount", 400);
10710
10854
  }
10855
+ // The approval sheet's in-place editor carries the cap it was shown
10856
+ // exactly, in millionths, and "none" for no limit (never a default).
10857
+ const exactBudgetGiven = body.get("budget-microusd");
10858
+ const exactBudget = exactBudgetGiven === null ? undefined : exactBudgetGiven === "none" ? null : /^[1-9][0-9]{0,14}$/.test(exactBudgetGiven) ? Number(exactBudgetGiven) : NaN;
10859
+ if (Number.isNaN(exactBudget))
10860
+ return taskScreen(response, who, taskId, "the dollar cap is a positive amount", 400);
10861
+ const inPlace = body.has("requirement-new");
10711
10862
  // The tournament controls (operator request): a count of 2–4 files
10712
10863
  // race terms BESIDE the scope — validated and priced BEFORE anything
10713
10864
  // saves, so a bad tournament never half-lands on a good scope.
@@ -10762,92 +10913,112 @@ export function createDecisionServer(options) {
10762
10913
  if (plannedRace !== null && store.mirrorByTask(taskId) !== null) {
10763
10914
  return taskScreen(response, who, taskId, "external work races in a follow-up release — file the tournament on a local task", 409);
10764
10915
  }
10765
- const saved = store.transact(() => {
10766
- if ((plannedRace !== null || plannedComparison !== null) && store.openAuthorizationFor(ref.id) !== null) {
10767
- return { ok: false, status: 409, message: "an attended authorization is open on this task — revoke it before filing a tournament or comparison" };
10768
- }
10769
- // C1/M3: the signer's own credentialed filing auto-approves —
10770
- // coverage is asked INSIDE this transaction, the escalated
10771
- // default and budget ride the filing, and the seal commits
10772
- // atomically with it. Plain scopes only: a tournament or
10773
- // comparison keeps its own human ceremony.
10774
- // COOKIE ONLY (C1's channel table; surfaces round 1, finding 1):
10775
- // bearer credentials are for machines, and a machine road must
10776
- // never spend the signer's mode.
10777
- const coverage = who.via === "cookie" && plannedRace === null && plannedComparison === null
10778
- ? modeFilingCoverage(store, ref.repo, who.name, now)
10779
- : null;
10780
- const proposed = proposeGuarded(store, {
10781
- taskId, author: who.name,
10782
- goal: body.get("goal") ?? "",
10783
- outOfScope: body.get("not") ?? null,
10784
- touches: (body.get("touches") ?? "").split(/[\n,]/),
10785
- acceptance: acceptanceLinesToInput((body.get("acceptance") ?? "").split("\n")),
10786
- permissionMode,
10787
- qualityMode,
10788
- ...(budgetUsd !== null
10789
- ? { budgetMicrousd: Math.round(budgetUsd * 1_000_000) }
10790
- : coverage?.defaultBudgetMicrousd != null
10791
- ? { budgetMicrousd: coverage.defaultBudgetMicrousd }
10792
- : {}),
10793
- ...(coverage?.escalated === true ? { posture: "escalated" } : {}),
10794
- sawDigest: sawDigest === null || sawDigest === "" ? null : sawDigest,
10795
- taskRef: ref.id,
10796
- now,
10797
- });
10798
- if (!proposed.ok) {
10799
- return {
10800
- ok: false,
10801
- status: proposed.reason === "changed" || proposed.reason === "claimed" ? 409 : 400,
10802
- message: proposed.message ?? `scope not saved: ${proposed.reason}`,
10803
- };
10804
- }
10805
- if (coverage !== null && ref.plan === "requested") {
10806
- authorizePlanUnderMode(store, taskId, who.name, now);
10807
- }
10808
- else if (coverage !== null) {
10809
- const filedScope = store.getScope(taskId);
10810
- if (filedScope?.profileState === "resolved") {
10811
- const sealedUnderMode = store.sealScopeApproval(taskId, who.name, now, {}, { kind: "mode", modeDigest: coverage.digest });
10812
- // The quarantine speaks at the caller (review finding 7).
10813
- if (!sealedUnderMode) {
10814
- return { ok: false, status: 200, message: "scope saved — coordinator-filed: mode coverage cannot admit it; sign the scope" };
10916
+ let saved;
10917
+ try {
10918
+ saved = store.transact(() => {
10919
+ if ((plannedRace !== null || plannedComparison !== null) && store.openAuthorizationFor(ref.id) !== null) {
10920
+ return { ok: false, status: 409, message: "an attended authorization is open on this task — revoke it before filing a tournament or comparison" };
10921
+ }
10922
+ // C1/M3: the signer's own credentialed filing auto-approves —
10923
+ // coverage is asked INSIDE this transaction, the escalated
10924
+ // default and budget ride the filing, and the seal commits
10925
+ // atomically with it. Plain scopes only: a tournament or
10926
+ // comparison keeps its own human ceremony.
10927
+ // COOKIE ONLY (C1's channel table; surfaces round 1, finding 1):
10928
+ // bearer credentials are for machines, and a machine road must
10929
+ // never spend the signer's mode.
10930
+ const coverage = who.via === "cookie" && plannedRace === null && plannedComparison === null
10931
+ ? modeFilingCoverage(store, ref.repo, who.name, now)
10932
+ : null;
10933
+ const before = store.getScope(taskId);
10934
+ const proposed = proposeGuarded(store, {
10935
+ taskId, author: who.name,
10936
+ goal: body.has("goal-brief") ? `${(body.get("goal") ?? "").trim()}${body.get("goal-brief")}` : body.get("goal") ?? "",
10937
+ outOfScope: body.get("not") ?? null,
10938
+ touches: (body.get("touches") ?? "").split(/[\n,]/),
10939
+ // The approval sheet's in-place editor sends Done when line by line.
10940
+ acceptance: inPlace
10941
+ ? requirementsFromEditor(body, before?.acceptance ?? [])
10942
+ : acceptanceLinesToInput((body.get("acceptance") ?? "").split("\n")),
10943
+ permissionMode,
10944
+ qualityMode,
10945
+ ...(exactBudget !== undefined
10946
+ ? exactBudget === null ? {} : { budgetMicrousd: exactBudget }
10947
+ : budgetUsd !== null
10948
+ ? { budgetMicrousd: Math.round(budgetUsd * 1_000_000) }
10949
+ : coverage?.defaultBudgetMicrousd != null
10950
+ ? { budgetMicrousd: coverage.defaultBudgetMicrousd }
10951
+ : {}),
10952
+ ...(coverage?.escalated === true ? { posture: "escalated" } : {}),
10953
+ sawDigest: sawDigest === null || sawDigest === "" ? null : sawDigest,
10954
+ taskRef: ref.id,
10955
+ now,
10956
+ });
10957
+ if (!proposed.ok) {
10958
+ return {
10959
+ ok: false,
10960
+ status: proposed.reason === "changed" || proposed.reason === "claimed" ? 409 : 400,
10961
+ message: proposed.message ?? (proposed.reason === "acceptance-required" ? "Not saved: add at least one requirement under Done when." : `scope not saved: ${proposed.reason}`),
10962
+ };
10963
+ }
10964
+ // Editing the plan in place never changes what the agent may do:
10965
+ // a save that would (a legacy setting, a newer policy or mode)
10966
+ // rolls back whole.
10967
+ if (inPlace && before?.profile != null && proposed.scope.profile != null && levelOfProfile(before.profile) !== levelOfProfile(proposed.scope.profile)) {
10968
+ throw new PermissionsWouldChange();
10969
+ }
10970
+ if (coverage !== null && ref.plan === "requested") {
10971
+ authorizePlanUnderMode(store, taskId, who.name, now);
10972
+ }
10973
+ else if (coverage !== null) {
10974
+ const filedScope = store.getScope(taskId);
10975
+ if (filedScope?.profileState === "resolved") {
10976
+ const sealedUnderMode = store.sealScopeApproval(taskId, who.name, now, {}, { kind: "mode", modeDigest: coverage.digest });
10977
+ // The quarantine speaks at the caller (review finding 7).
10978
+ if (!sealedUnderMode) {
10979
+ return { ok: false, status: 200, message: "scope saved — coordinator-filed: mode coverage cannot admit it; sign the scope" };
10980
+ }
10815
10981
  }
10816
10982
  }
10817
- }
10818
- if (plannedComparison !== null && plannedComparison.ok) {
10983
+ if (plannedComparison !== null && plannedComparison.ok) {
10984
+ store.fileTournamentTerms({
10985
+ taskRef: ref.id,
10986
+ kind: "comparison",
10987
+ raceDigest: plannedComparison.plan.comparisonDigest,
10988
+ agents: plannedComparison.plan.agents,
10989
+ perAgentBudgetMicrousd: 0,
10990
+ overrunReserveMicrousd: 0,
10991
+ totalBudgetMicrousd: 0,
10992
+ priceVersion: 0,
10993
+ publicationPolicy: plannedComparison.plan.publicationPolicy,
10994
+ }, now);
10995
+ return { ok: true };
10996
+ }
10997
+ if (plannedRace === null) {
10998
+ // Switching back to "one agent" withdraws a standing race OR
10999
+ // comparison — the deactivated row survives as history, and
11000
+ // the approval card returns to the scope alone.
11001
+ store.retractTournamentTerms(ref.id);
11002
+ return { ok: true };
11003
+ }
10819
11004
  store.fileTournamentTerms({
10820
11005
  taskRef: ref.id,
10821
- kind: "comparison",
10822
- raceDigest: plannedComparison.plan.comparisonDigest,
10823
- agents: plannedComparison.plan.agents,
10824
- perAgentBudgetMicrousd: 0,
10825
- overrunReserveMicrousd: 0,
10826
- totalBudgetMicrousd: 0,
10827
- priceVersion: 0,
10828
- publicationPolicy: plannedComparison.plan.publicationPolicy,
11006
+ raceDigest: plannedRace.raceDigest,
11007
+ agents: plannedRace.agents,
11008
+ perAgentBudgetMicrousd: plannedRace.perAgentBudgetMicrousd,
11009
+ overrunReserveMicrousd: plannedRace.overrunReserveMicrousd,
11010
+ totalBudgetMicrousd: plannedRace.totalBudgetMicrousd,
11011
+ priceVersion: plannedRace.priceVersion,
11012
+ publicationPolicy: plannedRace.publicationPolicy,
10829
11013
  }, now);
10830
11014
  return { ok: true };
10831
- }
10832
- if (plannedRace === null) {
10833
- // Switching back to "one agent" withdraws a standing race OR
10834
- // comparison — the deactivated row survives as history, and
10835
- // the approval card returns to the scope alone.
10836
- store.retractTournamentTerms(ref.id);
10837
- return { ok: true };
10838
- }
10839
- store.fileTournamentTerms({
10840
- taskRef: ref.id,
10841
- raceDigest: plannedRace.raceDigest,
10842
- agents: plannedRace.agents,
10843
- perAgentBudgetMicrousd: plannedRace.perAgentBudgetMicrousd,
10844
- overrunReserveMicrousd: plannedRace.overrunReserveMicrousd,
10845
- totalBudgetMicrousd: plannedRace.totalBudgetMicrousd,
10846
- priceVersion: plannedRace.priceVersion,
10847
- publicationPolicy: plannedRace.publicationPolicy,
10848
- }, now);
10849
- return { ok: true };
10850
- });
11015
+ });
11016
+ }
11017
+ catch (error) {
11018
+ if (!(error instanceof PermissionsWouldChange))
11019
+ throw error;
11020
+ saved = { ok: false, status: 409, message: "Not saved: this would change what the agent may do. Change permissions in Details, under edit the scope." };
11021
+ }
10851
11022
  if (!saved.ok)
10852
11023
  return taskScreen(response, who, taskId, saved.message, saved.status, body);
10853
11024
  return redirect(response, taskHref(taskId));
@@ -10963,6 +11134,9 @@ export function createDecisionServer(options) {
10963
11134
  }
10964
11135
  note = validated.note;
10965
11136
  }
11137
+ // A failed task's result is accepted only anyway, on purpose: with a reason.
11138
+ if (note === null && store.getTask(taskId)?.state === "failed")
11139
+ return taskScreen(response, who, taskId, "Accepting anyway needs a reason.", 400);
10966
11140
  store.acceptProof(latest.id, verifiedAuthor(who.name), note, now);
10967
11141
  return redirect(response, body.get("return") === null ? taskHref(taskId) : safeReturn(body.get("return")));
10968
11142
  }
@@ -10978,6 +11152,23 @@ export function createDecisionServer(options) {
10978
11152
  if (current === null || current.receipt?.taskId !== taskId || !/^[0-9]{1,15}$/.test(namedRun) || current.receipt.runId !== Number(namedRun)) {
10979
11153
  return taskScreen(response, who, taskId, "This result changed. Open the current result before marking it complete.", 409);
10980
11154
  }
11155
+ if (body.get("accept") === "1") {
11156
+ // Accept and finish: the person's acceptance and the completion of what it accepted, in one request
11157
+ // and one transaction. The receipt named is the one read, before accepting.
11158
+ const rawNote = (body.get("note") ?? "").trim();
11159
+ let note = null;
11160
+ if (rawNote !== "") {
11161
+ const validated = validateNote(rawNote);
11162
+ if (!validated.ok)
11163
+ return taskScreen(response, who, taskId, validated.problem, 400);
11164
+ note = validated.note;
11165
+ }
11166
+ const publish = body.get("publish") === "1";
11167
+ 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);
11168
+ if (!finished.ok)
11169
+ return taskScreen(response, who, taskId, finished.message, 409);
11170
+ return redirect(response, publish ? `${taskHref(familyOf(taskId)?.root.id ?? taskId)}#merge` : body.get("return") === null ? reviewHref(taskId, Number(namedRun)) : safeReturn(body.get("return")));
11171
+ }
10981
11172
  if (body.get("publish") === "1") {
10982
11173
  // "Complete and open a pull request": the same exact-receipt completion, plus the owed PR for this
10983
11174
  // exact commit in one transaction. The watch process pushes and opens it; the task shows its progress.
@@ -11398,6 +11589,36 @@ export function createDecisionServer(options) {
11398
11589
  const run = store.runsFor(ref.id).find(one => (one.outcome === "built" || one.outcome === "no-change") &&
11399
11590
  one.finishedAt !== null &&
11400
11591
  (one.role === "builder" || one.role === "scout")) ?? null;
11592
+ return resultRowOf(task, ref.repo, family.problem, run);
11593
+ }
11594
+ /**
11595
+ * A build attempt that didn't deliver a result (failed, refused, stopped,
11596
+ * never finished) still has a result page: its changes, its checks and what
11597
+ * went wrong. The row for one: the run named by `runId` when it is an ended
11598
+ * builder or scout attempt of this task that isn't a delivered result, or a
11599
+ * failed task's latest attempt; with no run named, that latest attempt. The
11600
+ * same admission as a completed row: in this project (or none) and inside
11601
+ * the ceiling. Null otherwise, which reads like a task that never existed.
11602
+ */
11603
+ function attemptRowFor(taskId, runId, project) {
11604
+ const family = familyOf(taskId);
11605
+ const task = store.getTask(taskId);
11606
+ const ref = store.lookupRef(taskId);
11607
+ if (family === null || task === null || ref === null)
11608
+ return null;
11609
+ if (!(project === null || ref.repo === null || ref.repo === project) || !visible(ref.repo))
11610
+ return null;
11611
+ const attempts = store.runsFor(ref.id).filter(one => (one.role === "builder" || one.role === "scout") && (one.finishedAt !== null || one.outcome !== null));
11612
+ const latest = task.state === "failed" ? latestFinishedAttempt(attempts) : null;
11613
+ const run = runId !== null ? attempts.find(one => one.id === runId) ?? null : latest;
11614
+ // A delivered result has its completed row; a failed task's latest attempt is what failed, whatever it ended as.
11615
+ if (run === null || runIsLive(run) || ((run.outcome === "built" || run.outcome === "no-change") && run.id !== latest?.id))
11616
+ return null;
11617
+ return resultRowOf(task, ref.repo, family.problem, run);
11618
+ }
11619
+ function resultRowOf(task, repo, historyProblem, run) {
11620
+ const ref = { repo };
11621
+ const family = { problem: historyProblem };
11401
11622
  const publication = run === null ? null : store.publicationForRun(run.id);
11402
11623
  const verdict = run === null ? null : store.proofVerdictFor(run.id);
11403
11624
  return {
@@ -11588,6 +11809,7 @@ export function createDecisionServer(options) {
11588
11809
  intent,
11589
11810
  plan,
11590
11811
  contest: contestOf(),
11812
+ ...(store.isDemo() ? { demo: true } : {}),
11591
11813
  };
11592
11814
  if (run === null) {
11593
11815
  return { ...base, run: null, detail: null, reviewRetry: null, review: null, notes: [] };
@@ -11909,8 +12131,8 @@ export function editorFileHref(worktree, path, line) {
11909
12131
  function matrixStateBadge(state) {
11910
12132
  // Red belongs to a Failed headline alone (task-status.ts); an unmet requirement is an amber note.
11911
12133
  const cls = state === "pass" ? "badge-done" : state === "failed" ? "badge-note" : state === "missing" ? "badge-note" : "badge-manual-review";
11912
- const word = state === "pass" ? "pass" : state === "missing" ? "missing" : state === "failed" ? "failed" : "manual review";
11913
- return `<span class="badge ${cls}" data-matrix-state="${escape(state)}">${escape(word)}</span>`;
12134
+ // The same words as the Checks tab and the card's Requirements row (task-status.ts).
12135
+ return `<span class="badge ${cls}" data-matrix-state="${escape(state)}">${escape(requirementWordOf({ state }))}</span>`;
11914
12136
  }
11915
12137
  /** v40: the bounded repair chain's own line — presented as the user-facing
11916
12138
  * recovery it represents, without exposing the internal workflow name.
@@ -12011,8 +12233,9 @@ function criterionMatrixHtml(matrix, options = {}) {
12011
12233
  const awaitingAssessment = row.assessment?.evidenceState === "pass" && row.review === null;
12012
12234
  const confirmed = row.assessment !== undefined && state === "pass";
12013
12235
  // Plain words (task-status brief): the retired assessment step is not a state a person acts on.
12014
- const label = awaitingAssessment ? "Met" : confirmed ? "Confirmed" : state === "pass" ? "Met" : state === "manual-review" ? "You check" : state === "missing" ? "Not shown yet" : "Not met";
12015
- const cls = awaitingAssessment ? "badge-done" : state === "pass" ? "badge-done" : state === "manual-review" ? "badge-manual-review" : "badge-note";
12236
+ // The card's Requirements row reads the same words from the same source (task-status.ts), so they agree.
12237
+ const label = requirementWordOf(row, options.verdict);
12238
+ const cls = label === "Met" ? "badge-done" : label === "You check" ? "badge-manual-review" : "badge-note";
12016
12239
  const warnings = [];
12017
12240
  // Only replace the known boilerplate. Other recorded failure details
12018
12241
  // remain verbatim, so concision cannot hide a different problem.
@@ -12082,10 +12305,10 @@ function planContractHtml(view, mode) {
12082
12305
  if ("problem" in view) {
12083
12306
  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>`;
12084
12307
  }
12085
- 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>`;
12308
+ 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>`;
12086
12309
  if (!view.filed) {
12087
12310
  return mode === "full"
12088
- ? `<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>`
12311
+ ? `<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>`
12089
12312
  : `<p class="meta contract-note">no scope was filed before planning — every term above is the planner's proposal</p>`;
12090
12313
  }
12091
12314
  if (view.revision === true) {
@@ -12095,19 +12318,19 @@ function planContractHtml(view, mode) {
12095
12318
  }
12096
12319
  return (`<div class="contract-panel contract-amended"${mode === "full" ? ` id="contract-amendment"` : ""}><p class="approval-label">updated plan</p>` +
12097
12320
  `<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>` +
12098
- (view.amendment === null ? "" : `<p class="recap contract-reason"><strong>why:</strong> ${escape(view.amendment)}</p>`) +
12321
+ (view.amendment === null ? "" : `<p class="recap contract-reason"><strong>Why:</strong> ${escape(view.amendment)}</p>`) +
12099
12322
  `<ul class="recap contract-changes">${view.changes.map(contractChangeHtml).join("")}</ul>` +
12100
12323
  stale +
12101
12324
  `</div>`);
12102
12325
  }
12103
12326
  if (view.changes.length === 0) {
12104
- 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>`;
12327
+ 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>`;
12105
12328
  }
12106
12329
  return (`<div class="contract-panel contract-amended"${mode === "full" ? ` id="contract-amendment"` : ""}><p class="approval-label">filed contract · amendment proposed</p>` +
12107
12330
  `<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>` +
12108
12331
  (view.amendment === null
12109
- ? `<p class="meta">the planner stated no reason for the amendment</p>`
12110
- : `<p class="recap contract-reason"><strong>why:</strong> ${escape(view.amendment)}</p>`) +
12332
+ ? `<p class="meta">The planner stated no reason for the amendment</p>`
12333
+ : `<p class="recap contract-reason"><strong>Why:</strong> ${escape(view.amendment)}</p>`) +
12111
12334
  `<ul class="recap contract-changes">${view.changes.map(contractChangeHtml).join("")}</ul>` +
12112
12335
  stale +
12113
12336
  `</div>`);
@@ -12136,7 +12359,7 @@ function approvalPathsHtml(touches) {
12136
12359
  function acceptanceCeremonyHtml(criteria) {
12137
12360
  if (criteria.length === 0)
12138
12361
  return "";
12139
- return (`<p class="meta">acceptance</p><ul class="recap acceptance-rubric">` +
12362
+ return (`<p class="meta">Acceptance</p><ul class="recap acceptance-rubric">` +
12140
12363
  criteria
12141
12364
  .map(c => `<li><code>${escape(c.id)}</code> ${escape(c.statement)} <span class="meta">[requires: ${c.evidence.map(escape).join(", ")}]</span></li>`)
12142
12365
  .join("") +
@@ -12254,10 +12477,20 @@ function approvalSheetHtml(input) {
12254
12477
  const planner = legOf("plan");
12255
12478
  if (planner !== undefined)
12256
12479
  who.push(`Planner ${agentNameWords(planner.provider, planner.model)}`);
12257
- if (scope.budgetMicrousd !== null)
12258
- who.push(`Cap ${money(scope.budgetMicrousd)} per attempt`);
12259
- if (permission === "Full access")
12260
- who.push("Full access");
12480
+ // What the yes allows, in plain words, right above it: the agent's
12481
+ // permissions, the per-attempt limit, and an earlier version still running.
12482
+ const allowing = [];
12483
+ const allowed = permissionPlainWordsOf(scope.profile);
12484
+ if (allowed !== null)
12485
+ allowing.push(allowed);
12486
+ if (raceTerms === null)
12487
+ allowing.push(scope.budgetMicrousd === null ? "no attempt limit" : `up to ${money(scope.budgetMicrousd)} per attempt`);
12488
+ const earlier = earlierVersionsWords(input.earlier.active, input.earlier.running);
12489
+ if (earlier !== null)
12490
+ allowing.push(earlier);
12491
+ const yoursToCheck = scope.acceptance.filter(one => one.evidence.includes("manual-review")).map(one => one.statement.trim().replace(/[.\s]+$/, ""));
12492
+ const consent = (allowing.length === 0 ? "" : `<p class="approval-allowing" data-approval-allowing>You’re allowing: ${allowing.map(escape).join(" · ")}</p>`) +
12493
+ (yoursToCheck.length === 0 ? "" : `<p class="approval-you-check" data-approval-you-check>You’ll check: ${yoursToCheck.map(escape).join("; ")}</p>`);
12261
12494
  const race = raceTerms === null
12262
12495
  ? ""
12263
12496
  : `<p class="approval-race">${raceTerms.n} agents build this separately: ${raceTerms.agents.map(one => escape(agentNameWords(one.provider, one.model))).join(" and ")}. ` +
@@ -12284,7 +12517,7 @@ function approvalSheetHtml(input) {
12284
12517
  : !contract.filed && contract.revision !== true ? `<p class="meta">Nothing was filed before planning, so every term is the planner's proposal.</p>`
12285
12518
  : `<p class="meta">${contract.revision === true ? "Same terms as the previous version." : "The plan keeps exactly what you filed."} <a href="/r/${contract.run}">Planning run ${contract.run}</a></p>`;
12286
12519
  const criteriaDetail = scope.acceptance.length === 0 ? ""
12287
- : `<ul class="approval-signed-criteria">${scope.acceptance.map(one => `<li><code>${escape(one.id)}</code> ${escape(one.statement)} <span class="meta">needs ${one.evidence.map(escape).join(", ")}</span></li>`).join("")}</ul>`;
12520
+ : `<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>`;
12288
12521
  const projection = route?.projection ?? null;
12289
12522
  const agentsDetail = route === null || route === undefined ? ""
12290
12523
  : projection === null
@@ -12298,7 +12531,7 @@ function approvalSheetHtml(input) {
12298
12531
  const limits = profileWords(scope) +
12299
12532
  `<p class="meta">Checks level: ${escape(qualityModeTitle(scope.qualityMode ?? "default"))}${permission === null ? "" : ` · ${escape(permission)}`}</p>` +
12300
12533
  (scope.budgetMicrousd === null ? "" : `<p class="meta">Each build attempt has a ${money(scope.budgetMicrousd)} agent-reported usage cap. On a subscription this limits work; it is not an API charge.</p>`);
12301
- const details = `<details class="approval-details"><summary>Details</summary>` +
12534
+ const details = `<details class="approval-details"><summary>Plan details</summary>` +
12302
12535
  revisionDetail +
12303
12536
  (repairText === null ? "" : detail("Signed goal", `<p>${escape(scope.goal)}</p>`)) +
12304
12537
  detail("Plan record", contractDetail) +
@@ -12308,6 +12541,53 @@ function approvalSheetHtml(input) {
12308
12541
  (scope.candidate ? detail("Saved commit", `<p><code class="approval-commit">${escape(scope.candidate)}</code></p>`) : "") +
12309
12542
  detail("Seal", `<p class="meta">Approval binds to this exact wording. <span class="seal mono">signs ${shortDigest(scope.digest)}</span></p>`) +
12310
12543
  `</details>`;
12544
+ const passwordNote = input.surface === "task" ? "approval-password-note" : "chat-approval-password-note";
12545
+ // Edit plan, in place (task page): the rows become fields and save
12546
+ // through the scope's own route. The fields belong to a form after this
12547
+ // one (forms never nest), so Approve never carries them.
12548
+ const edit = input.edit;
12549
+ const draft = edit?.draft ?? null;
12550
+ const editForm = "plan-editor-form";
12551
+ // Each field opens tall enough for what it holds (up to ten lines).
12552
+ 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>`}` +
12553
+ `<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>`;
12554
+ // Done when, one line per requirement in the signed order; the server
12555
+ // matches them back by position (the digest guard keeps that order).
12556
+ const requirement = (name, value, label, placeholder = "") => `<li><input type="text" name="${name}" value="${escape(value)}" aria-label="${label}"${placeholder === "" ? "" : ` placeholder="${placeholder}"`} form="${editForm}"></li>`;
12557
+ const requirements = draft?.getAll("requirement") ?? scope.acceptance.map(one => one.statement);
12558
+ const added = draft?.get("requirement-new") ?? "";
12559
+ const editor = edit === null ? null :
12560
+ `<details class="approval-edit" id="plan-editor"${draft === null && !edit.open ? "" : " open"}>` +
12561
+ `<summary class="approval-link"><span class="approval-edit-open">Edit plan</span><span class="approval-edit-close">Cancel</span></summary>` +
12562
+ `<div class="approval-editor">` +
12563
+ (edit.problem === null ? "" : `<p class="problem" role="alert">${escape(edit.problem)}</p>`) +
12564
+ field("goal", draft?.get("goal") ?? goalShown, 3, "Goal") +
12565
+ field("touches", draft?.get("touches") ?? scope.touches.join("\n"), 2, "Changes", "Only in these paths, one per line. Leave empty for any file.") +
12566
+ field("not", draft?.get("not") ?? scope.outOfScope ?? "", 2, "Won’t touch") +
12567
+ `<fieldset class="approval-field"><legend class="approval-field-label">Done when</legend>` +
12568
+ `<span class="approval-field-hint">Clear a line to drop it.</span><ul class="approval-requirements">` +
12569
+ requirements.map((one, index) => requirement("requirement", one, `Requirement ${index + 1}`)).join("") +
12570
+ requirement("requirement-new", added, "Add a requirement", "Add a requirement") +
12571
+ `</ul></fieldset>` +
12572
+ `<div class="approval-edit-act"><button type="submit" form="${editForm}">Save plan</button>` +
12573
+ // A plan with written steps keeps its own editor for them.
12574
+ (edit.stepsHref === null ? "" : `<a class="approval-link" href="${escape(edit.stepsHref)}">Edit steps</a>`) + `</div>` +
12575
+ `</div></details>`;
12576
+ const keptMode = permissionModeOfProfile(scope.profile);
12577
+ const editorForm = edit === null ? "" :
12578
+ `<form method="post" action="${edit.action}" id="${editForm}" class="approval-editor-form">` +
12579
+ `<input type="hidden" name="csrf" value="${escape(input.csrf)}">` +
12580
+ // A refused save keeps the version its draft was edited from, so a
12581
+ // plan that changed meanwhile is never silently overwritten.
12582
+ `<input type="hidden" name="sawDigest" value="${escape(draft?.get("sawDigest") ?? scope.digest)}">` +
12583
+ // The terms the editor doesn't show ride along unchanged: a repair's
12584
+ // brief after its goal (it stays in Details), permissions, checks, cap.
12585
+ (repairText === null ? "" : `<input type="hidden" name="goal-brief" value="${escape(repairText[0])}">`) +
12586
+ (keptMode === null ? "" : `<input type="hidden" name="permission-mode" value="${keptMode}">`) +
12587
+ `<input type="hidden" name="quality-mode" value="${escape(scope.qualityMode ?? "default")}">` +
12588
+ // The cap rides in exact millionths ("none" keeps no limit).
12589
+ `<input type="hidden" name="budget-microusd" value="${scope.budgetMicrousd === null ? "none" : String(scope.budgetMicrousd)}">` +
12590
+ `</form>`;
12311
12591
  return (`<form method="post" action="${input.action}" class="approve-form approval-sheet"${input.surface === "task" ? ` id="approve"` : ""}${input.sticky ? " data-sticky" : ""}>` +
12312
12592
  `<input type="hidden" name="csrf" value="${escape(input.csrf)}">` +
12313
12593
  `<input type="hidden" name="nonce" value="${escape(input.nonce)}">` +
@@ -12327,13 +12607,75 @@ function approvalSheetHtml(input) {
12327
12607
  approvalAmendmentHtml(contract) +
12328
12608
  race +
12329
12609
  (who.length === 0 ? "" : `<p class="approval-who">${who.map(escape).join(" · ")}</p>`) +
12610
+ consent +
12330
12611
  `<div class="approval-act" id="${input.surface === "task" ? "approval-confirm" : "chat-approval-confirm"}">` +
12331
- `<label class="approval-password"><span class="visually-hidden">Your password</span><input type="password" name="token" autocomplete="current-password" placeholder="Your password"></label>` +
12612
+ `<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>` +
12613
+ `<p class="approval-password-note" id="${passwordNote}">Your password signs this approval.</p>` +
12332
12614
  `<button type="submit" data-primary-action>${submit}</button></div>` +
12333
12615
  `<p class="approval-after">${after}</p>` +
12334
- `<p class="approval-secondary"><a class="approval-link" href="${escape(input.editHref)}">Edit plan</a><a class="approval-link" href="${escape(input.notNowHref)}">Not now</a></p>` +
12616
+ `<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>` +
12335
12617
  details +
12336
- `</form>`);
12618
+ `</form>` +
12619
+ editorForm);
12620
+ }
12621
+ /** Earlier versions of a task still in flight, in plain words: queued
12622
+ * while they only wait, running once one has started; null when none. */
12623
+ export function earlierVersionsWords(active, running) {
12624
+ if (active <= 0)
12625
+ return null;
12626
+ const still = running >= active ? "running" : running <= 0 ? "queued" : "queued or running";
12627
+ return active === 1 ? `an earlier version is still ${still}` : `${active} earlier versions are still ${still}`;
12628
+ }
12629
+ /** An in-place plan edit that would change the agent's permissions: thrown to roll the save back. */
12630
+ class PermissionsWouldChange extends Error {
12631
+ }
12632
+ /** Evidence kinds in plain words, for the signed criteria in Details. */
12633
+ const EVIDENCE_PLAIN = { check: "the project check", screenshot: "screenshots", "changed-path": "changed files", "manual-review": "your own check" };
12634
+ /** What an agent may do on its own, in a few plain words. */
12635
+ function permissionPlainWordsOf(profile) {
12636
+ if (profile === null || profile === undefined)
12637
+ return null;
12638
+ const full = "full access, nothing asks first";
12639
+ if (profile.provider === "claude") {
12640
+ return profile.permissionArgv === "bypassPermissions" ? full
12641
+ : profile.permissionArgv === "auto" ? "file edits and routine commands; anything risky stops"
12642
+ : "file edits only; commands are refused";
12643
+ }
12644
+ if (profile.provider === "gemini")
12645
+ return profile.approvalArgv === "yolo" ? full : "file edits only; other tools are refused";
12646
+ return profile.sandboxMode === "danger-full-access" ? full : "file edits and commands inside the project only";
12647
+ }
12648
+ /** The permission choice a sealed profile carries, as the scope form names it. */
12649
+ function permissionModeOfProfile(profile) {
12650
+ if (profile === null || profile === undefined)
12651
+ return null;
12652
+ return profile.provider === "claude"
12653
+ ? profile.permissionArgv === "bypassPermissions" ? "bypassPermissions" : "auto"
12654
+ : profile.provider === "gemini"
12655
+ ? profile.approvalArgv === "yolo" ? "bypassPermissions" : "auto"
12656
+ : profile.sandboxMode === "danger-full-access" ? "bypassPermissions" : "auto";
12657
+ }
12658
+ /** Done when, from the in-place editor: each line keeps its requirement's
12659
+ * id by position, and its evidence and guidance while its words stand; a
12660
+ * rewritten one is yours to check, like an added one; an emptied one is
12661
+ * dropped. */
12662
+ function requirementsFromEditor(body, current) {
12663
+ const used = new Set(current.map(one => one.id));
12664
+ let next = 1;
12665
+ while (used.has(`c${next}`))
12666
+ next++;
12667
+ const plain = (raw) => raw.replace(/\s+/g, " ").trim();
12668
+ const kept = body.getAll("requirement").slice(0, current.length).flatMap((raw, index) => {
12669
+ const statement = plain(raw);
12670
+ const criterion = current[index];
12671
+ if (statement === "")
12672
+ return [];
12673
+ return statement === plain(criterion.statement)
12674
+ ? [{ id: criterion.id, statement: criterion.statement, evidence: [...criterion.evidence], how: criterion.how }]
12675
+ : [{ id: criterion.id, statement, evidence: ["manual-review"], how: null }];
12676
+ });
12677
+ const added = plain(body.get("requirement-new") ?? "");
12678
+ return added === "" ? kept : [...kept, { id: `c${next}`, statement: added, evidence: ["manual-review"], how: null }];
12337
12679
  }
12338
12680
  function consentDoorOf(scope, route) {
12339
12681
  if (scope === null)
@@ -12386,31 +12728,31 @@ function consentClosedHtml(taskId, door, surface) {
12386
12728
  }
12387
12729
  function profileWords(scope) {
12388
12730
  if (scope.profileState === "unresolved") {
12389
- 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>`;
12731
+ 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>`;
12390
12732
  }
12391
12733
  const profile = scope.profile ?? null;
12392
12734
  if (profile === null) {
12393
12735
  return (scope.digestVersion ?? 1) < 2
12394
- ? `<p class="meta">approved before routing was bound — pinned at upgrade to the configuration of that day</p>`
12736
+ ? `<p class="meta">Approved before routing was bound — pinned at upgrade to the configuration of that day</p>`
12395
12737
  : "";
12396
12738
  }
12397
12739
  const repair = profile.repairModel === "inherit" ? "same model" : profile.repairModel;
12398
12740
  const base = profile.provider === "claude"
12399
- ? `<p class="meta">runs on <span class="mono">claude · ${escape(profile.model)}</span> — ${profile.permissionArgv === "bypassPermissions"
12741
+ ? `<p class="meta">Runs on <span class="mono">claude · ${escape(profile.model)}</span> — ${profile.permissionArgv === "bypassPermissions"
12400
12742
  ? "FULL permissions; claude runs with --dangerously-skip-permissions and nothing asks"
12401
12743
  : profile.permissionArgv === "auto"
12402
12744
  ? "safe unattended permissions; routine project commands and edits proceed, risky acts stop"
12403
- : "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>`
12745
+ : "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>`
12404
12746
  : profile.provider === "gemini"
12405
- ? `<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>`
12406
- : `<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>`;
12747
+ ? `<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>`
12748
+ : `<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>`;
12407
12749
  // The fallback chain rides EVERY surface these words sign (F+G review,
12408
12750
  // finding 2): the digest binds the whole chain, so the password form —
12409
12751
  // task page and /next alike — states every entry, credential included.
12410
12752
  const chain = chainFromJson(scope.proposedChainJson ?? null);
12411
12753
  const chainLine = chain === null || chain.length < 2
12412
12754
  ? ""
12413
- : `<p class="meta">if its subscription runs out: ${chain
12755
+ : `<p class="meta">If its subscription runs out: ${chain
12414
12756
  .slice(1)
12415
12757
  .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"}`)
12416
12758
  .join("; ")}</p>`;
@@ -12439,9 +12781,9 @@ function agentsSummaryWords(view) {
12439
12781
  /** Neutral metadata badges: risk, posture, and standing. */
12440
12782
  function agentsBadgesHtml(view) {
12441
12783
  const p = view.projection;
12442
- return (`<span class="badge">${escape(riskTitle(view.riskLevel))}</span>` +
12443
- (p === null ? "" : `<span class="badge">${escape(p.postureWords)}</span>`) +
12444
- `<span class="badge">${escape(agentsStandingWords(view))}</span>`);
12784
+ return (`<span class="badge">${escape(sentenceCase(riskTitle(view.riskLevel)))}</span>` +
12785
+ (p === null ? "" : `<span class="badge">${escape(sentenceCase(p.postureWords))}</span>`) +
12786
+ `<span class="badge">${escape(sentenceCase(agentsStandingWords(view)))}</span>`);
12445
12787
  }
12446
12788
  /** Volatile availability, per provider on the route — shown beside the
12447
12789
  * agents, never inside the approval's terms. */
@@ -12466,7 +12808,7 @@ function agentsWhyHtml(projection, summaryLabel = "Why these agents") {
12466
12808
  .map(leg => {
12467
12809
  const chosen = chosenWords(leg);
12468
12810
  return (`<dt>${escape(ROLE_NOUN[leg.phase])}</dt>` +
12469
- `<dd><span class="mono">${escape(leg.provider)} · ${escape(leg.model)}</span> <span class="badge">${escape(chosen)}</span>` +
12811
+ `<dd><span class="mono">${escape(leg.provider)} · ${escape(leg.model)}</span> <span class="badge">${escape(sentenceCase(chosen))}</span>` +
12470
12812
  `<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>`);
12471
12813
  })
12472
12814
  .join("") +
@@ -12487,7 +12829,7 @@ function agentsCeremonyHtml(view) {
12487
12829
  }
12488
12830
  return (`<div class="agents-ceremony"><p class="approval-label">agents</p>` +
12489
12831
  `<p class="agents-summary">${escape(view.projection.summary)}</p>` +
12490
- `<div class="agents-badges"><span class="badge">${escape(riskTitle(view.riskLevel))}</span><span class="badge">${escape(view.projection.postureWords)}</span></div>` +
12832
+ `<div class="agents-badges"><span class="badge">${escape(sentenceCase(riskTitle(view.riskLevel)))}</span><span class="badge">${escape(sentenceCase(view.projection.postureWords))}</span></div>` +
12491
12833
  `<p class="meta agents-risk-line">${escape(riskTitle(view.riskLevel))}: ${escape(riskConsequence(view.riskLevel))}.</p>` +
12492
12834
  agentsWhyHtml(view.projection) +
12493
12835
  `<p class="meta">These exact agents are part of what you approve; changing any of them asks for a fresh approval.</p></div>`);
@@ -12514,7 +12856,7 @@ function routineAgentsHtml(routine, approved, ceremony = false) {
12514
12856
  const projection = projectRoute(route, () => null);
12515
12857
  return (`<div class="agents-ceremony"><p class="approval-label">agents</p>` +
12516
12858
  `<p class="agents-summary">${escape(agentsSummary(route))}</p>` +
12517
- `<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>` +
12859
+ `<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>` +
12518
12860
  (ceremony ? agentsWhyHtml(projection) : "") +
12519
12861
  `<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>`);
12520
12862
  }
@@ -12544,7 +12886,7 @@ function agentsCardHtml(taskId, view, csrf, canEdit) {
12544
12886
  ? ""
12545
12887
  : `<ul class="agents-overrides">${view.overrides
12546
12888
  .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>` +
12547
- (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>` : "") +
12889
+ (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>` : "") +
12548
12890
  `</li>`)
12549
12891
  .join("")}</ul>`;
12550
12892
  // The controls (v48): a risk choice that says what each level does, and
@@ -12574,7 +12916,7 @@ function agentsCardHtml(taskId, view, csrf, canEdit) {
12574
12916
  ? `<p class="meta">${escape(view.editableWhy ?? "the agents cannot change right now")}</p>`
12575
12917
  : `<details class="agents-change"><summary>Change agents</summary>` +
12576
12918
  `<form method="post" action="${taskHref(taskId)}/route" class="agents-form-risk">${hidden}` +
12577
- `<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>` +
12919
+ `<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>` +
12578
12920
  `<button type="submit" class="secondary">Set risk</button></form>` +
12579
12921
  riskGuide +
12580
12922
  `<div class="agents-role-forms">${roleForms.join("")}</div>` +
@@ -13897,9 +14239,34 @@ ${THEME_DARK}
13897
14239
  .approval-act input[type=password] { margin: 0; font-family: var(--font-sans); }
13898
14240
  .approval-act button { margin: 0; min-height: 2.125rem; white-space: nowrap; }
13899
14241
  .approval-after { max-width: 75ch; color: var(--muted-foreground); font-size: 13px; line-height: 1.5; }
13900
- .approval-secondary { display: flex; flex-wrap: wrap; gap: 8px; }
14242
+ .approval-allowing, .approval-you-check { max-width: 75ch; font-size: 14px; line-height: 1.55; overflow-wrap: anywhere; }
14243
+ .approval-sheet .approval-allowing + .approval-you-check { margin-top: -10px; }
14244
+ .approval-act > button { grid-column: 2; grid-row: 1; }
14245
+ .approval-password-note { grid-column: 1; grid-row: 2; margin: 0; color: var(--muted-foreground); font-size: 13px; line-height: 1.5; }
14246
+ .approval-secondary { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
14247
+ details.approval-edit, details.approval-edit[open] { margin: 0; padding: 0; border: 0; border-radius: 0; background: none; }
14248
+ /* Edit plan, in place: the rows become fields where they stood; the
14249
+ approval act and Details step aside until it is saved or cancelled. */
14250
+ .approval-sheet { position: relative; }
14251
+ .approval-edit > summary { list-style: none; cursor: pointer; }
14252
+ .approval-edit > summary::-webkit-details-marker { display: none; }
14253
+ .approval-edit-close, .approval-edit[open] .approval-edit-open { display: none; }
14254
+ .approval-edit[open] .approval-edit-close { display: inline; }
14255
+ .approval-edit[open] > summary { position: absolute; top: 14px; right: 20px; }
14256
+ .approval-sheet:has(.approval-edit[open]) > :not(.approval-sheet-title, .approval-secondary) { display: none; }
14257
+ .approval-secondary:has(> .approval-edit[open]) { display: block; }
14258
+ .approval-secondary:has(> .approval-edit[open]) > a { display: none; }
14259
+ .approval-editor { display: grid; gap: 14px; }
14260
+ .approval-editor .problem { margin: 0; }
14261
+ .approval-field { display: grid; gap: 4px; min-width: 0; margin: 0; padding: 0; border: 0; }
14262
+ .approval-field-label { padding: 0; color: var(--muted-foreground); font-size: 13px; font-weight: 500; line-height: 1.6; }
14263
+ .approval-field-hint { color: var(--muted-foreground); font-size: 12px; line-height: 1.5; }
14264
+ .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; }
14265
+ .approval-requirements { display: grid; gap: 6px; margin: 2px 0 0; padding: 0; list-style: none; }
14266
+ .approval-edit-act { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
14267
+ .approval-edit-act button { margin: 0; min-height: 2.125rem; white-space: nowrap; }
13901
14268
  .approval-link { display: inline-flex; align-items: center; min-height: 32px; padding: 0 12px; border: 1px solid var(--border); border-radius: 8px; background: var(--card); color: var(--foreground); font-size: 13px; font-weight: 500; text-decoration: none; }
13902
- .approval-link + .approval-link { border-color: transparent; background: none; color: var(--muted-foreground); }
14269
+ .approval-secondary > .approval-link:not(:first-child) { border-color: transparent; background: none; color: var(--muted-foreground); }
13903
14270
  @media (hover: hover) and (pointer: fine) { .approval-link:hover { background: var(--muted); color: var(--foreground); } }
13904
14271
  details.approval-details { margin: 0; padding: 0; border: 0; border-top: 1px solid var(--border); border-radius: 0; background: none; }
13905
14272
  details.approval-details[open] { padding-bottom: 0; }
@@ -13919,6 +14286,7 @@ ${THEME_DARK}
13919
14286
  .approval-sheet { gap: 12px; padding: 14px; }
13920
14287
  .approval-row { grid-template-columns: 1fr; gap: 2px; }
13921
14288
  .approval-act { grid-template-columns: 1fr; }
14289
+ .approval-act > button, .approval-password-note { grid-column: auto; grid-row: auto; }
13922
14290
  .approval-act input[type=password], .approval-act button { min-height: 44px; font-size: 16px; }
13923
14291
  .approval-act button { width: 100%; }
13924
14292
  /* The one act stays under the thumb: sticky at the bottom, clear of the home indicator. */
@@ -13927,6 +14295,9 @@ ${THEME_DARK}
13927
14295
  border-top: 1px solid var(--border); background: var(--card);
13928
14296
  }
13929
14297
  .approval-link { min-height: 44px; padding: 0 16px; }
14298
+ .approval-edit[open] > summary { top: 6px; right: 6px; }
14299
+ .approval-field textarea, .approval-field input[type=text] { font-size: 16px; }
14300
+ .approval-edit-act button { width: 100%; min-height: 44px; font-size: 16px; }
13930
14301
  .approval-details > summary { min-height: 44px; }
13931
14302
  .approval-roles > div { grid-template-columns: 1fr; gap: 2px; }
13932
14303
  }
@@ -15442,7 +15813,7 @@ const INBOX_TABS_CSS = '.inbox-tabs{display:inline-flex;gap:2px;max-width:100%;o
15442
15813
  '.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}' +
15443
15814
  '.inbox-unread{display:none;position:absolute;top:4px;right:3px;width:6px;height:6px;border-radius:50%;background:var(--so-signal)}' +
15444
15815
  '@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}}';
15445
- const WORKSPACE_STYLE = styleAsset(STYLE + INBOX_TABS_CSS + APPROVAL_RULES_CSS + SPEND_CSS + RETENTION_CSS + STORAGE_CSS + UPDATES_CSS + LIMITS_CSS + MONITORING_CSS + INTEGRATIONS_CSS + BACKUP_CSS + EXPORT_CSS + PROJECT_DELETE_CSS + POLICY_CSS + EVIDENCE_PACK_CSS + THEME_CONTROLS_CSS + CODING_CSS + CODING_SHIPPING_CSS + RECIPE_CSS + SKILLS_CSS + TOOLS_CSS + FLOWS_CSS + TEAMMATE_CSS + KITS_CSS + STARTERS_CSS + GALLERY_CSS + SSO_CSS + CREDENTIALS_CSS + KNOWLEDGE_CSS + MODELS_CSS + CHAT_POLISH_CSS + TRANSITIONS_CSS + WORKSPACE_MOTION_CSS + ASSIGNMENT_CSS + TASK_STATUS_CSS + LEAD_CONTEXT_CSS + PULL_REQUEST_SETTINGS_CSS + CHECK_SETTINGS_CSS + '.learning{min-width:0;overflow-wrap:anywhere}.learning .card{min-width:0}.learning code,.learning blockquote,.learning pre{white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word}.learning button,.learning summary,.learning .button-link{min-height:44px}.learning button{white-space:nowrap}.learning summary{padding:12px 0;cursor:pointer}.learning form{margin:12px 0}.learning select{max-width:100%}.learning blockquote{margin:8px 0}.learning ul{padding-left:20px}');
15816
+ 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}');
15446
15817
  /**
15447
15818
  * The board's liveness: fetch this page's own fragment on a timer and swap
15448
15819
  * it in place — no flicker, no scroll reset, no long-lived stream to manage,
@@ -15697,7 +16068,7 @@ FINISH: unreviewed and undocumented is unfinished; this build ends with the fini
15697
16068
  * absent from sensitive pages. Navigation help in plain words — no key
15698
16069
  * ever posts. */
15699
16070
  const KBD_HELP = `<div class="kbd-help" hidden role="dialog" aria-label="keyboard shortcuts" tabindex="-1">` +
15700
- `<h2>keyboard shortcuts</h2><table>` +
16071
+ `<h2>Keyboard shortcuts</h2><table>` +
15701
16072
  `<tr><td><kbd>/</kbd></td><td>jump to a page or an open task</td></tr>` +
15702
16073
  `<tr><td><kbd>g</kbd> then <kbd>i</kbd></td><td>go to the inbox</td></tr>` +
15703
16074
  `<tr><td><kbd>g</kbd> then <kbd>b</kbd></td><td>go to the board</td></tr>` +
@@ -15772,7 +16143,7 @@ function shell(title, body, options = {}) {
15772
16143
  : `<span class="scope-status">` +
15773
16144
  (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>`) +
15774
16145
  `<a href="/runs">${peekCounts.running} live</a><a href="/board?view=order">${peekCounts.queued} queued</a></span>`;
15775
- const scopeName = effectiveScope === "project" && chrome.project !== null ? escape(projectName(chrome.project)) : "all projects";
16146
+ const scopeName = effectiveScope === "project" && chrome.project !== null ? escape(projectName(chrome.project)) : "All projects";
15776
16147
  // The switcher (board pass): the scope's name opens a menu of every
15777
16148
  // enrolled project — one tap to open one, or to widen to all — as plain
15778
16149
  // POST forms carrying the session's csrf, returning to this screen.
@@ -15787,7 +16158,7 @@ function shell(title, body, options = {}) {
15787
16158
  const allCurrent = effectiveScope !== "project";
15788
16159
  const rows = [
15789
16160
  `<form method="post" action="/projects/select">${hidden}<input type="hidden" name="path" value="">` +
15790
- `<button type="submit"${allCurrent ? ' class="current" aria-current="true"' : ""}>all projects</button></form>`,
16161
+ `<button type="submit"${allCurrent ? ' class="current" aria-current="true"' : ""}>All projects</button></form>`,
15791
16162
  ...(chrome.projects ?? []).map(one => `<form method="post" action="/projects/open">${hidden}<input type="hidden" name="path" value="${escape(one.path)}">` +
15792
16163
  `<button type="submit"${!allCurrent && chrome.project === one.path ? ' class="current" aria-current="true"' : ""}>${escape(one.name)}</button></form>`),
15793
16164
  ];
@@ -15831,10 +16202,10 @@ function shell(title, body, options = {}) {
15831
16202
  primaryItem("flows", "/flows", "Flows"),
15832
16203
  primaryItem("projects", "/projects", "Projects"),
15833
16204
  `</nav>`,
15834
- ...(chrome.active === "code" ? [] : [`<a class="new-task" href="/tasks/new" aria-label="new task">+ new task</a>`]),
16205
+ ...(chrome.active === "code" ? [] : [`<a class="new-task" href="/tasks/new" aria-label="New task">+ New task</a>`]),
15835
16206
  `<nav class="nav-groups">`,
15836
- navGroup("tools", "work tools", workToolRows(chrome.projectScoped, chrome.chat, chrome.code), TOOL_KEYS.has(chrome.active)),
15837
- navGroup("settings", "settings", settingsRows(chrome.projectScoped, chrome.settings), SETTINGS_KEYS.has(chrome.active)),
16207
+ navGroup("tools", "Work tools", workToolRows(chrome.projectScoped, chrome.chat, chrome.code), TOOL_KEYS.has(chrome.active)),
16208
+ navGroup("settings", "Settings", settingsRows(chrome.projectScoped, chrome.settings), SETTINGS_KEYS.has(chrome.active)),
15838
16209
  `</nav>`,
15839
16210
  `<span class="grow"></span>`,
15840
16211
  `</aside>`,
@@ -15843,11 +16214,11 @@ function shell(title, body, options = {}) {
15843
16214
  // must not pass as production (adoption review, finding 8; the FENCE is
15844
16215
  // the refuseDemo gate in operate.ts, this is the honest label).
15845
16216
  const demoBanner = (chrome.demo === true
15846
- ? `<div class="banner"><span class="badge">demo</span>${escape(DEMO_BANNER.replace(/^Demo: /, ""))}</div>`
16217
+ ? `<div class="banner"><span class="badge">Demo</span>${escape(DEMO_BANNER.replace(/^Demo: /, ""))}</div>`
15847
16218
  : "") +
15848
16219
  (chrome.modeBanner === undefined
15849
16220
  ? ""
15850
- : `<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>`) +
16221
+ : `<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>`) +
15851
16222
  (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)}`}` +
15852
16223
  `<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("") +
15853
16224
  (chrome.updateWaiting === undefined
@@ -16069,7 +16440,7 @@ function inboxPage(chrome, data) {
16069
16440
  /** The row's project, worn openly in the roll-up — null is UNPLACED,
16070
16441
  * said as such, never a silent missing chip (finding 13). */
16071
16442
  const chip = (repo) => !data.rollup ? "" : repo === null || repo === undefined
16072
- ? ` <span class="badge">unplaced</span>`
16443
+ ? ` <span class="badge">Unplaced</span>`
16073
16444
  : ` <span class="badge">${escape(projectName(repo))}</span>`;
16074
16445
  const empty = data.decisions.length + data.approvals.length + data.requeueables.length +
16075
16446
  data.cancelledBlockers.length + data.gaps.length + data.needsVerification.length === 0;
@@ -16078,19 +16449,19 @@ function inboxPage(chrome, data) {
16078
16449
  // the card itself (portfolio arc §2).
16079
16450
  const decisions = data.decisions.length === 0
16080
16451
  ? ""
16081
- : `<h2>answer a question</h2><p class="hint">an agent stopped mid-build to ask — nothing proceeds until you answer</p>` +
16452
+ : `<h2>Answer a question</h2><p class="hint">an agent stopped mid-build to ask — nothing proceeds until you answer</p>` +
16082
16453
  data.decisions
16083
16454
  .map(decision => data.interactive && !data.rollup
16084
16455
  ? decisionAnswerCard(decision, data.csrf, data.now, false)
16085
16456
  : `<a class="decide-card" href="/d/${decision.id}">` +
16086
16457
  `<p class="q">${escape(decision.question)}</p>` +
16087
16458
  `<span class="mono meta">${escape(decision.taskId)}</span>${chip(decision.repo)}` +
16088
- `${isOverdue(decision, data.now) ? ` <span class="badge badge-overdue">overdue</span>` : ""}` +
16459
+ `${isOverdue(decision, data.now) ? ` <span class="badge badge-overdue">Overdue</span>` : ""}` +
16089
16460
  `</a>`)
16090
16461
  .join("\n");
16091
16462
  const approvals = data.approvals.length === 0
16092
16463
  ? ""
16093
- : `<h2>approve a scope</h2><p class="hint">scopes waiting for your approval — each binds to the exact wording you sign</p>` +
16464
+ : `<h2>Approve a scope</h2><p class="hint">scopes waiting for your approval — each binds to the exact wording you sign</p>` +
16094
16465
  data.approvals
16095
16466
  .map(one => `<a class="decide-card" href="${taskHref(one.taskId)}">` +
16096
16467
  `<p class="q">${escape(one.title)}</p>` +
@@ -16100,7 +16471,7 @@ function inboxPage(chrome, data) {
16100
16471
  .join("\n");
16101
16472
  const requeueables = data.requeueables.length === 0
16102
16473
  ? ""
16103
- : `<h2>retry stalled work</h2><p class="hint">failed builds waiting for a person — retry clears the incidents and requeues</p>` +
16474
+ : `<h2>Retry stalled work</h2><p class="hint">failed builds waiting for a person — retry clears the incidents and requeues</p>` +
16104
16475
  data.requeueables
16105
16476
  .map(one => `<p class="row"><a href="${taskHref(one.taskId)}">${escape(one.taskId)}</a> ${escape(one.title)}${chip(one.repo)}` +
16106
16477
  `${one.incidentCount > 0 ? ` <span class="badge badge-failed">${one.incidentCount} incident${one.incidentCount > 1 ? "s" : ""}</span>` : ""}` +
@@ -16110,11 +16481,11 @@ function inboxPage(chrome, data) {
16110
16481
  : `<span class="right"><form method="post" action="${taskHref(one.taskId)}/requeue" class="inline">` +
16111
16482
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
16112
16483
  `<input type="hidden" name="return" value="inbox">` +
16113
- `<button type="submit">retry</button></form></span></p>`))
16484
+ `<button type="submit">Retry</button></form></span></p>`))
16114
16485
  .join("\n");
16115
16486
  const needsVerification = data.needsVerification.length === 0
16116
16487
  ? ""
16117
- : `<h2>needs review</h2><p class="hint">finished, but the evidence needs your attention before accepting</p>` +
16488
+ : `<h2>Needs review</h2><p class="hint">finished, but the evidence needs your attention before accepting</p>` +
16118
16489
  data.needsVerification
16119
16490
  .map(one => `<p class="row"><a href="${taskHref(one.taskId)}">${escape(one.taskId)}</a> ${escape(one.title)}${chip(one.repo)}` +
16120
16491
  ` <span class="badge badge-failed">${one.verdict === "refuted" ? "conflicting evidence" : "missing evidence"}</span>${criterionMatrixSummary(one.matrix ?? [])}` +
@@ -16123,7 +16494,7 @@ function inboxPage(chrome, data) {
16123
16494
  .join("\n");
16124
16495
  const cancelled = data.cancelledBlockers.length === 0
16125
16496
  ? ""
16126
- : `<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>` +
16497
+ : `<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>` +
16127
16498
  data.cancelledBlockers
16128
16499
  .map(one => `<p class="row"><a href="${taskHref(one.exampleDependent)}">${escape(one.exampleDependent)}</a>${chip(one.repo)} ` +
16129
16500
  `<span class="meta">${one.dependentCount > 1 ? `one of ${one.dependentCount} tasks waiting` : "waiting"} for cancelled task ${escape(one.blockerId)}` +
@@ -16131,7 +16502,7 @@ function inboxPage(chrome, data) {
16131
16502
  .join("\n");
16132
16503
  const gaps = data.gaps.length === 0
16133
16504
  ? ""
16134
- : `<h2>supply a requirement</h2><p class="hint">approved work is ready except for these — fill one and its tasks start</p>` +
16505
+ : `<h2>Supply a requirement</h2><p class="hint">approved work is ready except for these — fill one and its tasks start</p>` +
16135
16506
  data.gaps
16136
16507
  .map(gap => `<p class="row"><a href="/caps">${escape(gap.key)}</a> ` +
16137
16508
  `<span class="meta">frees ${gap.unblocks.length} task${gap.unblocks.length > 1 ? "s" : ""}</span>` +
@@ -16163,8 +16534,8 @@ function inboxPage(chrome, data) {
16163
16534
  const tab = data.tab ?? "all";
16164
16535
  const shows = (one) => tab === "all" || tab === one;
16165
16536
  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");
16166
- const ready = (data.ready ?? []).length === 0 ? "" : `<h2>ready to review</h2>` + listRows(data.ready ?? []);
16167
- const running = (data.running ?? []).length === 0 ? "" : `<h2>running now</h2>` + listRows(data.running ?? []);
16537
+ const ready = (data.ready ?? []).length === 0 ? "" : `<h2>Ready to review</h2>` + listRows(data.ready ?? []);
16538
+ const running = (data.running ?? []).length === 0 ? "" : `<h2>Running now</h2>` + listRows(data.running ?? []);
16168
16539
  const asks = { decide: data.decisions.length + data.approvals.length, unblock: data.requeueables.length + data.cancelledBlockers.length + data.gaps.length };
16169
16540
  const counts = { ...asks,
16170
16541
  "needs-you": asks.decide + asks.unblock,
@@ -16183,7 +16554,7 @@ function inboxPage(chrome, data) {
16183
16554
  ? `<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>` : "";
16184
16555
  return screen("inbox", [
16185
16556
  `<h1>Inbox</h1>`,
16186
- `<p class="meta">everything that waits on you \u2014 empty means the fleet is working</p>`,
16557
+ `<p class="meta">Everything that waits on you \u2014 empty means the fleet is working</p>`,
16187
16558
  tabs,
16188
16559
  noWorker,
16189
16560
  wizard,
@@ -16195,21 +16566,21 @@ function inboxPage(chrome, data) {
16195
16566
  shows("needs-you") ? askGroup("unblock", counts.unblock, [requeueables, cancelled, gaps]) : "",
16196
16567
  shows("running") ? running : "",
16197
16568
  data.rollup
16198
- ? `<p class="meta">requirement gaps are checked one project at a time \u2014 open a project to see and fill its gaps · <a href="/projects">open a project</a></p>`
16569
+ ? `<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>`
16199
16570
  : "",
16200
16571
  // Quick capture: the shortest path from "I want this done" to the
16201
16572
  // approve card — title and goal here, the yes on the next screen. The
16202
16573
  // one-shot form posts to the same guarded handler as the full page.
16203
- data.rollup ? "" : `<h2>capture new work</h2>`,
16574
+ data.rollup ? "" : `<h2>Capture new work</h2>`,
16204
16575
  data.rollup ? "" : `<form method="post" action="/tasks/add" class="card">`,
16205
16576
  ...(data.rollup
16206
16577
  ? []
16207
16578
  : [
16208
16579
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
16209
16580
  `<input type="hidden" name="projectRevision" value="${data.revision}">`,
16210
- `<label>what should get done<input type="text" name="title" placeholder="task title" maxlength="200"></label>`,
16211
- `<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>`,
16212
- `<button type="submit">queue it \u2192 approve its scope next</button>`,
16581
+ `<label>What should get done<input type="text" name="title" placeholder="task title" maxlength="200"></label>`,
16582
+ `<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>`,
16583
+ `<button type="submit">Queue it \u2192 approve its scope next</button>`,
16213
16584
  `</form>`,
16214
16585
  ]),
16215
16586
  ].join("\n"), {
@@ -16258,7 +16629,7 @@ function systemPage(chrome, data) {
16258
16629
  const agentLines = data.agents
16259
16630
  .map(one => {
16260
16631
  if (one.problem !== undefined) {
16261
- 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>`;
16632
+ 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>`;
16262
16633
  }
16263
16634
  const who = one.source === "project"
16264
16635
  ? `chosen for this project${one.setBy === null ? "" : ` by ${escape(one.setBy)}`}`
@@ -16273,10 +16644,10 @@ function systemPage(chrome, data) {
16273
16644
  `<p class="meta" style="margin-top:0">${PHASE_SAID[one.phase] ?? ""}</p>`);
16274
16645
  })
16275
16646
  .join("\n");
16276
- const agentsCard = `<h2>agents</h2>` +
16277
- `<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>` +
16647
+ const agentsCard = `<h2>Agents</h2>` +
16648
+ `<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>` +
16278
16649
  `<div class="card">${agentLines}` +
16279
- `<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>` +
16650
+ `<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>` +
16280
16651
  `</div>`;
16281
16652
  return screen("system", [
16282
16653
  `<h1>System</h1>`,
@@ -16285,16 +16656,16 @@ function systemPage(chrome, data) {
16285
16656
  cards.length === 0
16286
16657
  ? `<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>`
16287
16658
  : `<div class="cards">${cards.join("")}</div>`,
16288
- data.outboxPending > 0 ? `<p class="meta">notifications: ${data.outboxPending} pending delivery</p>` : "",
16659
+ data.outboxPending > 0 ? `<p class="meta">Notifications: ${data.outboxPending} pending delivery</p>` : "",
16289
16660
  (data.externalWork ?? []).length === 0
16290
16661
  ? ""
16291
- : `<h2>external work</h2>` +
16662
+ : `<h2>External work</h2>` +
16292
16663
  (data.externalWork ?? [])
16293
16664
  .map(one => `<p class="row"><span class="mono">${escape(one.remoteRepo)}</span> ` +
16294
16665
  (one.blocked !== null
16295
- ? `<span class="badge badge-failed">dispatch blocked</span> <span class="meta">the tracker connection needs repair — \`toolroll sync\` says why</span>`
16666
+ ? `<span class="badge badge-failed">Dispatch blocked</span> <span class="meta">the tracker connection needs repair — \`toolroll sync\` says why</span>`
16296
16667
  : one.openEpisode !== null
16297
- ? `<span class="badge badge-failed">sync failing</span> <span class="meta">${escape(one.openEpisode)}</span>`
16668
+ ? `<span class="badge badge-failed">Sync failing</span> <span class="meta">${escape(one.openEpisode)}</span>`
16298
16669
  : `<span class="meta">syncing normally</span>`) +
16299
16670
  `</p>`)
16300
16671
  .join("\n"),
@@ -16397,7 +16768,7 @@ function boardBody(data, ciRed) {
16397
16768
  queuedHeads.get(`${card.assignedRunner ?? ""}|${card.repo ?? ""}`) === card.taskId
16398
16769
  ? `<span class="badge">${card.assignedRunner === null ? "next up" : `next for ${escape(card.assignedRunner)}`}</span>`
16399
16770
  : "",
16400
- card.assignedRunner === null ? "" : `<span class="badge">reserved</span>`,
16771
+ card.assignedRunner === null ? "" : `<span class="badge">Reserved</span>`,
16401
16772
  card.routineName === null ? "" : `<span class="badge">${escape(card.routineName)}</span>`,
16402
16773
  chip(card.repo).trim(),
16403
16774
  ]) +
@@ -16425,7 +16796,7 @@ function boardBody(data, ciRed) {
16425
16796
  ...(claim.branch === null ? [] : [["branch", `${claim.branch}${workspace === null ? "" : ` · ${workspace}`}`]]),
16426
16797
  ]) +
16427
16798
  chips([
16428
- card.attempt === null ? "" : `<span class="badge">attempt ${card.attempt}</span>`,
16799
+ card.attempt === null ? "" : `<span class="badge">Attempt ${card.attempt}</span>`,
16429
16800
  chip(card.repo).trim(),
16430
16801
  ]) +
16431
16802
  `</a>`);
@@ -16447,7 +16818,7 @@ function boardBody(data, ciRed) {
16447
16818
  ["usage", runCostWords({ authMode: row.authMode, costUsd: row.costUsd, tokensIn: null, tokensOut: null })],
16448
16819
  ]) +
16449
16820
  chips([
16450
- `<span class="badge badge-done">${row.outcome === "no-change" ? "no change" : "built"}</span>`,
16821
+ `<span class="badge badge-done">${row.outcome === "no-change" ? "No change" : "Built"}</span>`,
16451
16822
  pr.trim(),
16452
16823
  data.all && row.repo !== null ? `<span class="badge">${escape(projectName(row.repo))}</span>` : "",
16453
16824
  ]) +
@@ -16459,7 +16830,7 @@ function boardBody(data, ciRed) {
16459
16830
  : `<p class="meta board-scope">` +
16460
16831
  (data.all
16461
16832
  ? (data.project === null
16462
- ? `<strong>all projects</strong>`
16833
+ ? `<strong>All projects</strong>`
16463
16834
  : `<a href="/board">${escape(projectName(data.project))}</a> \u00b7 <strong>all projects</strong>`) +
16464
16835
  ` \u2014 every project this server serves, each card wearing its project`
16465
16836
  : `<strong>${escape(projectName(data.project))}</strong> \u00b7 <a href="/board?scope=all">all projects</a>`) +
@@ -16476,7 +16847,7 @@ function boardBody(data, ciRed) {
16476
16847
  const ago = (minutes) => minutes < 60 ? `${minutes}m` : minutes < 48 * 60 ? `${Math.round(minutes / 60)}h` : `${Math.round(minutes / (24 * 60))}d`;
16477
16848
  const deltaLine = data.delta === null
16478
16849
  ? ""
16479
- : `<p class="meta"><strong>since you last looked</strong> (${ago(data.delta.agoMinutes)} ago): ` +
16850
+ : `<p class="meta"><strong>Since you last looked</strong> (${ago(data.delta.agoMinutes)} ago): ` +
16480
16851
  [
16481
16852
  data.delta.built > 0 ? `<span class="good">${data.delta.built} built</span>` : "",
16482
16853
  data.delta.failed > 0 ? `<span class="bad">${data.delta.failed} failed</span>` : "",
@@ -16487,7 +16858,7 @@ function boardBody(data, ciRed) {
16487
16858
  `</p>`;
16488
16859
  return [
16489
16860
  `<h1>Board</h1>`,
16490
- 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>`,
16861
+ 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>`,
16491
16862
  deltaLine,
16492
16863
  toggle,
16493
16864
  `<div class="board">`,
@@ -16509,14 +16880,14 @@ function donePage(chrome, rows, ciRed) {
16509
16880
  const pr = row.prNumber === null
16510
16881
  ? row.publicationState === null
16511
16882
  ? ""
16512
- : ` <span class="badge">${escape(row.publicationState)}</span>`
16883
+ : ` <span class="badge">${escape(sentenceCase(row.publicationState))}</span>`
16513
16884
  : ` <a href="${escape(row.prUrl ?? "#")}" class="badge badge-open">PR #${row.prNumber}</a>` +
16514
16885
  (ciRed(row.prNumber) ? ` <span class="badge badge-failed">CI failing</span>` : "");
16515
16886
  const needsVerification = (row.proofVerdict === "short" || row.proofVerdict === "refuted") && !row.proofAccepted
16516
16887
  ? ` <span class="badge badge-failed">${row.proofVerdict === "refuted" ? "conflicting evidence" : "missing evidence"}</span>`
16517
16888
  : "";
16518
16889
  return (`<div class="card"><p><a href="${taskHref(row.taskId)}"><strong>${escape(row.title)}</strong></a>` +
16519
- `${row.outcome === "no-change" ? ` <span class="badge">no change needed</span>` : ""}${pr}${needsVerification}${criterionMatrixSummary(row.proofMatrix)}</p>` +
16890
+ `${row.outcome === "no-change" ? ` <span class="badge">No change needed</span>` : ""}${pr}${needsVerification}${criterionMatrixSummary(row.proofMatrix)}</p>` +
16520
16891
  `${row.handoff === null ? "" : `<p class="meta">${escape(row.handoff.length > 200 ? row.handoff.slice(0, 200) + "\u2026" : row.handoff)}</p>`}` +
16521
16892
  `<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 }))}`}` +
16522
16893
  ` \u00b7 <a href="${reviewHref(row.taskId)}">review \u2192</a></p></div>`);
@@ -16582,9 +16953,9 @@ function trackRow(track, all) {
16582
16953
  `<p>${trackStrip(fires)}` +
16583
16954
  `<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>` +
16584
16955
  (blocker !== null
16585
- ? `<p class="meta">stopped behind <a href="${taskHref(blocker.taskId)}" class="mono">${escape(blocker.taskId)}</a> (${escape(blocker.state)})</p>`
16956
+ ? `<p class="meta">Stopped behind <a href="${taskHref(blocker.taskId)}" class="mono">${escape(blocker.taskId)}</a> (${escape(blocker.state)})</p>`
16586
16957
  : latest?.instanceTaskId !== undefined && latest.instanceTaskId !== null
16587
- ? `<p class="meta">latest: <a href="${taskHref(latest.instanceTaskId)}" class="mono">${escape(latest.instanceTaskId)}</a>${latest.instanceState === null ? "" : ` (${escape(latest.instanceState)})`}</p>`
16958
+ ? `<p class="meta">Latest: <a href="${taskHref(latest.instanceTaskId)}" class="mono">${escape(latest.instanceTaskId)}</a>${latest.instanceState === null ? "" : ` (${escape(latest.instanceState)})`}</p>`
16588
16959
  : "") +
16589
16960
  `</div>`);
16590
16961
  }
@@ -16701,12 +17072,13 @@ function checkProgressHtml(progress, id = "check-progress") {
16701
17072
  : Object.values(progress.suites).every(one => one.state === "passed") ? "passed" : "unknown";
16702
17073
  return `<p id="${id}" class="check-progress mono" data-check-progress data-final="${escape(state)}" role="status" aria-live="polite">${escape(progress.line)}</p>`;
16703
17074
  }
16704
- function taskControlDetailsHtml(control, taskId, csrf, surface, inert = false) {
16705
- const html = taskControlHtml(control, taskId, csrf, surface, inert);
17075
+ function taskControlDetailsHtml(control, taskId, csrf, surface, inert = false, stopElsewhere = false) {
17076
+ const html = taskControlHtml(control, taskId, csrf, surface, inert, stopElsewhere);
16706
17077
  return control.kind === "paused" || control.kind === "review-stopped" || control.kind === "stopping"
16707
17078
  ? `<details id="task-control-details"><summary>${control.kind === "paused" ? "Preserved work and resume" : "Stop details"}</summary>${html}</details>` : html;
16708
17079
  }
16709
- function taskControlHtml(control, taskId, csrf, surface, inert = false) {
17080
+ /** `stopElsewhere`: the Building card already carries this exact run's Stop, so the control card keeps only its details. */
17081
+ function taskControlHtml(control, taskId, csrf, surface, inert = false, stopElsewhere = false) {
16710
17082
  if (control.kind === "none")
16711
17083
  return "";
16712
17084
  const back = `<input type="hidden" name="return" value="${surface}">`;
@@ -16720,7 +17092,7 @@ function taskControlHtml(control, taskId, csrf, surface, inert = false) {
16720
17092
  if (control.kind === "stop") {
16721
17093
  return (`<section class="card task-control" id="task-control" data-task-control="stop" data-control-run="${control.run}" aria-label="stop this attempt">` +
16722
17094
  `<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>` +
16723
- (guarded
17095
+ (stopElsewhere ? "" : guarded
16724
17096
  ? `<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>`
16725
17097
  : `<button type="button" class="danger task-control-button" disabled>Stop</button>`) +
16726
17098
  `</section>`);
@@ -16760,7 +17132,7 @@ function resumeCeremonyPage(chrome, data) {
16760
17132
  `<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>`,
16761
17133
  `<p class="row">Resuming grants no new approval or publishing permission. Scope, budget, agents, risk, fallback, verification, and review limits still apply.</p>`,
16762
17134
  `<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>`,
16763
- 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>`,
17135
+ 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>`,
16764
17136
  data.gate === null ? "" : `<p class="row"><strong>Before work starts:</strong> ${escape(data.gate.summary)} — ${escape(data.gate.detail)}</p>`,
16765
17137
  `</div>`,
16766
17138
  `<form method="post" action="${taskHref(data.taskId)}/resume" class="card resume-form">`,
@@ -16836,12 +17208,23 @@ function taskChatApproval(focus, csrf) {
16836
17208
  route: focus.route,
16837
17209
  coordinator: approval.coordinator,
16838
17210
  deliverable: approval.deliverable,
16839
- editHref: `${taskHref(focus.id)}#${approval.planDocument === null ? "scope" : "plan-edit"}`,
17211
+ // Edit plan opens the task page's sheet with its fields open for editing.
17212
+ editHref: approval.raceTerms !== null ? `${taskHref(focus.id)}#${approval.planDocument === null ? "scope" : "plan-edit"}` : `${taskHref(focus.id)}?edit=plan#plan-editor`,
16840
17213
  notNowHref: "/chat",
16841
17214
  sticky: false,
17215
+ earlier: { active: focus.assignment?.earlierActive ?? 0, running: focus.assignment?.earlierRunning ?? 0 },
17216
+ edit: null,
16842
17217
  }) +
16843
17218
  `</section>`);
16844
17219
  }
17220
+ /** The chat's status sentence, worded as the task page words it: what a failed attempt missed, or which step a live
17221
+ * build is on (and the step it is stuck on). Undefined keeps the shared status sentence. */
17222
+ function chatStatusSentence(focus) {
17223
+ if (focus.state === "failed" && focus.failure != null)
17224
+ return [focus.failure.line, focus.failure.evidence].filter(Boolean).join(" ");
17225
+ const steps = focus.liveRun === null ? null : buildProgressOf(focus.milestoneProgress);
17226
+ return steps === null ? undefined : steps.line;
17227
+ }
16845
17228
  /** One live, server-derived journey from request to proof. The fragment is
16846
17229
  * safe to refresh independently, so an in-progress message is never lost. */
16847
17230
  function taskChatLiveRegion(focus, csrf, fragment = false, inert = false, approvalHref = taskChatHref(focus.id) + "#task-chat-action") {
@@ -16857,7 +17240,7 @@ function taskChatLiveRegion(focus, csrf, fragment = false, inert = false, approv
16857
17240
  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 ?? "")}">` +
16858
17241
  (focus.guide === true && focus.state !== "cancelled" ? firstTaskJourneyHtml(focus) : "") +
16859
17242
  `<div class="task-live-summary">` +
16860
- (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")) +
17243
+ (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")) +
16861
17244
  checkProgressHtml(focus.checkProgress) +
16862
17245
  // The exact-run control (v52): the SAME component the task page
16863
17246
  // renders, refreshed with the live region — typed input in the
@@ -16968,7 +17351,7 @@ function chatFleetOverview(snapshot, projects, csrf, interactive) {
16968
17351
  return (`<section class="card chat-overview" aria-label="live portfolio overview" data-card-kind="fleet-overview">` +
16969
17352
  `<div class="chat-overview-head"><h2>${projects.length} project${projects.length === 1 ? "" : "s"}</h2>` +
16970
17353
  (interactive
16971
- ? `<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>`
17354
+ ? `<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>`
16972
17355
  : `<a href="/board?scope=all" class="chat-overview-link">open board</a>`) +
16973
17356
  `</div>` +
16974
17357
  `<div class="chat-overview-stats">` +
@@ -17003,9 +17386,9 @@ function chatProjectRail(projects, csrf, inert) {
17003
17386
  const ask = `Give me a concise status for ${one.id} (${one.label}) and recommend the next reversible action.`;
17004
17387
  return (`<div class="chat-project-card">` +
17005
17388
  `<div class="chat-project-name"><span class="mono">${escape(one.id)}</span><strong>${escape(one.label)}</strong>` +
17006
- `<span class="badge">${escape(statusOf(peek))}</span></div>` +
17389
+ `<span class="badge">${escape(sentenceCase(statusOf(peek)))}</span></div>` +
17007
17390
  (peek === null
17008
- ? `<p class="meta">pulse unavailable</p>`
17391
+ ? `<p class="meta">Pulse unavailable</p>`
17009
17392
  : `<div class="chat-project-stats">` +
17010
17393
  `<span${peek.waiting > 0 ? ' class="hot"' : ""}><b>${peek.waiting}</b> need you</span>` +
17011
17394
  `<span><b>${peek.running}</b> live</span><span><b>${peek.queued}</b> queued</span>` +
@@ -17014,13 +17397,13 @@ function chatProjectRail(projects, csrf, inert) {
17014
17397
  (inert
17015
17398
  ? ""
17016
17399
  : `<form method="post" action="/chat" class="inline"><input type="hidden" name="csrf" value="${escape(csrf)}">` +
17017
- `<button type="submit" name="message" value="${escape(ask)}" class="quiet" aria-label="ask about ${escape(one.label)}">ask</button></form>`) +
17400
+ `<button type="submit" name="message" value="${escape(ask)}" class="quiet" aria-label="ask about ${escape(one.label)}">Ask</button></form>`) +
17018
17401
  `<form method="post" action="/projects/open" class="inline"><input type="hidden" name="csrf" value="${escape(csrf)}">` +
17019
17402
  `<input type="hidden" name="path" value="${escape(one.path)}"><input type="hidden" name="return" value="/board">` +
17020
- `<button type="submit" class="quiet" aria-label="open ${escape(one.label)} board">board</button></form></div></div>`);
17403
+ `<button type="submit" class="quiet" aria-label="open ${escape(one.label)} board">Board</button></form></div></div>`);
17021
17404
  }).join("");
17022
17405
  return (`<aside class="chat-projects" id="chat-project-panel" aria-label="projects in this conversation">` +
17023
- `<div class="chat-projects-head"><h2>projects</h2><span class="badge">${projects.length}</span>` +
17406
+ `<div class="chat-projects-head"><h2>Projects</h2><span class="badge">${projects.length}</span>` +
17024
17407
  `<button type="button" class="chat-project-close quiet" aria-label="close projects">×</button></div>` +
17025
17408
  `<div class="chat-project-list">${rows}</div></aside>`);
17026
17409
  }
@@ -17033,9 +17416,9 @@ function matePromptStarters(csrf, focus = null) {
17033
17416
  ["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."],
17034
17417
  ]
17035
17418
  : [
17036
- ["what’s happening", "Read this task and explain its current status, what is blocking it, and what should happen next."],
17037
- ["review results", "Review the evidence recorded for this task and tell me what is proven and what is still unverified."],
17038
- ["adjust the plan", "Read this task and propose a tighter scope if that would improve the outcome. Explain why before I confirm anything."],
17419
+ ["What’s happening", "Read this task and explain its current status, what is blocking it, and what should happen next."],
17420
+ ["Review results", "Review the evidence recorded for this task and tell me what is proven and what is still unverified."],
17421
+ ["Adjust the plan", "Read this task and propose a tighter scope if that would improve the outcome. Explain why before I confirm anything."],
17039
17422
  ];
17040
17423
  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)}">` +
17041
17424
  (focus === null ? "" : `<input type="hidden" name="task" value="${escape(focus.id)}">`) +
@@ -17184,23 +17567,23 @@ function leadConfigForm(current, data) {
17184
17567
  `<form method="post" action="/chat/config" class="card">`,
17185
17568
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
17186
17569
  `<input type="hidden" name="return" value="${escape(data.returnTo)}">`,
17187
- `<label>provider<select name="provider">`,
17570
+ `<label>Provider<select name="provider">`,
17188
17571
  `<option value="codex-subscription"${current?.provider === "codex-subscription" ? " selected" : ""}>Codex membership (logged-in CLI)</option>`,
17189
17572
  `<option value="claude-subscription"${current?.provider === "claude-subscription" ? " selected" : ""}>Anthropic membership (logged-in CLI)</option>`,
17190
17573
  `<option value="anthropic-api"${current?.provider === "anthropic-api" ? " selected" : ""}>anthropic-api (direct API)</option>`,
17191
17574
  `<option value="openrouter-api"${current?.provider === "openrouter-api" ? " selected" : ""}>openrouter-api (direct API)</option>`,
17192
17575
  `</select></label>`,
17193
- `<label>model <span class="meta">(use default for your membership's current model; direct API models need a pinned price)</span>` +
17576
+ `<label>Model <span class="meta">(use default for your membership's current model; direct API models need a pinned price)</span>` +
17194
17577
  `<input name="model" list="chat-models" value="${escape(current?.model ?? "default")}"><datalist id="chat-models">` +
17195
17578
  `${models.map(model => `<option value="${escape(model)}">${escape(labels.get(model) ?? "")}</option>`).join("")}</datalist></label>`,
17196
17579
  data.openrouterModels === null
17197
- ? `<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>`
17580
+ ? `<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>`
17198
17581
  : `<p class="meta">${data.openrouterModels.length} models live from OpenRouter's catalog; saving pins today's price — re-save to re-pin</p>`,
17199
17582
  currentSubscription
17200
- ? `<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>`
17201
- : `<label>weekly ceiling <span class="meta">(direct API only; leave blank when choosing a membership)</span>` +
17583
+ ? `<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>`
17584
+ : `<label>Weekly ceiling <span class="meta">(direct API only; leave blank when choosing a membership)</span>` +
17202
17585
  `<input type="text" name="weekly-usd" inputmode="decimal" style="width:8rem" value="${current === null ? "" : (current.weeklyCeilingMicrousd / 1_000_000).toFixed(2)}"></label>`,
17203
- `<label>daily turns <span class="meta">(default 50)</span>` +
17586
+ `<label>Daily turns <span class="meta">(default 50)</span>` +
17204
17587
  `<input type="text" name="daily-turns" inputmode="numeric" style="width:8rem" value="${current === null ? "" : String(current.dailyTurns)}"></label>`,
17205
17588
  currentSubscription
17206
17589
  ? `<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>`
@@ -17212,13 +17595,13 @@ function leadConfigForm(current, data) {
17212
17595
  : `${escape(one.provider)}: stored ${escape(one.tail ?? "")}`)
17213
17596
  .join(" · ")})</span>` +
17214
17597
  `<input type="password" name="key" placeholder="direct API only — leave empty to keep" autocomplete="off"></label>`,
17215
- `<label>your password <span class="meta">(typed again to change the provider)</span>` +
17598
+ `<label>Your password <span class="meta">(typed again to change the provider)</span>` +
17216
17599
  `<input type="password" name="token" autocomplete="current-password"></label>`,
17217
17600
  `<button type="submit">${current === null ? "turn chat on" : "save"}</button>`,
17218
17601
  `</form>`,
17219
17602
  currentSubscription
17220
17603
  ? `<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>`
17221
- : `<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 (` +
17604
+ : `<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 (` +
17222
17605
  `<span class="mono">ANTHROPIC_API_KEY</span> / <span class="mono">OPENROUTER_API_KEY</span>) always wins when set</p>`,
17223
17606
  ].join("\n");
17224
17607
  }
@@ -17247,7 +17630,7 @@ function chatPage(chrome, data) {
17247
17630
  ? `<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>`
17248
17631
  : code === "unpriced" || code === "no-key"
17249
17632
  ? `<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>`
17250
- : `<div class="card" id="latest"><p><strong>chat is off.</strong></p><p class="meta">${escape(data.enabled.why)}</p></div>`);
17633
+ : `<div class="card" id="latest"><p><strong>Chat is off.</strong></p><p class="meta">${escape(data.enabled.why)}</p></div>`);
17251
17634
  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 } });
17252
17635
  }
17253
17636
  const config = data.enabled.config;
@@ -17263,7 +17646,7 @@ function chatPage(chrome, data) {
17263
17646
  parts.push(data.coordinatorProposals);
17264
17647
  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 } }));
17265
17648
  for (const turn of data.latched) {
17266
- parts.push(`<div class="problem"><strong>unknown spend blocks chat.</strong> turn #${turn.id} may have cost up to ${chatMoney(turn.reservedMicrousd)} — ` +
17649
+ parts.push(`<div class="problem"><strong>Unknown spend blocks chat.</strong> turn #${turn.id} may have cost up to ${chatMoney(turn.reservedMicrousd)} — ` +
17267
17650
  `<a href="/chat/ack/${turn.id}">read and acknowledge it</a> to re-enable this credential.</div>`);
17268
17651
  }
17269
17652
  if (data.pending !== null) {
@@ -17278,7 +17661,7 @@ function chatPage(chrome, data) {
17278
17661
  }
17279
17662
  else if (last.reply !== null) {
17280
17663
  parts.push(`<div class="card" id="latest">${renderChatText(last.reply)}` +
17281
- (last.proposalsDiscarded ? `<p class="meta">a draft block in this answer was malformed and was discarded whole</p>` : "") +
17664
+ (last.proposalsDiscarded ? `<p class="meta">A draft block in this answer was malformed and was discarded whole</p>` : "") +
17282
17665
  `</div>`);
17283
17666
  }
17284
17667
  }
@@ -17286,21 +17669,21 @@ function chatPage(chrome, data) {
17286
17669
  for (const one of candidates) {
17287
17670
  const draft = one.draft;
17288
17671
  parts.push(`<div class="card">` +
17289
- `<p><strong>${draft.kind === "task" ? escape(draft.title) : escape(draft.name)}</strong> <span class="badge">draft ${escape(draft.kind)}</span></p>` +
17290
- `<p class="meta">drafted by the model from fleet context — nothing is filed; drafts do not survive a restart</p>` +
17672
+ `<p><strong>${draft.kind === "task" ? escape(draft.title) : escape(draft.name)}</strong> <span class="badge">Draft ${escape(draft.kind)}</span></p>` +
17673
+ `<p class="meta">Drafted by the model from fleet context — nothing is filed; drafts do not survive a restart</p>` +
17291
17674
  `<p style="white-space:pre-wrap">${escape(draft.goal)}</p>` +
17292
- (draft.outOfScope === null ? "" : `<p class="meta">not: ${escape(draft.outOfScope)}</p>`) +
17293
- (draft.touches.length > 0 ? `<p class="meta">touches: ${escape(draft.touches.join(", "))}</p>` : "") +
17294
- (draft.kind === "routine" ? `<p class="meta">schedule: ${escape(draft.schedule)}</p>` : "") +
17295
- `<p class="meta">repo: <span class="mono">${escape(projectName(one.repoPath))}</span></p>` +
17675
+ (draft.outOfScope === null ? "" : `<p class="meta">Not: ${escape(draft.outOfScope)}</p>`) +
17676
+ (draft.touches.length > 0 ? `<p class="meta">Touches: ${escape(draft.touches.join(", "))}</p>` : "") +
17677
+ (draft.kind === "routine" ? `<p class="meta">Schedule: ${escape(draft.schedule)}</p>` : "") +
17678
+ `<p class="meta">Repo: <span class="mono">${escape(projectName(one.repoPath))}</span></p>` +
17296
17679
  `<form method="post" action="/chat/file/${escape(one.key)}" class="inline">` +
17297
17680
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
17298
17681
  `<input type="password" name="token" placeholder="your password" autocomplete="current-password">` +
17299
- `<button type="submit">file unapproved</button></form>` +
17682
+ `<button type="submit">File unapproved</button></form>` +
17300
17683
  `</div>`);
17301
17684
  }
17302
17685
  if (!subscription && data.canManage && data.focusTask === null) {
17303
- 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>`);
17686
+ 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>`);
17304
17687
  }
17305
17688
  if (data.canManage) {
17306
17689
  parts.push(`<p class="meta" id="chat-settings"><a href="/settings/lead">Lead settings</a> · provider, model and limits</p>`);
@@ -17323,12 +17706,12 @@ function chatAckPage(chrome, turn, nonce, csrf) {
17323
17706
  `<p>Turn <span class="mono">#${turn.id}</span> on <span class="mono">${escape(turn.provider)} · ${escape(turn.model)}</span> ` +
17324
17707
  `may have started before it failed (${escape(turn.failureReason ?? "crashed")}), and its cost could not be measured.</p>`,
17325
17708
  `<p><strong>Acknowledging charges the reserved worst case, ${chatMoney(turn.reservedMicrousd)}, to the ledger and re-enables chat on this credential.</strong></p>`,
17326
- `<p class="meta">check the provider's own usage dashboard if you want the exact figure first; the ledger keeps whichever is known.</p>`,
17709
+ `<p class="meta">Check the provider's own usage dashboard if you want the exact figure first; the ledger keeps whichever is known.</p>`,
17327
17710
  `<form method="post" action="/chat/ack/${turn.id}">`,
17328
17711
  `<input type="hidden" name="csrf" value="${escape(csrf)}">`,
17329
17712
  `<input type="hidden" name="nonce" value="${escape(nonce)}">`,
17330
- `<label>your password<input type="password" name="token" autocomplete="current-password"></label>`,
17331
- `<button type="submit">accept the charge — re-enable chat</button>`,
17713
+ `<label>Your password<input type="password" name="token" autocomplete="current-password"></label>`,
17714
+ `<button type="submit">Accept the charge — re-enable chat</button>`,
17332
17715
  `</form>`,
17333
17716
  `</div>`,
17334
17717
  ].join("\n"), { chrome });
@@ -17535,10 +17918,10 @@ function proposalCardParts(view, csrf, inert, decision, returnTo = null) {
17535
17918
  const decisionId = typeof payload["decision"] === "number" ? payload["decision"] : 0;
17536
17919
  const pick = text("option");
17537
17920
  const options = decision === null
17538
- ? `<p class="meta">the decision is gone</p>`
17921
+ ? `<p class="meta">The decision is gone</p>`
17539
17922
  : `<ul class="answer-options">` +
17540
17923
  decision.options
17541
- .map(one => `<li${one.id === pick ? ' class="picked"' : ""}><strong>${escape(one.label)}</strong>${one.reversible ? "" : ' <span class="badge">irreversible</span>'}` +
17924
+ .map(one => `<li${one.id === pick ? ' class="picked"' : ""}><strong>${escape(one.label)}</strong>${one.reversible ? "" : ' <span class="badge">Irreversible</span>'}` +
17542
17925
  `${one.id === decision.recommendation ? ' <span class="meta">— the builder recommends this</span>' : ""}` +
17543
17926
  `${one.id === pick ? ' <span class="meta">— proposed</span>' : ""}` +
17544
17927
  `<p class="meta">${escape(one.consequence)}</p></li>`)
@@ -17568,13 +17951,13 @@ function proposalCardParts(view, csrf, inert, decision, returnTo = null) {
17568
17951
  : view.kind === "control"
17569
17952
  ? (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>`)
17570
17953
  : view.kind === "cancel"
17571
- ? `<p class="meta">cancelling is armed on the task itself — <a href="${taskHref(task)}">open ${escape(task)}</a></p>` +
17572
- `<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>`
17954
+ ? `<p class="meta">Cancelling is armed on the task itself — <a href="${taskHref(task)}">open ${escape(task)}</a></p>` +
17955
+ `<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>`
17573
17956
  : `<div class="acts">` +
17574
17957
  `<form method="post" action="${view.actionBase}/${view.id}/confirm" class="inline"><input type="hidden" name="csrf" value="${escape(csrf)}">${returnField}` +
17575
17958
  (irreversible ? `<label class="arm"><input type="checkbox" name="confirm" value="yes"> I understand this cannot be undone</label>` : "") +
17576
17959
  `<button type="submit">${escape(presentation.action)}</button></form>` +
17577
- `<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>` +
17960
+ `<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>` +
17578
17961
  `</div>`;
17579
17962
  }
17580
17963
  else if (view.state === "pending") {
@@ -17605,7 +17988,7 @@ function proposalCardParts(view, csrf, inert, decision, returnTo = null) {
17605
17988
  const html = (`<article class="card proposal proposal-${escape(view.kind)} ${escape(view.state)}" data-card-kind="${escape(view.kind)}">` +
17606
17989
  `<header class="proposal-head"><span class="proposal-icon">${strokeIcon(presentation.icon)}</span>` +
17607
17990
  `<span><strong>${escape(presentation.label)}</strong><small>proposed by ${provenance}</small></span>` +
17608
- `<span class="badge ${stateClass}">${escape(view.state)}</span></header>` +
17991
+ `<span class="badge ${stateClass}">${escape(sentenceCase(view.state))}</span></header>` +
17609
17992
  `<div class="proposal-body">${what}</div>` +
17610
17993
  `<footer class="proposal-actions">${acts}</footer></article>`);
17611
17994
  // The same card as data (chat cards): one act, in the door's own words.
@@ -17653,7 +18036,7 @@ function relativeAge(iso, now) {
17653
18036
  function coordinatorProposalsSection(proposals, decisions, csrf, now, heading = true, returnTo = null) {
17654
18037
  if (proposals.length === 0)
17655
18038
  return "";
17656
- return ((heading ? `<h2>proposed by coordinators <span class="meta">${proposals.length}</span></h2>` : "") +
18039
+ return ((heading ? `<h2>Proposed by coordinators <span class="meta">${proposals.length}</span></h2>` : "") +
17657
18040
  `<div class="coordinator-proposals">` +
17658
18041
  proposals.map(one => coordinatorProposalCard(one, csrf, now, decisions.get(typeof one.payload["decision"] === "number" ? one.payload["decision"] : -1) ?? null, returnTo)).join("") +
17659
18042
  `</div>`);
@@ -17777,10 +18160,10 @@ function matePage(chrome, data) {
17777
18160
  `<span>this week ${chatMoney(data.weeklySpent)} of ${chatMoney(data.config.weeklyCeilingMicrousd)}</span>`) +
17778
18161
  `</div>`,
17779
18162
  `<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>`,
17780
- `<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>`,
18163
+ `<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>`,
17781
18164
  data.recent.length === 0
17782
18165
  ? ""
17783
- : `<p class="meta">recent turns: ${data.recent
18166
+ : `<p class="meta">Recent turns: ${data.recent
17784
18167
  .map(turn => `<span class="mono">#${turn.id}</span> ${escape(turn.state)}${turn.failureReason === null ? "" : ` · ${escape(turn.failureReason)}`} · ${subscription ? "membership" : chatMoney(turn.settledMicrousd ?? turn.reservedMicrousd)}`)
17785
18168
  .join(" · ")}</p>`,
17786
18169
  `<p class="meta"><a href="/settings/lead">Lead settings</a> · provider, model and limits</p>`,
@@ -17797,7 +18180,7 @@ function matePage(chrome, data) {
17797
18180
  if (data.problem !== null)
17798
18181
  conversation.push(`<div class="problem">${escape(data.problem)}</div>`);
17799
18182
  for (const turn of data.latched) {
17800
- conversation.push(`<div class="problem"><strong>unknown spend blocks chat.</strong> turn #${turn.id} may have cost up to ${chatMoney(turn.reservedMicrousd)} — ` +
18183
+ conversation.push(`<div class="problem"><strong>Unknown spend blocks chat.</strong> turn #${turn.id} may have cost up to ${chatMoney(turn.reservedMicrousd)} — ` +
17801
18184
  `<a href="/chat/ack/${turn.id}">read and acknowledge it</a> to re-enable this credential.</div>`);
17802
18185
  }
17803
18186
  const lastMessage = data.messages.at(-1);
@@ -17807,7 +18190,7 @@ function matePage(chrome, data) {
17807
18190
  // The New update action (package 2): hidden until a live update lands
17808
18191
  // while the reader is above the latest message; a real button, so the
17809
18192
  // keyboard reaches it. Only this act moves the reader.
17810
- `<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>`,
18193
+ `<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>`,
17811
18194
  // Concise pass (2026-09-13): the status line speaks only when there is
17812
18195
  // a state to report — a reply in progress here, the connection from
17813
18196
  // the continuity script — and the composer carries no second intro.
@@ -17839,25 +18222,25 @@ function routinesPage(chrome, tracks, form) {
17839
18222
  const capture = chrome.project === null
17840
18223
  ? ""
17841
18224
  : [
17842
- `<h2>file a standing order</h2>`,
18225
+ `<h2>File a standing order</h2>`,
17843
18226
  form.problem === null ? "" : `<div class="problem">${escape(form.problem)}</div>`,
17844
18227
  `<form method="post" action="/routines/add" class="card">`,
17845
18228
  `<input type="hidden" name="csrf" value="${escape(form.csrf)}">`,
17846
18229
  `<input type="hidden" name="projectRevision" value="${form.revision}">`,
17847
18230
  fill === null
17848
- ? `<p class="meta">start from a template: ${TEMPLATES.filter(one => one.kind === "routine")
18231
+ ? `<p class="meta">Start from a template: ${TEMPLATES.filter(one => one.kind === "routine")
17849
18232
  .map(one => `<a href="/routines?template=${escape(one.name)}">${escape(one.name)}</a>`)
17850
18233
  .join(" · ")}</p>`
17851
18234
  : `<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>`,
17852
- `<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>`,
17853
- `<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>`,
17854
- `<label>not this <span class="meta">(optional)</span><input type="text" name="not" value="${fill === null ? "" : escape(fill.not)}"></label>`,
17855
- `<label>touches <span class="meta">(paths, comma-separated, optional)</span><input type="text" name="touches" value="${fill === null ? "" : escape(fill.touches)}"></label>`,
17856
- `<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>`,
18235
+ `<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>`,
18236
+ `<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>`,
18237
+ `<label>Not this <span class="meta">(optional)</span><input type="text" name="not" value="${fill === null ? "" : escape(fill.not)}"></label>`,
18238
+ `<label>Touches <span class="meta">(paths, comma-separated, optional)</span><input type="text" name="touches" value="${fill === null ? "" : escape(fill.touches)}"></label>`,
18239
+ `<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>`,
17857
18240
  scheduleEditorHtml(fill?.schedule ?? null, form.values),
17858
- `<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>`,
17859
- `<button type="submit">file it \u2192 approve the standing order next</button>`,
17860
- `<p class="meta">filing is cheap — nothing fires until you approve the template on the next screen, password and all</p>`,
18241
+ `<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>`,
18242
+ `<button type="submit">File it \u2192 approve the standing order next</button>`,
18243
+ `<p class="meta">Filing is cheap — nothing fires until you approve the template on the next screen, password and all</p>`,
17861
18244
  `</form>`,
17862
18245
  ].join("\n");
17863
18246
  const list = tracks.length === 0
@@ -17877,19 +18260,19 @@ function routineScreenPage(chrome, data) {
17877
18260
  const schedule = parseSchedule(routine.schedule);
17878
18261
  const scheduleSaid = schedule === null ? routine.schedule : describeSchedule(schedule);
17879
18262
  const terms = `<div class="card">` +
17880
- `<p class="meta">goal</p><p class="recap" style="margin-top:0">${escape(routine.goal)}</p>` +
17881
- `<p class="meta">not this</p><p class="recap" style="margin-top:0">${routine.outOfScope === null ? "<em>no exclusions</em>" : escape(routine.outOfScope)}</p>` +
17882
- `<p class="meta">touches · ${routine.touches.length === 0 ? "anything" : routine.touches.map(one => escape(one)).join(", ")}</p>` +
18263
+ `<p class="meta">Goal</p><p class="recap" style="margin-top:0">${escape(routine.goal)}</p>` +
18264
+ `<p class="meta">Not this</p><p class="recap" style="margin-top:0">${routine.outOfScope === null ? "<em>no exclusions</em>" : escape(routine.outOfScope)}</p>` +
18265
+ `<p class="meta">Touches · ${routine.touches.length === 0 ? "anything" : routine.touches.map(one => escape(one)).join(", ")}</p>` +
17883
18266
  (routine.acceptance.length === 0
17884
18267
  ? ""
17885
- : `<p class="meta">acceptance</p><ul class="recap">${routine.acceptance
18268
+ : `<p class="meta">Acceptance</p><ul class="recap">${routine.acceptance
17886
18269
  .map(c => `<li><code>${escape(c.id)}</code> ${escape(c.statement)} <span class="meta">[requires: ${c.evidence.map(escape).join(", ")}]</span></li>`)
17887
18270
  .join("")}</ul>`) +
17888
- `<p class="meta">needs · ${routine.requirements.length === 0 ? "nothing beyond the repository" : routine.requirements.map(one => escape(one)).join(", ")}</p>` +
18271
+ `<p class="meta">Needs · ${routine.requirements.length === 0 ? "nothing beyond the repository" : routine.requirements.map(one => escape(one)).join(", ")}</p>` +
17889
18272
  routineAgentsHtml(routine, approved) +
17890
- `<p class="meta">schedule · ${escape(scheduleSaid)}</p>` +
17891
- `<p class="meta">budget · ${routine.costCeilingUsd === null ? "no ceiling" : `$${routine.costCeilingUsd.toFixed(2)} per rolling 7 days`}</p>` +
17892
- `<p class="meta">one at a time — a firing skips while the previous instance is unfinished</p>` +
18273
+ `<p class="meta">Schedule · ${escape(scheduleSaid)}</p>` +
18274
+ `<p class="meta">Budget · ${routine.costCeilingUsd === null ? "no ceiling" : `$${routine.costCeilingUsd.toFixed(2)} per rolling 7 days`}</p>` +
18275
+ `<p class="meta">One at a time — a firing skips while the previous instance is unfinished</p>` +
17893
18276
  `</div>`;
17894
18277
  // THE CONSENT DOOR (v48): the password and the approve act exist only
17895
18278
  // where an exact, readable route can be restated — never over agents
@@ -17921,13 +18304,13 @@ function routineScreenPage(chrome, data) {
17921
18304
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
17922
18305
  `<input type="hidden" name="nonce" value="${escape(data.nonce)}">`,
17923
18306
  `<input type="hidden" name="digest" value="${escape(routine.digest)}">`,
17924
- `<p><strong>approve this standing order:</strong></p>`,
18307
+ `<p><strong>Approve this standing order:</strong></p>`,
17925
18308
  `<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>`,
17926
18309
  // The agents the yes freezes (v48): the same concise block every
17927
18310
  // approval shows, before the password.
17928
18311
  routineAgentsHtml(routine, false, true),
17929
- `<label>your password, typed again — a signed-in session alone cannot agree to standing work<input type="password" name="token" autocomplete="current-password"></label>` +
17930
- `<button type="submit">approve this routine</button>`,
18312
+ `<label>Your password, typed again — a signed-in session alone cannot agree to standing work<input type="password" name="token" autocomplete="current-password"></label>` +
18313
+ `<button type="submit">Approve this routine</button>`,
17931
18314
  `</form>`,
17932
18315
  ].join("\n");
17933
18316
  const verb = (name, label, danger = false) => `<form method="post" action="${routineHref(routine.id)}/${name}" class="inline">` +
@@ -17937,7 +18320,7 @@ function routineScreenPage(chrome, data) {
17937
18320
  ? `<form method="post" action="${routineHref(routine.id)}/run-now" class="inline">` +
17938
18321
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
17939
18322
  `<input type="password" name="token" autocomplete="current-password" class="inline" placeholder="your password" aria-label="password for run now" style="width:11rem"> ` +
17940
- `<button type="submit">run now</button></form>`
18323
+ `<button type="submit">Run now</button></form>`
17941
18324
  : "";
17942
18325
  const acts = `<div class="card">` +
17943
18326
  (routine.paused ? verb("resume", "resume") : verb("pause", "pause")) +
@@ -17951,7 +18334,7 @@ function routineScreenPage(chrome, data) {
17951
18334
  const said = fire.outcome === "fired"
17952
18335
  ? fire.instanceTaskId === null
17953
18336
  ? "fired"
17954
- : `<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>`}`
18337
+ : `<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>`}`
17955
18338
  : `<span class="meta">skipped — ${escape(fire.reason ?? "")}</span>`;
17956
18339
  const slot = fire.scheduledFor.replace(/^manual:/, "");
17957
18340
  return `<p class="row">${said}<span class="right meta mono">${fire.reason === "manual" ? "run now · " : ""}${whenTime(slot)}</span></p>`;
@@ -17966,14 +18349,14 @@ function routineScreenPage(chrome, data) {
17966
18349
  ? `<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>`
17967
18350
  : "",
17968
18351
  approved && !routine.paused && routine.nextFireAt !== null
17969
- ? `<p class="meta">next fire ${whenTime(routine.nextFireAt)}</p>`
18352
+ ? `<p class="meta">Next fire ${whenTime(routine.nextFireAt)}</p>`
17970
18353
  : "",
17971
- "<h2>the standing order</h2>",
18354
+ "<h2>The standing order</h2>",
17972
18355
  terms,
17973
18356
  approveForm,
17974
- "<h2>acts</h2>",
18357
+ "<h2>Acts</h2>",
17975
18358
  acts,
17976
- "<h2>firings</h2>",
18359
+ "<h2>Firings</h2>",
17977
18360
  ledger,
17978
18361
  ].join("\n"), { chrome });
17979
18362
  }
@@ -17992,21 +18375,21 @@ function homePage(chrome, data) {
17992
18375
  .map(decision => `<a class="decide-card" href="/d/${decision.id}">` +
17993
18376
  `<p class="q">${escape(decision.question)}</p>` +
17994
18377
  `<span class="mono meta">${escape(decision.taskId)}</span>` +
17995
- `${isOverdue(decision, data.now) ? ` <span class="badge badge-overdue">overdue</span>` : ""}` +
18378
+ `${isOverdue(decision, data.now) ? ` <span class="badge badge-overdue">Overdue</span>` : ""}` +
17996
18379
  `</a>`)
17997
18380
  .join("\n");
17998
18381
  const incidents = data.incidents.length === 0
17999
18382
  ? ""
18000
- : `<h2>incidents</h2><p class="hint">builds that stopped and need a person — resolve here, or open the task to retry it</p>` +
18383
+ : `<h2>Incidents</h2><p class="hint">builds that stopped and need a person — resolve here, or open the task to retry it</p>` +
18001
18384
  data.incidents
18002
18385
  .map(one => `<p class="row"><a href="${taskHref(one.taskId)}">${escape(one.taskId)}</a> — ${escape(incidentWords(one.kind))}` +
18003
18386
  `<span class="right"><form method="post" action="/i/${one.id}/resolve" class="inline">` +
18004
18387
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
18005
- `<button type="submit">resolve</button></form></span></p>`)
18388
+ `<button type="submit">Resolve</button></form></span></p>`)
18006
18389
  .join("\n");
18007
18390
  const stranded = data.stranded.length === 0
18008
18391
  ? ""
18009
- : `<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>` +
18392
+ : `<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>` +
18010
18393
  data.stranded
18011
18394
  .map(one => `<p class="row"><a href="${taskHref(one.id)}">${escape(one.id)}</a> waits on ${one.blockedBy
18012
18395
  .map(blocker => `<a href="${taskHref(blocker)}">${escape(blocker)}</a>`)
@@ -18016,7 +18399,7 @@ function homePage(chrome, data) {
18016
18399
  ? ""
18017
18400
  : data.gaps.length === 0
18018
18401
  ? ""
18019
- : `<h2>missing requirements</h2><p class="hint">tools or credentials builds need — checked before any money is spent</p>` +
18402
+ : `<h2>Missing requirements</h2><p class="hint">tools or credentials builds need — checked before any money is spent</p>` +
18020
18403
  data.gaps
18021
18404
  .map(gap => `<p class="row"><a href="/caps">${escape(gap.key)}</a> — ${escape(gap.state)}<span class="right meta">how to fix →</span></p>`)
18022
18405
  .join("\n") +
@@ -18028,7 +18411,7 @@ function homePage(chrome, data) {
18028
18411
  // an operator actually watches this page, so it re-renders itself.
18029
18412
  const building = data.building.length === 0
18030
18413
  ? ""
18031
- : `<h2>building now</h2><p class="hint">live builds — this page refreshes itself every 10 seconds while anything runs</p><div class="cards">` +
18414
+ : `<h2>Building now</h2><p class="hint">live builds — this page refreshes itself every 10 seconds while anything runs</p><div class="cards">` +
18032
18415
  data.building
18033
18416
  .map(claim => `<a class="stat-card" href="${taskHref(claim.taskId)}" style="text-decoration:none">` +
18034
18417
  `<span class="k"><span class="dot dot-ok pulse"></span>${escape(claim.taskId)}</span>` +
@@ -18064,8 +18447,8 @@ function homePage(chrome, data) {
18064
18447
  : `last window: ${data.episode.built} built, ${data.episode.broke} broke \u00b7 ended ${whenTime(data.episode.endedAt)}`}</span></div>`;
18065
18448
  const fleetCards = [...runnerCards, watchCard, ...worktreeCards].filter(one => one !== "");
18066
18449
  const fleet = fleetCards.length === 0
18067
- ? `<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>`
18068
- : `<h2>system status</h2><p class="hint">builders execute tasks in isolated temporary copies of each project</p><div class="cards">${fleetCards.join("")}</div>`;
18450
+ ? `<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>`
18451
+ : `<h2>System status</h2><p class="hint">builders execute tasks in isolated temporary copies of each project</p><div class="cards">${fleetCards.join("")}</div>`;
18069
18452
  const startHere = data.taskCount === 0
18070
18453
  ? [
18071
18454
  `<div class="card">`,
@@ -18085,10 +18468,10 @@ function homePage(chrome, data) {
18085
18468
  : `<p class="meta"><strong>${escape(projectName(data.repo))}</strong> — the last 24 hours, honestly labeled: a rolling window, whatever your hours are</p>`,
18086
18469
  startHere,
18087
18470
  ledger,
18088
- `<p class="meta">spend: ${escape(consoleSpend(summary))}</p>`,
18089
- data.outboxPending > 0 ? `<p class="meta">notifications: ${data.outboxPending} pending delivery</p>` : "",
18471
+ `<p class="meta">Spend: ${escape(consoleSpend(summary))}</p>`,
18472
+ data.outboxPending > 0 ? `<p class="meta">Notifications: ${data.outboxPending} pending delivery</p>` : "",
18090
18473
  building,
18091
- `<h2>needs your decision</h2><p class="hint">an agent stopped mid-build to ask — nothing proceeds until you answer</p>`,
18474
+ `<h2>Needs your decision</h2><p class="hint">an agent stopped mid-build to ask — nothing proceeds until you answer</p>`,
18092
18475
  decide,
18093
18476
  incidents,
18094
18477
  stranded,
@@ -18157,21 +18540,21 @@ function taskComposerHtml(data) {
18157
18540
  `<details class="task-options"${prefill === null ? "" : " open"}>`,
18158
18541
  `<summary><span>Edit details</span><small>optional · defaults are remembered</small></summary>`,
18159
18542
  `<div class="task-options-grid">`,
18160
- `<label class="wide">goal <span class="meta">— provide upfront for automatic approval of an unchanged plan</span>` +
18543
+ `<label class="wide">Goal <span class="meta">— provide upfront for automatic approval of an unchanged plan</span>` +
18161
18544
  `<textarea name="goal" rows="3" placeholder="What success looks like">${prefill === null ? "" : escape(prefill.goal)}</textarea></label>`,
18162
- `<label class="wide">acceptance <span class="meta">— required with a goal; one per line: <code>statement | evidence,kinds | how</code></span>` +
18545
+ `<label class="wide">Acceptance <span class="meta">— required with a goal; one per line: <code>statement | evidence,kinds | how</code></span>` +
18163
18546
  `<textarea name="acceptance" rows="3" placeholder="Requests over the limit return 429 | check">${prefill === null ? "" : escape(prefill.acceptance)}</textarea></label>`,
18164
- `<label>not this <span class="meta">— optional boundary</span><input type="text" name="not" value="${prefill === null ? "" : escape(prefill.not)}"></label>`,
18165
- `<label>likely touches <span class="meta">— paths, comma-separated</span><input type="text" name="touches" value="${prefill === null ? "" : escape(prefill.touches)}"></label>`,
18547
+ `<label>Not this <span class="meta">— optional boundary</span><input type="text" name="not" value="${prefill === null ? "" : escape(prefill.not)}"></label>`,
18548
+ `<label>Likely touches <span class="meta">— paths, comma-separated</span><input type="text" name="touches" value="${prefill === null ? "" : escape(prefill.touches)}"></label>`,
18166
18549
  `<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>`,
18167
18550
  `<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>`,
18168
- `<label>task id <span class="meta">— optional</span><input type="text" name="id" value="${escape(values?.get("id") ?? "")}" placeholder="made from the request"></label>`,
18551
+ `<label>Task id <span class="meta">— optional</span><input type="text" name="id" value="${escape(values?.get("id") ?? "")}" placeholder="made from the request"></label>`,
18169
18552
  candidates.length === 0
18170
18553
  ? ""
18171
- : `<label>starts after <span class="meta">— optional</span><select name="after"><option value="">right away</option>` +
18554
+ : `<label>Starts after <span class="meta">— optional</span><select name="after"><option value="">right away</option>` +
18172
18555
  candidates.map(one => `<option value="${escape(one.id)}"${one.id === after ? " selected" : ""}>${escape(one.id)} — ${escape(one.title)}</option>`).join("") +
18173
18556
  `</select></label>`,
18174
- `<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)}` +
18557
+ `<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)}` +
18175
18558
  `<p class="meta permission-note">Inherited from Settings. You can still change it on the proposed scope before approval.</p></fieldset>`,
18176
18559
  `</div>`,
18177
18560
  `</details>`,
@@ -18267,9 +18650,9 @@ function workPage(chrome, data) {
18267
18650
  project: data.multiProject || row.repo === null ? (row.repo === null ? 'Unplaced' : projectName(row.repo)) : null,
18268
18651
  age: relativeAge(row.updatedAt, data.now),
18269
18652
  status: { label: row.status.label, tone: row.status.tone, token: row.status.token },
18270
- ask: row.ask, group: row.ask ?? (row.status.rank === 1 ? 'building' : 'rest'),
18653
+ ask: row.ask, chip: row.chip, group: row.ask ?? (row.status.rank === 1 ? 'building' : 'rest'),
18271
18654
  action: actionHref === null || row.primaryAction === null ? null : { label: row.primaryAction.label, href: actionHref },
18272
- detail: needsYouDetail ? row.status.detail : null,
18655
+ detail: needsYouDetail ? row.status.detail : row.progress ?? null,
18273
18656
  problem: row.familyProblem,
18274
18657
  notes: (row.status.diagnostics ?? []).map(one => `${one.label} · ${one.detail}`),
18275
18658
  };
@@ -18297,12 +18680,12 @@ function tasksPage(chrome, tasks, state, csrf, problem, repo = null, prefill = n
18297
18680
  .join("\n");
18298
18681
  return screen("tasks", [
18299
18682
  "<h1>Tasks</h1>",
18300
- `<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>`,
18683
+ `<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>`,
18301
18684
  repo === null ? "" : `<p class="meta path-words"><span class="mono">${escape(repo)}</span></p>`,
18302
18685
  problem === null ? "" : `<div class="problem">${escape(problem)}</div>`,
18303
- `<p class="meta">filter: <a href="/tasks">all</a> · ${filters}</p>`,
18686
+ `<p class="meta">Filter: <a href="/tasks">all</a> · ${filters}</p>`,
18304
18687
  rows,
18305
- `<h2>add a task</h2>`,
18688
+ `<h2>Add a task</h2>`,
18306
18689
  taskComposerHtml({ csrf, project: repo, prefill, permissionDefault, qualityDefault }),
18307
18690
  ].join("\n"), { chrome });
18308
18691
  }
@@ -18312,21 +18695,21 @@ function browsePage(chrome, data) {
18312
18695
  `<form method="post" action="/projects/open" class="inline">`,
18313
18696
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
18314
18697
  `<input type="hidden" name="path" value="${escape(path)}">`,
18315
- `<button type="submit">open</button>`,
18698
+ `<button type="submit">Open</button>`,
18316
18699
  `</form>`,
18317
18700
  ].join("");
18318
18701
  return screen("projects", [
18319
18702
  `<h1>Choose a folder</h1>`,
18320
18703
  `<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>`,
18321
18704
  data.roots.length > 1
18322
- ? `<p class="meta">roots: ${data.roots.map(one => `<a href="/projects/browse?at=${encodeURIComponent(one)}" class="mono">${escape(projectName(one))}</a>`).join(" · ")}</p>`
18705
+ ? `<p class="meta">Roots: ${data.roots.map(one => `<a href="/projects/browse?at=${encodeURIComponent(one)}" class="mono">${escape(projectName(one))}</a>`).join(" · ")}</p>`
18323
18706
  : "",
18324
18707
  `<p class="mono meta">${escape(crumb)}</p>`,
18325
18708
  data.parent === null
18326
18709
  ? ""
18327
18710
  : `<p class="row"><a href="/projects/browse?at=${encodeURIComponent(data.parent)}">\u2190 up one level</a></p>`,
18328
18711
  data.entries.length === 0
18329
- ? `<p class="meta">no folders here</p>`
18712
+ ? `<p class="meta">No folders here</p>`
18330
18713
  : data.entries
18331
18714
  .map(one => `<p class="row">` +
18332
18715
  `<a href="/projects/browse?at=${encodeURIComponent(one.path)}"><strong>${escape(one.name)}</strong></a>` +
@@ -18344,7 +18727,7 @@ function browsePage(chrome, data) {
18344
18727
  * switching context must never depend on remembering which repo is open.
18345
18728
  */
18346
18729
  function workbenchRail(data) {
18347
- const workspace = (repo) => `<span class="badge">${repo === null ? "unplaced" : escape(projectName(repo))}</span>`;
18730
+ const workspace = (repo) => `<span class="badge">${repo === null ? "Unplaced" : escape(projectName(repo))}</span>`;
18348
18731
  const row = (card, reason) => `<a class="wb-row${card.taskId === data.selected ? " wb-selected" : ""}" href="/workbench?t=${encodeURIComponent(card.taskId)}"` +
18349
18732
  `${card.taskId === data.selected ? ` aria-current="true"` : ""}>` +
18350
18733
  `<span class="wb-title">${escape(card.title)}</span>` +
@@ -18354,7 +18737,7 @@ function workbenchRail(data) {
18354
18737
  (cards.length === 0 ? `<p class="meta">${empty}</p>` : cards.slice(0, 100).map(render).join("\n")) +
18355
18738
  `</section>`;
18356
18739
  const parts = [];
18357
- parts.push(`<div class="wb-rail-head"><div><span class="eyebrow">portfolio</span><h2>all projects</h2></div>` +
18740
+ parts.push(`<div class="wb-rail-head"><div><span class="eyebrow">portfolio</span><h2>All projects</h2></div>` +
18358
18741
  `<a href="/projects">manage →</a></div>`);
18359
18742
  parts.push(group("needs you", data.attention, "Nothing needs your input.", card => row(card, escape(card.reason))));
18360
18743
  parts.push(group("in progress", data.building, "No agent is working right now.", card => {
@@ -18366,23 +18749,23 @@ function workbenchRail(data) {
18366
18749
  parts.push(group("blocked & waiting", data.waiting, "Nothing is blocked or paused.", card => row(card, escape(card.reason))));
18367
18750
  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)}`}`)));
18368
18751
  if (data.done.length > 0) {
18369
- parts.push(`<section class="wb-group"><h2>just finished <span class="lane-count">${data.done.length}</span></h2>`);
18752
+ parts.push(`<section class="wb-group"><h2>Just finished <span class="lane-count">${data.done.length}</span></h2>`);
18370
18753
  parts.push(data.done
18371
18754
  .map(one => `<a class="wb-row${one.taskId === data.selected ? " wb-selected" : ""}" href="/workbench?t=${encodeURIComponent(one.taskId)}">` +
18372
18755
  `<span class="wb-title">${escape(one.title)}</span>` +
18373
18756
  `<span class="wb-meta"><span class="mono meta">${escape(one.taskId)}</span>${workspace(one.repo)} ` +
18374
- `<span class="badge badge-${one.outcome === "built" || one.outcome === "no-change" ? "done" : "failed"}">${escape(one.outcome ?? "?")}</span></span></a>`)
18757
+ `<span class="badge badge-${one.outcome === "built" || one.outcome === "no-change" ? "done" : "failed"}">${escape(sentenceCase(one.outcome ?? "?"))}</span></span></a>`)
18375
18758
  .join("\n"));
18376
18759
  parts.push(`</section>`);
18377
18760
  }
18378
18761
  if (data.saturated)
18379
- parts.push(`<p class="meta">more exists — this rail is capped; the <a href="/board?scope=all">board</a> holds the rest</p>`);
18762
+ parts.push(`<p class="meta">More exists — this rail is capped; the <a href="/board?scope=all">board</a> holds the rest</p>`);
18380
18763
  return parts.join("\n");
18381
18764
  }
18382
18765
  /** The project chip every all-scope row wears: null is UNPLACED, said so. */
18383
18766
  function projectChip(repo) {
18384
18767
  return repo === null || repo === undefined
18385
- ? ` <span class="badge">unplaced</span>`
18768
+ ? ` <span class="badge">Unplaced</span>`
18386
18769
  : ` <span class="badge">${escape(projectName(repo))}</span>`;
18387
18770
  }
18388
18771
  /**
@@ -18400,11 +18783,11 @@ function decisionAnswerCard(decision, csrf, now, chip, returnTo = null) {
18400
18783
  const options = decision.options
18401
18784
  .map(option => {
18402
18785
  const recommended = option.id === decision.recommendation
18403
- ? ` <span class="badge">recommended</span>`
18786
+ ? ` <span class="badge">Recommended</span>`
18404
18787
  : "";
18405
18788
  if (!option.reversible) {
18406
18789
  return (`<p class="decide-option"><a href="/d/${decision.id}${returnTo === null ? "" : `?return=${encodeURIComponent(returnTo)}`}">${escape(option.label)}</a>` +
18407
- ` <span class="badge badge-overdue">irreversible</span>${recommended}` +
18790
+ ` <span class="badge badge-overdue">Irreversible</span>${recommended}` +
18408
18791
  ` <span class="meta">${escape(option.consequence)}</span></p>`);
18409
18792
  }
18410
18793
  return (`<form class="decide-option decide-inline" method="post" action="/d/${decision.id}/answer">` +
@@ -18419,7 +18802,7 @@ function decisionAnswerCard(decision, csrf, now, chip, returnTo = null) {
18419
18802
  `<p class="q">${escape(decision.question)}</p>` +
18420
18803
  `<details class="decision-context"><summary>Context</summary><p class="meta">${escape(decision.recap)}</p><span class="meta mono">${escape(decision.taskId)}</span></details>` +
18421
18804
  `<p class="meta">${chip ? projectChip(decision.repo) : ""}` +
18422
- `${isOverdue(decision, now) ? ` <span class="badge badge-overdue">overdue</span>` : ""}` +
18805
+ `${isOverdue(decision, now) ? ` <span class="badge badge-overdue">Overdue</span>` : ""}` +
18423
18806
  ` <a href="/d/${decision.id}${returnTo === null ? "" : `?return=${encodeURIComponent(returnTo)}`}">View details →</a></p>` +
18424
18807
  `<div class="decide-options">${options}</div></div>`);
18425
18808
  }
@@ -18500,10 +18883,10 @@ function portfolioOverview(data) {
18500
18883
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
18501
18884
  `<input type="hidden" name="path" value="${escape(one.repo)}">` +
18502
18885
  `<input type="hidden" name="return" value="/board">` +
18503
- `<button type="submit">board →</button></form>`;
18886
+ `<button type="submit">Board →</button></form>`;
18504
18887
  return (`<div class="workspace-card${one.attention > 0 ? " hot" : ""}">` +
18505
18888
  `<div class="workspace-head"><span class="workspace-name">${escape(workspace(one.repo))}</span>` +
18506
- `<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>` +
18889
+ `<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>` +
18507
18890
  `<div class="workspace-stats">` +
18508
18891
  `<span class="pulse-stat${one.attention > 0 ? " hot" : ""}"><b>${one.attention}</b> need you</span>` +
18509
18892
  `<span class="pulse-stat"><b>${one.building}</b> live</span>` +
@@ -18555,7 +18938,7 @@ function portfolioOverview(data) {
18555
18938
  // ---- running: current live claims, project chips on ----
18556
18939
  const liveRows = data.live
18557
18940
  .map(one => `<p class="row"><a href="${taskHref(one.taskId)}">${escape(one.taskId)}</a>${projectChip(one.repo)} ` +
18558
- `<span class="badge badge-running">running</span> ` +
18941
+ `<span class="badge badge-running">Running</span> ` +
18559
18942
  `<span class="mono meta">${escape(one.runner)}${one.model === null ? "" : ` · ${escape(one.model)}`} · ` +
18560
18943
  `<time data-elapsed-since="${escape(one.claimedAt)}"></time></span></p>`)
18561
18944
  .join("\n");
@@ -18563,8 +18946,8 @@ function portfolioOverview(data) {
18563
18946
  const chipClass = (outcome) => outcome === "built" || outcome === "no-change" ? "badge-done" : outcome === "failed" ? "badge-failed" : "";
18564
18947
  const ledgerRows = data.ledger
18565
18948
  .map(one => `<p class="row"><a href="/r/${one.runId}">${escape(one.title)}</a>${projectChip(one.repo)} ` +
18566
- `<span class="badge ${chipClass(one.outcome)}">${escape(one.outcome)}</span> ` +
18567
- `${one.role === "scout" ? `<a class="badge" href="${taskHref(one.taskId)}#report">report</a> ` : ""}` +
18949
+ `<span class="badge ${chipClass(one.outcome)}">${escape(sentenceCase(one.outcome))}</span> ` +
18950
+ `${one.role === "scout" ? `<a class="badge" href="${taskHref(one.taskId)}#report">Report</a> ` : ""}` +
18568
18951
  `<span class="mono meta">${one.provider === null ? "" : escape(one.provider)}${one.model === null ? "" : ` · ${escape(one.model)}`}` +
18569
18952
  `${one.ranMinutes === null ? "" : ` · ${one.ranMinutes}m`}` +
18570
18953
  ` · ${escape(runCostWords({ authMode: one.authMode, costUsd: one.costUsd, tokensIn: null, tokensOut: null }))}` +
@@ -18578,11 +18961,11 @@ function portfolioOverview(data) {
18578
18961
  })()}</span></p>`)
18579
18962
  .join("\n");
18580
18963
  return [
18581
- `<div class="control-room-head"><div><h1>portfolio</h1>` +
18582
- `<p class="meta">every project and live build in one place</p></div>` +
18583
- `<div class="actions"><a class="badge" href="/tasks/new">+ new task</a><a class="badge" href="/board?scope=all">full board →</a></div></div>`,
18964
+ `<div class="control-room-head"><div><h1>Portfolio</h1>` +
18965
+ `<p class="meta">Every project and live build in one place</p></div>` +
18966
+ `<div class="actions"><a class="badge" href="/tasks/new">+ New task</a><a class="badge" href="/board?scope=all">Full board →</a></div></div>`,
18584
18967
  data.saturated ? `<div class="problem">This overview reached its 200-task display cap; the task list holds the rest.</div>` : "",
18585
- `<h2>waits on you</h2>`,
18968
+ `<h2>Waits on you</h2>`,
18586
18969
  waitCount === 0
18587
18970
  ? `<div class="answered"><strong>Nothing needs you.</strong> <span class="meta">You can leave this open; live state updates in the rail.</span></div>`
18588
18971
  : [
@@ -18594,17 +18977,17 @@ function portfolioOverview(data) {
18594
18977
  `<p class="meta"><a href="/next">clear the queue → one thing at a time</a></p>`,
18595
18978
  ].filter(part => part !== "").join("\n"),
18596
18979
  data.gapsProject === null
18597
- ? `<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>`
18980
+ ? `<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>`
18598
18981
  : "",
18599
- `<h2>project pulse</h2>`,
18982
+ `<h2>Project pulse</h2>`,
18600
18983
  `<p class="hint">one row per repository</p>`,
18601
18984
  pulseRows.length === 0
18602
18985
  ? `<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>`
18603
18986
  : `<div class="workspace-pulse">${workspaceRows}</div>`,
18604
- `<h2>the last 24 hours</h2>`,
18987
+ `<h2>The last 24 hours</h2>`,
18605
18988
  `<p class="hint">runs started in the last 24 hours</p>`,
18606
18989
  data.runs24.length === 0
18607
- ? `<p class="meta">no runs started in the window</p>`
18990
+ ? `<p class="meta">No runs started in the window</p>`
18608
18991
  : `<p class="row"><span class="meta">runs started</span> <span class="mono">${data.runs24.length}</span></p>` +
18609
18992
  `<p class="row"><span class="meta">outcomes</span> <span class="mono">${escape(outcomeWords)}</span></p>` +
18610
18993
  `<p class="row"><span class="meta">spend</span> <span class="mono">${escape(spendLine(summary))}</span></p>` +
@@ -18614,10 +18997,10 @@ function portfolioOverview(data) {
18614
18997
  (summary.tokens > 0
18615
18998
  ? `<p class="row"><span class="meta">tokens</span> <span class="mono">${summary.tokens.toLocaleString()}</span></p>`
18616
18999
  : ""),
18617
- `<h2>running</h2>`,
18618
- data.live.length === 0 ? `<p class="meta">no agent is working right now</p>` : liveRows,
18619
- `<h2>terminal runs started in the last 24 hours</h2>`,
18620
- data.ledger.length === 0 ? `<p class="meta">none yet</p>` : ledgerRows,
19000
+ `<h2>Running</h2>`,
19001
+ data.live.length === 0 ? `<p class="meta">No agent is working right now</p>` : liveRows,
19002
+ `<h2>Terminal runs started in the last 24 hours</h2>`,
19003
+ data.ledger.length === 0 ? `<p class="meta">None yet</p>` : ledgerRows,
18621
19004
  ].join("\n");
18622
19005
  }
18623
19006
  /** Dollars for a screen: micro-USD stated as money, unknowables in words. */
@@ -18713,7 +19096,7 @@ function contestPage(chrome, data) {
18713
19096
  const glance = summaries.length < 2
18714
19097
  ? ""
18715
19098
  : `<div class="scroll-x"><table class="contest-glance">` +
18716
- `<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>` +
19099
+ `<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>` +
18717
19100
  `<tr><td class="meta">outcome</td>${summaries.map(one => `<td>${escape(one.outcome)}</td>`).join("")}</tr>` +
18718
19101
  `<tr><td class="meta">changed</td>${summaries.map(one => `<td>${escape(one.diffWords)}</td>`).join("")}</tr>` +
18719
19102
  `<tr><td class="meta">time</td>${summaries.map(one => `<td>${one.minutes === null ? "—" : `${one.minutes} min`}</td>`).join("")}</tr>` +
@@ -18726,22 +19109,22 @@ function contestPage(chrome, data) {
18726
19109
  const agent = summary.agent;
18727
19110
  const parts = [
18728
19111
  `<div class="card${winner ? " picked" : ""}">`,
18729
- `<p><strong>agent ${contestant.ordinal}</strong> <span class="meta">${escape(contestant.provider)} · ${escape(contestant.model)}</span>` +
18730
- `${winner ? ` <span class="badge badge-done">picked</span>` : ""}</p>`,
19112
+ `<p><strong>Agent ${contestant.ordinal}</strong> <span class="meta">${escape(contestant.provider)} · ${escape(contestant.model)}</span>` +
19113
+ `${winner ? ` <span class="badge badge-done">Picked</span>` : ""}</p>`,
18731
19114
  `<p class="row">${escape(outcome)}` +
18732
19115
  `${minutes === null ? "" : ` <span class="meta">· ${minutes} min</span>`}` +
18733
19116
  `${asked > 0 ? ` <span class="meta">· asked ${asked} question${asked > 1 ? "s" : ""}</span>` : ""}` +
18734
19117
  `${run === null ? "" : ` <span class="meta">· <a href="/r/${run.id}">the build</a></span>`}</p>`,
18735
- `<p class="row"><strong>cost</strong> ${escape(cost)}</p>`,
18736
- run === null || run.handoff === null ? "" : `<p><strong>its own conclusion</strong></p><p class="recap">${escape(run.handoff)}</p>`,
18737
- diff === null ? `<p class="meta">no diff was captured</p>` : terminalDiffCard(diff, run === null ? 0 : run.id),
19118
+ `<p class="row"><strong>Cost</strong> ${escape(cost)}</p>`,
19119
+ run === null || run.handoff === null ? "" : `<p><strong>Its own conclusion</strong></p><p class="recap">${escape(run.handoff)}</p>`,
19120
+ diff === null ? `<p class="meta">No diff was captured</p>` : terminalDiffCard(diff, run === null ? 0 : run.id),
18738
19121
  ];
18739
19122
  if (picking) {
18740
19123
  if (agent.pickable) {
18741
- 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>`);
19124
+ 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>`);
18742
19125
  }
18743
19126
  else {
18744
- parts.push(`<p class="meta">cannot be picked — ${escape(agent.unpickableReason ?? "")}</p>`);
19127
+ parts.push(`<p class="meta">Cannot be picked — ${escape(agent.unpickableReason ?? "")}</p>`);
18745
19128
  }
18746
19129
  }
18747
19130
  parts.push(`</div>`);
@@ -18753,11 +19136,11 @@ function contestPage(chrome, data) {
18753
19136
  `only one result will be kept as the task's outcome; the rest stay archived with their evidence</p>`,
18754
19137
  data.problem === null ? "" : `<div class="problem">${escape(data.problem)}</div>`,
18755
19138
  `<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>`,
18756
- contest.pickedBy === null ? "" : `<p class="meta">picked by ${escape(contest.pickedBy)} at ${whenTime(contest.pickedAt ?? "")}</p>`,
19139
+ contest.pickedBy === null ? "" : `<p class="meta">Picked by ${escape(contest.pickedBy)} at ${whenTime(contest.pickedAt ?? "")}</p>`,
18757
19140
  contest.kind === "comparison"
18758
- ? `<p class="row"><strong>spend</strong> ${escape(contestDollars(data.totalMicrousd))} measured on the lanes that report dollars` +
19141
+ ? `<p class="row"><strong>Spend</strong> ${escape(contestDollars(data.totalMicrousd))} measured on the lanes that report dollars` +
18759
19142
  `${data.anyUnknown ? ` <span class="meta">— the rest report tokens only</span>` : ""}</p>`
18760
- : `<p class="row"><strong>charged so far</strong> ${escape(contestDollars(data.totalMicrousd))}` +
19143
+ : `<p class="row"><strong>Charged so far</strong> ${escape(contestDollars(data.totalMicrousd))}` +
18761
19144
  `${data.anyUnknown ? ` <span class="meta">— includes at least one agent charged its full reservation because the exact figure was unknowable</span>` : ""}</p>`,
18762
19145
  glance,
18763
19146
  `<div class="contest-compare">${summaries.map(agentCard).join("\n")}</div>`,
@@ -18767,9 +19150,9 @@ function contestPage(chrome, data) {
18767
19150
  `<form method="post" action="/contest/${contest.id}/arm" class="inline">`,
18768
19151
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
18769
19152
  `<input type="hidden" name="act" value="abandon">`,
18770
- `<button type="submit">abandon the ${contestNoun(contest.kind)}…</button>`,
19153
+ `<button type="submit">Abandon the ${contestNoun(contest.kind)}…</button>`,
18771
19154
  `</form>`,
18772
- `<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>`,
19155
+ `<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>`,
18773
19156
  `</div>`,
18774
19157
  ].join("\n")
18775
19158
  : "",
@@ -18786,7 +19169,7 @@ function contestCeremonyPage(chrome, data) {
18786
19169
  const back = `<p class="meta"><a href="/contest/${data.contestId}">back — decide nothing</a></p>`;
18787
19170
  if (data.kind === "abandon") {
18788
19171
  return screen("tournament", [
18789
- `<h1>abandon this ${contestNoun(data.contestKind)}?</h1>`,
19172
+ `<h1>Abandon this ${contestNoun(data.contestKind)}?</h1>`,
18790
19173
  `<div class="card">`,
18791
19174
  `<p class="row">${data.agents} agents ${data.contestKind === "comparison" ? "built independently" : "raced"} on <strong>${escape(data.taskTitle)}</strong>. Abandoning picks nothing:</p>`,
18792
19175
  `<p class="row">— the task is marked <strong>failed</strong> and can be re-queued later</p>`,
@@ -18798,20 +19181,20 @@ function contestCeremonyPage(chrome, data) {
18798
19181
  `<form method="post" action="/contest/${data.contestId}/abandon" class="card">`,
18799
19182
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
18800
19183
  `<input type="hidden" name="nonce" value="${escape(data.nonceValue)}">`,
18801
- `<label>your password, typed again<input type="password" name="token" autocomplete="current-password"></label>`,
18802
- `<button type="submit" class="danger">abandon the ${contestNoun(data.contestKind)}</button>`,
19184
+ `<label>Your password, typed again<input type="password" name="token" autocomplete="current-password"></label>`,
19185
+ `<button type="submit" class="danger">Abandon the ${contestNoun(data.contestKind)}</button>`,
18803
19186
  `</form>`,
18804
19187
  back,
18805
19188
  ].join("\n"), { chrome });
18806
19189
  }
18807
19190
  const agent = data.chosen;
18808
19191
  if (agent === undefined || agent.run === null)
18809
- return screen("tournament", `<p class="meta">nothing to confirm</p>`, { chrome });
19192
+ return screen("tournament", `<p class="meta">Nothing to confirm</p>`, { chrome });
18810
19193
  const run = agent.run;
18811
19194
  return screen("tournament", [
18812
- `<h1>pick agent ${agent.contestant.ordinal}'s result?</h1>`,
19195
+ `<h1>Pick agent ${agent.contestant.ordinal}'s result?</h1>`,
18813
19196
  `<div class="card">`,
18814
- `<p class="row"><strong>agent ${agent.contestant.ordinal}</strong> — ${escape(agent.contestant.provider)} · ${escape(agent.contestant.model)}</p>`,
19197
+ `<p class="row"><strong>Agent ${agent.contestant.ordinal}</strong> — ${escape(agent.contestant.provider)} · ${escape(agent.contestant.model)}</p>`,
18815
19198
  `<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>`,
18816
19199
  run.outcome === "no-change"
18817
19200
  ? `<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>`
@@ -18821,8 +19204,8 @@ function contestCeremonyPage(chrome, data) {
18821
19204
  ? ""
18822
19205
  : `<p class="row">the diff being picked: <span class="mono">${escape(agent.diff.sha256.slice(0, 12))}</span> · ${agent.diff.bytesStored} bytes, verified</p>`,
18823
19206
  data.publication === null || data.publication === undefined
18824
- ? `<p class="row"><strong>nothing is published</strong> — the branch stays local to this machine</p>`
18825
- : `<p class="row"><strong>a ${data.publication.draft ? "draft " : ""}pull request will be opened</strong> on ` +
19207
+ ? `<p class="row"><strong>Nothing is published</strong> — the branch stays local to this machine</p>`
19208
+ : `<p class="row"><strong>A ${data.publication.draft ? "draft " : ""}pull request will be opened</strong> on ` +
18826
19209
  `<span class="mono">${escape(data.publication.githubRepo)}</span> from <span class="mono">${escape(data.publication.branch)}</span></p>`,
18827
19210
  data.contestKind === "comparison"
18828
19211
  ? `<p class="row">this agent's spend: ${agent.contestant.unknownSpend ? "unmeasured — its harness reports tokens, not dollars" : `${escape(contestDollars(agent.contestant.measuredMicrousd))} measured`}` +
@@ -18836,8 +19219,8 @@ function contestCeremonyPage(chrome, data) {
18836
19219
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
18837
19220
  `<input type="hidden" name="nonce" value="${escape(data.nonceValue)}">`,
18838
19221
  `<input type="hidden" name="choice" value="${agent.contestant.id}">`,
18839
- `<label>your password, typed again<input type="password" name="token" autocomplete="current-password"></label>`,
18840
- `<div class="sticky-actions"><button type="submit">yes — pick this result</button></div>`,
19222
+ `<label>Your password, typed again<input type="password" name="token" autocomplete="current-password"></label>`,
19223
+ `<div class="sticky-actions"><button type="submit">Yes — pick this result</button></div>`,
18841
19224
  `</form>`,
18842
19225
  back,
18843
19226
  ].filter(one => one !== "").join("\n"), { chrome });
@@ -18928,18 +19311,18 @@ function githubReposPage(chrome, data) {
18928
19311
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
18929
19312
  `<input type="hidden" name="repo" value="${escape(nameWithOwner)}">`,
18930
19313
  `<input type="hidden" name="root" value="0">`,
18931
- `<button type="submit">clone here →</button>`,
19314
+ `<button type="submit">Clone here →</button>`,
18932
19315
  `</form>`,
18933
19316
  ].join("");
18934
19317
  const rows = !data.listed.ok
18935
19318
  ? `<p class="meta">${escape(data.listed.message)}</p>`
18936
19319
  : data.listed.repos.length === 0
18937
- ? `<p class="meta">the signed-in GitHub account has no repositories to list</p>`
19320
+ ? `<p class="meta">The signed-in GitHub account has no repositories to list</p>`
18938
19321
  : data.listed.repos
18939
19322
  .map(repo => {
18940
19323
  const localPath = data.local.get(repo.nameWithOwner.toLowerCase()) ?? null;
18941
19324
  const action = localPath !== null && data.openProject === localPath
18942
- ? `<span class="badge badge-done">open now</span>`
19325
+ ? `<span class="badge badge-done">Open now</span>`
18943
19326
  : localPath !== null
18944
19327
  ? openForm(localPath, data.registered.has(localPath) ? "open →" : "add + open →")
18945
19328
  : data.cloneReady
@@ -18947,10 +19330,10 @@ function githubReposPage(chrome, data) {
18947
19330
  : `<span class="meta">choose a projects folder first</span>`;
18948
19331
  return [
18949
19332
  `<div class="card project-card">`,
18950
- `<div class="row"><strong>${escape(repo.nameWithOwner)}</strong>${repo.isPrivate ? ` <span class="badge">private</span>` : ""}`,
19333
+ `<div class="row"><strong>${escape(repo.nameWithOwner)}</strong>${repo.isPrivate ? ` <span class="badge">Private</span>` : ""}`,
18951
19334
  `<span class="right">${action}</span></div>`,
18952
19335
  localPath === null
18953
- ? `<p class="meta">not on this machine yet${/^\d{4}-\d{2}-\d{2}T/.test(repo.updatedAt) ? ` · pushed ${whenTime(repo.updatedAt)}` : ""}</p>`
19336
+ ? `<p class="meta">Not on this machine yet${/^\d{4}-\d{2}-\d{2}T/.test(repo.updatedAt) ? ` · pushed ${whenTime(repo.updatedAt)}` : ""}</p>`
18954
19337
  : `<p class="meta mono" style="overflow-wrap:anywhere;margin:.2rem 0">${escape(localPath)}</p>`,
18955
19338
  repo.description === "" ? "" : `<p class="meta">${escape(repo.description)}</p>`,
18956
19339
  `</div>`,
@@ -18958,8 +19341,8 @@ function githubReposPage(chrome, data) {
18958
19341
  })
18959
19342
  .join("\n");
18960
19343
  return screen("projects", [
18961
- `<h1>your GitHub repositories</h1>`,
18962
- `<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>`,
19344
+ `<h1>Your GitHub repositories</h1>`,
19345
+ `<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>`,
18963
19346
  rows,
18964
19347
  `<p class="row" style="margin-top:.6rem"><a class="badge" href="/projects">← back to projects</a></p>`,
18965
19348
  ].join("\n"), { chrome });
@@ -18974,16 +19357,16 @@ function projectsPage(chrome, recent, candidates, open, csrf, problem, _unscoped
18974
19357
  ? `<p class="meta">${escape(onboard.why)}</p>`
18975
19358
  : [
18976
19359
  `<details class="project-add-more"${onboard.record === null ? '' : ' open'}><summary>${onboard.record === null ? 'Paste a GitHub link' : 'Review repository'}</summary>`,
18977
- `<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>`,
19360
+ `<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>`,
18978
19361
  onboard.record === null
18979
19362
  ? [
18980
19363
  `<form method="post" action="/projects/onboard-preview" class="card">`,
18981
19364
  `<input type="hidden" name="csrf" value="${escape(csrf)}">`,
18982
- `<label>repository <input type="text" name="repo" placeholder="owner/name or https://github.com/owner/name"></label>`,
19365
+ `<label>Repository <input type="text" name="repo" placeholder="owner/name or https://github.com/owner/name"></label>`,
18983
19366
  onboard.roots.length > 1
18984
- ? `<label>into <select name="root">${onboard.roots.map((one, index) => `<option value="${index}">${escape(one)}</option>`).join("")}</select></label>`
18985
- : `<input type="hidden" name="root" value="0"><p class="meta">into ${escape(onboard.roots[0] ?? "")}</p>`,
18986
- `<button type="submit">preview</button>`,
19367
+ ? `<label>Into <select name="root">${onboard.roots.map((one, index) => `<option value="${index}">${escape(one)}</option>`).join("")}</select></label>`
19368
+ : `<input type="hidden" name="root" value="0"><p class="meta">Into ${escape(onboard.roots[0] ?? "")}</p>`,
19369
+ `<button type="submit">Preview</button>`,
18987
19370
  `</form>`,
18988
19371
  ].join("\n")
18989
19372
  : [
@@ -18995,8 +19378,8 @@ function projectsPage(chrome, recent, candidates, open, csrf, problem, _unscoped
18995
19378
  onboard.record[1].large
18996
19379
  ? `<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>`
18997
19380
  : "",
18998
- `<label>your password, typed again <input type="password" name="token" autocomplete="current-password"></label>`,
18999
- `<div class="sticky-actions"><button type="submit">clone and open</button></div>`,
19381
+ `<label>Your password, typed again <input type="password" name="token" autocomplete="current-password"></label>`,
19382
+ `<div class="sticky-actions"><button type="submit">Clone and open</button></div>`,
19000
19383
  `</form>`,
19001
19384
  `</div>`,
19002
19385
  ].join("\n"),
@@ -19038,7 +19421,7 @@ function projectsPage(chrome, recent, candidates, open, csrf, problem, _unscoped
19038
19421
  `<div class="row">${open !== null && open === one.path
19039
19422
  ? `<a class="project-name" href="${escape(returnTo)}"><strong>${escape(one.name)}</strong></a>`
19040
19423
  : openForm(one.path, one.name, returnTo, "project-name")}`,
19041
- `<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>`,
19424
+ `<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>`,
19042
19425
  `<p class="meta mono" style="overflow-wrap:anywhere;margin:.2rem 0">${escape(one.path)}</p>`,
19043
19426
  `<p class="row" style="gap:.35rem;flex-wrap:wrap">${peekChips(one.path, one.peek)}</p>`,
19044
19427
  `<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>`,
@@ -19080,13 +19463,13 @@ function projectsPage(chrome, recent, candidates, open, csrf, problem, _unscoped
19080
19463
  `<form method="post" action="/projects/open" class="card">`,
19081
19464
  `<input type="hidden" name="csrf" value="${escape(csrf)}">`,
19082
19465
  `<input type="hidden" name="return" value="${escape(returnTo)}">`,
19083
- `<label>path on this server<input type="text" name="path" placeholder="/Users/you/code/your-repo"></label>`,
19084
- `<button type="submit">open project</button>`,
19466
+ `<label>Path on this server<input type="text" name="path" placeholder="/Users/you/code/your-repo"></label>`,
19467
+ `<button type="submit">Open project</button>`,
19085
19468
  `</form></details>`,
19086
19469
  ].join("\n");
19087
19470
  const addCard = [
19088
19471
  `<div class="card project-add-card">`,
19089
- `<h2 class="project-add-title">add a project</h2>`,
19472
+ `<h2 class="project-add-title">Add a project</h2>`,
19090
19473
  `<div class="project-add-actions">`,
19091
19474
  browsable
19092
19475
  ? addAction("/projects/browse", FOLDER_PATHS, "Choose a local folder", "Browse the project folders on this machine")
@@ -19107,8 +19490,8 @@ function projectsPage(chrome, recent, candidates, open, csrf, problem, _unscoped
19107
19490
  recentItems.length === 0 && candidateItems.length === 0
19108
19491
  ? `<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>`
19109
19492
  : "",
19110
- recentItems.length > 0 ? `<h2>recent</h2>${cards(recentItems)}` : "",
19111
- candidateItems.length > 0 ? `<h2>available</h2>${cards(candidateItems)}` : "",
19493
+ recentItems.length > 0 ? `<h2>Recent</h2>${cards(recentItems)}` : "",
19494
+ candidateItems.length > 0 ? `<h2>Available</h2>${cards(candidateItems)}` : "",
19112
19495
  addCard,
19113
19496
  ].join("\n"), { chrome, workspace: { view: {
19114
19497
  kind: "projects", choosing, problem, returnTo,
@@ -19280,25 +19663,25 @@ const NAV_ICONS = {
19280
19663
  function workToolRows(scoped = false, _offersChat = false, offersCode = false) {
19281
19664
  const rows = [
19282
19665
  ...(offersCode ? [{ key: "code", href: "/code", label: "Coding sessions", hint: "Direct coding sessions and their saved results" }] : []),
19283
- { key: "inbox", href: "/inbox", label: "inbox", hint: "questions, approvals, and repairs to act on" },
19284
- { key: "board", href: "/board", label: "board", hint: "lanes by state, with the order view" },
19285
- { key: "tasks", href: "/tasks", label: "task list", hint: "everything, filterable by state" },
19286
- { key: "recipes", href: "/recipes", label: "recipes", hint: "choose, customize, and reuse a workflow" },
19287
- { key: "routines", href: "/routines", label: "routines", hint: "scheduled tracks and their firings" },
19288
- { key: "workbench", href: "/workbench", label: "portfolio", hint: "every project and live build in one place" },
19289
- { key: "ledger", href: "/ledger", label: "action ledger", hint: "who acted, what happened, and the result" },
19290
- { key: "spend", href: "/spend", label: "spend", hint: "what agent work cost, and monthly budgets" },
19666
+ { key: "inbox", href: "/inbox", label: "Inbox", hint: "questions, approvals, and repairs to act on" },
19667
+ { key: "board", href: "/board", label: "Board", hint: "lanes by state, with the order view" },
19668
+ { key: "tasks", href: "/tasks", label: "Task list", hint: "everything, filterable by state" },
19669
+ { key: "recipes", href: "/recipes", label: "Recipes", hint: "choose, customize, and reuse a workflow" },
19670
+ { key: "routines", href: "/routines", label: "Routines", hint: "scheduled tracks and their firings" },
19671
+ { key: "workbench", href: "/workbench", label: "Portfolio", hint: "every project and live build in one place" },
19672
+ { key: "ledger", href: "/ledger", label: "Action ledger", hint: "who acted, what happened, and the result" },
19673
+ { key: "spend", href: "/spend", label: "Spend", hint: "what agent work cost, and monthly budgets" },
19291
19674
  ];
19292
19675
  return scoped ? rows.filter(row => row.key !== "workbench" && row.key !== "spend") : rows;
19293
19676
  }
19294
19677
  function settingsRows(scoped = false, offersSettings = false) {
19295
19678
  const rows = [
19296
- ...(offersSettings ? [{ key: "settings", href: "/settings", label: "settings", hint: "agent defaults, alerts, credentials" }] : []),
19297
- { key: "fleet", href: "/fleet", label: "fleet", hint: "who is working, and on what" },
19298
- { key: "caps", href: "/caps", label: "requirements", hint: "tools and credentials builds need" },
19299
- { key: "people", href: "/people", label: "people", hint: "who can sign in, and what they have done" },
19300
- { key: "mode", href: "/mode", label: "operating mode", hint: "the signed posture this repository runs under" },
19301
- { key: "system", href: "/system", label: "system", hint: "workers, providers, and grants" },
19679
+ ...(offersSettings ? [{ key: "settings", href: "/settings", label: "Settings", hint: "agent defaults, alerts, credentials" }] : []),
19680
+ { key: "fleet", href: "/fleet", label: "Fleet", hint: "who is working, and on what" },
19681
+ { key: "caps", href: "/caps", label: "Requirements", hint: "tools and credentials builds need" },
19682
+ { key: "people", href: "/people", label: "People", hint: "who can sign in, and what they have done" },
19683
+ { key: "mode", href: "/mode", label: "Operating mode", hint: "the signed posture this repository runs under" },
19684
+ { key: "system", href: "/system", label: "System", hint: "workers, providers, and grants" },
19302
19685
  ];
19303
19686
  return scoped ? rows.filter(row => row.key === "people" || row.key === "settings") : rows;
19304
19687
  }
@@ -19328,10 +19711,10 @@ function queueCard(one, csrf, revision, queueRevision, workers, column) {
19328
19711
  // and the reservation owner. Money is not in this query and is not
19329
19712
  // invented here — it stays on the task page, labeled.
19330
19713
  const state = one.taken
19331
- ? `<span class="badge">being taken — keeps its claim</span>`
19714
+ ? `<span class="badge">Being taken — keeps its claim</span>`
19332
19715
  : column === "anyone"
19333
- ? `<span class="badge">queued</span>`
19334
- : `<span class="badge">reserved for ${escape(column)}</span>`;
19716
+ ? `<span class="badge">Queued</span>`
19717
+ : `<span class="badge">Reserved for ${escape(column)}</span>`;
19335
19718
  const chips = ` ${state}` +
19336
19719
  `${one.dispatch === null || one.dispatch === undefined ? "" : ` <a class="badge" href="${taskHref(one.id)}" title="${escape(one.dispatch.summary)}">${escape(dispatchHeadline(one.dispatch))}</a>`}`;
19337
19720
  const hidden = `<input type="hidden" name="csrf" value="${escape(csrf)}">` +
@@ -19348,7 +19731,7 @@ function queueCard(one, csrf, revision, queueRevision, workers, column) {
19348
19731
  `<select name="column" aria-label="reserve for">` +
19349
19732
  `<option value="anyone"${column === "anyone" ? " selected" : ""}>anyone</option>` +
19350
19733
  workers.filter(worker => !worker.retired).map(worker => `<option value="${escape(worker.name)}"${column === worker.name ? " selected" : ""}>${escape(worker.name)}</option>`).join("") +
19351
- `</select><button type="submit">move</button></form>`;
19734
+ `</select><button type="submit">Move</button></form>`;
19352
19735
  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)}"`}>` +
19353
19736
  `<p class="row">${one.taken ? "" : `${GRIP_HANDLE}`}` +
19354
19737
  `<a href="${taskHref(one.id)}">${escape(one.title)}</a>${chips}</p>` +
@@ -19370,22 +19753,22 @@ function queueBody(tasks, workers, csrf, revision, queueRevision) {
19370
19753
  ? `<p class="meta lane-empty">${escape(empty)}</p>`
19371
19754
  : rows
19372
19755
  .map((one, index) => index === 0 && key === "anyone" && allWorkersBusy && !one.taken
19373
- ? 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">`)
19756
+ ? 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">`)
19374
19757
  : queueCard(one, csrf, revision, queueRevision, workers, key))
19375
19758
  .join("\n")) +
19376
19759
  `</section>`;
19377
19760
  const noteForm = (worker) => worker.retired
19378
- ? `<p class="meta">this worker is retired — drag these elsewhere, or register the name again</p>`
19761
+ ? `<p class="meta">This worker is retired — drag these elsewhere, or register the name again</p>`
19379
19762
  : `<p class="meta mono">${worker.building} building in this project · unattended capacity ${worker.capacity}</p>` +
19380
19763
  `<form method="post" action="/queue/note" class="row">` +
19381
19764
  `<input type="hidden" name="csrf" value="${escape(csrf)}">` +
19382
19765
  `<input type="hidden" name="projectRevision" value="${revision}">` +
19383
19766
  `<input type="hidden" name="runner" value="${escape(worker.name)}">` +
19384
19767
  `<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">` +
19385
- `<button type="submit">save</button></form>` +
19386
- `<p class="meta">takes from the shared queue when this column is empty</p>`;
19768
+ `<button type="submit">Save</button></form>` +
19769
+ `<p class="meta">Takes from the shared queue when this column is empty</p>`;
19387
19770
  return (`<div class="lanes" data-queue-revision="${queueRevision}">` +
19388
- 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") +
19771
+ 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") +
19389
19772
  workers
19390
19773
  .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"))
19391
19774
  .join("\n") +
@@ -19407,7 +19790,7 @@ function fleetBody(queued, building, runners, csrf, queueRevision, visibleRepo)
19407
19790
  const live = building.filter(one => one.runner === runner.name);
19408
19791
  const retired = runner.retiredAt !== null;
19409
19792
  const head = retired
19410
- ? `<p class="meta">this worker is retired — drag these elsewhere, or register the name again</p>`
19793
+ ? `<p class="meta">This worker is retired — drag these elsewhere, or register the name again</p>`
19411
19794
  : `<form method="post" action="/queue/note" class="row">` +
19412
19795
  `<input type="hidden" name="csrf" value="${escape(csrf)}">` +
19413
19796
  `<input type="hidden" name="from" value="fleet">` +
@@ -19424,9 +19807,9 @@ function fleetBody(queued, building, runners, csrf, queueRevision, visibleRepo)
19424
19807
  `<p class="row">${one.taken ? "" : `${GRIP_HANDLE}`}` +
19425
19808
  `<a href="${taskHref(one.id)}">${escape(one.title)}</a></p>` +
19426
19809
  `<p class="row meta"><span class="mono">${escape(one.id)}</span>${chip(one.repo)}` +
19427
- `${one.approved ? "" : ` <span class="badge">unapproved scope</span>`}` +
19428
- `${one.blockers > 0 ? ` <span class="badge">waits for ${one.blockers}</span>` : ""}` +
19429
- `${one.taken ? ` <span class="badge">being taken</span>` : ""}</p></div>`)
19810
+ `${one.approved ? "" : ` <span class="badge">Unapproved scope</span>`}` +
19811
+ `${one.blockers > 0 ? ` <span class="badge">Waits for ${one.blockers}</span>` : ""}` +
19812
+ `${one.taken ? ` <span class="badge">Being taken</span>` : ""}</p></div>`)
19430
19813
  .join("\n");
19431
19814
  const empty = live.length === 0 && own.length === 0
19432
19815
  ? `<p class="meta lane-empty">${retired ? "nothing left" : "idle — will take from the shared queue"}</p>`
@@ -19441,12 +19824,12 @@ function fleetBody(queued, building, runners, csrf, queueRevision, visibleRepo)
19441
19824
  `<p class="row">${one.taken ? "" : `${GRIP_HANDLE}`}` +
19442
19825
  `<a href="${taskHref(one.id)}">${escape(one.title)}</a></p>` +
19443
19826
  `<p class="row meta"><span class="mono">${escape(one.id)}</span>${chip(one.repo)}` +
19444
- `${one.approved ? "" : ` <span class="badge">unapproved scope</span>`}` +
19445
- `${one.blockers > 0 ? ` <span class="badge">waits for ${one.blockers}</span>` : ""}` +
19446
- `${one.taken ? ` <span class="badge">being taken</span>` : ""}</p></div>`)
19827
+ `${one.approved ? "" : ` <span class="badge">Unapproved scope</span>`}` +
19828
+ `${one.blockers > 0 ? ` <span class="badge">Waits for ${one.blockers}</span>` : ""}` +
19829
+ `${one.taken ? ` <span class="badge">Being taken</span>` : ""}</p></div>`)
19447
19830
  .join("\n");
19448
- const sharedLane = `<section class="lane queue-column" data-column="anyone"><h2>shared queue</h2>` +
19449
- `<p class="meta">any free worker takes from here, top first</p>${sharedCards}` +
19831
+ const sharedLane = `<section class="lane queue-column" data-column="anyone"><h2>Shared queue</h2>` +
19832
+ `<p class="meta">Any free worker takes from here, top first</p>${sharedCards}` +
19450
19833
  (shared.length === 0 ? `<p class="meta lane-empty">nothing waiting — every task is reserved or running</p>` : "") +
19451
19834
  `</section>`;
19452
19835
  return (`<div class="lanes" data-queue-revision="${queueRevision}">` +
@@ -19588,13 +19971,15 @@ function newTaskPage(chrome, project, csrf, projectRevision, problem, candidates
19588
19971
  * brief, but the console renders its known structure as a useful control
19589
19972
  * surface. Historical free-form plans keep their safe plain-text fallback. */
19590
19973
  const MILESTONE_STATE_WORDS = {
19591
- pending: "pending",
19592
- current: "in progress",
19593
- completed: "completed",
19594
- blocked: "blocked",
19974
+ pending: "Pending",
19975
+ current: "In progress",
19976
+ completed: "Completed",
19977
+ blocked: "Blocked",
19595
19978
  };
19979
+ /** Badges, labels and buttons read in sentence case, whatever word a record stores. */
19980
+ const sentenceCase = (word) => word.charAt(0).toUpperCase() + word.slice(1);
19596
19981
  function milestoneStateWord(state) {
19597
- return MILESTONE_STATE_WORDS[state] ?? "pending";
19982
+ return MILESTONE_STATE_WORDS[state] ?? "Pending";
19598
19983
  }
19599
19984
  /** Adaptive execution plans (v44): the live milestone projection a running
19600
19985
  * (or finished) build has reported. A shared render used by BOTH the task
@@ -19653,9 +20038,9 @@ function planRevisionLedgerHtml(ledger, taskId, csrf) {
19653
20038
  `</div>`;
19654
20039
  const history = ledger.history.length <= 1
19655
20040
  ? ""
19656
- : `<details class="plan-revision-history"><summary>revision history (${ledger.history.length})</summary>` +
20041
+ : `<details class="plan-revision-history"><summary>Revision history (${ledger.history.length})</summary>` +
19657
20042
  ledger.history
19658
- .map(one => `<p class="row"><span class="mono">rev ${one.revision}</span> <span class="badge">${escape(one.status)}</span> ` +
20043
+ .map(one => `<p class="row"><span class="mono">rev ${one.revision}</span> <span class="badge">${escape(sentenceCase(one.status))}</span> ` +
19659
20044
  `<span class="meta">${escape(one.author)} · ${whenTime(one.createdAt)}</span> — ${escape(one.reason)}</p>`)
19660
20045
  .join("\n") +
19661
20046
  `</details>`;
@@ -19774,25 +20159,24 @@ function taskBodyParts(data) {
19774
20159
  if (liveRunId === null || liveRun === undefined)
19775
20160
  return "";
19776
20161
  const minutes = Math.max(0, Math.round((data.now.getTime() - new Date(liveRun.startedAt).getTime()) / 60_000));
19777
- const head = `<p><strong>build #${liveRun.id} · ${escape(liveRun.runner)} · running ` +
20162
+ const head = `<p><strong>Build #${liveRun.id} · ${escape(liveRun.runner)} · running ` +
19778
20163
  `<time data-elapsed-since="${escape(liveRun.startedAt)}">${minutes}m</time></strong></p>`;
19779
20164
  const door = `<p class="row"><a href="/r/${liveRun.id}">full build view →</a></p>`;
19780
20165
  if (data.degraded !== undefined) {
19781
20166
  return `<div class="card attempt-live" data-live-run="${liveRun.id}">${head}` +
19782
- `<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>`;
20167
+ `<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>`;
19783
20168
  }
19784
- const peek = data.peekable === true
19785
- ? `<p class="meta">what is changing right now</p>` +
19786
- `<div id="run-peek"><p class="meta">watching\u2026 the first look lands within 15 seconds</p></div>` +
19787
- `<p class="meta" id="run-peek-stamp"></p>`
19788
- : `<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>`;
19789
- const transcript = data.peekable !== true
19790
- ? ""
19791
- : liveRun.provider !== "claude"
19792
- ? `<p class="meta">the live transcript needs the claude harness for now \u2014 this build runs on ${escape(liveRun.provider)}</p>`
19793
- : `<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>` +
19794
- `<pre id="live-transcript" class="mono" style="max-height:18rem;overflow:auto;white-space:pre-wrap"></pre>` +
19795
- `<p class="meta" id="live-transcript-state"></p>`;
20169
+ // With the live file view off there is nothing live to show here: no panel (the build's own page says why).
20170
+ if (data.peekable !== true)
20171
+ return "";
20172
+ const peek = `<p class="meta">What is changing right now</p>` +
20173
+ `<div id="run-peek"><p class="meta">Watching\u2026 the first look lands within 15 seconds</p></div>` +
20174
+ `<p class="meta" id="run-peek-stamp"></p>`;
20175
+ const transcript = liveRun.provider !== "claude"
20176
+ ? `<p class="meta">The live transcript needs the claude harness for now \u2014 this build runs on ${escape(liveRun.provider)}</p>`
20177
+ : `<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>` +
20178
+ `<pre id="live-transcript" class="mono" style="max-height:18rem;overflow:auto;white-space:pre-wrap"></pre>` +
20179
+ `<p class="meta" id="live-transcript-state"></p>`;
19796
20180
  return `<div class="card attempt-live" data-live-run="${liveRun.id}">${head}${peek}${transcript}${door}</div>`;
19797
20181
  })();
19798
20182
  // One truthful answer to the first question on a queued task: "will this
@@ -19828,7 +20212,8 @@ function taskBodyParts(data) {
19828
20212
  case "approve-scope": return ` <a href="#approve">Review and sign the exact scope</a>.`;
19829
20213
  case "answer-decision": return ` <a href="#decisions">Answer the waiting question</a>.`;
19830
20214
  case "unhold": return ` Use <strong>Remove hold</strong> when it can continue.`;
19831
- case "retry-task": return ` Review the incident, then use <strong>retry</strong> below.`;
20215
+ // The reason is the detail itself; Retry is the status card's own act, said once.
20216
+ case "retry-task": return "";
19832
20217
  case "repair-capability": return ` <a href="/caps">Repair the requirement</a>.`;
19833
20218
  case "repair-dependency":
19834
20219
  return blocker?.admitted === true
@@ -19871,7 +20256,8 @@ function taskBodyParts(data) {
19871
20256
  : `<p class="meta">This task resumes automatically when the builder reconnects. You do not need to file or approve it again.</p>`) +
19872
20257
  `<p><a href="/system">See connection status →</a></p></div></details>`);
19873
20258
  }
19874
- const href = taskRecoveryHref(task.id, diagnosis);
20259
+ // Never a link to this page's own acts: Retry sits on the status card and below.
20260
+ const href = diagnosis.action === "retry-task" ? null : taskRecoveryHref(task.id, diagnosis);
19875
20261
  return href === null ? "" : `<a class="button-link dispatch-action-link" href="${href}">${escape(dispatchActionLabel(diagnosis))}</a>`;
19876
20262
  })();
19877
20263
  const positive = diagnosis.code === "running" || diagnosis.code === "ready" || diagnosis.code === "planning-ready" || diagnosis.code === "scouting-ready";
@@ -19893,7 +20279,7 @@ function taskBodyParts(data) {
19893
20279
  `<p class="meta">Saved assessment history. Current task status and checks are shown above.</p>` +
19894
20280
  (accepted === null ? "" : `<p class="meta">Accepted with an exception by ${escape(accepted.approver)}. Check results are unchanged.${accepted.note === null ? "" : ` ${escape(accepted.note)}`}</p>`) +
19895
20281
  (proof.proofReasons.length === 0 ? "" : `<ul>${[...new Set(proof.proofReasons.map(plainReasonWords))].map(reason => `<li>${escape(reason)}</li>`).join("")}</ul>`) +
19896
- criterionMatrixHtml(proof.proofMatrix, { compact: true, runId: proof.runId, links: proof.proofMatrixLinks }) +
20282
+ criterionMatrixHtml(proof.proofMatrix, { compact: true, runId: proof.runId, links: proof.proofMatrixLinks, verdict: proof.proofVerdict }) +
19897
20283
  semanticCoverageHtml(proof.proofMatrix, proof.qualityMode ?? "default") + `</div></details>`;
19898
20284
  }
19899
20285
  // v50: the independent review's own card — attempt counts, what is
@@ -19945,7 +20331,7 @@ function taskBodyParts(data) {
19945
20331
  const proofDetails = proof.proofMatrix.length === 0 && machineNote === "" && chainHtml === ""
19946
20332
  ? ""
19947
20333
  : `<details class="dispatch-proof-details"><summary>${proof.proofMatrix.length === 0 ? "Verification details" : `${proof.proofMatrix.length} requirement${proof.proofMatrix.length === 1 ? "" : "s"}`} · View details</summary>` +
19948
- `<div class="dispatch-proof-body">${criterionMatrixHtml(proof.proofMatrix, { compact: true, runId: proof.runId, links: proof.proofMatrixLinks })}${coverageHtml}${machineNote}${chainHtml}</div></details>`;
20334
+ `<div class="dispatch-proof-body">${criterionMatrixHtml(proof.proofMatrix, { compact: true, runId: proof.runId, links: proof.proofMatrixLinks, verdict: proof.proofVerdict })}${coverageHtml}${machineNote}${chainHtml}</div></details>`;
19949
20335
  // The publication fact is separate from the evidence fact: a PR or
19950
20336
  // an observed merge is named from its record; nothing is "deployed".
19951
20337
  const publicationWords = (data.publication === null || data.publication === undefined ? "" : ` ${escape(receiptPublicationWords(publicationFactsOf(data.publication)))}`) +
@@ -20005,26 +20391,30 @@ function taskBodyParts(data) {
20005
20391
  : `<form method="post" action="${taskHref(task.id)}/steer" class="row">` +
20006
20392
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
20007
20393
  `<input type="text" name="note" placeholder="guidance for the next attempt" aria-label="steering note" style="width:100%;max-width:28rem">` +
20008
- `<button type="submit">steer</button></form>` +
20009
- `<p class="meta">lands when the next attempt starts — a running agent is not interrupted, and a note cannot widen the approved scope</p>`;
20010
- const steeringCard = steerRows === "" && steerForm === "" ? "" : `<h2>steering</h2>${steerRows}${steerForm}`;
20394
+ `<button type="submit">Steer</button></form>` +
20395
+ `<p class="meta">Lands when the next attempt starts — a running agent is not interrupted, and a note cannot widen the approved scope</p>`;
20396
+ const steeringCard = steerRows === "" && steerForm === "" ? "" : `<h2>Steering</h2>${steerRows}${steerForm}`;
20011
20397
  const holds = data.holds.length === 0
20012
20398
  ? ""
20013
- : `<h2>holds</h2>` +
20399
+ : `<h2>Holds</h2>` +
20014
20400
  data.holds
20015
20401
  .map(hold => `<p class="row">${escape(holdOwnerWords(hold.ownerKind))} — ${escape(hold.reason)}` +
20016
20402
  `${hold.until === null ? "" : ` <span class="meta">until ${whenTime(hold.until)}</span>`}</p>`)
20017
20403
  .join("\n") +
20018
- `<p class="meta">only your hold can be lifted here — waits caused by questions, incidents, or retry delays clear on their own</p>`;
20404
+ `<p class="meta">Only your hold can be lifted here — waits caused by questions, incidents, or retry delays clear on their own</p>`;
20019
20405
  const approval = approvalOf(scope);
20406
+ // A refused save from the sheet's in-place editor reopens that editor with
20407
+ // its draft; the full scope form below stays as it was.
20408
+ const inlineScopeDraft = data.scopeDraft?.has("requirement-new") === true;
20409
+ const scopeDraft = inlineScopeDraft ? undefined : data.scopeDraft;
20020
20410
  const scopeCard = scope === null
20021
- ? `<p class="meta">no scope proposed — nothing builds this until one is approved</p>`
20411
+ ? `<p class="meta">No scope proposed — nothing builds this until one is approved</p>`
20022
20412
  : [
20023
20413
  `<div class="card">`,
20024
- `<p><strong>goal</strong></p><p class="recap">${escape(scope.goal)}</p>`,
20025
- scope.outOfScope === null ? "" : `<p><strong>not this</strong></p><p class="recap">${escape(scope.outOfScope)}</p>`,
20026
- scope.touches.length === 0 ? "" : `<p class="scope-paths"><strong>touches</strong> ${scope.touches.map(one => escape(one)).join(", ")}</p>`,
20027
- `<p><strong>quality</strong> ${escape(qualityModeTitle(scope.qualityMode ?? "default"))}</p>`,
20414
+ `<p><strong>Goal</strong></p><p class="recap">${escape(scope.goal)}</p>`,
20415
+ scope.outOfScope === null ? "" : `<p><strong>Not this</strong></p><p class="recap">${escape(scope.outOfScope)}</p>`,
20416
+ scope.touches.length === 0 ? "" : `<p class="scope-paths"><strong>Touches</strong> ${scope.touches.map(one => escape(one)).join(", ")}</p>`,
20417
+ `<p><strong>Quality</strong> ${escape(qualityModeTitle(scope.qualityMode ?? "default"))}</p>`,
20028
20418
  // The fallback chain, on the card the yes reads (Layer F): the
20029
20419
  // digest binds the WHOLE chain, so every entry — credential
20030
20420
  // included — is said before anyone signs.
@@ -20032,7 +20422,7 @@ function taskBodyParts(data) {
20032
20422
  const chain = chainFromJson(scope.proposedChainJson ?? null);
20033
20423
  if (chain === null || chain.length < 2)
20034
20424
  return "";
20035
- return `<p><strong>if its subscription runs out</strong></p><p class="recap">${chain
20425
+ return `<p><strong>If its subscription runs out</strong></p><p class="recap">${chain
20036
20426
  .slice(1)
20037
20427
  .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"}`)
20038
20428
  .join("; ")}</p>`;
@@ -20050,10 +20440,10 @@ function taskBodyParts(data) {
20050
20440
  // blank — the same rule as the revision brief.
20051
20441
  const reportCard = data.report === null || data.report === undefined
20052
20442
  ? data.deliverable === "report"
20053
- ? `<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>`
20443
+ ? `<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>`
20054
20444
  : ""
20055
20445
  : !data.report.ok
20056
- ? `<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>`
20446
+ ? `<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>`
20057
20447
  : [
20058
20448
  `<div class="card report">`,
20059
20449
  `<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>`,
@@ -20062,11 +20452,11 @@ function taskBodyParts(data) {
20062
20452
  ...(data.report.report.followUps.length === 0
20063
20453
  ? []
20064
20454
  : [
20065
- `<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>`,
20455
+ `<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>`,
20066
20456
  ...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>` +
20067
20457
  (data.csrf === "" || data.repo === null
20068
20458
  ? `<p class="meta">${data.repo === null ? "this task has no repository — file it by hand" : ""}</p>`
20069
- : `<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>`) +
20459
+ : `<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>`) +
20070
20460
  `</div>`),
20071
20461
  ]),
20072
20462
  `</div>`,
@@ -20098,7 +20488,7 @@ function taskBodyParts(data) {
20098
20488
  ? data.revision != null && !("problem" in data.revision)
20099
20489
  ? `<div class="card planner-status"><span class="planner-orb" aria-hidden="true"></span><p><strong>Updating the plan</strong>` +
20100
20490
  `<span class="meta">Adding your notes to the plan. You approve any change before it builds.</span></p></div>`
20101
- : `<div class="card planner-status"><span class="planner-orb" aria-hidden="true"></span><p><strong>planning requested</strong>` +
20491
+ : `<div class="card planner-status"><span class="planner-orb" aria-hidden="true"></span><p><strong>Planning requested</strong>` +
20102
20492
  `<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>`
20103
20493
  : ""
20104
20494
  : `${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">`}` +
@@ -20111,7 +20501,7 @@ function taskBodyParts(data) {
20111
20501
  ? ""
20112
20502
  : `<details class="plan-editor" id="plan-edit"><summary>Edit plan</summary><form method="post" action="${taskHref(task.id)}/plan-edit">` +
20113
20503
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}"><input type="hidden" name="saw-plan" value="${escape(data.planSha)}">` +
20114
- `<label>plan details <span class="meta">Keep the five headings. Approval locks this version for the build.</span>` +
20504
+ `<label>Plan details <span class="meta">Keep the five headings. Approval locks this version for the build.</span>` +
20115
20505
  `<textarea name="plan-document" rows="14">${escape(data.planDocument)}</textarea></label>` +
20116
20506
  `<button type="submit">Save plan</button></form></details>`) +
20117
20507
  `${approval.approved ? `</div></details>` : `</section>`}`;
@@ -20123,7 +20513,7 @@ function taskBodyParts(data) {
20123
20513
  const revisionCard = data.revision === null || data.revision === undefined
20124
20514
  ? ""
20125
20515
  : "problem" in data.revision
20126
- ? `<div class="card"><p><strong>revision brief</strong></p><p class="meta">${escape(data.revision.problem)}</p></div>`
20516
+ ? `<div class="card"><p><strong>Revision brief</strong></p><p class="meta">${escape(data.revision.problem)}</p></div>`
20127
20517
  : [
20128
20518
  `<div class="revision-card" data-revision-feedback>`,
20129
20519
  `<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>`,
@@ -20151,7 +20541,7 @@ function taskBodyParts(data) {
20151
20541
  : scope.profileState === "unresolved"
20152
20542
  ? `<div class="card approve-form" id="approve"><p><strong>This task is waiting on you: its scope cannot be approved yet.</strong></p>` +
20153
20543
  profileWords(scope) +
20154
- `<p class="ceremony-road"><a class="button-link" href="#scope">edit the scope to fix it →</a></p></div>`
20544
+ `<p class="ceremony-road"><a class="button-link" href="#scope">Edit the scope to fix it →</a></p></div>`
20155
20545
  : !consentDoor.open
20156
20546
  ? consentClosedHtml(task.id, consentDoor, "task")
20157
20547
  : approvalSheetHtml({
@@ -20174,6 +20564,11 @@ function taskBodyParts(data) {
20174
20564
  editHref: data.csrf !== "" && data.planSha != null && data.planDocument !== null ? "#plan-edit" : "#scope",
20175
20565
  notNowHref: "/work",
20176
20566
  sticky: data.dispatch?.action === "approve-scope",
20567
+ earlier: { active: data.assignment?.earlierActive ?? 0, running: data.assignment?.earlierRunning ?? 0 },
20568
+ // A tournament's terms live in the full scope editor; every other plan edits in place.
20569
+ edit: data.csrf === "" || (data.raceTerms ?? null) !== null ? null
20570
+ : { action: `${taskHref(task.id)}/scope`, draft: inlineScopeDraft ? data.scopeDraft ?? null : null, problem: inlineScopeDraft ? data.problem : null,
20571
+ open: data.editPlan === true, stepsHref: data.planSha != null && data.planDocument !== null ? "#plan-edit" : null },
20177
20572
  });
20178
20573
  // The attended road (Phase 2E): beside the approval, never replacing it.
20179
20574
  // The mint button leads to the CONFIRM screen where every term renders
@@ -20186,12 +20581,12 @@ function taskBodyParts(data) {
20186
20581
  : attended.open !== null
20187
20582
  ? [
20188
20583
  `<div class="card" data-attended="${escape(attended.open.id)}">`,
20189
- `<p><strong>attended session</strong> <span class="meta">${escape(attended.open.state)}</span></p>`,
20584
+ `<p><strong>Attended session</strong> <span class="meta">${escape(attended.open.state)}</span></p>`,
20190
20585
  `<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>`,
20191
- `<p class="meta">this page being open keeps it alive — close it and the session winds down within a minute</p>`,
20586
+ `<p class="meta">This page being open keeps it alive — close it and the session winds down within a minute</p>`,
20192
20587
  `<form method="post" action="${taskHref(task.id)}/attend-revoke" class="inline">`,
20193
20588
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
20194
- `<button type="submit">revoke — stop the session</button>`,
20589
+ `<button type="submit">Revoke — stop the session</button>`,
20195
20590
  `</form>`,
20196
20591
  `</div>`,
20197
20592
  ].join("\n")
@@ -20199,36 +20594,36 @@ function taskBodyParts(data) {
20199
20594
  ? [
20200
20595
  `<form method="post" action="${taskHref(task.id)}/attend-preview" class="card">`,
20201
20596
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
20202
- `<p><strong>or run it once while you watch</strong></p>`,
20203
- `<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>`,
20597
+ `<p><strong>Or run it once while you watch</strong></p>`,
20598
+ `<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>`,
20204
20599
  attended.mint === undefined || attended.mint.models.length <= 1
20205
20600
  ? ""
20206
- : `<label>model <span class="meta">(watched sessions run claude only)</span><select name="model">` +
20601
+ : `<label>Model <span class="meta">(watched sessions run claude only)</span><select name="model">` +
20207
20602
  attended.mint.models
20208
20603
  .map(model => `<option value="${escape(model)}"${model === attended.mint?.pinnedModel ? " selected" : ""}>${escape(model)}</option>`)
20209
20604
  .join("") +
20210
20605
  `</select></label>`,
20211
20606
  attended.mint === undefined
20212
20607
  ? ""
20213
- : `<label>permissions<select name="posture">` +
20608
+ : `<label>Permissions<select name="posture">` +
20214
20609
  `<option value="auto"${attended.mint.posture === "auto" ? " selected" : ""}>safe unattended — routine project commands and edits proceed</option>` +
20215
20610
  `<option value="acceptEdits"${attended.mint.posture === "acceptEdits" ? " selected" : ""}>legacy acceptEdits — commands that ask are denied unattended</option>` +
20216
20611
  `<option value="bypassPermissions"${attended.mint.posture === "bypassPermissions" ? " selected" : ""}>full permissions — nothing asks</option>` +
20217
20612
  `</select></label>`,
20218
- `<button type="submit">read the terms</button>`,
20613
+ `<button type="submit">Read the terms</button>`,
20219
20614
  `</form>`,
20220
20615
  ].join("\n")
20221
20616
  : "";
20222
20617
  const scopeForm = [
20223
- `<details${scope === null || data.scopeDraft !== undefined ? " open" : ""}><summary>${scope === null ? "write the scope" : "edit the scope"}${approval.approved ? " (editing voids the approval)" : ""}</summary>`,
20618
+ `<details${scope === null || scopeDraft !== undefined ? " open" : ""}><summary>${scope === null ? "Write the scope" : "Edit the scope"}${approval.approved ? " (editing voids the approval)" : ""}</summary>`,
20224
20619
  `<form method="post" action="${taskHref(task.id)}/scope" class="scope-editor">`,
20225
- data.scopeDraft === undefined || data.problem === null ? "" : `<div class="problem" role="alert" id="scope-error">${escape(data.problem)}</div>`,
20620
+ scopeDraft === undefined || data.problem === null ? "" : `<div class="problem" role="alert" id="scope-error">${escape(data.problem)}</div>`,
20226
20621
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
20227
- `<input type="hidden" name="sawDigest" value="${escape(data.scopeDraft?.get("sawDigest") ?? scope?.digest ?? "")}">`,
20228
- `<label>goal<textarea name="goal" rows="3"${data.scopeDraft === undefined ? "" : ' autofocus aria-describedby="scope-error"'}>${escape(data.scopeDraft?.get("goal") ?? scope?.goal ?? "")}</textarea></label>`,
20229
- `<label>not this<textarea name="not" rows="2">${escape(data.scopeDraft?.get("not") ?? scope?.outOfScope ?? "")}</textarea></label>`,
20230
- `<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>`,
20231
- `<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>`,
20622
+ `<input type="hidden" name="sawDigest" value="${escape(scopeDraft?.get("sawDigest") ?? scope?.digest ?? "")}">`,
20623
+ `<label>Goal<textarea name="goal" rows="3"${scopeDraft === undefined ? "" : ' autofocus aria-describedby="scope-error"'}>${escape(scopeDraft?.get("goal") ?? scope?.goal ?? "")}</textarea></label>`,
20624
+ `<label>Not this<textarea name="not" rows="2">${escape(scopeDraft?.get("not") ?? scope?.outOfScope ?? "")}</textarea></label>`,
20625
+ `<label>Touches <span class="meta">(one per line)</span><textarea name="touches" rows="2">${escape(scopeDraft?.get("touches") ?? (scope?.touches ?? []).join("\n"))}</textarea></label>`,
20626
+ `<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>`,
20232
20627
  (() => {
20233
20628
  const defaults = data.spendDefaults ?? null;
20234
20629
  const budgetPrefill = scope?.budgetMicrousd != null
@@ -20236,8 +20631,8 @@ function taskBodyParts(data) {
20236
20631
  : defaults?.buildPerRunMicrousd != null
20237
20632
  ? (defaults.buildPerRunMicrousd / 1_000_000).toFixed(2)
20238
20633
  : "";
20239
- return `<label>agent-reported usage cap <span class="meta">(optional — leave blank for uncapped subscription work)</span>` +
20240
- `<input type="number" name="budget-usd" step="0.01" min="0.01" value="${escape(data.scopeDraft?.get("budget-usd") ?? budgetPrefill)}" placeholder="no cap"></label>` +
20634
+ return `<label>Agent-reported usage cap <span class="meta">(optional — leave blank for uncapped subscription work)</span>` +
20635
+ `<input type="number" name="budget-usd" step="0.01" min="0.01" value="${escape(scopeDraft?.get("budget-usd") ?? budgetPrefill)}" placeholder="no cap"></label>` +
20241
20636
  `<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>`;
20242
20637
  })(),
20243
20638
  (() => {
@@ -20248,14 +20643,14 @@ function taskBodyParts(data) {
20248
20643
  : scope?.profile?.provider === "codex" || scope?.profile?.provider === "openrouter"
20249
20644
  ? scope.profile.sandboxMode === "danger-full-access" ? "bypassPermissions" : "auto"
20250
20645
  : null;
20251
- const selected = data.scopeDraft?.get("permission-mode") === "bypassPermissions" ? "bypassPermissions" : data.scopeDraft?.get("permission-mode") === "auto" ? "auto" : data.permissionMode ?? profileMode ?? data.permissionDefault ?? "auto";
20252
- return `<fieldset class="permission-field"><legend>agent permissions</legend>` +
20646
+ const selected = scopeDraft?.get("permission-mode") === "bypassPermissions" ? "bypassPermissions" : scopeDraft?.get("permission-mode") === "auto" ? "auto" : data.permissionMode ?? profileMode ?? data.permissionDefault ?? "auto";
20647
+ return `<fieldset class="permission-field"><legend>Agent permissions</legend>` +
20253
20648
  permissionModeChoices("permission-mode", selected) +
20254
20649
  `<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>`;
20255
20650
  })(),
20256
20651
  (() => {
20257
- const selected = data.scopeDraft?.get("quality-mode") === "strict" ? "strict" : data.scopeDraft?.get("quality-mode") === "default" ? "default" : scope?.qualityMode ?? data.qualityMode ?? data.qualityDefault ?? "default";
20258
- return `<fieldset class="permission-field"><legend>quality</legend>` +
20652
+ const selected = scopeDraft?.get("quality-mode") === "strict" ? "strict" : scopeDraft?.get("quality-mode") === "default" ? "default" : scope?.qualityMode ?? data.qualityMode ?? data.qualityDefault ?? "default";
20653
+ return `<fieldset class="permission-field"><legend>Quality</legend>` +
20259
20654
  qualityModeChoices("quality-mode", selected) +
20260
20655
  `<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>`;
20261
20656
  })(),
@@ -20266,8 +20661,8 @@ function taskBodyParts(data) {
20266
20661
  // approval above restates and covers both.
20267
20662
  const defaults = data.spendDefaults ?? null;
20268
20663
  const terms = data.raceTerms ?? null;
20269
- const selectedCount = data.scopeDraft === undefined ? terms !== null ? terms.n : defaults?.raceAgents ?? 0 : Number(data.scopeDraft.get("race-count") ?? "0");
20270
- const selectedModel = data.scopeDraft?.get("race-model") ?? terms?.agents[0]?.model ?? "claude-sonnet-5";
20664
+ const selectedCount = scopeDraft === undefined ? terms !== null ? terms.n : defaults?.raceAgents ?? 0 : Number(scopeDraft.get("race-count") ?? "0");
20665
+ const selectedModel = scopeDraft?.get("race-model") ?? terms?.agents[0]?.model ?? "claude-sonnet-5";
20271
20666
  const perPrefill = terms !== null
20272
20667
  ? (terms.perAgentBudgetMicrousd / 1_000_000).toFixed(2)
20273
20668
  : defaults?.racePerAgentMicrousd != null
@@ -20279,31 +20674,31 @@ function taskBodyParts(data) {
20279
20674
  ? (defaults.raceTotalMicrousd / 1_000_000).toFixed(2)
20280
20675
  : "";
20281
20676
  return [
20282
- `<details class="more-agents"${selectedCount > 0 ? " open" : ""}><summary>more than one agent (optional)</summary>`,
20283
- `<label>how many agents compete <span class="meta">(a tournament builds the task independently N times — you compare and pick one)</span>` +
20677
+ `<details class="more-agents"${selectedCount > 0 ? " open" : ""}><summary>More than one agent (optional)</summary>`,
20678
+ `<label>How many agents compete <span class="meta">(a tournament builds the task independently N times — you compare and pick one)</span>` +
20284
20679
  `<select name="race-count">` +
20285
20680
  `<option value=""${selectedCount === 0 ? " selected" : ""}>one agent — no tournament</option>` +
20286
20681
  [2, 3, 4].map(count => `<option value="${count}"${selectedCount === count ? " selected" : ""}>${count} agents</option>`).join("") +
20287
20682
  `</select></label>`,
20288
- `<label>competing model <select name="race-model">` +
20683
+ `<label>Competing model <select name="race-model">` +
20289
20684
  PRICED_BUILD_MODELS.map(model => `<option value="${escape(model)}"${model === selectedModel ? " selected" : ""}>${escape(model)}</option>`).join("") +
20290
20685
  `</select></label>`,
20291
- `<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>`,
20292
- `<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>`,
20686
+ `<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>`,
20687
+ `<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>`,
20293
20688
  // The comparison lanes (Phase 3 slice B): 2-4 rows, any registered
20294
20689
  // provider, exact model required — no dollar fields exist. Blank
20295
20690
  // rows are unused; filling any row files a comparison INSTEAD of a
20296
20691
  // tournament, and mixing the two refuses in words.
20297
20692
  `<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>`,
20298
- ...[1, 2, 3, 4].map(lane => `<div class="row"><label>agent ${lane} <select name="compare-provider-${lane}">` +
20693
+ ...[1, 2, 3, 4].map(lane => `<div class="row"><label>Agent ${lane} <select name="compare-provider-${lane}">` +
20299
20694
  `<option value="">—</option>` +
20300
- PROVIDER_IDS.map(provider => `<option value="${escape(provider)}"${data.scopeDraft?.get(`compare-provider-${lane}`) === provider ? " selected" : ""}>${escape(provider)}</option>`).join("") +
20695
+ PROVIDER_IDS.map(provider => `<option value="${escape(provider)}"${scopeDraft?.get(`compare-provider-${lane}`) === provider ? " selected" : ""}>${escape(provider)}</option>`).join("") +
20301
20696
  `</select></label>` +
20302
- `<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>`),
20697
+ `<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>`),
20303
20698
  `</details>`,
20304
20699
  ].join("\n");
20305
20700
  })(),
20306
- `<button type="submit">save scope</button>`,
20701
+ `<button type="submit">Save scope</button>`,
20307
20702
  `</form></details>`,
20308
20703
  ].join("\n");
20309
20704
  // 41_237 → "41k": token counts read at a glance; exactness lives on the run page.
@@ -20325,7 +20720,7 @@ function taskBodyParts(data) {
20325
20720
  // duration · tokens · measured-or-unmeasured cost.
20326
20721
  const runs = data.runs.length === 0
20327
20722
  ? ""
20328
- : `<h2>attempts</h2>` +
20723
+ : `<h2>Attempts</h2>` +
20329
20724
  data.runs
20330
20725
  .map(run => {
20331
20726
  const bits = [
@@ -20518,20 +20913,20 @@ function taskBodyParts(data) {
20518
20913
  })();
20519
20914
  const decisions = data.decisions.length === 0
20520
20915
  ? ""
20521
- : `<h2>decisions</h2>` +
20916
+ : `<h2>Decisions</h2>` +
20522
20917
  data.decisions
20523
20918
  .map(decision => `<p class="row"><a href="/d/${decision.id}">${escape(decision.question)}</a> ` +
20524
20919
  `<span class="meta">${escape(decision.state)}${isOverdue(decision, data.now) ? " · overdue" : ""}</span></p>`)
20525
20920
  .join("\n");
20526
20921
  const incidents = data.incidents.length === 0
20527
20922
  ? ""
20528
- : `<h2>incidents</h2>` +
20923
+ : `<h2>Incidents</h2>` +
20529
20924
  data.incidents
20530
20925
  .map(one => one.resolvedAt === null
20531
20926
  ? `<p class="row">${escape(incidentWords(one.kind))} ` +
20532
20927
  `<form method="post" action="/i/${one.id}/resolve" class="inline">` +
20533
20928
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
20534
- `<button type="submit">resolve</button></form></p>`
20929
+ `<button type="submit">Resolve</button></form></p>`
20535
20930
  : `<p class="row meta">${escape(incidentWords(one.kind))} — resolved by ${escape(one.resolvedBy ?? "?")}</p>`)
20536
20931
  .join("\n");
20537
20932
  const stalled = task.state === "failed" || data.incidents.some(one => one.resolvedAt === null);
@@ -20543,11 +20938,11 @@ function taskBodyParts(data) {
20543
20938
  const waitsFor = data.waitsFor ?? [];
20544
20939
  const candidates = (data.waitCandidates ?? []).filter(one => !waitsFor.some(existing => existing.id === one.id));
20545
20940
  const waitRows = waitsFor
20546
- .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>`}` +
20941
+ .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>`}` +
20547
20942
  `<form method="post" action="${taskHref(task.id)}/unblock" class="inline">` +
20548
20943
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
20549
20944
  `<input type="hidden" name="on" value="${escape(one.id)}">` +
20550
- `<button type="submit">don't wait for this</button></form></p>`)
20945
+ `<button type="submit">Don't wait for this</button></form></p>`)
20551
20946
  .join("\n");
20552
20947
  const waitAdd = data.csrf === "" || candidates.length === 0
20553
20948
  ? ""
@@ -20556,11 +20951,11 @@ function taskBodyParts(data) {
20556
20951
  `<select name="on" aria-label="task to wait for">` +
20557
20952
  candidates.map(one => `<option value="${escape(one.id)}">${escape(one.id)} — ${escape(one.title)}</option>`).join("") +
20558
20953
  `</select>` +
20559
- `<button type="submit">wait for this task</button>` +
20954
+ `<button type="submit">Wait for this task</button>` +
20560
20955
  `<span class="meta"> — this task starts only after it finishes</span></form>`;
20561
20956
  const waitsForCard = waitRows === "" && waitAdd === ""
20562
20957
  ? ""
20563
- : `<h2>waits for</h2>${waitRows === "" ? `<p class="meta">nothing — it starts when a worker is free</p>` : waitRows}${waitAdd}`;
20958
+ : `<h2>Waits for</h2>${waitRows === "" ? `<p class="meta">Nothing — it starts when a worker is free</p>` : waitRows}${waitAdd}`;
20564
20959
  // The acts bar (task page pass): every verb in one row under the title,
20565
20960
  // the one that resolves this task's state first and primary — retry on a
20566
20961
  // stalled task, build-next in the queue, plan-first with no scope. The
@@ -20569,8 +20964,10 @@ function taskBodyParts(data) {
20569
20964
  // hold (the hold governs the NEXT start), and requeue refuses while a
20570
20965
  // claim is live — so the words say exactly when each becomes real.
20571
20966
  const canPlan = data.plan === null && !approval.approved && !data.claimed && task.state === "queued" && (data.coordinator === null || data.coordinator === undefined);
20572
- const primaryAct = stopControlsActive ? null : stalled && !data.claimed
20573
- ? { html: act("requeue", "retry — branch and workspace kept"), why: "resolves the incidents, clears the failed attempts, and queues the task again; the preserved branch and workspace are NOT erased", whyClass: "retry" }
20967
+ // A failed task's status card carries Retry itself (the same requeue); here it would be a second ink act.
20968
+ const cardRetries = task.state === "failed" && data.assignment != null && data.csrf !== "";
20969
+ const primaryAct = stopControlsActive || cardRetries ? null : stalled && !data.claimed
20970
+ ? { html: act("requeue", "Retry, keeping the branch and workspace"), why: "Resolves the incidents, clears the failed attempts and queues the task again. The branch and workspace are kept.", whyClass: "retry" }
20574
20971
  : canPlan
20575
20972
  ? { html: act("plan", "plan first"), why: "plan first sends an agent to read the repository, ask you questions, and propose a scope — nothing builds until you approve it", whyClass: "plan" }
20576
20973
  : data.plan === "requested"
@@ -20581,7 +20978,7 @@ function taskBodyParts(data) {
20581
20978
  const holdAct = `<form method="post" action="${taskHref(task.id)}/hold" class="inline act-hold">` +
20582
20979
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
20583
20980
  `<input type="text" name="reason" class="inline" placeholder="reason (optional)" aria-label="hold reason">` +
20584
- `<button type="submit">hold next attempt</button></form>`;
20981
+ `<button type="submit">Hold the next attempt</button></form>`;
20585
20982
  const canHold = task.state === "queued" || task.state === "running" || task.state === "failed";
20586
20983
  const actsBar = [
20587
20984
  `<span id="task-actions"></span><div class="acts-bar">`,
@@ -20598,7 +20995,7 @@ function taskBodyParts(data) {
20598
20995
  `</div>`,
20599
20996
  primaryAct === null ? "" : `<p class="meta acts-why acts-why-${primaryAct.whyClass}">${primaryAct.why}</p>`,
20600
20997
  data.claimed && !stopControlsActive
20601
- ? `<p class="meta acts-why">a worker is building this right now — <em>hold next attempt</em> stops the one after it; cancel waits for the current build to finish${stalled ? "; retry becomes available after this attempt finishes" : ""}</p>`
20998
+ ? `<p class="meta acts-why">a worker is building this right now — <em>Hold the next attempt</em> stops the one after it; cancel waits for the current build to finish${stalled ? "; retry becomes available after this attempt finishes" : ""}</p>`
20602
20999
  : "",
20603
21000
  ].join("\n");
20604
21001
  // Cancel gets the same ceremony as an irreversible answer: armed behind
@@ -20620,9 +21017,10 @@ function taskBodyParts(data) {
20620
21017
  const section = (title, html, open, count, id = title.replace(/\s+/g, "-")) => {
20621
21018
  if (html === "")
20622
21019
  return "";
20623
- const inner = html.replace(`<h2>${title}</h2>`, "");
20624
- sectionParts.push({ id, title: title[0].toUpperCase() + title.slice(1), html: inner, open, count: count ?? null });
20625
- return `<details class="section" id="${id}"${open ? " open" : ""}><summary><h2>${title}${count === undefined ? "" : ` <span class="lane-count">${count}</span>`}</h2></summary>` + inner + `</details>`;
21020
+ const heading = sentenceCase(title);
21021
+ const inner = html.replace(`<h2>${title}</h2>`, "").replace(`<h2>${heading}</h2>`, "");
21022
+ sectionParts.push({ id, title: heading, html: inner, open, count: count ?? null });
21023
+ return `<details class="section" id="${id}"${open ? " open" : ""}><summary><h2>${heading}${count === undefined ? "" : ` <span class="lane-count">${count}</span>`}</h2></summary>` + inner + `</details>`;
20626
21024
  };
20627
21025
  const receiptLeads = data.assignment == null && task.state === "done" && data.completion?.receipt != null;
20628
21026
  // Exact identity stays available in Task options; failures stay in the
@@ -20634,7 +21032,7 @@ function taskBodyParts(data) {
20634
21032
  ? ` · filed by <span class="mono">${escape(data.coordinator.label)}</span>${data.coordinator.filedAgo === null ? "" : ` ${escape(data.coordinator.filedAgo)}`}`
20635
21033
  : data.filedVia === null || data.filedVia === undefined
20636
21034
  ? ""
20637
- : ` · filed via ${escape(data.filedVia)}`}${data.deliverable === "report" ? ` · <span class="badge">scout</span>` : ""}</p>`;
21035
+ : ` · filed via ${escape(data.filedVia)}`}${data.deliverable === "report" ? ` · <span class="badge">Scout</span>` : ""}</p>`;
20638
21036
  // A pull request that can be merged (or opened) owns the one primary action; the result stays in its section.
20639
21037
  const pullRequestActs = data.csrf !== "" && (data.pullRequest?.view?.canMerge === true || (data.pullRequest?.view == null && data.pullRequest?.offer != null));
20640
21038
  const pullRequestFact = data.pullRequest?.fact != null && data.pullRequest.view?.runId === data.assignment?.receipt?.runId ? data.pullRequest.fact : undefined;
@@ -20675,12 +21073,12 @@ function taskBodyParts(data) {
20675
21073
  : "gone from the tracker";
20676
21074
  const reopenable = mirror.remoteState === "open" && !mirror.dispatchOk && mirror.closeGeneration !== null &&
20677
21075
  mirror.syncGeneration > mirror.closeGeneration && ["cancelled", "failed", "queued"].includes(task.state) && data.csrf !== "";
20678
- return (`<div class="card"><p><strong>external work</strong> <span class="meta">${link} · ${escape(state)}</span></p>` +
21076
+ return (`<div class="card"><p><strong>External work</strong> <span class="meta">${link} · ${escape(state)}</span></p>` +
20679
21077
  (reopenable
20680
21078
  ? `<form method="post" action="${taskHref(task.id)}/reopen" class="row">` +
20681
21079
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
20682
21080
  `<input type="password" name="token" placeholder="your password" aria-label="your password" autocomplete="current-password">` +
20683
- `<button type="submit">reopen — the approved scope stands</button></form>`
21081
+ `<button type="submit">Reopen — the approved scope stands</button></form>`
20684
21082
  : "") +
20685
21083
  `</div>`);
20686
21084
  })();
@@ -20694,7 +21092,7 @@ function taskBodyParts(data) {
20694
21092
  // is as fenced as the builder on a coordinator filing, so "plan
20695
21093
  // first" would recommend a road that refuses.
20696
21094
  ? `<div class="card"><p><strong>This task is waiting on you: an agent filed it, and it has no scope.</strong></p>` +
20697
- `<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>`
21095
+ `<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>`
20698
21096
  : `<div class="card task-scope-needed"><p><strong>No approved scope yet</strong></p>` +
20699
21097
  `<p class="meta"><strong>Plan first</strong> drafts it from the repository, or <a href="#scope">write it yourself</a>.</p></div>`
20700
21098
  : "";
@@ -20729,7 +21127,7 @@ function taskBodyParts(data) {
20729
21127
  attemptPanel,
20730
21128
  data.coordinatorProposals == null || data.coordinatorProposals.rows.length === 0
20731
21129
  ? ""
20732
- : section("proposals", `<h2>proposed by coordinators</h2>` +
21130
+ : section("proposals", `<h2>Proposed by coordinators</h2>` +
20733
21131
  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),
20734
21132
  section("decisions", decisions, true, data.decisions.length),
20735
21133
  section("incidents", incidents, true, data.incidents.length),
@@ -20741,13 +21139,13 @@ function taskBodyParts(data) {
20741
21139
  ? ` · CI ${data.publication.lastCheckState} at last observation`
20742
21140
  : " · no checks observed"}</span></p>`,
20743
21141
  section("report", reportCard, true),
20744
- data.assignment == null ? section("attempts", runs, true, data.runs.length) : section("Build activity", runs.replace("<h2>attempts</h2>", ""), false, data.runs.length, "attempts"),
21142
+ data.assignment == null ? section("attempts", runs, true, data.runs.length) : section("Build activity", runs.replace("<h2>Attempts</h2>", ""), false, data.runs.length, "attempts"),
20745
21143
  section("usage", spendCard, false),
20746
21144
  section("steering", steeringCard, (data.steering ?? []).length > 0, (data.steering ?? []).length),
20747
21145
  section("scope",
20748
21146
  // The recipe road rides with the scope it reuses (UI polish
20749
21147
  // 2026-09-13), off the title-to-action path.
20750
- ["<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))),
21148
+ ["<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))),
20751
21149
  dependencyChoiceNeeded ? "" : section("waits for", waitsForCard, (data.waitsFor ?? []).length > 0, (data.waitsFor ?? []).length),
20752
21150
  section("holds", holds, true, data.holds.length),
20753
21151
  cancelAct,
@@ -20767,7 +21165,19 @@ function taskBodyParts(data) {
20767
21165
  thread.push({ key: "filed", at: rootFiled, kind: "filed", who: "person", author: filedBy ?? "", title: "Filed the task",
20768
21166
  text: (() => { const goal = data.family?.root.goal ?? scope?.goal ?? null; return goal === null ? null : oneLineOf(goal, 280); })(), link: null, html: "", more: null });
20769
21167
  const newestBuilt = familyRuns.filter(run => run.outcome === "built" && run.role !== "planner" && run.role !== "reviewer").sort((a, b) => b.id - a.id)[0];
21168
+ // Failed: the card says what went wrong in one line, and its one act is Retry itself (the same requeue), never a link to
21169
+ // this page; someone who can't retry here (a viewer, a live claim) gets no act at all.
21170
+ const statusCard = data.assignment != null && assignmentOptions !== null ? assignmentCardOf(data.assignment, assignmentOptions) : null;
21171
+ // A live build says which step it is on, or which step it is stuck on and why.
21172
+ const building = statusCard?.status.headline === "Building" && liveRun !== undefined && liveRun.role !== "planner" && liveRun.role !== "reviewer";
21173
+ // While a build runs, the attempts that stopped before it fold into one quiet line under its Building card. Only
21174
+ // there: with any other card (or none) they stay in the thread, never vanish.
21175
+ const earlierStopped = !building ? []
21176
+ : familyRuns.filter(run => run.id < liveRun.id && (run.role === "builder" || run.role === "scout") && run.outcome !== "built" && run.outcome !== "no-change" && run.id !== liveHistoryRunId);
21177
+ const foldedAttempts = new Set(earlierStopped.map(run => run.id));
20770
21178
  for (const run of familyRuns) {
21179
+ if (foldedAttempts.has(run.id))
21180
+ continue;
20771
21181
  const live = run.id === liveHistoryRunId || (run.outcome === null && run.id === liveRunId);
20772
21182
  const noun = runNoun(run);
20773
21183
  const href = { label: `${noun[0].toUpperCase()}${noun.slice(1)} #${run.id}`, href: `/r/${run.id}` };
@@ -20791,9 +21201,9 @@ function taskBodyParts(data) {
20791
21201
  : run.outcome === null ? "Attempt never finished"
20792
21202
  : `${noun[0].toUpperCase()}${noun.slice(1)} ${run.outcome}`;
20793
21203
  thread.push({ key: `run-${run.id}`, at: run.finishedAt ?? run.startedAt, kind: "result", who: "agent", author: run.runner,
20794
- title: `${title}${revision}`, text: run.handoff === null ? (run.reason === null || run.outcome === "built" ? null : reasonWords(run.reason)) : oneLineOf(run.handoff, 600),
21204
+ 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),
20795
21205
  link: href, html: "",
20796
- more: previousResult !== null && run.id === newestBuilt?.id && run.taskId === task.id ? { summary: "Result details", html: previousResult.html } : null });
21206
+ more: previousResult !== null && run.id === newestBuilt?.id && run.taskId === task.id ? { summary: STATUS_MORE, html: previousResult.html } : null });
20797
21207
  }
20798
21208
  // What the agent is working through now, and how its plan changed.
20799
21209
  const liveAt = liveRun?.startedAt ?? task.updatedAt;
@@ -20809,7 +21219,7 @@ function taskBodyParts(data) {
20809
21219
  const feedback = data.revision;
20810
21220
  thread.push({ key: "revision", at: task.createdAt, kind: "reply", who: "person", author: feedback.comments[0]?.author ?? "You",
20811
21221
  title: feedback.kind === "ci-repair" ? "Asked for a CI repair" : feedback.kind === "criterion-repair" ? "Asked for a repair" : "Asked for changes",
20812
- text: feedback.comments.map(one => `${one.path === null ? "" : `${one.path}${one.line === null ? "" : `:${one.line}`} — `}${one.note}`).join("\n"),
21222
+ text: feedback.comments.map(one => `${one.path === null ? "" : `${one.path}${one.line === null ? "" : `:${one.line}`} — `}${one.note.trim()}`).filter(one => one !== "").join("\n") || null,
20813
21223
  link: { label: `From build #${feedback.sourceRun}`, href: `/r/${feedback.sourceRun}` }, html: "", more: null });
20814
21224
  }
20815
21225
  for (const decision of data.decisions) {
@@ -20886,6 +21296,45 @@ function taskBodyParts(data) {
20886
21296
  // needs a person, then facts, then folds; the mechanics under Manage.
20887
21297
  const MANAGE = new Set(["steering", "waits-for", "holds"]);
20888
21298
  const finished = task.state === "done" || task.state === "cancelled";
21299
+ const failedCard = statusCard?.status.headline === "Failed";
21300
+ // A link to this very page, with no section to open, goes nowhere: a card never offers one.
21301
+ const bareSelfLink = (href) => {
21302
+ if (href.includes("#"))
21303
+ return false;
21304
+ const [path, query = ""] = href.split("?");
21305
+ const version = new URLSearchParams(query).get("version");
21306
+ return [data.assignment?.rootId, data.rootId, task.id].some(id => id != null && path === `/t/${encodeURIComponent(id)}`) && (version === null || version === task.id);
21307
+ };
21308
+ // Build again, in place, names its form after the card's own act.
21309
+ const rebuild = data.csrf !== "" && data.assignment?.primaryAction?.code === "retry-task" && data.assignment.need != null && "key" in data.assignment.need && data.assignment.need.key === "rebuild"
21310
+ ? { action: `${taskHref(data.assignment.rootId)}/requeue` } : null;
21311
+ const retry = failedCard && task.state === "failed" && !data.claimed && !stopControlsActive && data.csrf !== ""
21312
+ ? { action: `${taskHref(task.id)}/requeue`, ...(data.failure == null ? {} : { note: retryNoteOf(data.failure.suggestion) }) } : null;
21313
+ // Run checks happens in place: a status row's Run checks posts here and comes back; with nothing to run, the row offers nothing.
21314
+ const runChecks = data.runChecks ?? null;
21315
+ // A row's link to a result in Chat opens that result's own page instead: Chat would only lead back here.
21316
+ const resultPageOf = (href) => {
21317
+ 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);
21318
+ return chat === null ? href : `/review?result=${chat[1]}&run=${chat[2]}${chat[3] === undefined ? "" : `&tab=${chat[3]}`}${chat[4] ?? ""}`;
21319
+ };
21320
+ const inPlaceChecks = (card) => {
21321
+ const status = data.demo === true ? demoChecksOf(card.status) : card.status;
21322
+ 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 } }
21323
+ : { ...one, href: resultPageOf(one.href), action: one.action === null ? null : { ...one.action, href: resultPageOf(one.action.href) } }) } };
21324
+ };
21325
+ const steps = building ? buildProgressOf(data.milestoneProgress ?? null) : null;
21326
+ const progress = steps === null ? null : { line: steps.line,
21327
+ stuck: steps.stuck === null ? null : { ...steps.stuck, action: data.csrf === "" ? null : { label: "Send the agent a note", href: "#steering" } } };
21328
+ // The Building card carries Stop for the build it describes (the same form), in place of a second card below it.
21329
+ const stop = building && data.control?.kind === "stop" && data.control.run === liveRun.id && data.csrf !== ""
21330
+ ? { action: `${taskHref(task.id)}/stop`, run: data.control.run } : null;
21331
+ // The Building card keeps its way to the build's own record (/r/<id>), whatever act it shows.
21332
+ const record = building ? { label: `Build #${liveRun.id} record`, href: `/r/${liveRun.id}` } : null;
21333
+ const earlier = earlierStopped.length === 0 ? null : {
21334
+ summary: earlierAttemptsWords(earlierStopped.length),
21335
+ 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}`,
21336
+ 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)) })),
21337
+ };
20889
21338
  const view = {
20890
21339
  kind: "task",
20891
21340
  id: task.id,
@@ -20897,16 +21346,24 @@ function taskBodyParts(data) {
20897
21346
  { label: "Ask", href: taskChatHref(data.rootId ?? task.id), active: false },
20898
21347
  ],
20899
21348
  version: data.versionLabel == null ? null : { label: data.versionLabel, current: { label: "Current work", href: taskHref(data.rootId ?? task.id) } },
20900
- status: data.assignment != null && assignmentOptions !== null ? assignmentCardOf(data.assignment, assignmentOptions) : null,
21349
+ status: statusCard === null ? null : inPlaceChecks(failedCard ? { ...statusCard, action: null, status: { ...statusCard.status, sentence: data.failure?.line ?? statusCard.status.sentence } }
21350
+ : progress !== null || record !== null ? { ...statusCard, action: statusCard.action?.href === record?.href ? null : statusCard.action, status: { ...statusCard.status, sentence: progress?.line ?? statusCard.status.sentence } }
21351
+ : statusCard.action !== null && rebuild === null && bareSelfLink(statusCard.action.href) ? { ...statusCard, action: null } : statusCard),
20901
21352
  statusHtml,
21353
+ failure: failedCard && data.failure != null ? { line: data.failure.line, evidence: data.failure.evidence, suggestion: data.failure.suggestion, link: data.failure.link } : null,
21354
+ retry,
21355
+ runChecks,
21356
+ progress,
21357
+ stop,
21358
+ record,
21359
+ earlier,
20902
21360
  approval: approvalHtml,
20903
21361
  confirmStopped: data.csrf !== "" && data.assignment?.primaryAction?.code === "confirm-stopped" && data.assignment.primaryAction.target.runId !== null
20904
21362
  ? { action: `${taskHref(data.assignment.rootId)}/confirm-stopped`, run: data.assignment.primaryAction.target.runId, checked: needsCheck(data.assignment) } : null,
20905
- rebuild: data.csrf !== "" && data.assignment?.primaryAction?.code === "retry-task" && data.assignment.need != null && "key" in data.assignment.need && data.assignment.need.key === "rebuild"
20906
- ? { action: `${taskHref(data.assignment.rootId)}/requeue` } : null,
21363
+ rebuild,
20907
21364
  // The plan, progress and plan changes are thread entries now; the rest still needs a person here.
20908
21365
  lead: [
20909
- { key: "history", html: data.history ?? "" }, { key: "control", html: controlHtml }, { key: "problem", html: problemHtml },
21366
+ { 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 },
20910
21367
  { key: "pull-request", html: pullRequestCard },
20911
21368
  { key: "needs-scope", html: needsScopeCard },
20912
21369
  { key: "mirror", html: mirrorCard }, { key: "contest", html: contestCard }, { key: "attempt", html: attemptPanel },
@@ -20917,7 +21374,7 @@ function taskBodyParts(data) {
20917
21374
  sections: [
20918
21375
  ...(previousResult === null || thread.some(one => one.more !== null) ? [] : [{ id: "result", title: previousResult.title, html: previousResult.html, open: false, count: null }]),
20919
21376
  // Questions are in the thread; the scope and the ledgers fold here until asked.
20920
- ...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),
21377
+ ...sectionParts.filter(one => !MANAGE.has(one.id) && one.id !== "decisions").map(one => one.id === "scope" && scopeDraft === undefined && (finished || approval.approved) ? { ...one, open: false } : one),
20921
21378
  ],
20922
21379
  manage: [
20923
21380
  ...sectionParts.filter(one => MANAGE.has(one.id)),
@@ -21363,26 +21820,27 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
21363
21820
  const intent = view.intent;
21364
21821
  let intentView = null;
21365
21822
  if (intent === null) {
21366
- 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>`);
21823
+ 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>`);
21367
21824
  }
21368
21825
  else {
21826
+ // The time itself goes to the page as a stamp: the one formatter words it in the viewer's zone.
21369
21827
  const approvalWords = intent.approval.approved
21370
- ? `approved by ${intent.approvedBy ?? "an operator"} · ${when(intent.approval.at)}`
21828
+ ? `Approved by ${intent.approvedBy ?? "an operator"}`
21371
21829
  : intent.approval.reason === "changed"
21372
- ? "the scope changed after approval — the words below are the current text, not the signed one"
21373
- : "never approved — the result was built without a signed scope";
21830
+ ? "The scope changed after approval. The words below are the current text, not the signed one."
21831
+ : "Never approved. The result was built without a signed scope.";
21374
21832
  const approval = intent.approval.approved
21375
- ? `approved by ${escape(intent.approvedBy ?? "an operator")} · ${whenTime(intent.approval.at)}`
21376
- : approvalWords;
21377
- const body = `<p class="recap" style="margin-top:.25rem"><strong>goal</strong> ${escape(intent.goal)}</p>` +
21378
- (intent.outOfScope === null ? `<p class="meta">no boundary was stated</p>` : `<p class="recap"><strong>not this</strong> ${escape(intent.outOfScope)}</p>`) +
21833
+ ? `Approved by ${escape(intent.approvedBy ?? "an operator")} · ${whenTime(intent.approval.at)}`
21834
+ : escape(approvalWords);
21835
+ const body = `<p class="recap" style="margin-top:.25rem"><strong>Goal</strong> ${escape(intent.goal)}</p>` +
21836
+ (intent.outOfScope === null ? `<p class="meta">No boundary was stated</p>` : `<p class="recap"><strong>Not this</strong> ${escape(intent.outOfScope)}</p>`) +
21379
21837
  (intent.touches.length === 0
21380
- ? `<p class="meta">no expected paths were signed — every changed file reads as in bounds</p>`
21381
- : `<p class="row"><span class="meta">expected to touch</span> ${intent.touches.map(one => `<span class="mono">${escape(one)}</span>`).join(" ")}</p>`) +
21838
+ ? `<p class="meta">No expected paths were signed — every changed file reads as in bounds</p>`
21839
+ : `<p class="row"><span class="meta">Expected to touch</span> ${intent.touches.map(one => `<span class="mono">${escape(one)}</span>`).join(" ")}</p>`) +
21382
21840
  (view.plan === null
21383
21841
  ? ""
21384
- : `<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>`);
21385
- intentView = { approval: approvalWords, html: body };
21842
+ : `<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>`);
21843
+ intentView = { approval: approvalWords, approvedAt: intent.approval.approved ? intent.approval.at ?? null : null, html: body };
21386
21844
  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>` +
21387
21845
  `<div class="cockpit-disclosure-body">` + body + `</div></details>`);
21388
21846
  }
@@ -21393,7 +21851,7 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
21393
21851
  problem: view.detail === null ? view.historyProblem : null,
21394
21852
  next: reviewNextActionOf(view, csrf), complete: null, decision: null, checks: null, intent: intentView, noRun: null, panel: null, contest: "",
21395
21853
  notes: view.notes.map(one => ({ author: one.author, at: one.createdAt, note: one.note })),
21396
- acts: { primary: null, secondary: null, line: null }, runChecks: null,
21854
+ acts: { primary: null, secondary: null, line: null }, runChecks: null, mismatch: null,
21397
21855
  // The raw run record lives under Details now (2026-10-02): /r/<id> for this result redirects here.
21398
21856
  record: run === null ? null : { build: run.id, href: `/r/${run.id}?record=1`, facts: [
21399
21857
  { label: "Agent", value: [run.provider, run.model].filter(Boolean).join(" · ") },
@@ -21404,7 +21862,7 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
21404
21862
  ] },
21405
21863
  };
21406
21864
  if (run === null || view.detail === null) {
21407
- parts.push(`<section class="card cockpit-section" id="verification" data-cockpit-section="proof"><h3>verification</h3>` +
21865
+ parts.push(`<section class="card cockpit-section" id="verification" data-cockpit-section="proof"><h3>Verification</h3>` +
21408
21866
  `<p class="meta">This task has no finished build record, so there are no captured changes or checks to review.</p>` +
21409
21867
  `<p class="row"><a href="${taskHref(view.taskId)}">Open the task →</a></p></section>`);
21410
21868
  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." } };
@@ -21437,24 +21895,31 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
21437
21895
  });
21438
21896
  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>`;
21439
21897
  parts.push(`<div id="verification" data-cockpit-section="result">` + panel.html + contest + `</div>`);
21898
+ const blocked = cantAcceptYetOf(proof?.verdict ?? null, proof?.reasons ?? [], accepted);
21899
+ const youCheck = panel.panel.youCheck;
21900
+ // Accept and finish posts the completion; when an acceptance is owed (the person's own checks, or a reason a
21901
+ // report that doesn't match its changes asks for), the same request records it first.
21902
+ const owed = accepted ? null : youCheck?.accept != null ? { note: null } : panel.panel.need?.accept != null ? { note: panel.panel.need.accept.note }
21903
+ : blocked === ACCEPT_NEEDS_REASON ? { note: "Why is this safe to accept?" } : null;
21440
21904
  const complete = assignment?.state === "ready-to-check" && assignment.receipt !== null && canRetryReview && csrf !== ""
21441
- ? { action: `${taskHref(view.taskId)}/complete`, receipt: assignment.receipt.digest, run: run.id } : null;
21905
+ ? { action: `${taskHref(view.taskId)}/complete`, receipt: assignment.receipt.digest, run: run.id, accept: owed } : null;
21442
21906
  if (complete !== null)
21443
- parts.push(completionForm(view.taskId, run.id, complete.receipt, csrf, view.detail?.pullRequestTo ?? null));
21444
- // The one decision, after the evidence: Accept only when every requirement is met and the checks passed.
21445
- const acceptsHere = complete !== null || panel.panel.need?.accept != null;
21907
+ parts.push(completionForm(view.taskId, run.id, complete.receipt, csrf, view.detail?.pullRequestTo ?? null, owed));
21908
+ // The one decision, after the evidence: Accept and finish only when every requirement is met and the checks passed.
21909
+ const acceptsHere = complete !== null || panel.panel.need?.accept != null || youCheck?.accept != null;
21446
21910
  const matrix = proof === null || proof.proofProblem !== null ? [] : proof.matrix;
21447
- const blocked = cantAcceptYetOf(proof?.verdict ?? null, proof?.reasons ?? [], accepted);
21448
- const words = !acceptsHere ? null : { sentence: RESULT_DECISION_SENTENCE, ...acceptWordsOf({
21449
- checks: checks === undefined ? null : checks.running != null ? "running" : checks.level === "off" && checks.status !== "passed" ? "off" : checks.status,
21450
- unmet: matrix.filter(row => row.state !== "pass" && row.state !== "manual-review").length,
21451
- yours: complete === null || accepted ? 0 : panel.panel.youCheck?.lines.length ?? 0,
21452
- action: complete !== null ? "complete" : "accept",
21453
- publishing: view.detail?.publishing ?? "other",
21454
- proof: proof !== null && proof.proof !== null && proof.proofProblem === null,
21455
- }) };
21456
- // Refuted: plain Accept stays as allowed, in outline, and the one line before the acts (acts.line) says why.
21457
- const decision = words === null || blocked === null ? words : { ...words, label: "Accept", ready: false };
21911
+ const unanswered = accepted || youCheck == null ? [] : youCheck.items.length > 0 ? youCheck.items.map(one => one.statement === "" ? one.words : one.statement) : youCheck.lines;
21912
+ const base = !acceptsHere ? null : acceptWordsOf({
21913
+ checks: checks === undefined ? null : checks.running != null ? "running" : checks.level === "off" && checks.status !== "passed" ? "off" : checks.status,
21914
+ unmet: matrix.filter(row => row.state !== "pass" && row.state !== "manual-review").length,
21915
+ action: complete !== null ? "complete" : "accept",
21916
+ publishing: view.detail?.publishing ?? "other",
21917
+ proof: proof !== null && proof.proof !== null && proof.proofProblem === null,
21918
+ });
21919
+ // Refuted: plain Accept and finish stays as allowed, in outline, and the one line before the acts (acts.line) says why.
21920
+ const settled = base === null || blocked === null ? base : { ...base, label: "Accept and finish", ready: false };
21921
+ const decision = settled === null ? null : { sentence: RESULT_DECISION_SENTENCE, ...settled, ...acceptWithChecksOf(settled, { unanswered, notRight: [] }),
21922
+ base: { label: settled.label, ready: settled.ready, why: settled.why } };
21458
21923
  // Run checks: no check ran on this result (or its saved one can't be read), the project has one, and an approver may run it.
21459
21924
  const followUps = view.detail.followUps ?? null;
21460
21925
  const checksRunning = checks?.running != null || (followUps?.checks.some(one => one.state === "waiting" || one.state === "running") ?? false);
@@ -21464,35 +21929,107 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
21464
21929
  ? { action: `/r/${run.id}/checks`, level: followUps.full ? "full" : "quick", returnTo: `${here}&run=${run.id}&tab=checks` } : null;
21465
21930
  const need = panel.panel.need;
21466
21931
  const nextKind = selected.next?.kind;
21467
- const acts = resultActsOf({
21468
- accept: decision === null ? null : { ready: decision.ready },
21469
- runChecks: runChecks !== null,
21470
- checksRunning,
21471
- blocked,
21472
- canRequest: panel.panel.canRequest && panel.panel.request !== null,
21473
- need: need === null || need.accept != null ? null : need.rebuild != null ? "rebuild" : need.confirm !== null ? "confirm-stopped" : null,
21474
- next: nextKind === "revise" || nextKind === "draft-repair" ? nextKind : null,
21475
- });
21932
+ // 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.
21933
+ const failure = view.failure == null ? null : { line: view.failure.line, evidence: view.failure.evidence, suggestion: view.failure.suggestion, retry: view.failure.retry,
21934
+ link: view.failure.link === null || (view.failure.link.href.startsWith(`${here}&run=${run.id}`) && !view.failure.link.href.includes("#")) ? null : view.failure.link,
21935
+ ...(view.failure.acceptAnyway == null ? {} : { acceptAnyway: { ...view.failure.acceptAnyway, returnTo: `${here}&run=${run.id}` } }) };
21936
+ const actFacts = failure !== null
21937
+ ? { accept: null, runChecks: runChecks !== null, checksRunning, blocked: null, canRequest: false, need: null, next: null, failed: { retry: failure.retry !== null, acceptAnyway: failure.acceptAnyway !== undefined } }
21938
+ : {
21939
+ accept: settled === null ? null : { ready: settled.ready },
21940
+ runChecks: runChecks !== null,
21941
+ checksRunning,
21942
+ blocked,
21943
+ canRequest: panel.panel.canRequest && panel.panel.request !== null,
21944
+ need: need === null || need.accept != null ? null : need.rebuild != null ? "rebuild" : need.confirm !== null ? "confirm-stopped" : null,
21945
+ next: nextKind === "revise" || nextKind === "draft-repair" ? nextKind : null,
21946
+ unanswered: settled === null ? 0 : unanswered.length,
21947
+ notRight: 0,
21948
+ };
21949
+ const acts = resultActsOf(actFacts);
21950
+ // A refuted result's card lists every recorded disagreement, each tied to its lines; when the report itself doesn't
21951
+ // match the changes (not a failed check), that is the headline.
21952
+ const mismatch = failure !== null || proof === null || accepted || proof.verdict !== "refuted" ? null : (() => {
21953
+ const patch = view.detail?.terminal?.patch ?? null;
21954
+ const files = patch === null || "problem" in patch ? [] : parseReviewDiff(patch.text).files;
21955
+ // Each file's first change: the lines it added (else every line it shows), and where that change starts (its link target).
21956
+ const hunkStart = new Map();
21957
+ const changes = new Map(files.map(file => {
21958
+ const shown = file.hunks[0]?.lines.filter(line => line.newLine !== null && line.kind !== "meta") ?? [];
21959
+ if (shown[0]?.newLine != null)
21960
+ hunkStart.set(file.path, shown[0].newLine);
21961
+ const added = shown.filter(line => line.kind === "addition");
21962
+ const lines = (added.length > 0 ? added : shown).map(line => line.newLine);
21963
+ return [file.path, lines.length === 0 ? null : { from: Math.min(...lines), to: Math.max(...lines) }];
21964
+ }));
21965
+ const changesHref = `${here}&run=${run.id}&tab=changes`;
21966
+ const found = reportMismatchesOf(proof.reasons, proof.proofProblem === null ? proof.matrix : [], changes);
21967
+ // The saved report itself can't be read: that is the evidence problem, said once.
21968
+ if (proof.proofProblem !== null)
21969
+ found.push({ text: `The saved report can't be read: ${proof.proofProblem}`, path: null, lines: null, inChanges: null, note: null, reason: proof.proofProblem });
21970
+ if (found.length === 0)
21971
+ return null;
21972
+ return {
21973
+ headline: evidenceProblemOf(proof.verdict, proof.reasons) === "mismatched" ? MISMATCH_HEADLINE : null,
21974
+ rows: found.map(one => ({
21975
+ text: one.text, path: one.path, absent: one.inChanges === false,
21976
+ lines: one.lines === null ? null : one.lines.from === one.lines.to ? `line ${one.lines.from}` : `lines ${one.lines.from}–${one.lines.to}`,
21977
+ href: one.path !== null && one.inChanges === true ? `${changesHref}#${diffFileAnchor(one.path)}${hunkStart.has(one.path) ? `-L${hunkStart.get(one.path)}` : ""}`
21978
+ : one.note !== null && (proof.proof?.caveats.length ?? 0) >= one.note ? `${here}&run=${run.id}#report-note-${one.note}` : null,
21979
+ noteLabel: one.path === null && one.note !== null ? `The report, note ${one.note}` : null,
21980
+ })),
21981
+ said: [...new Set([...proof.reasons, ...found.map(one => one.reason)].flatMap(reason => [reason, plainReasonWords(reason)]))],
21982
+ };
21983
+ })();
21476
21984
  if (view.notes.length > 0) {
21477
- parts.push(`<section class="card cockpit-section" data-cockpit-section="notes"><h3>operator notes</h3>` +
21985
+ parts.push(`<section class="card cockpit-section" data-cockpit-section="notes"><h3>Operator notes</h3>` +
21478
21986
  view.notes.map(one => `<p class="row"><span class="meta">${escape(one.author)} · ${whenTime(one.createdAt)}</span> ${escape(one.note)}</p>`).join("\n") +
21479
21987
  `</section>`);
21480
21988
  }
21481
21989
  return {
21482
21990
  html: `<section class="cockpit-detail">${parts.join("\n")}</section>`,
21483
21991
  selected: {
21484
- ...selected, complete, decision, panel: panel.panel, contest, acts, runChecks,
21992
+ ...selected, complete: failure === null ? complete : null, decision: failure === null ? decision : null, contest, acts, actFacts, runChecks, mismatch, failure,
21993
+ // A failed build reads Failed, whatever its task has done since; its outcome is what went wrong.
21994
+ // Run checks is the decision's own act on this page; a status row never sends the person to Chat for it.
21995
+ panel: (() => {
21996
+ // A Chat link to this same result would lead back here: it isn't one.
21997
+ const elsewhere = (href) => href !== null && !href.endsWith("#follow-ups") && !new RegExp(`^/chat\\?task=[^&]+&result=${run.id}(?:&|$)`).test(href);
21998
+ const shown = panel.panel.status === null ? null : view.demo === true ? demoChecksOf(panel.panel.status) : panel.panel.status;
21999
+ const missed = proof === null || proof.proofProblem !== null ? 0 : requirementsOf(proof.matrix)?.missed ?? 0;
22000
+ const status = shown === null ? null : { ...shown, details: shown.details.map(one => ({ ...one,
22001
+ href: elsewhere(one.href) ? one.href : null, action: one.action !== null && elsewhere(one.action.href) ? one.action : null })) };
22002
+ return failure === null ? { ...panel.panel, status }
22003
+ : { ...panel.panel, outcome: failure.line, need: null, youCheck: null, status: status === null ? null : { ...status, headline: "Failed", tone: "danger", sentence: failure.line, need: null,
22004
+ // Read as Failed, the Requirements row counts what it missed, as the task's card does.
22005
+ details: status.details.map(one => one.key !== "requirements" || missed === 0 ? one : { ...one, text: `${missed} missed`, mark: "failed", action: null }) },
22006
+ // What it missed is said once, in plain words above; the recorded wording stays under Details.
22007
+ attention: panel.panel.attention.filter(one => !(proof?.reasons ?? []).includes(one) && !(proof?.matrix ?? []).some(row => row.detail.includes(one))) };
22008
+ })(),
22009
+ ...(failure === null ? {} : { status: { label: "Failed", tone: "problem", token: "failed" } }),
21485
22010
  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}` },
21486
22011
  },
21487
22012
  };
21488
22013
  }
21489
- function completionForm(taskId, runId, digest, csrf, pullRequestTo = null) {
21490
- 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}">`;
22014
+ /** What Accept and finish owes before it completes, as the form asks for it: the person's own check (no
22015
+ * reason), an exception a report that doesn't match its changes needs a reason for, or nothing. */
22016
+ function owedAcceptanceOf(receipt) {
22017
+ if (receipt === null || receipt.proofAcceptance !== null)
22018
+ return null;
22019
+ if (personCheckPending(receipt))
22020
+ return { note: null };
22021
+ return cantAcceptYetOf(receipt.proof?.verdict ?? null, receipt.proof?.reasons ?? [], false) === ACCEPT_NEEDS_REASON ? { note: "Why is this safe to accept?" } : null;
22022
+ }
22023
+ function completionForm(taskId, runId, digest, csrf, pullRequestTo = null, accept = null) {
22024
+ // Accept and finish, in one request: with an acceptance owed (the person's own checks, or an exception and
22025
+ // its reason, asked for right here), the same post records it first.
22026
+ 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">`}` +
22027
+ (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)}">`);
21491
22028
  if (pullRequestTo !== null) {
21492
- // With pull requests set up: the PR is the primary road, a bare completion the quiet one beside it.
21493
- 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>`;
22029
+ // With pull requests set up: the PR is the primary road, a bare finish the quiet one beside it.
22030
+ 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>`;
21494
22031
  }
21495
- 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>`;
22032
+ 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>`;
21496
22033
  }
21497
22034
  /** The pull request, in one card: its state and link, one line of detail, and the one action it needs — Merge
21498
22035
  * behind the password when checks passed, or Open a pull request for a result completed without one. */
@@ -21576,38 +22113,9 @@ function homePhaseWords(run) {
21576
22113
  function phaseWords(phase) {
21577
22114
  return PHASE_WORDS[phase] ?? "the agent is working";
21578
22115
  }
21579
- const REASON_WORDS = {
21580
- agent: "the agent failed",
21581
- "agent-reported": "the agent reported it could not finish",
21582
- "no-op": "nothing changed when something should have",
21583
- "no-handoff": "the agent stopped before handing off; its work was kept and it is being resumed",
21584
- "moved-head": "the branch moved underneath the build",
21585
- "moved-branch": "the branch moved underneath the build",
21586
- timeout: "ran out of time",
21587
- git: "a git step failed",
21588
- "malformed-decision": "the agent's question was malformed",
21589
- "malformed-plan": "the plan was malformed",
21590
- fenced: "another worker took the task over",
21591
- unapproved: "the scope was not approved",
21592
- "scope-changed": "the scope changed after approval",
21593
- capability: "a requirement was missing",
21594
- setup: "the workspace preparation step failed",
21595
- "provider-init": "the agent could not start",
21596
- "commit-failure": "the commit failed",
21597
- "protected-branch": "refused to touch a protected branch",
21598
- "wrong-branch": "the checkout was on the wrong branch",
21599
- "not-leased": "the lease was not valid",
21600
- "no-claim": "the lease was not valid",
21601
- "not-yours": "the lease was not valid",
21602
- "no-run-record": "the run record was missing",
21603
- "missing-mailbox": "the resume mailbox could not be read",
21604
- "unreadable-mailbox": "the resume mailbox could not be read",
21605
- "revision-brief": "the revision brief could not be read",
21606
- "repaired-park": "resumed from a parked question",
21607
- stopped: "stopped by the operator",
21608
- };
22116
+ /** A recorded reason in one plain line; machine output (a stack trace, a path, "Error:" text) waits on the run record. */
21609
22117
  function reasonWords(reason) {
21610
- return REASON_WORDS[reason] ?? "stopped — the build records have the detail";
22118
+ return isInternalErrorReason(reason) ? "stopped with an internal error" : oneLineOf(runReasonWords(reason.trim().split(/\r?\n/, 1)[0] ?? ""), 140);
21611
22119
  }
21612
22120
  const INCIDENT_WORDS = {
21613
22121
  "malformed-decision": "the agent's question was malformed",
@@ -21659,13 +22167,13 @@ function shortDigest(digest) {
21659
22167
  * orphaned run keeps saying what actually became of it. */
21660
22168
  function runOutcomeBadge(run, live) {
21661
22169
  if (!live && run.role === "planner" && run.reason === "plan-drafted") {
21662
- return `<span class="badge">planned</span>`;
22170
+ return `<span class="badge">Planned</span>`;
21663
22171
  }
21664
22172
  if (!live && run.reason === "interrupted")
21665
- return `<span class="badge">interrupted</span>`;
22173
+ return `<span class="badge">Interrupted</span>`;
21666
22174
  return live
21667
- ? `<span class="badge badge-running">running</span>`
21668
- : `<span class="badge badge-${escape(run.outcome ?? "cut")}">${escape(run.outcome ?? "never finished")}</span>`;
22175
+ ? `<span class="badge badge-running">Running</span>`
22176
+ : `<span class="badge badge-${escape(run.outcome ?? "cut")}">${escape(sentenceCase(run.outcome ?? "never finished"))}</span>`;
21669
22177
  }
21670
22178
  const runNoun = (run) => run.role === "planner" ? "plan" : run.role === "reviewer" ? "review" : run.role === "scout" ? "report" : "build";
21671
22179
  function runsPage(chrome, rows, liveIds, nextCursor, verdicts = new Map(), accepted = new Set()) {
@@ -21680,7 +22188,7 @@ function runsPage(chrome, rows, liveIds, nextCursor, verdicts = new Map(), accep
21680
22188
  return (`<p class="row"><a href="/r/${run.id}" class="mono">#${run.id}</a> ` +
21681
22189
  `<a href="${taskHref(run.taskId)}" class="mono">${escape(run.taskId)}</a> ` +
21682
22190
  runOutcomeBadge(run, liveIds.has(run.id)) +
21683
- `${run.qualityMode === "strict" ? ` <span class="badge">strict review</span>` : ""}` +
22191
+ `${run.qualityMode === "strict" ? ` <span class="badge">Strict review</span>` : ""}` +
21684
22192
  needsVerification +
21685
22193
  criterionMatrixSummary(verdicts.get(run.id)?.matrix ?? []) +
21686
22194
  `${run.provider === "claude" ? "" : ` <span class="meta mono">${escape(run.provider)}</span>`}` +
@@ -21689,7 +22197,7 @@ function runsPage(chrome, rows, liveIds, nextCursor, verdicts = new Map(), accep
21689
22197
  })
21690
22198
  .join("\n");
21691
22199
  const older = nextCursor === null ? "" : `<p><a href="/runs?before=${nextCursor}">older →</a></p>`;
21692
- 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 });
22200
+ 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 });
21693
22201
  }
21694
22202
  function structuredHandoffView(artifacts, root) {
21695
22203
  const artifact = [...artifacts].reverse().find(one => one.kind === "handoff");
@@ -22044,7 +22552,7 @@ function reviewDiffHtml(patch, stat, runId, commentable, anchors = new Map()) {
22044
22552
  const parsed = parseReviewDiff(patch.text);
22045
22553
  const structured = parsed.files.some(file => file.hunks.length > 0);
22046
22554
  if (!structured) {
22047
- return (`<details><summary>the patch${patch.truncated ? " (TRUNCATED — the raw record says how much was cut)" : ""}</summary>` +
22555
+ return (`<details><summary>The patch${patch.truncated ? " (TRUNCATED — the raw record says how much was cut)" : ""}</summary>` +
22048
22556
  `<pre class="mono" style="overflow-x:auto">${escape(patch.text)}</pre></details>` +
22049
22557
  storedDownloadLink(runId, patch.artifactId, "diff", patch.truncated));
22050
22558
  }
@@ -22057,18 +22565,23 @@ function reviewDiffHtml(patch, stat, runId, commentable, anchors = new Map()) {
22057
22565
  : counts.additions === null || counts.deletions === null
22058
22566
  ? `<span class="diff-file-counts">binary</span>`
22059
22567
  : `<span class="diff-file-counts"><b>+${counts.additions}</b><i>−${counts.deletions}</i></span>`;
22060
- const hunks = one.hunks.map(hunk => `<section class="diff-hunk"><div class="diff-hunk-head">${escape(hunk.header)}</div>` +
22061
- `<div class="diff-lines">${hunk.lines.map(line => {
22062
- const lineNumber = line.newLine ?? line.oldLine;
22063
- const side = line.newLine === null && line.oldLine !== null ? "old" : "new";
22064
- const annotate = !commentable || lineNumber === null || line.kind === "meta"
22065
- ? `<span class="diff-annotate-space"></span>`
22066
- : `<button type="button" class="diff-annotate pick-line" data-path="${escape(one.path)}" data-line="${lineNumber}" data-side="${side}" aria-label="Annotate ${escape(one.path)}, ${side} line ${lineNumber}" title="Annotate this line">${annotateIcon}</button>`;
22067
- const marker = line.kind === "addition" ? "+" : line.kind === "deletion" ? "−" : line.kind === "context" ? " " : "·";
22068
- return (`<div class="diff-line diff-${line.kind}">${annotate}` +
22069
- `<span class="diff-gutter">${line.oldLine ?? ""}</span><span class="diff-gutter">${line.newLine ?? ""}</span>` +
22070
- `<code><b aria-hidden="true">${marker}</b>${escape(line.text)}</code></div>`);
22071
- }).join("")}</div></section>`).join("");
22568
+ const fileAnchor = anchors.get(one.path);
22569
+ // Each change is a link target of its own (#<file anchor>-L<first new line>): a result names the exact lines.
22570
+ const hunks = one.hunks.map(hunk => {
22571
+ const first = hunk.lines.find(line => line.newLine !== null && line.kind !== "meta")?.newLine ?? null;
22572
+ return `<section class="diff-hunk"${fileAnchor === undefined || first === null ? "" : ` id="${fileAnchor}-L${first}"`}><div class="diff-hunk-head">${escape(hunk.header)}</div>` +
22573
+ `<div class="diff-lines">${hunk.lines.map(line => {
22574
+ const lineNumber = line.newLine ?? line.oldLine;
22575
+ const side = line.newLine === null && line.oldLine !== null ? "old" : "new";
22576
+ const annotate = !commentable || lineNumber === null || line.kind === "meta"
22577
+ ? `<span class="diff-annotate-space"></span>`
22578
+ : `<button type="button" class="diff-annotate pick-line" data-path="${escape(one.path)}" data-line="${lineNumber}" data-side="${side}" aria-label="Annotate ${escape(one.path)}, ${side} line ${lineNumber}" title="Annotate this line">${annotateIcon}</button>`;
22579
+ const marker = line.kind === "addition" ? "+" : line.kind === "deletion" ? "−" : line.kind === "context" ? " " : "·";
22580
+ return (`<div class="diff-line diff-${line.kind}">${annotate}` +
22581
+ `<span class="diff-gutter">${line.oldLine ?? ""}</span><span class="diff-gutter">${line.newLine ?? ""}</span>` +
22582
+ `<code><b aria-hidden="true">${marker}</b>${escape(line.text)}</code></div>`);
22583
+ }).join("")}</div></section>`;
22584
+ }).join("");
22072
22585
  const anchor = anchors.get(one.path);
22073
22586
  return (`<details class="diff-file"${fileIndex === 0 ? " open" : ""}${anchor === undefined ? "" : ` id="${anchor}"`}>` +
22074
22587
  `<summary><span class="diff-file-name">${escape(one.path)}</span>${countWords}</summary>` +
@@ -22096,7 +22609,9 @@ function checkLogHtml(log, runId, attributes) {
22096
22609
  if (log.problem !== null) {
22097
22610
  return `<p class="problem" data-check-log="damaged"${attributes}>The check log no longer verifies (${escape(log.problem)}). Its output is not shown, and there is nothing to download.</p>`;
22098
22611
  }
22099
- return (`<details data-check-log="${log.truncated ? "shortened" : "ok"}"${attributes}><summary>Check output${log.truncated ? ` (shortened — ${log.bytesStored} of ${log.bytesOriginal} bytes stored)` : ""}</summary><pre class="mono" style="overflow-x:auto;max-height:18rem">${escape(log.text)}</pre></details>` +
22612
+ return (`<details data-check-log="${log.truncated ? "shortened" : "ok"}"${attributes}><summary>Check output${log.truncated ? ` (shortened — ${log.bytesStored} of ${log.bytesOriginal} bytes stored)` : ""}</summary><pre class="mono" style="overflow-x:auto;max-height:18rem">${
22613
+ // Each line is a link target (#check-log-L<n>): a failed task names the exact line its check ended on.
22614
+ log.text.split("\n").map((line, index) => `<span id="check-log-L${index + 1}">${escape(line)}</span>`).join("\n")}</pre></details>` +
22100
22615
  storedDownloadLink(runId, log.artifactId, "check log", log.truncated));
22101
22616
  }
22102
22617
  /** The download link for a stored record (repair 2026-09-14): a shortened
@@ -22111,12 +22626,12 @@ function storedDownloadLink(runId, artifactId, what, truncated) {
22111
22626
  * page's prefill script reads — a real button, keyboard-reachable, separate
22112
22627
  * from the link so the two never fight over one click (finding 4). */
22113
22628
  function terminalDiffCard(view, runId, editor = null, commentable = false) {
22114
- const parts = ["<h2>what changed</h2>"];
22629
+ const parts = ["<h2>What changed</h2>"];
22115
22630
  if (view.stat === null) {
22116
- parts.push(`<p class="meta">no change summary was captured for this build</p>`);
22631
+ parts.push(`<p class="meta">No change summary was captured for this build</p>`);
22117
22632
  }
22118
22633
  else if ("problem" in view.stat) {
22119
- parts.push(`<p class="meta">stat: ${escape(view.stat.problem)}</p>`);
22634
+ parts.push(`<p class="meta">Stat: ${escape(view.stat.problem)}</p>`);
22120
22635
  }
22121
22636
  else {
22122
22637
  const s = view.stat;
@@ -22138,23 +22653,23 @@ function terminalDiffCard(view, runId, editor = null, commentable = false) {
22138
22653
  .slice(0, 40)
22139
22654
  .map(file => `<p class="row mono">${fileName(file.path)}${file.renamedFrom === undefined ? "" : ` (was ${escape(file.renamedFrom)})`} ` +
22140
22655
  `<span class="meta">${file.additions === null || file.deletions === null ? "binary" : `+${file.additions} −${file.deletions}`}</span>` +
22141
- `${commentable ? ` <button type="button" class="pick-file" data-path="${escape(file.path)}">comment</button>` : ""}</p>`)
22656
+ `${commentable ? ` <button type="button" class="pick-file" data-path="${escape(file.path)}">Comment</button>` : ""}</p>`)
22142
22657
  .join("\n") +
22143
22658
  (s.files.length > 40 ? `<p class="meta">…and ${s.files.length - 40} more file(s)</p>` : "") +
22144
22659
  `</div>`);
22145
22660
  if (editor !== null) {
22146
- 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>`);
22661
+ 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>`);
22147
22662
  }
22148
22663
  }
22149
22664
  }
22150
22665
  if (view.patch === null) {
22151
- parts.push(`<p class="meta">the final diff was not captured for this build</p>`);
22666
+ parts.push(`<p class="meta">The final diff was not captured for this build</p>`);
22152
22667
  }
22153
22668
  else if ("problem" in view.patch) {
22154
- parts.push(`<p class="meta">patch: ${escape(view.patch.problem)}</p>`);
22669
+ parts.push(`<p class="meta">Patch: ${escape(view.patch.problem)}</p>`);
22155
22670
  }
22156
22671
  else if (view.patch.text.trim() === "") {
22157
- parts.push(`<p class="meta">empty diff — captured successfully, nothing changed</p>`);
22672
+ parts.push(`<p class="meta">Empty diff — captured successfully, nothing changed</p>`);
22158
22673
  }
22159
22674
  else {
22160
22675
  parts.push(reviewDiffHtml(view.patch, view.stat, runId, commentable));
@@ -22173,7 +22688,7 @@ function evidenceBundleCard(view, run, publication = null, review = null) {
22173
22688
  if (view === null)
22174
22689
  return "";
22175
22690
  const runId = run.id;
22176
- const parts = ["<h2>verification and evidence</h2>"];
22691
+ const parts = ["<h2>Verification and evidence</h2>"];
22177
22692
  if (view.verdict !== null) {
22178
22693
  // The same status line every other surface renders for this run
22179
22694
  // (workspace package 1), beside the verdict's own explanation.
@@ -22183,7 +22698,7 @@ function evidenceBundleCard(view, run, publication = null, review = null) {
22183
22698
  if (view.accepted !== null) {
22184
22699
  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>`);
22185
22700
  }
22186
- parts.push(criterionMatrixHtml(view.matrix, { runId, links: view.matrixLinks }));
22701
+ parts.push(criterionMatrixHtml(view.matrix, { runId, links: view.matrixLinks, verdict: view.verdict }));
22187
22702
  parts.push(semanticCoverageHtml(view.matrix, view.qualityMode));
22188
22703
  if (reviewConflict(view.matrix, view.machineVerdict, view.verdict)) {
22189
22704
  parts.push(`<p class="meta">An independent review found conflicting evidence.</p>`);
@@ -22194,25 +22709,25 @@ function evidenceBundleCard(view, run, publication = null, review = null) {
22194
22709
  }
22195
22710
  else if (view.proof !== null) {
22196
22711
  if (view.proof.criteria.length > 0) {
22197
- parts.push(`<div class="result-section"><strong>acceptance criteria</strong><ul>` +
22712
+ parts.push(`<div class="result-section"><strong>Acceptance criteria</strong><ul>` +
22198
22713
  view.proof.criteria
22199
- .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>`)
22714
+ .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>`)
22200
22715
  .join("") +
22201
22716
  `</ul></div>`);
22202
22717
  }
22203
22718
  if (view.proof.checks.length > 0) {
22204
- parts.push(`<div class="result-section"><strong>checks reported by the agent</strong><ul>` +
22719
+ parts.push(`<div class="result-section"><strong>Checks reported by the agent</strong><ul>` +
22205
22720
  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("") +
22206
22721
  `</ul></div>`);
22207
22722
  }
22208
22723
  if (view.proof.caveats.length > 0) {
22209
- parts.push(`<div class="result-section"><strong>caveats</strong><ul>${view.proof.caveats.map(one => `<li>${escape(one)}</li>`).join("")}</ul></div>`);
22724
+ parts.push(`<div class="result-section"><strong>Caveats</strong><ul>${view.proof.caveats.map(one => `<li>${escape(one)}</li>`).join("")}</ul></div>`);
22210
22725
  }
22211
22726
  }
22212
22727
  if (view.checkLog !== null)
22213
22728
  parts.push(checkLogHtml(view.checkLog, runId, ""));
22214
22729
  if (view.screenshots.length > 0) {
22215
- parts.push(`<div class="result-section"><strong>screenshots</strong>` +
22730
+ parts.push(`<div class="result-section"><strong>Screenshots</strong>` +
22216
22731
  view.screenshots
22217
22732
  .map(shot => `<p class="row"><a href="/r/${runId}/evidence/${shot.artifactId}">` +
22218
22733
  `<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> ` +
@@ -22329,7 +22844,7 @@ function completionReceiptCard(view, taskId, place, standing = receiptStatusOf(v
22329
22844
  (view.publication?.state === "failed" ? `<p class="receipt-note">${statusIconSvg("note")} Pull request couldn't open. The commit is safe locally.</p>` : "") +
22330
22845
  caveats +
22331
22846
  coverage +
22332
- `<details class="receipt-details"><summary>Result details</summary>` +
22847
+ `<details class="receipt-details"><summary>${STATUS_MORE}</summary>` +
22333
22848
  (view.summary !== null && view.summary !== conciseOutcomeOf(view.summary) ? `<p class="recap">${escape(view.summary)}</p>` : "") +
22334
22849
  `<p class="receipt-publication" data-receipt-publication="${escape(facts.publicationState)}">${escape(facts.publicationWords)}</p>` +
22335
22850
  // A review in flight is the receipt's primary status too (its chip
@@ -22460,9 +22975,10 @@ function resultPanelParts(detail, o) {
22460
22975
  items: personCheckItems(proof, patchOk ? patch.text : null, shown, runId),
22461
22976
  accept: acceptable && need?.accept == null ? { action: `${taskHref(detail.taskId)}/accept-proof`, run: run.id, returnTo: o.returnTo } : null,
22462
22977
  };
22978
+ const youCheckForm = youCheck?.accept != null && o.finishes !== true ? youCheck.accept : null;
22463
22979
  const youCheckHtml = youCheck === null ? "" :
22464
22980
  `<div class="result-you-check" data-result-you-check="${youCheck.lines.length}"><ul>${youCheck.lines.map(one => `<li>${escape(one)}</li>`).join("")}</ul>` +
22465
- (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>`) +
22981
+ (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>`) +
22466
22982
  `</div>`;
22467
22983
  const attentionHtml = attention.length === 0
22468
22984
  ? ""
@@ -22512,6 +23028,8 @@ function resultPanelParts(detail, o) {
22512
23028
  ...(conclusion !== outcome ? [`<p class="recap">${escape(conclusion)}</p>`] : []),
22513
23029
  ...(handoff !== null && handoff.changes.length > 0 ? [`<ul class="result-changes">${handoff.changes.map(one => `<li>${escape(one)}</li>`).join("")}</ul>`] : []),
22514
23030
  ...(handoff !== null && handoff.verification.length > 0 ? [`<p class="meta">Checked by the agent:</p><ul class="result-changes" data-cockpit-source="agent-words">${handoff.verification.map(one => `<li>${escape(one)}</li>`).join("")}</ul>`] : []),
23031
+ // The report's own notes, each a link target (#report-note-<n>) a mismatch can point at.
23032
+ ...(proof?.proof == null || proof.proof.caveats.length === 0 ? [] : [`<p class="meta">Its notes:</p><ol class="result-changes" data-report-notes>${proof.proof.caveats.map((one, index) => `<li id="report-note-${index + 1}">${escape(one)}</li>`).join("")}</ol>`]),
22515
23033
  ];
22516
23034
  if (agentAccount.length > 0)
22517
23035
  summaryParts.push(`<details class="result-notes" data-result-notes-agent><summary>What the agent reported</summary>${agentAccount.join("")}</details>`);
@@ -22587,9 +23105,9 @@ function resultPanelParts(detail, o) {
22587
23105
  detail.files
22588
23106
  .map(file => {
22589
23107
  const counts = file.additions === null || file.deletions === null ? "binary" : `+${file.additions} −${file.deletions}`;
22590
- 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>` : "");
23108
+ 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>` : "");
22591
23109
  const why = file.priority.reasons.length === 0 ? "" : ` <span class="meta">— ${file.priority.reasons.map(escape).join("; ")}</span>`;
22592
- 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>`;
23110
+ 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>`;
22593
23111
  })
22594
23112
  .join("") +
22595
23113
  `</ol>` +
@@ -22630,14 +23148,14 @@ function resultPanelParts(detail, o) {
22630
23148
  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>`);
22631
23149
  }
22632
23150
  else {
22633
- checkParts.push(criterionMatrixHtml(proof.matrix, { runId, links: proof.matrixLinks, fileAnchors: detail.fileAnchors }));
23151
+ checkParts.push(criterionMatrixHtml(proof.matrix, { runId, links: proof.matrixLinks, fileAnchors: detail.fileAnchors, verdict: proof.verdict }));
22634
23152
  }
22635
23153
  checkParts.push(semanticCoverageHtml(proof.matrix, proof.qualityMode));
22636
23154
  if (reviewConflict(proof.matrix, proof.machineVerdict, proof.verdict))
22637
23155
  checkParts.push(`<p class="meta">An independent review found conflicting evidence.</p>`);
22638
23156
  checkParts.push(repairChainHtml(proof.repairChain));
22639
23157
  if (detail.reviewerFindings.length > 0) {
22640
- checkParts.push(`<div class="result-section" data-cockpit-source="reviewer"><strong>independent review</strong><ul>` +
23158
+ checkParts.push(`<div class="result-section" data-cockpit-source="reviewer"><strong>Independent review</strong><ul>` +
22641
23159
  reviewerNotes +
22642
23160
  `</ul></div>`);
22643
23161
  }
@@ -22666,7 +23184,7 @@ function resultPanelParts(detail, o) {
22666
23184
  // The handoff's own account of its checks — the agent's words, labeled
22667
23185
  // as such, whether or not a proof exists.
22668
23186
  if (!directAssessment && handoff !== null && handoff.verification.length > 0)
22669
- 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>`);
23187
+ 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>`);
22670
23188
  if (o.extraChecks !== undefined)
22671
23189
  checkParts.push(o.extraChecks);
22672
23190
  // ---- request changes: both feedback styles, one sealed road ------------
@@ -22711,8 +23229,8 @@ function resultPanelParts(detail, o) {
22711
23229
  `<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>` +
22712
23230
  `<span class="meta diff-comment-limit" id="comment-note-limit">up to ${LIMITS.note} characters</span>` +
22713
23231
  `<details class="result-pin"><summary>Attach to a file or line</summary>` +
22714
- `<div class="diff-comment-target"><label>file<input type="text" name="path" placeholder="src/…" aria-label="file" class="mono"></label>` +
22715
- `<label>line<input type="text" name="line" placeholder="—" aria-label="line" inputmode="numeric"></label></div></details>` +
23232
+ `<div class="diff-comment-target"><label>File<input type="text" name="path" placeholder="src/…" aria-label="file" class="mono"></label>` +
23233
+ `<label>Line<input type="text" name="line" placeholder="—" aria-label="line" inputmode="numeric"></label></div></details>` +
22716
23234
  `<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>`);
22717
23235
  }
22718
23236
  // ---- the panel ---------------------------------------------------------
@@ -22754,6 +23272,8 @@ function resultPanelParts(detail, o) {
22754
23272
  reviewHistory: REVIEW_TOKENS.has(status.token) ? status.detail : null,
22755
23273
  attention: attention.filter(one => !shortenedMaterialReason(one)).map(plainReasonWords),
22756
23274
  youCheck,
23275
+ // The same counts as the Requirements row (a refuted report verifies none of them, and says Unverified).
23276
+ requirements: proof === null || proof.verdict === "refuted" ? null : requirementsOf(proof.matrix),
22757
23277
  limits: attention.filter(one => shortenedMaterialReason(one)),
22758
23278
  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 })),
22759
23279
  views: [{ key: "summary", html: summaryParts.join("\n") }, { key: "changes", html: changeParts.join("\n") }, { key: "checks", html: checkParts.join("\n") }],
@@ -22901,7 +23421,7 @@ function runFactsRows(run, taskId, live, route = null) {
22901
23421
  ["quality", qualityModeTitle(run.qualityMode ?? "default")],
22902
23422
  ["outcome", live ? "running" : (run.outcome ?? "never finished")],
22903
23423
  ["phase", live && run.phase !== null ? phaseWords(run.phase) : null],
22904
- ["reason", run.reason === null ? null : reasonWords(run.reason)],
23424
+ ["reason", run.reason === null ? null : isInternalErrorReason(run.reason) ? "stopped with an internal error (below)" : reasonWords(run.reason)],
22905
23425
  ["runner", run.runner, true],
22906
23426
  ["branch", run.branch, true],
22907
23427
  ["model", run.model, true],
@@ -22927,7 +23447,11 @@ function runFactsRows(run, taskId, live, route = null) {
22927
23447
  .filter((fact) => fact[1] !== null && fact[1] !== "")
22928
23448
  .map(([label, value, mono]) => `<p class="row"><span class="meta" style="min-width:8.5rem">${escape(label)}</span> ` +
22929
23449
  `<span${mono === true ? ` class="mono"` : ""}>${escape(value)}</span></p>`)
22930
- .join("\n") + elapsed);
23450
+ .join("\n") + elapsed +
23451
+ // Machine output (a stack trace, a path, "Error:" text) reads as an internal error everywhere else; its detail is here, as recorded.
23452
+ (run.reason !== null && isInternalErrorReason(run.reason)
23453
+ ? `<p class="row"><span class="meta" style="min-width:8.5rem">recorded error</span></p><pre class="mono" id="run-reason-detail" style="white-space:pre-wrap;overflow-wrap:anywhere">${escape(run.reason)}</pre>`
23454
+ : ""));
22931
23455
  }
22932
23456
  function runPage(chrome, run, taskId, artifacts, terminal = null, notes = [], csrf = "", comments = [], ciRepair = null, liveScript, peekable = false, running = false, editor = null, editorToggle = null, noted = false, heldTurns = null, continueOffer = null, structuredHandoff = null, proofBundle = null, route = null, publicationRow = null, review = null, result = null, sourceDigest = null) {
22933
23457
  const rows = runFactsRows(run, taskId, running, route);
@@ -22944,9 +23468,9 @@ function runPage(chrome, run, taskId, artifacts, terminal = null, notes = [], cs
22944
23468
  : " · the agent is working on this";
22945
23469
  const conversation = heldTurns === null
22946
23470
  ? ""
22947
- : `<h2>conversation</h2>` +
23471
+ : `<h2>Conversation</h2>` +
22948
23472
  (heldTurns.turns.length === 0
22949
- ? `<p class="meta">nothing said yet</p>`
23473
+ ? `<p class="meta">Nothing said yet</p>`
22950
23474
  : heldTurns.turns
22951
23475
  .map(turn => `<p class="row"><span class="meta">${turn.sourceKind === "operator"
22952
23476
  ? escape(turn.author ?? "operator")
@@ -22960,11 +23484,11 @@ function runPage(chrome, run, taskId, artifacts, terminal = null, notes = [], cs
22960
23484
  ? `<form method="post" action="/r/${run.id}/turn" class="row">` +
22961
23485
  `<input type="hidden" name="csrf" value="${escape(csrf)}">` +
22962
23486
  `<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">` +
22963
- `<button type="submit">send</button></form>` +
23487
+ `<button type="submit">Send</button></form>` +
22964
23488
  `<p class="meta">${escape(heldTurns.state)} · ${heldTurns.turns.length}/${heldTurns.cap} messages · a waiting question must be answered before free-form messages</p>`
22965
23489
  : heldTurns.open
22966
23490
  ? ""
22967
- : `<p class="meta">the session has ended — the record above is complete</p>`);
23491
+ : `<p class="meta">The session has ended — the record above is complete</p>`);
22968
23492
  // The live peek region (A2): the poller fills it only on a serve that
22969
23493
  // asserted its runner. Without the assertion the section still appears
22970
23494
  // for a running build and says honestly why it is empty \u2014 a page that
@@ -22973,24 +23497,24 @@ function runPage(chrome, run, taskId, artifacts, terminal = null, notes = [], cs
22973
23497
  const peek = !running
22974
23498
  ? ""
22975
23499
  : peekable
22976
- ? `<h2>what is changing right now</h2>` +
22977
- `<div id="run-peek"><p class="meta">watching\u2026 the first look lands within 15 seconds</p></div>` +
23500
+ ? `<h2>What is changing right now</h2>` +
23501
+ `<div id="run-peek"><p class="meta">Watching\u2026 the first look lands within 15 seconds</p></div>` +
22978
23502
  `<p class="meta" id="run-peek-stamp"></p>`
22979
- : `<h2>what is changing right now</h2>` +
22980
- `<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>`;
23503
+ : `<h2>What is changing right now</h2>` +
23504
+ `<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>`;
22981
23505
  // The live transcript (arc 1): the agent's own words, streamed to a file
22982
23506
  // beside the run and polled as raw text. Honesty stated on the surface:
22983
23507
  // this is display only, and the machine running the agent could alter it.
22984
23508
  const transcript = !running
22985
23509
  ? ""
22986
23510
  : !peekable
22987
- ? `<h2>what the agent is saying</h2>` +
22988
- `<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>`
23511
+ ? `<h2>What the agent is saying</h2>` +
23512
+ `<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>`
22989
23513
  : run.provider !== "claude"
22990
- ? `<h2>what the agent is saying</h2>` +
22991
- `<p class="meta">the live transcript needs the claude harness for now \u2014 this build runs on ${escape(run.provider)}</p>`
22992
- : `<h2>what the agent is saying</h2>` +
22993
- `<p class="meta">display only \u2014 this is not evidence, and the machine running the agent could alter it</p>` +
23514
+ ? `<h2>What the agent is saying</h2>` +
23515
+ `<p class="meta">The live transcript needs the claude harness for now \u2014 this build runs on ${escape(run.provider)}</p>`
23516
+ : `<h2>What the agent is saying</h2>` +
23517
+ `<p class="meta">Display only \u2014 this is not evidence, and the machine running the agent could alter it</p>` +
22994
23518
  `<pre id="live-transcript" class="mono" style="max-height:24rem;overflow:auto;white-space:pre-wrap"></pre>` +
22995
23519
  `<p class="meta" id="live-transcript-state"></p>`;
22996
23520
  const resultSummary = structuredHandoff?.conclusion ?? run.handoff;
@@ -22999,7 +23523,7 @@ function runPage(chrome, run, taskId, artifacts, terminal = null, notes = [], cs
22999
23523
  : `<div class="result-section"><strong>${escape(label)}</strong><ul>${items.map(one => `<li>${escape(one)}</li>`).join("")}</ul></div>`;
23000
23524
  const handoff = resultSummary === null
23001
23525
  ? ""
23002
- : `<section class="card result-card"><h2>result</h2><p class="recap">${escape(resultSummary)}</p>` +
23526
+ : `<section class="card result-card"><h2>Result</h2><p class="recap">${escape(resultSummary)}</p>` +
23003
23527
  (structuredHandoff === null
23004
23528
  ? ""
23005
23529
  : resultList("completed", structuredHandoff.changes) +
@@ -23035,9 +23559,9 @@ function runPage(chrome, run, taskId, artifacts, terminal = null, notes = [], cs
23035
23559
  ? ""
23036
23560
  : `<form method="post" action="/r/${run.id}/comment" class="card diff-comment-form" id="comment-form">` +
23037
23561
  `<input type="hidden" name="csrf" value="${escape(csrf)}">` +
23038
- `<div class="diff-comment-target"><label>file<input type="text" name="path" placeholder="select a line above" aria-label="file" class="mono"></label>` +
23039
- `<label>line<input type="text" name="line" placeholder="—" aria-label="line" inputmode="numeric"></label></div>` +
23040
- `<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>` +
23562
+ `<div class="diff-comment-target"><label>File<input type="text" name="path" placeholder="select a line above" aria-label="file" class="mono"></label>` +
23563
+ `<label>Line<input type="text" name="line" placeholder="—" aria-label="line" inputmode="numeric"></label></div>` +
23564
+ `<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>` +
23041
23565
  `<span class="meta diff-comment-limit" id="comment-note-limit">up to ${LIMITS.note} characters</span>` +
23042
23566
  `<button type="submit">Add annotation</button></form>`;
23043
23567
  // The device-side half of the editor-link activation (arc 6, finding 1):
@@ -23065,7 +23589,7 @@ function runPage(chrome, run, taskId, artifacts, terminal = null, notes = [], cs
23065
23589
  : `<div class="card"><p><strong>CI is failing on PR #${ciRepair.pr}</strong> <span class="meta">observed by the episode watcher</span></p>` +
23066
23590
  `<form method="post" action="/r/${run.id}/draft-repair">` +
23067
23591
  `<input type="hidden" name="csrf" value="${escape(csrf)}">` +
23068
- `<button type="submit">draft a repair task</button>` +
23592
+ `<button type="submit">Draft a repair task</button>` +
23069
23593
  `<span class="meta"> — one unapproved task; you approve its scope before anything builds</span></form></div>`;
23070
23594
  const reviewCard = (commentRows === "" && commentForm === "" ? "" : `<h2 id="review">Review and revise</h2>` +
23071
23595
  `<p class="meta">Annotate the diff above. When the batch is ready, create one scoped revision task from it.</p>` +
@@ -23081,8 +23605,8 @@ function runPage(chrome, run, taskId, artifacts, terminal = null, notes = [], cs
23081
23605
  : `<form method="post" action="/r/${run.id}/note" class="row">` +
23082
23606
  `<input type="hidden" name="csrf" value="${escape(csrf)}">` +
23083
23607
  `<input type="text" name="note" placeholder="a note for whoever reads this run next" aria-label="run note" style="width:100%;max-width:28rem">` +
23084
- `<button type="submit">add note</button></form>`;
23085
- const notesCard = noteRows === "" && noteForm === "" ? "" : `<h2>operator notes</h2>${noteRows}${noteForm}`;
23608
+ `<button type="submit">Add note</button></form>`;
23609
+ const notesCard = noteRows === "" && noteForm === "" ? "" : `<h2>Operator notes</h2>${noteRows}${noteForm}`;
23086
23610
  // Continuation (Phase 2E, A4): a finished attempt offers a watched
23087
23611
  // follow-up — the text you type here enters the SIGNED terms on the
23088
23612
  // confirm screen; nothing runs until your password agrees to exactly it.
@@ -23222,7 +23746,7 @@ function capsPage(chrome, caps, gaps, repo, now) {
23222
23746
  if (caps === null) {
23223
23747
  return screen("requirements", [
23224
23748
  `<h1>Requirements</h1>`,
23225
- `<p class="meta">open a project to see its requirements — <a href="/projects">projects</a></p>`,
23749
+ `<p class="meta">Open a project to see its requirements — <a href="/projects">projects</a></p>`,
23226
23750
  ].join("\n"), { chrome });
23227
23751
  }
23228
23752
  const list = caps.length === 0
@@ -23232,7 +23756,7 @@ function capsPage(chrome, caps, gaps, repo, now) {
23232
23756
  `${escape(describeCapability(capability, now ?? new Date()))}</p>`)
23233
23757
  .join("\n");
23234
23758
  const blocked = gaps.length === 0
23235
- ? `<p class="meta">no gaps — everything recorded is verified</p>`
23759
+ ? `<p class="meta">No gaps — everything recorded is verified</p>`
23236
23760
  : gaps
23237
23761
  .map(gap => `<div class="card"><p>${escape(gap.key)} — ${escape(gap.state)}</p>` +
23238
23762
  `<p class="meta">${gap.unblocks.length > 0
@@ -23240,16 +23764,16 @@ function capsPage(chrome, caps, gaps, repo, now) {
23240
23764
  : gap.alsoBlocks.length > 0
23241
23765
  ? `part of what holds: ${gap.alsoBlocks.map(one => escape(one)).join(", ")}`
23242
23766
  : "nothing queued needs it yet"}</p>` +
23243
- `<p class="meta">verify: ${escape(gap.verify)}</p>` +
23767
+ `<p class="meta">Verify: ${escape(gap.verify)}</p>` +
23244
23768
  `<p class="meta">${escape(gap.instructions)}</p></div>`)
23245
23769
  .join("\n");
23246
23770
  return screen("requirements", [
23247
23771
  `<h1>Requirements</h1>`,
23248
23772
  `<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>`,
23249
23773
  list,
23250
- `<h2>missing, ranked by what filling them frees</h2>`,
23774
+ `<h2>Missing, ranked by what filling them frees</h2>`,
23251
23775
  blocked,
23252
- `<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>`,
23776
+ `<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>`,
23253
23777
  ].join("\n"), { chrome });
23254
23778
  }
23255
23779
  /** v98: how the bot's messages reach Toolroll, in words: pushed to the public address, or asked for. */
@@ -23278,7 +23802,7 @@ function settingsPage(chrome, existing, hasEnv, csrf, problem, messaging = null,
23278
23802
  `<input type="hidden" name="csrf" value="${escape(csrf)}">` +
23279
23803
  permissionModeChoices("permission-mode", permissionDefault.mode) +
23280
23804
  `<button type="submit">Save default</button></form>`
23281
- : `<div class="card"><p><strong>${permissionDefault.mode === "bypassPermissions" ? "Full access" : "Auto"}</strong></p><p class="meta">an approver can change this default</p></div>`,
23805
+ : `<div class="card"><p><strong>${permissionDefault.mode === "bypassPermissions" ? "Full access" : "Auto"}</strong></p><p class="meta">An approver can change this default</p></div>`,
23282
23806
  permissionDefault.updatedAt === null
23283
23807
  ? ""
23284
23808
  : `<p class="meta settings-changed">Changed ${whenTime(permissionDefault.updatedAt)}${permissionDefault.updatedBy === null ? "" : ` by ${escape(permissionDefault.updatedBy)}`}</p>`,
@@ -23292,7 +23816,7 @@ function settingsPage(chrome, existing, hasEnv, csrf, problem, messaging = null,
23292
23816
  `<input type="hidden" name="csrf" value="${escape(csrf)}">` +
23293
23817
  qualityModeChoices("quality-mode", qualityDefault.mode) +
23294
23818
  `<button type="submit">Save default</button></form>`
23295
- : `<div class="card"><p><strong>${escape(qualityModeTitle(qualityDefault.mode))}</strong></p><p class="meta">an approver can change this default</p></div>`,
23819
+ : `<div class="card"><p><strong>${escape(qualityModeTitle(qualityDefault.mode))}</strong></p><p class="meta">An approver can change this default</p></div>`,
23296
23820
  qualityDefault.updatedAt === null
23297
23821
  ? ""
23298
23822
  : `<p class="meta settings-changed">Changed ${whenTime(qualityDefault.updatedAt)}${qualityDefault.updatedBy === null ? "" : ` by ${escape(qualityDefault.updatedBy)}`}</p>`,
@@ -23682,18 +24206,18 @@ function nextPage(chrome, data) {
23682
24206
  `<p>${escape(item.approval.title)}</p>` +
23683
24207
  (data.planDocument === null
23684
24208
  ? ""
23685
- : `<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>`) +
24209
+ : `<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>`) +
23686
24210
  `<form method="post" action="${taskHref(item.approval.taskId)}/approve" class="card approve-form">` +
23687
24211
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
23688
24212
  `<input type="hidden" name="nonce" value="${escape(data.nonce)}">` +
23689
24213
  `<input type="hidden" name="digest" value="${escape(data.approvalDigest ?? item.approval.digest)}">` +
23690
24214
  `<input type="hidden" name="return" value="next">` +
23691
- `<p><strong>approve exactly this:</strong></p>` +
23692
- (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>` : "") +
24215
+ `<p><strong>Approve exactly this:</strong></p>` +
24216
+ (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>` : "") +
23693
24217
  (scope === null || scope.profileState !== "unresolved" ? "" : profileWords(scope)) +
23694
- `<p class="meta">goal</p><p class="recap" style="margin-top:0">${escape(scope?.goal ?? item.approval.goal)}</p>` +
23695
- `<p class="meta">not this</p><p class="recap" style="margin-top:0">${scope?.outOfScope == null ? "<em>no exclusions</em>" : escape(scope.outOfScope)}</p>` +
23696
- `<p class="meta">touches · ${scope === null || scope.touches.length === 0 ? "anything" : scope.touches.map(one => escape(one)).join(", ")}</p>` +
24218
+ `<p class="meta">Goal</p><p class="recap" style="margin-top:0">${escape(scope?.goal ?? item.approval.goal)}</p>` +
24219
+ `<p class="meta">Not this</p><p class="recap" style="margin-top:0">${scope?.outOfScope == null ? "<em>no exclusions</em>" : escape(scope.outOfScope)}</p>` +
24220
+ `<p class="meta">Touches · ${scope === null || scope.touches.length === 0 ? "anything" : scope.touches.map(one => escape(one)).join(", ")}</p>` +
23697
24221
  (scope === null ? "" : acceptanceCeremonyHtml(scope.acceptance)) +
23698
24222
  (data.raceTerms === null
23699
24223
  ? ""
@@ -23703,8 +24227,8 @@ function nextPage(chrome, data) {
23703
24227
  // tap away, never in the way.
23704
24228
  agentsCeremonyHtml(data.route) +
23705
24229
  (scope === null ? "" : runtimeDetailsHtml(scope)) +
23706
- `<label>your password, typed again — a signed-in session alone cannot agree to work<input type="password" name="token" autocomplete="current-password"></label>` +
23707
- `<div class="sticky-actions"><button type="submit">approve this scope</button></div>` +
24230
+ `<label>Your password, typed again — a signed-in session alone cannot agree to work<input type="password" name="token" autocomplete="current-password"></label>` +
24231
+ `<div class="sticky-actions"><button type="submit">Approve this scope</button></div>` +
23708
24232
  `</form>` +
23709
24233
  `<p class="meta"><a href="${taskHref(item.approval.taskId)}">open the full task</a> to edit the scope first</p>`;
23710
24234
  }
@@ -23712,22 +24236,22 @@ function nextPage(chrome, data) {
23712
24236
  card =
23713
24237
  `<h1>${escape(item.stalled.taskId)}</h1>` +
23714
24238
  `<p>${escape(item.stalled.title)}</p>` +
23715
- `<p class="meta">stopped — ${item.stalled.incidentCount} incident(s)${item.stalled.strikes > 0 ? ` after ${item.stalled.strikes} attempt(s)` : ""}</p>` +
24239
+ `<p class="meta">Stopped — ${item.stalled.incidentCount} incident(s)${item.stalled.strikes > 0 ? ` after ${item.stalled.strikes} attempt(s)` : ""}</p>` +
23716
24240
  `<form method="post" action="${taskHref(item.stalled.taskId)}/requeue" class="card">` +
23717
24241
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
23718
24242
  `<input type="hidden" name="return" value="next">` +
23719
- `<p class="meta">requeue resolves the incidents, clears the failed attempts, and puts it back in line</p>` +
23720
- `<button type="submit">retry this work</button>` +
24243
+ `<p class="meta">Requeue resolves the incidents, clears the failed attempts, and puts it back in line</p>` +
24244
+ `<button type="submit">Retry this work</button>` +
23721
24245
  `</form>` +
23722
24246
  `<p class="meta"><a href="${taskHref(item.stalled.taskId)}">open the full task</a> to read the runs first</p>`;
23723
24247
  }
23724
24248
  else {
23725
24249
  const { gap } = item;
23726
24250
  card =
23727
- `<h1>supply ${escape(gap.key)}</h1>` +
24251
+ `<h1>Supply ${escape(gap.key)}</h1>` +
23728
24252
  `<p class="meta">${escape(gap.state)}</p>` +
23729
24253
  `<p>Filling this starts ${gap.unblocks.length} task(s): ${gap.unblocks.map(one => `<span class="mono">${escape(one)}</span>`).join(", ")}</p>` +
23730
- `<div class="card"><p class="meta">prove it filled from the terminal:</p><pre class="recap">${escape(gap.verify)}</pre></div>`;
24254
+ `<div class="card"><p class="meta">Prove it filled from the terminal:</p><pre class="recap">${escape(gap.verify)}</pre></div>`;
23731
24255
  }
23732
24256
  return screen("next", [header, card].join("\n"), { chrome });
23733
24257
  }
@@ -23748,9 +24272,9 @@ function decisionOptionForms(decision, csrf, returnTo) {
23748
24272
  `<input type="hidden" name="choice" value="${escape(option.id)}">`,
23749
24273
  ...(returnTo === null ? [] : [`<input type="hidden" name="return" value="${escape(returnTo)}">`]),
23750
24274
  ...(option.reversible ? [] : [`<input type="hidden" name="confirm" value="yes">`]),
23751
- recommended ? `<p class="meta" style="margin:0 0 .375rem"><span class="badge">recommended</span></p>` : "",
24275
+ recommended ? `<p class="meta" style="margin:0 0 .375rem"><span class="badge">Recommended</span></p>` : "",
23752
24276
  `<p class="consequence">${escape(option.consequence)}</p>`,
23753
- `<button type="submit">${escape(option.label)}${option.reversible ? "" : ` <span class="badge badge-overdue">irreversible</span>`}</button>`,
24277
+ `<button type="submit">${escape(option.label)}${option.reversible ? "" : ` <span class="badge badge-overdue">Irreversible</span>`}</button>`,
23754
24278
  `<input type="text" name="note" placeholder="optional note — travels with this answer" aria-label="optional note">`,
23755
24279
  `</form>`,
23756
24280
  ].join("\n");
@@ -23768,14 +24292,14 @@ function decisionPage(chrome, decision, taskId, artifacts, who, now, returnTo =
23768
24292
  : "";
23769
24293
  const evidence = artifacts.length === 0
23770
24294
  ? ""
23771
- : `<div class="evidence"><strong>evidence</strong>` +
24295
+ : `<div class="evidence"><strong>Evidence</strong>` +
23772
24296
  artifacts
23773
24297
  .map(artifact => `<a href="/d/${decision.id}/evidence/${artifact.id}">${escape(evidenceWords(artifact.kind))}` +
23774
24298
  `${artifact.truncated ? " (truncated)" : ""} · ${artifact.bytesStored} bytes</a>`)
23775
24299
  .join("\n") +
23776
24300
  "</div>";
23777
24301
  return screen(`decide \u00b7 ${taskId}`, [
23778
- `<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>`,
24302
+ `<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>`,
23779
24303
  `<div class="recap">${escape(decision.recap)}</div>`,
23780
24304
  `<div class="question">${escape(decision.question)}</div>`,
23781
24305
  decision.state === "answered" ? answered : options,