toolroll 0.9.14 → 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.
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, NO_REASON_RECORDED, failedAttemptSentence, isInternalErrorReason, latestFinishedAttempt, runReasonWords } 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, evidenceProblemOf, lastErrorLineOf, reportMismatchesOf, ACCEPT_NEEDS_REASON, MISMATCH_HEADLINE, 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,34 +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
- /** What went wrong with the latest finished attempt (whatever its outcome, never an older failure), in one line: a
5899
- * failing check's last error line (linked to that line in its saved log), else the attempt's recorded reason in plain
5900
- * words, else that no reason was recorded. A reason that is machine output reads as an internal error, its detail
5901
- * behind a link to that line of the attempt's record; nothing links to this task's own page. */
5902
- function failureOf(runs, assignment) {
5960
+ /** What went wrong with the latest finished attempt (whatever its outcome, never an older failure). */
5961
+ function failureOf(runs, assignment, readLog = true) {
5903
5962
  const last = latestFinishedAttempt(runs);
5904
5963
  if (last === null)
5905
- return { line: NO_REASON_RECORDED, link: null };
5906
- const receipt = assignment?.receipt?.runId === last.id ? assignment.receipt : null;
5907
- if (receipt !== null && receipt.checks.status === "failed" && receipt.checks.logArtifactId !== null) {
5908
- const artifact = store.artifactsFor(last.id).find(one => one.id === receipt.checks.logArtifactId) ?? null;
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) {
5909
5984
  let read = null;
5910
5985
  try {
5911
- read = artifact === null ? null : readVerifiedArtifact(evidenceRoot, artifact);
5986
+ read = readVerifiedArtifact(evidenceRoot, logArtifact);
5912
5987
  }
5913
5988
  catch {
5914
5989
  read = null;
5915
5990
  }
5916
- const found = read !== null && read.ok ? lastErrorLineOf(read.content.toString("utf8")) : null;
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;
5917
5995
  if (found !== null) {
5918
- return { line: oneLineOf(found.text, 140), link: { label: `Check output, line ${found.line}`,
5919
- href: `${reviewHref(receipt.taskId)}&run=${last.id}&tab=checks#check-log-L${found.line}` } };
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}` } };
5920
5999
  }
5921
6000
  }
5922
- return { line: failedAttemptSentence(last.reason),
5923
- link: isInternalErrorReason(last.reason) ? { label: "The recorded error", href: `/r/${last.id}?record=1#run-reason-detail` } : null };
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 };
5924
6010
  }
5925
- function taskScreen(response, who, taskId, problem, status, scopeDraft, cancelDraft) {
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) {
5926
6023
  const admittedFamily = familyOf(taskId);
5927
6024
  if (admittedFamily?.problem != null)
5928
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") }));
@@ -5937,6 +6034,8 @@ export function createDecisionServer(options) {
5937
6034
  presentedData.scopeDraft = scopeDraft;
5938
6035
  if (cancelDraft !== undefined)
5939
6036
  presentedData.cancelDraft = cancelDraft;
6037
+ if (editPlan)
6038
+ presentedData.editPlan = true;
5940
6039
  presentedData.pullRequest = taskPullRequestOf(taskId, who);
5941
6040
  // The thread reads the whole family: the original, its revisions, and every attempt across them.
5942
6041
  if (family !== null && family.problem === null) {
@@ -5946,6 +6045,8 @@ export function createDecisionServer(options) {
5946
6045
  }
5947
6046
  // What went wrong reads the family's latest finished attempt, as the Tasks row does.
5948
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();
5949
6050
  const paneProject = restricted() ? store.lookupRef(taskId)?.repo ?? null : who.via === "cookie" ? who.session.project : null;
5950
6051
  const page = taskPage(paneProject === null && !unscopedMode
5951
6052
  ? chromeFor(paneProject, "tasks")
@@ -6126,7 +6227,7 @@ export function createDecisionServer(options) {
6126
6227
  return peekSay(seen.reason);
6127
6228
  const stamp = clock().toISOString().slice(11, 19);
6128
6229
  const parts = [
6129
- `<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>`,
6130
6231
  ];
6131
6232
  const changed = seen.rows.filter(one => one.kind === "changed");
6132
6233
  const deleted = seen.rows.filter(one => one.kind === "deleted");
@@ -6151,7 +6252,7 @@ export function createDecisionServer(options) {
6151
6252
  for (const row of deleted)
6152
6253
  parts.push(line(row, "−"));
6153
6254
  if (fresh.total > 0) {
6154
- parts.push(`<p class="meta">new files · ${fresh.total}</p>`);
6255
+ parts.push(`<p class="meta">New files · ${fresh.total}</p>`);
6155
6256
  for (const row of fresh.rows) {
6156
6257
  parts.push(row.collapsed
6157
6258
  ? `<p class="row mono">+ ${peekName(row.label)} <span class="meta">collapsed names — ${row.count} files</span></p>`
@@ -6162,7 +6263,7 @@ export function createDecisionServer(options) {
6162
6263
  }
6163
6264
  }
6164
6265
  if (unchecked.length > 0) {
6165
- 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>`);
6166
6267
  for (const row of unchecked)
6167
6268
  parts.push(line(row, "?"));
6168
6269
  }
@@ -6173,7 +6274,7 @@ export function createDecisionServer(options) {
6173
6274
  // The byte cap is enforced AFTER escaping (finding 32): an oversize
6174
6275
  // rendering is replaced whole by its exact counts.
6175
6276
  fragment =
6176
- `<p class="meta">best-effort look at ${escape(stamp)} UTC</p>` +
6277
+ `<p class="meta">Best-effort look at ${escape(stamp)} UTC</p>` +
6177
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>`;
6178
6279
  }
6179
6280
  peekCache.set(key, { fragment, at: Date.now() });
@@ -8604,7 +8705,7 @@ export function createDecisionServer(options) {
8604
8705
  // THE CEREMONY (M1): every resolved term in words, and the password
8605
8706
  // signs exactly this digest — a drifted form refuses at /mode/sign.
8606
8707
  const nonce = mintApprovalNonce(who.name, "mode-sign", `${project}:${digest}`);
8607
- const bodyHtml = `<h1 style="overflow-wrap:anywhere">sign the ${escape(name)} mode for ${escape(project)}</h1>` +
8708
+ const bodyHtml = `<h1 style="overflow-wrap:anywhere">Sign the ${escape(name)} mode for ${escape(project)}</h1>` +
8608
8709
  `<form method="post" action="/mode/sign" class="card approve-form">` +
8609
8710
  `<input type="hidden" name="csrf" value="${escape(who.session.csrf)}">` +
8610
8711
  `<input type="hidden" name="nonce" value="${escape(nonce)}">` +
@@ -8613,12 +8714,12 @@ export function createDecisionServer(options) {
8613
8714
  .map(field => `<input type="hidden" name="${field}" value="${escape(body.get(field) ?? "")}">`)
8614
8715
  .join("") +
8615
8716
  `<input type="hidden" name="expiry" value="${escape(expiry)}">` +
8616
- `<p><strong>your password signs exactly this:</strong></p>` +
8717
+ `<p><strong>Your password signs exactly this:</strong></p>` +
8617
8718
  modeWords(terms)
8618
8719
  .map(words => `<p class="recap" style="margin-top:.4rem">${escape(words)}</p>`)
8619
8720
  .join("") +
8620
- `<label>your password, typed again<input type="password" name="token" autocomplete="current-password"></label>` +
8621
- `<div class="sticky-actions"><button type="submit">sign it</button></div>` +
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>` +
8622
8723
  `</form>` +
8623
8724
  `<p class="meta"><a href="/mode">back — sign nothing</a></p>`;
8624
8725
  return sendScreen(response, 200, screen("mode", bodyHtml, { chrome: chromeFor(project, "mode") }));
@@ -9936,7 +10037,9 @@ export function createDecisionServer(options) {
9936
10037
  const found = store.getRun(id);
9937
10038
  if (found === null || !visible(taskRepoOf(found.taskRef)))
9938
10039
  return refuse(response, who, 404, "no such run");
9939
- 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]);
9940
10043
  if (who.via !== "cookie")
9941
10044
  return refuse(response, who, 403, "Sign in with a browser session to do this.", back);
9942
10045
  if (followUp[2] === "checks") {
@@ -9948,7 +10051,8 @@ export function createDecisionServer(options) {
9948
10051
  const asked = requestFollowUpChecks(store, { runId: id, level, actor: who.name }, now);
9949
10052
  if (!asked.ok)
9950
10053
  return refuse(response, who, 409, asked.message, back);
9951
- 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`);
9952
10056
  }
9953
10057
  const filed = fileAddTestsTask(store, evidenceRoot, { runId: id, actor: who.name, filedVia: "console", ...(admissionList() === null ? {} : { admittedRepos: admissionList() }) }, now);
9954
10058
  if (!filed.ok)
@@ -10162,7 +10266,7 @@ export function createDecisionServer(options) {
10162
10266
  const quickMode = quickRef?.repo == null ? null : store.activeMode(quickRef.repo, clock());
10163
10267
  const quickTerms = quickMode === null ? null : modeTermsFromJson(quickMode.termsJson);
10164
10268
  const quick = quickTerms?.quickMint === true && quickMode !== null && quickMode.signedBy === who.name;
10165
- 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>` +
10166
10270
  `<form method="post" action="${taskHref(terms.taskId)}/attend" class="card approve-form">` +
10167
10271
  `<input type="hidden" name="csrf" value="${escape(csrf)}">` +
10168
10272
  `<input type="hidden" name="nonce" value="${escape(nonce)}">` +
@@ -10175,10 +10279,10 @@ export function createDecisionServer(options) {
10175
10279
  (inputs.posture === undefined ? "" : `<input type="hidden" name="posture" value="${escape(inputs.posture)}">`) +
10176
10280
  (terms.parentRun == null ? "" : `<input type="hidden" name="parent" value="${terms.parentRun}">`) +
10177
10281
  (terms.followup == null ? "" : `<input type="hidden" name="followup" value="${escape(terms.followup)}">`) +
10178
- `<p><strong>your password signs exactly this:</strong></p>` +
10179
- `<p class="meta">goal</p><p class="recap" style="margin-top:0">${escape(scope?.goal ?? "")}</p>` +
10180
- `<p class="meta">not this</p><p class="recap" style="margin-top:0">${scope?.outOfScope == null ? "<em>no exclusions</em>" : escape(scope.outOfScope)}</p>` +
10181
- `<p class="meta">touches · ${scope === null || scope.touches.length === 0 ? "anything" : scope.touches.map(one => escape(one)).join(", ")}</p>` +
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>` +
10182
10286
  (() => {
10183
10287
  const profile = JSON.parse(terms.profileJson).profile;
10184
10288
  const posture = profile?.permissionArgv === "bypassPermissions"
@@ -10186,21 +10290,21 @@ export function createDecisionServer(options) {
10186
10290
  : profile?.permissionArgv === "auto"
10187
10291
  ? "safe unattended permissions — routine project commands and edits proceed; risky acts stop"
10188
10292
  : "legacy acceptEdits — edits proceed, commands that ask are denied unattended";
10189
- return `<p class="meta">runs on</p><p class="recap" style="margin-top:0">claude · ${escape(String(profile?.model ?? ""))} — ${posture}</p>`;
10293
+ return `<p class="meta">Runs on</p><p class="recap" style="margin-top:0">claude · ${escape(String(profile?.model ?? ""))} — ${posture}</p>`;
10190
10294
  })() +
10191
10295
  (terms.parentRun == null
10192
10296
  ? ""
10193
- : `<p class="meta">continues</p><p class="recap" style="margin-top:0">attempt <a href="/r/${terms.parentRun}" class="mono">#${terms.parentRun}</a>, from exactly where it finished</p>` +
10194
- `<p class="meta">the follow-up — this is the instruction</p><p class="recap" style="margin-top:0">${escape(terms.followup ?? "")}</p>`) +
10195
- `<p class="meta">repository · head</p><p class="recap mono" style="margin-top:0">${escape(terms.repo)} @ ${escape(terms.head.slice(0, 12))}</p>` +
10196
- `<p class="meta">worker</p><p class="recap" style="margin-top:0">${escape(terms.runner)} (this machine)</p>` +
10197
- `<p class="meta">spending</p><p class="recap" style="margin-top:0">up to about $${(terms.budgetMicrousd / 1_000_000).toFixed(2)} — the agent stops as soon as its total crosses this; the final step may run a little past it</p>` +
10198
- `<p class="meta">conversation</p><p class="recap" style="margin-top:0">at most ${terms.maxSessionTurns} messages to the agent, this whole session; each may work up to ${Math.round(terms.turnTimeoutSeconds / 60)} minutes — one that runs past that ends the whole session. If it needs a repair, the repair uses the same session, model, and clock.</p>` +
10199
- `<p class="meta">while you watch — a signed term</p><p class="recap" style="margin-top:0">your console being open is what keeps it running — any page of it, this one included; close the console and the session winds down within a minute. Everything ends by ${whenTime(terms.absoluteExpiry)} regardless. One attempt; it never converts into unattended work.</p>` +
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>` +
10200
10304
  (quick
10201
- ? `<p class="meta">your signed mode covers this mint — no password; the mode is re-proved as you confirm, and the session is stamped with its signature</p>`
10202
- : `<label>your password, typed again — a signed-in session alone cannot authorize work<input type="password" name="token" autocomplete="current-password"></label>`) +
10203
- `<div class="sticky-actions"><button type="submit">run it while I watch</button></div>` +
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>` +
10204
10308
  `</form>` +
10205
10309
  `<p class="meta"><a href="${taskHref(terms.taskId)}">back to the task</a></p>`;
10206
10310
  // The chrome binds to the AUTHORITY repository (surfaces round 1,
@@ -10748,6 +10852,13 @@ export function createDecisionServer(options) {
10748
10852
  if (budgetUsd !== null && (!Number.isFinite(budgetUsd) || budgetUsd <= 0)) {
10749
10853
  return taskScreen(response, who, taskId, "the dollar cap is a positive amount", 400);
10750
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");
10751
10862
  // The tournament controls (operator request): a count of 2–4 files
10752
10863
  // race terms BESIDE the scope — validated and priced BEFORE anything
10753
10864
  // saves, so a bad tournament never half-lands on a good scope.
@@ -10802,92 +10913,112 @@ export function createDecisionServer(options) {
10802
10913
  if (plannedRace !== null && store.mirrorByTask(taskId) !== null) {
10803
10914
  return taskScreen(response, who, taskId, "external work races in a follow-up release — file the tournament on a local task", 409);
10804
10915
  }
10805
- const saved = store.transact(() => {
10806
- if ((plannedRace !== null || plannedComparison !== null) && store.openAuthorizationFor(ref.id) !== null) {
10807
- return { ok: false, status: 409, message: "an attended authorization is open on this task — revoke it before filing a tournament or comparison" };
10808
- }
10809
- // C1/M3: the signer's own credentialed filing auto-approves —
10810
- // coverage is asked INSIDE this transaction, the escalated
10811
- // default and budget ride the filing, and the seal commits
10812
- // atomically with it. Plain scopes only: a tournament or
10813
- // comparison keeps its own human ceremony.
10814
- // COOKIE ONLY (C1's channel table; surfaces round 1, finding 1):
10815
- // bearer credentials are for machines, and a machine road must
10816
- // never spend the signer's mode.
10817
- const coverage = who.via === "cookie" && plannedRace === null && plannedComparison === null
10818
- ? modeFilingCoverage(store, ref.repo, who.name, now)
10819
- : null;
10820
- const proposed = proposeGuarded(store, {
10821
- taskId, author: who.name,
10822
- goal: body.get("goal") ?? "",
10823
- outOfScope: body.get("not") ?? null,
10824
- touches: (body.get("touches") ?? "").split(/[\n,]/),
10825
- acceptance: acceptanceLinesToInput((body.get("acceptance") ?? "").split("\n")),
10826
- permissionMode,
10827
- qualityMode,
10828
- ...(budgetUsd !== null
10829
- ? { budgetMicrousd: Math.round(budgetUsd * 1_000_000) }
10830
- : coverage?.defaultBudgetMicrousd != null
10831
- ? { budgetMicrousd: coverage.defaultBudgetMicrousd }
10832
- : {}),
10833
- ...(coverage?.escalated === true ? { posture: "escalated" } : {}),
10834
- sawDigest: sawDigest === null || sawDigest === "" ? null : sawDigest,
10835
- taskRef: ref.id,
10836
- now,
10837
- });
10838
- if (!proposed.ok) {
10839
- return {
10840
- ok: false,
10841
- status: proposed.reason === "changed" || proposed.reason === "claimed" ? 409 : 400,
10842
- message: proposed.message ?? `scope not saved: ${proposed.reason}`,
10843
- };
10844
- }
10845
- if (coverage !== null && ref.plan === "requested") {
10846
- authorizePlanUnderMode(store, taskId, who.name, now);
10847
- }
10848
- else if (coverage !== null) {
10849
- const filedScope = store.getScope(taskId);
10850
- if (filedScope?.profileState === "resolved") {
10851
- const sealedUnderMode = store.sealScopeApproval(taskId, who.name, now, {}, { kind: "mode", modeDigest: coverage.digest });
10852
- // The quarantine speaks at the caller (review finding 7).
10853
- if (!sealedUnderMode) {
10854
- return { ok: false, status: 200, message: "scope saved — coordinator-filed: mode coverage cannot admit it; sign the scope" };
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
+ }
10855
10981
  }
10856
10982
  }
10857
- }
10858
- 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
+ }
10859
11004
  store.fileTournamentTerms({
10860
11005
  taskRef: ref.id,
10861
- kind: "comparison",
10862
- raceDigest: plannedComparison.plan.comparisonDigest,
10863
- agents: plannedComparison.plan.agents,
10864
- perAgentBudgetMicrousd: 0,
10865
- overrunReserveMicrousd: 0,
10866
- totalBudgetMicrousd: 0,
10867
- priceVersion: 0,
10868
- publicationPolicy: plannedComparison.plan.publicationPolicy,
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,
10869
11013
  }, now);
10870
11014
  return { ok: true };
10871
- }
10872
- if (plannedRace === null) {
10873
- // Switching back to "one agent" withdraws a standing race OR
10874
- // comparison — the deactivated row survives as history, and
10875
- // the approval card returns to the scope alone.
10876
- store.retractTournamentTerms(ref.id);
10877
- return { ok: true };
10878
- }
10879
- store.fileTournamentTerms({
10880
- taskRef: ref.id,
10881
- raceDigest: plannedRace.raceDigest,
10882
- agents: plannedRace.agents,
10883
- perAgentBudgetMicrousd: plannedRace.perAgentBudgetMicrousd,
10884
- overrunReserveMicrousd: plannedRace.overrunReserveMicrousd,
10885
- totalBudgetMicrousd: plannedRace.totalBudgetMicrousd,
10886
- priceVersion: plannedRace.priceVersion,
10887
- publicationPolicy: plannedRace.publicationPolicy,
10888
- }, now);
10889
- return { ok: true };
10890
- });
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
+ }
10891
11022
  if (!saved.ok)
10892
11023
  return taskScreen(response, who, taskId, saved.message, saved.status, body);
10893
11024
  return redirect(response, taskHref(taskId));
@@ -11003,6 +11134,9 @@ export function createDecisionServer(options) {
11003
11134
  }
11004
11135
  note = validated.note;
11005
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);
11006
11140
  store.acceptProof(latest.id, verifiedAuthor(who.name), note, now);
11007
11141
  return redirect(response, body.get("return") === null ? taskHref(taskId) : safeReturn(body.get("return")));
11008
11142
  }
@@ -11018,6 +11152,23 @@ export function createDecisionServer(options) {
11018
11152
  if (current === null || current.receipt?.taskId !== taskId || !/^[0-9]{1,15}$/.test(namedRun) || current.receipt.runId !== Number(namedRun)) {
11019
11153
  return taskScreen(response, who, taskId, "This result changed. Open the current result before marking it complete.", 409);
11020
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
+ }
11021
11172
  if (body.get("publish") === "1") {
11022
11173
  // "Complete and open a pull request": the same exact-receipt completion, plus the owed PR for this
11023
11174
  // exact commit in one transaction. The watch process pushes and opens it; the task shows its progress.
@@ -11438,6 +11589,36 @@ export function createDecisionServer(options) {
11438
11589
  const run = store.runsFor(ref.id).find(one => (one.outcome === "built" || one.outcome === "no-change") &&
11439
11590
  one.finishedAt !== null &&
11440
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 };
11441
11622
  const publication = run === null ? null : store.publicationForRun(run.id);
11442
11623
  const verdict = run === null ? null : store.proofVerdictFor(run.id);
11443
11624
  return {
@@ -11628,6 +11809,7 @@ export function createDecisionServer(options) {
11628
11809
  intent,
11629
11810
  plan,
11630
11811
  contest: contestOf(),
11812
+ ...(store.isDemo() ? { demo: true } : {}),
11631
11813
  };
11632
11814
  if (run === null) {
11633
11815
  return { ...base, run: null, detail: null, reviewRetry: null, review: null, notes: [] };
@@ -11949,8 +12131,8 @@ export function editorFileHref(worktree, path, line) {
11949
12131
  function matrixStateBadge(state) {
11950
12132
  // Red belongs to a Failed headline alone (task-status.ts); an unmet requirement is an amber note.
11951
12133
  const cls = state === "pass" ? "badge-done" : state === "failed" ? "badge-note" : state === "missing" ? "badge-note" : "badge-manual-review";
11952
- const word = state === "pass" ? "pass" : state === "missing" ? "missing" : state === "failed" ? "failed" : "manual review";
11953
- return `<span class="badge ${cls}" data-matrix-state="${escape(state)}">${escape(word)}</span>`;
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>`;
11954
12136
  }
11955
12137
  /** v40: the bounded repair chain's own line — presented as the user-facing
11956
12138
  * recovery it represents, without exposing the internal workflow name.
@@ -12051,8 +12233,9 @@ function criterionMatrixHtml(matrix, options = {}) {
12051
12233
  const awaitingAssessment = row.assessment?.evidenceState === "pass" && row.review === null;
12052
12234
  const confirmed = row.assessment !== undefined && state === "pass";
12053
12235
  // Plain words (task-status brief): the retired assessment step is not a state a person acts on.
12054
- const label = awaitingAssessment ? "Met" : confirmed ? "Confirmed" : state === "pass" ? "Met" : state === "manual-review" ? "You check" : state === "missing" ? "Not shown yet" : "Not met";
12055
- const cls = awaitingAssessment ? "badge-done" : state === "pass" ? "badge-done" : state === "manual-review" ? "badge-manual-review" : "badge-note";
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";
12056
12239
  const warnings = [];
12057
12240
  // Only replace the known boilerplate. Other recorded failure details
12058
12241
  // remain verbatim, so concision cannot hide a different problem.
@@ -12122,10 +12305,10 @@ function planContractHtml(view, mode) {
12122
12305
  if ("problem" in view) {
12123
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>`;
12124
12307
  }
12125
- const stale = view.current ? "" : `<p class="meta">the scope was edited after this draft landed — the record below describes the draft as the planner proposed it</p>`;
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>`;
12126
12309
  if (!view.filed) {
12127
12310
  return mode === "full"
12128
- ? `<div class="contract-panel contract-drafted"><p class="approval-label">filed contract</p><p class="meta">no scope was filed before planning — the planner drafted this contract from the title and the repository; review every term as new</p>${stale}</div>`
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>`
12129
12312
  : `<p class="meta contract-note">no scope was filed before planning — every term above is the planner's proposal</p>`;
12130
12313
  }
12131
12314
  if (view.revision === true) {
@@ -12135,19 +12318,19 @@ function planContractHtml(view, mode) {
12135
12318
  }
12136
12319
  return (`<div class="contract-panel contract-amended"${mode === "full" ? ` id="contract-amendment"` : ""}><p class="approval-label">updated plan</p>` +
12137
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>` +
12138
- (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>`) +
12139
12322
  `<ul class="recap contract-changes">${view.changes.map(contractChangeHtml).join("")}</ul>` +
12140
12323
  stale +
12141
12324
  `</div>`);
12142
12325
  }
12143
12326
  if (view.changes.length === 0) {
12144
- return `<div class="contract-panel contract-preserved"><p class="approval-label">filed contract</p><p><strong>preserved exactly</strong> <span class="meta">the plan reproduces the filed goal, exclusions, touches, and acceptance criteria — approving binds the terms you filed · <a href="/r/${view.run}">run ${view.run}</a></span></p>${stale}</div>`;
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>`;
12145
12328
  }
12146
12329
  return (`<div class="contract-panel contract-amended"${mode === "full" ? ` id="contract-amendment"` : ""}><p class="approval-label">filed contract · amendment proposed</p>` +
12147
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>` +
12148
12331
  (view.amendment === null
12149
- ? `<p class="meta">the planner stated no reason for the amendment</p>`
12150
- : `<p class="recap contract-reason"><strong>why:</strong> ${escape(view.amendment)}</p>`) +
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>`) +
12151
12334
  `<ul class="recap contract-changes">${view.changes.map(contractChangeHtml).join("")}</ul>` +
12152
12335
  stale +
12153
12336
  `</div>`);
@@ -12176,7 +12359,7 @@ function approvalPathsHtml(touches) {
12176
12359
  function acceptanceCeremonyHtml(criteria) {
12177
12360
  if (criteria.length === 0)
12178
12361
  return "";
12179
- return (`<p class="meta">acceptance</p><ul class="recap acceptance-rubric">` +
12362
+ return (`<p class="meta">Acceptance</p><ul class="recap acceptance-rubric">` +
12180
12363
  criteria
12181
12364
  .map(c => `<li><code>${escape(c.id)}</code> ${escape(c.statement)} <span class="meta">[requires: ${c.evidence.map(escape).join(", ")}]</span></li>`)
12182
12365
  .join("") +
@@ -12294,10 +12477,20 @@ function approvalSheetHtml(input) {
12294
12477
  const planner = legOf("plan");
12295
12478
  if (planner !== undefined)
12296
12479
  who.push(`Planner ${agentNameWords(planner.provider, planner.model)}`);
12297
- if (scope.budgetMicrousd !== null)
12298
- who.push(`Cap ${money(scope.budgetMicrousd)} per attempt`);
12299
- if (permission === "Full access")
12300
- who.push("Full access");
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>`);
12301
12494
  const race = raceTerms === null
12302
12495
  ? ""
12303
12496
  : `<p class="approval-race">${raceTerms.n} agents build this separately: ${raceTerms.agents.map(one => escape(agentNameWords(one.provider, one.model))).join(" and ")}. ` +
@@ -12324,7 +12517,7 @@ function approvalSheetHtml(input) {
12324
12517
  : !contract.filed && contract.revision !== true ? `<p class="meta">Nothing was filed before planning, so every term is the planner's proposal.</p>`
12325
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>`;
12326
12519
  const criteriaDetail = scope.acceptance.length === 0 ? ""
12327
- : `<ul class="approval-signed-criteria">${scope.acceptance.map(one => `<li><code>${escape(one.id)}</code> ${escape(one.statement)} <span class="meta">needs ${one.evidence.map(escape).join(", ")}</span></li>`).join("")}</ul>`;
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>`;
12328
12521
  const projection = route?.projection ?? null;
12329
12522
  const agentsDetail = route === null || route === undefined ? ""
12330
12523
  : projection === null
@@ -12338,7 +12531,7 @@ function approvalSheetHtml(input) {
12338
12531
  const limits = profileWords(scope) +
12339
12532
  `<p class="meta">Checks level: ${escape(qualityModeTitle(scope.qualityMode ?? "default"))}${permission === null ? "" : ` · ${escape(permission)}`}</p>` +
12340
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>`);
12341
- const details = `<details class="approval-details"><summary>Details</summary>` +
12534
+ const details = `<details class="approval-details"><summary>Plan details</summary>` +
12342
12535
  revisionDetail +
12343
12536
  (repairText === null ? "" : detail("Signed goal", `<p>${escape(scope.goal)}</p>`)) +
12344
12537
  detail("Plan record", contractDetail) +
@@ -12348,6 +12541,53 @@ function approvalSheetHtml(input) {
12348
12541
  (scope.candidate ? detail("Saved commit", `<p><code class="approval-commit">${escape(scope.candidate)}</code></p>`) : "") +
12349
12542
  detail("Seal", `<p class="meta">Approval binds to this exact wording. <span class="seal mono">signs ${shortDigest(scope.digest)}</span></p>`) +
12350
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>`;
12351
12591
  return (`<form method="post" action="${input.action}" class="approve-form approval-sheet"${input.surface === "task" ? ` id="approve"` : ""}${input.sticky ? " data-sticky" : ""}>` +
12352
12592
  `<input type="hidden" name="csrf" value="${escape(input.csrf)}">` +
12353
12593
  `<input type="hidden" name="nonce" value="${escape(input.nonce)}">` +
@@ -12367,13 +12607,75 @@ function approvalSheetHtml(input) {
12367
12607
  approvalAmendmentHtml(contract) +
12368
12608
  race +
12369
12609
  (who.length === 0 ? "" : `<p class="approval-who">${who.map(escape).join(" · ")}</p>`) +
12610
+ consent +
12370
12611
  `<div class="approval-act" id="${input.surface === "task" ? "approval-confirm" : "chat-approval-confirm"}">` +
12371
- `<label class="approval-password"><span class="visually-hidden">Your password</span><input type="password" name="token" autocomplete="current-password" placeholder="Your password"></label>` +
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>` +
12372
12614
  `<button type="submit" data-primary-action>${submit}</button></div>` +
12373
12615
  `<p class="approval-after">${after}</p>` +
12374
- `<p class="approval-secondary"><a class="approval-link" href="${escape(input.editHref)}">Edit plan</a><a class="approval-link" href="${escape(input.notNowHref)}">Not now</a></p>` +
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>` +
12375
12617
  details +
12376
- `</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 }];
12377
12679
  }
12378
12680
  function consentDoorOf(scope, route) {
12379
12681
  if (scope === null)
@@ -12426,31 +12728,31 @@ function consentClosedHtml(taskId, door, surface) {
12426
12728
  }
12427
12729
  function profileWords(scope) {
12428
12730
  if (scope.profileState === "unresolved") {
12429
- return `<p class="meta"><strong>filed but unapprovable</strong> — ${escape(scope.unresolvedReason ?? "the scope cannot say exactly what would run")}. Restate the scope to fix it.</p>`;
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>`;
12430
12732
  }
12431
12733
  const profile = scope.profile ?? null;
12432
12734
  if (profile === null) {
12433
12735
  return (scope.digestVersion ?? 1) < 2
12434
- ? `<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>`
12435
12737
  : "";
12436
12738
  }
12437
12739
  const repair = profile.repairModel === "inherit" ? "same model" : profile.repairModel;
12438
12740
  const base = profile.provider === "claude"
12439
- ? `<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"
12440
12742
  ? "FULL permissions; claude runs with --dangerously-skip-permissions and nothing asks"
12441
12743
  : profile.permissionArgv === "auto"
12442
12744
  ? "safe unattended permissions; routine project commands and edits proceed, risky acts stop"
12443
- : "legacy acceptEdits; edits proceed, commands that ask are denied unattended"}, ${profile.maxTurns}-turn runaway breaker, ${Math.round(profile.timeoutSeconds / 60)} min ${profile.timeoutKind === "idle" ? "without progress" : "per attempt"}; repairs on ${escape(repair)}, ${profile.repairMaxTurns} turns / ${Math.round(profile.repairTimeoutSeconds / 60)} min</p>`
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>`
12444
12746
  : profile.provider === "gemini"
12445
- ? `<p class="meta">runs on <span class="mono">gemini · ${escape(profile.model)}</span> — ${profile.approvalArgv === "yolo" ? "Full access via --approval-mode yolo; every tool auto-approved" : "Auto via --approval-mode auto_edit; edits auto-approved, other tools refused"}, no turn limit (${Math.round(profile.timeoutSeconds / 60)} min ${profile.timeoutKind === "idle" ? "without progress" : "per attempt"}), spend reported in tokens only; repairs on ${escape(repair)}, ${Math.round(profile.repairTimeoutSeconds / 60)} min</p>`
12446
- : `<p class="meta">runs on <span class="mono">${escape(profile.provider)} · ${escape(profile.model)}</span> — ${profile.sandboxMode === "danger-full-access" ? "FULL permissions via --dangerously-bypass-approvals-and-sandbox; nothing asks" : "workspace-write sandbox"}, no turn limit (${Math.round(profile.timeoutSeconds / 60)} min ${profile.timeoutKind === "idle" ? "without progress" : "per attempt"}); repairs on ${escape(repair)}, ${Math.round(profile.repairTimeoutSeconds / 60)} min</p>`;
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>`;
12447
12749
  // The fallback chain rides EVERY surface these words sign (F+G review,
12448
12750
  // finding 2): the digest binds the whole chain, so the password form —
12449
12751
  // task page and /next alike — states every entry, credential included.
12450
12752
  const chain = chainFromJson(scope.proposedChainJson ?? null);
12451
12753
  const chainLine = chain === null || chain.length < 2
12452
12754
  ? ""
12453
- : `<p class="meta">if its subscription runs out: ${chain
12755
+ : `<p class="meta">If its subscription runs out: ${chain
12454
12756
  .slice(1)
12455
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"}`)
12456
12758
  .join("; ")}</p>`;
@@ -12479,9 +12781,9 @@ function agentsSummaryWords(view) {
12479
12781
  /** Neutral metadata badges: risk, posture, and standing. */
12480
12782
  function agentsBadgesHtml(view) {
12481
12783
  const p = view.projection;
12482
- return (`<span class="badge">${escape(riskTitle(view.riskLevel))}</span>` +
12483
- (p === null ? "" : `<span class="badge">${escape(p.postureWords)}</span>`) +
12484
- `<span class="badge">${escape(agentsStandingWords(view))}</span>`);
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>`);
12485
12787
  }
12486
12788
  /** Volatile availability, per provider on the route — shown beside the
12487
12789
  * agents, never inside the approval's terms. */
@@ -12506,7 +12808,7 @@ function agentsWhyHtml(projection, summaryLabel = "Why these agents") {
12506
12808
  .map(leg => {
12507
12809
  const chosen = chosenWords(leg);
12508
12810
  return (`<dt>${escape(ROLE_NOUN[leg.phase])}</dt>` +
12509
- `<dd><span class="mono">${escape(leg.provider)} · ${escape(leg.model)}</span> <span class="badge">${escape(chosen)}</span>` +
12811
+ `<dd><span class="mono">${escape(leg.provider)} · ${escape(leg.model)}</span> <span class="badge">${escape(sentenceCase(chosen))}</span>` +
12510
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>`);
12511
12813
  })
12512
12814
  .join("") +
@@ -12527,7 +12829,7 @@ function agentsCeremonyHtml(view) {
12527
12829
  }
12528
12830
  return (`<div class="agents-ceremony"><p class="approval-label">agents</p>` +
12529
12831
  `<p class="agents-summary">${escape(view.projection.summary)}</p>` +
12530
- `<div class="agents-badges"><span class="badge">${escape(riskTitle(view.riskLevel))}</span><span class="badge">${escape(view.projection.postureWords)}</span></div>` +
12832
+ `<div class="agents-badges"><span class="badge">${escape(sentenceCase(riskTitle(view.riskLevel)))}</span><span class="badge">${escape(sentenceCase(view.projection.postureWords))}</span></div>` +
12531
12833
  `<p class="meta agents-risk-line">${escape(riskTitle(view.riskLevel))}: ${escape(riskConsequence(view.riskLevel))}.</p>` +
12532
12834
  agentsWhyHtml(view.projection) +
12533
12835
  `<p class="meta">These exact agents are part of what you approve; changing any of them asks for a fresh approval.</p></div>`);
@@ -12554,7 +12856,7 @@ function routineAgentsHtml(routine, approved, ceremony = false) {
12554
12856
  const projection = projectRoute(route, () => null);
12555
12857
  return (`<div class="agents-ceremony"><p class="approval-label">agents</p>` +
12556
12858
  `<p class="agents-summary">${escape(agentsSummary(route))}</p>` +
12557
- `<div class="agents-badges"><span class="badge">${escape(riskTitle(route.risk))}</span><span class="badge">${escape(postureWords(route))}</span><span class="badge">${approved ? "frozen by the approval" : "frozen when you approve"}</span></div>` +
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>` +
12558
12860
  (ceremony ? agentsWhyHtml(projection) : "") +
12559
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>`);
12560
12862
  }
@@ -12584,7 +12886,7 @@ function agentsCardHtml(taskId, view, csrf, canEdit) {
12584
12886
  ? ""
12585
12887
  : `<ul class="agents-overrides">${view.overrides
12586
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>` +
12587
- (canEdit && view.editable ? `<form method="post" action="${taskHref(taskId)}/route" class="agents-clear">${hidden}<input type="hidden" name="clear-phase" value="${escape(one.phase)}"><button type="submit" class="secondary" aria-label="clear the ${escape(ROLE_NOUN[one.phase].toLowerCase())} choice">clear</button></form>` : "") +
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>` : "") +
12588
12890
  `</li>`)
12589
12891
  .join("")}</ul>`;
12590
12892
  // The controls (v48): a risk choice that says what each level does, and
@@ -12614,7 +12916,7 @@ function agentsCardHtml(taskId, view, csrf, canEdit) {
12614
12916
  ? `<p class="meta">${escape(view.editableWhy ?? "the agents cannot change right now")}</p>`
12615
12917
  : `<details class="agents-change"><summary>Change agents</summary>` +
12616
12918
  `<form method="post" action="${taskHref(taskId)}/route" class="agents-form-risk">${hidden}` +
12617
- `<label>risk<select name="risk" aria-label="declared risk">${RISK_LEVELS.map(one => `<option value="${one}"${one === view.riskLevel ? " selected" : ""}>${escape(riskTitle(one))}</option>`).join("")}</select></label>` +
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>` +
12618
12920
  `<button type="submit" class="secondary">Set risk</button></form>` +
12619
12921
  riskGuide +
12620
12922
  `<div class="agents-role-forms">${roleForms.join("")}</div>` +
@@ -13937,9 +14239,34 @@ ${THEME_DARK}
13937
14239
  .approval-act input[type=password] { margin: 0; font-family: var(--font-sans); }
13938
14240
  .approval-act button { margin: 0; min-height: 2.125rem; white-space: nowrap; }
13939
14241
  .approval-after { max-width: 75ch; color: var(--muted-foreground); font-size: 13px; line-height: 1.5; }
13940
- .approval-secondary { display: flex; flex-wrap: wrap; gap: 8px; }
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; }
13941
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; }
13942
- .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); }
13943
14270
  @media (hover: hover) and (pointer: fine) { .approval-link:hover { background: var(--muted); color: var(--foreground); } }
13944
14271
  details.approval-details { margin: 0; padding: 0; border: 0; border-top: 1px solid var(--border); border-radius: 0; background: none; }
13945
14272
  details.approval-details[open] { padding-bottom: 0; }
@@ -13959,6 +14286,7 @@ ${THEME_DARK}
13959
14286
  .approval-sheet { gap: 12px; padding: 14px; }
13960
14287
  .approval-row { grid-template-columns: 1fr; gap: 2px; }
13961
14288
  .approval-act { grid-template-columns: 1fr; }
14289
+ .approval-act > button, .approval-password-note { grid-column: auto; grid-row: auto; }
13962
14290
  .approval-act input[type=password], .approval-act button { min-height: 44px; font-size: 16px; }
13963
14291
  .approval-act button { width: 100%; }
13964
14292
  /* The one act stays under the thumb: sticky at the bottom, clear of the home indicator. */
@@ -13967,6 +14295,9 @@ ${THEME_DARK}
13967
14295
  border-top: 1px solid var(--border); background: var(--card);
13968
14296
  }
13969
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; }
13970
14301
  .approval-details > summary { min-height: 44px; }
13971
14302
  .approval-roles > div { grid-template-columns: 1fr; gap: 2px; }
13972
14303
  }
@@ -15482,7 +15813,7 @@ const INBOX_TABS_CSS = '.inbox-tabs{display:inline-flex;gap:2px;max-width:100%;o
15482
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}' +
15483
15814
  '.inbox-unread{display:none;position:absolute;top:4px;right:3px;width:6px;height:6px;border-radius:50%;background:var(--so-signal)}' +
15484
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}}';
15485
- const WORKSPACE_STYLE = styleAsset(STYLE + INBOX_TABS_CSS + APPROVAL_RULES_CSS + SPEND_CSS + RETENTION_CSS + STORAGE_CSS + UPDATES_CSS + LIMITS_CSS + MONITORING_CSS + INTEGRATIONS_CSS + BACKUP_CSS + EXPORT_CSS + PROJECT_DELETE_CSS + POLICY_CSS + EVIDENCE_PACK_CSS + THEME_CONTROLS_CSS + CODING_CSS + CODING_SHIPPING_CSS + RECIPE_CSS + SKILLS_CSS + TOOLS_CSS + FLOWS_CSS + TEAMMATE_CSS + KITS_CSS + STARTERS_CSS + GALLERY_CSS + SSO_CSS + CREDENTIALS_CSS + KNOWLEDGE_CSS + MODELS_CSS + CHAT_POLISH_CSS + TRANSITIONS_CSS + WORKSPACE_MOTION_CSS + ASSIGNMENT_CSS + TASK_STATUS_CSS + LEAD_CONTEXT_CSS + PULL_REQUEST_SETTINGS_CSS + CHECK_SETTINGS_CSS + '.learning{min-width:0;overflow-wrap:anywhere}.learning .card{min-width:0}.learning code,.learning blockquote,.learning pre{white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word}.learning button,.learning summary,.learning .button-link{min-height:44px}.learning button{white-space:nowrap}.learning summary{padding:12px 0;cursor:pointer}.learning form{margin:12px 0}.learning select{max-width:100%}.learning blockquote{margin:8px 0}.learning ul{padding-left:20px}');
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}');
15486
15817
  /**
15487
15818
  * The board's liveness: fetch this page's own fragment on a timer and swap
15488
15819
  * it in place — no flicker, no scroll reset, no long-lived stream to manage,
@@ -15737,7 +16068,7 @@ FINISH: unreviewed and undocumented is unfinished; this build ends with the fini
15737
16068
  * absent from sensitive pages. Navigation help in plain words — no key
15738
16069
  * ever posts. */
15739
16070
  const KBD_HELP = `<div class="kbd-help" hidden role="dialog" aria-label="keyboard shortcuts" tabindex="-1">` +
15740
- `<h2>keyboard shortcuts</h2><table>` +
16071
+ `<h2>Keyboard shortcuts</h2><table>` +
15741
16072
  `<tr><td><kbd>/</kbd></td><td>jump to a page or an open task</td></tr>` +
15742
16073
  `<tr><td><kbd>g</kbd> then <kbd>i</kbd></td><td>go to the inbox</td></tr>` +
15743
16074
  `<tr><td><kbd>g</kbd> then <kbd>b</kbd></td><td>go to the board</td></tr>` +
@@ -15812,7 +16143,7 @@ function shell(title, body, options = {}) {
15812
16143
  : `<span class="scope-status">` +
15813
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>`) +
15814
16145
  `<a href="/runs">${peekCounts.running} live</a><a href="/board?view=order">${peekCounts.queued} queued</a></span>`;
15815
- const scopeName = effectiveScope === "project" && chrome.project !== null ? escape(projectName(chrome.project)) : "all projects";
16146
+ const scopeName = effectiveScope === "project" && chrome.project !== null ? escape(projectName(chrome.project)) : "All projects";
15816
16147
  // The switcher (board pass): the scope's name opens a menu of every
15817
16148
  // enrolled project — one tap to open one, or to widen to all — as plain
15818
16149
  // POST forms carrying the session's csrf, returning to this screen.
@@ -15827,7 +16158,7 @@ function shell(title, body, options = {}) {
15827
16158
  const allCurrent = effectiveScope !== "project";
15828
16159
  const rows = [
15829
16160
  `<form method="post" action="/projects/select">${hidden}<input type="hidden" name="path" value="">` +
15830
- `<button type="submit"${allCurrent ? ' class="current" aria-current="true"' : ""}>all projects</button></form>`,
16161
+ `<button type="submit"${allCurrent ? ' class="current" aria-current="true"' : ""}>All projects</button></form>`,
15831
16162
  ...(chrome.projects ?? []).map(one => `<form method="post" action="/projects/open">${hidden}<input type="hidden" name="path" value="${escape(one.path)}">` +
15832
16163
  `<button type="submit"${!allCurrent && chrome.project === one.path ? ' class="current" aria-current="true"' : ""}>${escape(one.name)}</button></form>`),
15833
16164
  ];
@@ -15871,10 +16202,10 @@ function shell(title, body, options = {}) {
15871
16202
  primaryItem("flows", "/flows", "Flows"),
15872
16203
  primaryItem("projects", "/projects", "Projects"),
15873
16204
  `</nav>`,
15874
- ...(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>`]),
15875
16206
  `<nav class="nav-groups">`,
15876
- navGroup("tools", "work tools", workToolRows(chrome.projectScoped, chrome.chat, chrome.code), TOOL_KEYS.has(chrome.active)),
15877
- navGroup("settings", "settings", settingsRows(chrome.projectScoped, chrome.settings), SETTINGS_KEYS.has(chrome.active)),
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)),
15878
16209
  `</nav>`,
15879
16210
  `<span class="grow"></span>`,
15880
16211
  `</aside>`,
@@ -15883,11 +16214,11 @@ function shell(title, body, options = {}) {
15883
16214
  // must not pass as production (adoption review, finding 8; the FENCE is
15884
16215
  // the refuseDemo gate in operate.ts, this is the honest label).
15885
16216
  const demoBanner = (chrome.demo === true
15886
- ? `<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>`
15887
16218
  : "") +
15888
16219
  (chrome.modeBanner === undefined
15889
16220
  ? ""
15890
- : `<div class="banner"><span class="badge badge-running">mode</span>${escape(chrome.modeBanner.words)} \u00b7 <a href="/mode">the terms \u00b7 end it</a></div>`) +
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>`) +
15891
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)}`}` +
15892
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("") +
15893
16224
  (chrome.updateWaiting === undefined
@@ -16109,7 +16440,7 @@ function inboxPage(chrome, data) {
16109
16440
  /** The row's project, worn openly in the roll-up — null is UNPLACED,
16110
16441
  * said as such, never a silent missing chip (finding 13). */
16111
16442
  const chip = (repo) => !data.rollup ? "" : repo === null || repo === undefined
16112
- ? ` <span class="badge">unplaced</span>`
16443
+ ? ` <span class="badge">Unplaced</span>`
16113
16444
  : ` <span class="badge">${escape(projectName(repo))}</span>`;
16114
16445
  const empty = data.decisions.length + data.approvals.length + data.requeueables.length +
16115
16446
  data.cancelledBlockers.length + data.gaps.length + data.needsVerification.length === 0;
@@ -16118,19 +16449,19 @@ function inboxPage(chrome, data) {
16118
16449
  // the card itself (portfolio arc §2).
16119
16450
  const decisions = data.decisions.length === 0
16120
16451
  ? ""
16121
- : `<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>` +
16122
16453
  data.decisions
16123
16454
  .map(decision => data.interactive && !data.rollup
16124
16455
  ? decisionAnswerCard(decision, data.csrf, data.now, false)
16125
16456
  : `<a class="decide-card" href="/d/${decision.id}">` +
16126
16457
  `<p class="q">${escape(decision.question)}</p>` +
16127
16458
  `<span class="mono meta">${escape(decision.taskId)}</span>${chip(decision.repo)}` +
16128
- `${isOverdue(decision, data.now) ? ` <span class="badge badge-overdue">overdue</span>` : ""}` +
16459
+ `${isOverdue(decision, data.now) ? ` <span class="badge badge-overdue">Overdue</span>` : ""}` +
16129
16460
  `</a>`)
16130
16461
  .join("\n");
16131
16462
  const approvals = data.approvals.length === 0
16132
16463
  ? ""
16133
- : `<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>` +
16134
16465
  data.approvals
16135
16466
  .map(one => `<a class="decide-card" href="${taskHref(one.taskId)}">` +
16136
16467
  `<p class="q">${escape(one.title)}</p>` +
@@ -16140,7 +16471,7 @@ function inboxPage(chrome, data) {
16140
16471
  .join("\n");
16141
16472
  const requeueables = data.requeueables.length === 0
16142
16473
  ? ""
16143
- : `<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>` +
16144
16475
  data.requeueables
16145
16476
  .map(one => `<p class="row"><a href="${taskHref(one.taskId)}">${escape(one.taskId)}</a> ${escape(one.title)}${chip(one.repo)}` +
16146
16477
  `${one.incidentCount > 0 ? ` <span class="badge badge-failed">${one.incidentCount} incident${one.incidentCount > 1 ? "s" : ""}</span>` : ""}` +
@@ -16150,11 +16481,11 @@ function inboxPage(chrome, data) {
16150
16481
  : `<span class="right"><form method="post" action="${taskHref(one.taskId)}/requeue" class="inline">` +
16151
16482
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
16152
16483
  `<input type="hidden" name="return" value="inbox">` +
16153
- `<button type="submit">retry</button></form></span></p>`))
16484
+ `<button type="submit">Retry</button></form></span></p>`))
16154
16485
  .join("\n");
16155
16486
  const needsVerification = data.needsVerification.length === 0
16156
16487
  ? ""
16157
- : `<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>` +
16158
16489
  data.needsVerification
16159
16490
  .map(one => `<p class="row"><a href="${taskHref(one.taskId)}">${escape(one.taskId)}</a> ${escape(one.title)}${chip(one.repo)}` +
16160
16491
  ` <span class="badge badge-failed">${one.verdict === "refuted" ? "conflicting evidence" : "missing evidence"}</span>${criterionMatrixSummary(one.matrix ?? [])}` +
@@ -16163,7 +16494,7 @@ function inboxPage(chrome, data) {
16163
16494
  .join("\n");
16164
16495
  const cancelled = data.cancelledBlockers.length === 0
16165
16496
  ? ""
16166
- : `<h2>choose how waiting tasks continue</h2><p class="hint">these tasks were waiting for work that was cancelled — open one and choose what happens next</p>` +
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>` +
16167
16498
  data.cancelledBlockers
16168
16499
  .map(one => `<p class="row"><a href="${taskHref(one.exampleDependent)}">${escape(one.exampleDependent)}</a>${chip(one.repo)} ` +
16169
16500
  `<span class="meta">${one.dependentCount > 1 ? `one of ${one.dependentCount} tasks waiting` : "waiting"} for cancelled task ${escape(one.blockerId)}` +
@@ -16171,7 +16502,7 @@ function inboxPage(chrome, data) {
16171
16502
  .join("\n");
16172
16503
  const gaps = data.gaps.length === 0
16173
16504
  ? ""
16174
- : `<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>` +
16175
16506
  data.gaps
16176
16507
  .map(gap => `<p class="row"><a href="/caps">${escape(gap.key)}</a> ` +
16177
16508
  `<span class="meta">frees ${gap.unblocks.length} task${gap.unblocks.length > 1 ? "s" : ""}</span>` +
@@ -16203,8 +16534,8 @@ function inboxPage(chrome, data) {
16203
16534
  const tab = data.tab ?? "all";
16204
16535
  const shows = (one) => tab === "all" || tab === one;
16205
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");
16206
- const ready = (data.ready ?? []).length === 0 ? "" : `<h2>ready to review</h2>` + listRows(data.ready ?? []);
16207
- const running = (data.running ?? []).length === 0 ? "" : `<h2>running now</h2>` + listRows(data.running ?? []);
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 ?? []);
16208
16539
  const asks = { decide: data.decisions.length + data.approvals.length, unblock: data.requeueables.length + data.cancelledBlockers.length + data.gaps.length };
16209
16540
  const counts = { ...asks,
16210
16541
  "needs-you": asks.decide + asks.unblock,
@@ -16223,7 +16554,7 @@ function inboxPage(chrome, data) {
16223
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>` : "";
16224
16555
  return screen("inbox", [
16225
16556
  `<h1>Inbox</h1>`,
16226
- `<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>`,
16227
16558
  tabs,
16228
16559
  noWorker,
16229
16560
  wizard,
@@ -16235,21 +16566,21 @@ function inboxPage(chrome, data) {
16235
16566
  shows("needs-you") ? askGroup("unblock", counts.unblock, [requeueables, cancelled, gaps]) : "",
16236
16567
  shows("running") ? running : "",
16237
16568
  data.rollup
16238
- ? `<p class="meta">requirement gaps are checked one project at a time \u2014 open a project to see and fill its gaps · <a href="/projects">open a project</a></p>`
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>`
16239
16570
  : "",
16240
16571
  // Quick capture: the shortest path from "I want this done" to the
16241
16572
  // approve card — title and goal here, the yes on the next screen. The
16242
16573
  // one-shot form posts to the same guarded handler as the full page.
16243
- data.rollup ? "" : `<h2>capture new work</h2>`,
16574
+ data.rollup ? "" : `<h2>Capture new work</h2>`,
16244
16575
  data.rollup ? "" : `<form method="post" action="/tasks/add" class="card">`,
16245
16576
  ...(data.rollup
16246
16577
  ? []
16247
16578
  : [
16248
16579
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
16249
16580
  `<input type="hidden" name="projectRevision" value="${data.revision}">`,
16250
- `<label>what should get done<input type="text" name="title" placeholder="task title" maxlength="200"></label>`,
16251
- `<label>what success looks like <span class="meta">(becomes the scope you approve on the next screen)</span><textarea name="goal" rows="2"></textarea></label>`,
16252
- `<button type="submit">queue it \u2192 approve its scope next</button>`,
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>`,
16253
16584
  `</form>`,
16254
16585
  ]),
16255
16586
  ].join("\n"), {
@@ -16298,7 +16629,7 @@ function systemPage(chrome, data) {
16298
16629
  const agentLines = data.agents
16299
16630
  .map(one => {
16300
16631
  if (one.problem !== undefined) {
16301
- return `<p class="row"><span class="mono">${escape(one.phase)}</span> <span class="badge badge-failed">misconfigured</span> <span class="meta">${escape(one.problem)}</span></p>`;
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>`;
16302
16633
  }
16303
16634
  const who = one.source === "project"
16304
16635
  ? `chosen for this project${one.setBy === null ? "" : ` by ${escape(one.setBy)}`}`
@@ -16313,10 +16644,10 @@ function systemPage(chrome, data) {
16313
16644
  `<p class="meta" style="margin-top:0">${PHASE_SAID[one.phase] ?? ""}</p>`);
16314
16645
  })
16315
16646
  .join("\n");
16316
- const agentsCard = `<h2>agents</h2>` +
16317
- `<p class="meta">which AI provider runs each phase — changed from the terminal with your credentials (<code>toolroll config</code>), never by a browser click</p>` +
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>` +
16318
16649
  `<div class="card">${agentLines}` +
16319
- `<p class="meta">repair always stays on the provider that built — only its model can differ. A routine pins its agent the moment it fires; nothing after that can re-route it.</p>` +
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>` +
16320
16651
  `</div>`;
16321
16652
  return screen("system", [
16322
16653
  `<h1>System</h1>`,
@@ -16325,16 +16656,16 @@ function systemPage(chrome, data) {
16325
16656
  cards.length === 0
16326
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>`
16327
16658
  : `<div class="cards">${cards.join("")}</div>`,
16328
- 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>` : "",
16329
16660
  (data.externalWork ?? []).length === 0
16330
16661
  ? ""
16331
- : `<h2>external work</h2>` +
16662
+ : `<h2>External work</h2>` +
16332
16663
  (data.externalWork ?? [])
16333
16664
  .map(one => `<p class="row"><span class="mono">${escape(one.remoteRepo)}</span> ` +
16334
16665
  (one.blocked !== null
16335
- ? `<span class="badge badge-failed">dispatch blocked</span> <span class="meta">the tracker connection needs repair — \`toolroll sync\` says why</span>`
16666
+ ? `<span class="badge badge-failed">Dispatch blocked</span> <span class="meta">the tracker connection needs repair — \`toolroll sync\` says why</span>`
16336
16667
  : one.openEpisode !== null
16337
- ? `<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>`
16338
16669
  : `<span class="meta">syncing normally</span>`) +
16339
16670
  `</p>`)
16340
16671
  .join("\n"),
@@ -16437,7 +16768,7 @@ function boardBody(data, ciRed) {
16437
16768
  queuedHeads.get(`${card.assignedRunner ?? ""}|${card.repo ?? ""}`) === card.taskId
16438
16769
  ? `<span class="badge">${card.assignedRunner === null ? "next up" : `next for ${escape(card.assignedRunner)}`}</span>`
16439
16770
  : "",
16440
- card.assignedRunner === null ? "" : `<span class="badge">reserved</span>`,
16771
+ card.assignedRunner === null ? "" : `<span class="badge">Reserved</span>`,
16441
16772
  card.routineName === null ? "" : `<span class="badge">${escape(card.routineName)}</span>`,
16442
16773
  chip(card.repo).trim(),
16443
16774
  ]) +
@@ -16465,7 +16796,7 @@ function boardBody(data, ciRed) {
16465
16796
  ...(claim.branch === null ? [] : [["branch", `${claim.branch}${workspace === null ? "" : ` · ${workspace}`}`]]),
16466
16797
  ]) +
16467
16798
  chips([
16468
- card.attempt === null ? "" : `<span class="badge">attempt ${card.attempt}</span>`,
16799
+ card.attempt === null ? "" : `<span class="badge">Attempt ${card.attempt}</span>`,
16469
16800
  chip(card.repo).trim(),
16470
16801
  ]) +
16471
16802
  `</a>`);
@@ -16487,7 +16818,7 @@ function boardBody(data, ciRed) {
16487
16818
  ["usage", runCostWords({ authMode: row.authMode, costUsd: row.costUsd, tokensIn: null, tokensOut: null })],
16488
16819
  ]) +
16489
16820
  chips([
16490
- `<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>`,
16491
16822
  pr.trim(),
16492
16823
  data.all && row.repo !== null ? `<span class="badge">${escape(projectName(row.repo))}</span>` : "",
16493
16824
  ]) +
@@ -16499,7 +16830,7 @@ function boardBody(data, ciRed) {
16499
16830
  : `<p class="meta board-scope">` +
16500
16831
  (data.all
16501
16832
  ? (data.project === null
16502
- ? `<strong>all projects</strong>`
16833
+ ? `<strong>All projects</strong>`
16503
16834
  : `<a href="/board">${escape(projectName(data.project))}</a> \u00b7 <strong>all projects</strong>`) +
16504
16835
  ` \u2014 every project this server serves, each card wearing its project`
16505
16836
  : `<strong>${escape(projectName(data.project))}</strong> \u00b7 <a href="/board?scope=all">all projects</a>`) +
@@ -16516,7 +16847,7 @@ function boardBody(data, ciRed) {
16516
16847
  const ago = (minutes) => minutes < 60 ? `${minutes}m` : minutes < 48 * 60 ? `${Math.round(minutes / 60)}h` : `${Math.round(minutes / (24 * 60))}d`;
16517
16848
  const deltaLine = data.delta === null
16518
16849
  ? ""
16519
- : `<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): ` +
16520
16851
  [
16521
16852
  data.delta.built > 0 ? `<span class="good">${data.delta.built} built</span>` : "",
16522
16853
  data.delta.failed > 0 ? `<span class="bad">${data.delta.failed} failed</span>` : "",
@@ -16527,7 +16858,7 @@ function boardBody(data, ciRed) {
16527
16858
  `</p>`;
16528
16859
  return [
16529
16860
  `<h1>Board</h1>`,
16530
- data.all ? "" : `<p class="meta board-view"><strong>state</strong> \u00b7 <a href="/board?view=order">order \u2192</a> <span class="meta">drag to reorder, or to reserve a task for one worker</span></p>`,
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>`,
16531
16862
  deltaLine,
16532
16863
  toggle,
16533
16864
  `<div class="board">`,
@@ -16549,14 +16880,14 @@ function donePage(chrome, rows, ciRed) {
16549
16880
  const pr = row.prNumber === null
16550
16881
  ? row.publicationState === null
16551
16882
  ? ""
16552
- : ` <span class="badge">${escape(row.publicationState)}</span>`
16883
+ : ` <span class="badge">${escape(sentenceCase(row.publicationState))}</span>`
16553
16884
  : ` <a href="${escape(row.prUrl ?? "#")}" class="badge badge-open">PR #${row.prNumber}</a>` +
16554
16885
  (ciRed(row.prNumber) ? ` <span class="badge badge-failed">CI failing</span>` : "");
16555
16886
  const needsVerification = (row.proofVerdict === "short" || row.proofVerdict === "refuted") && !row.proofAccepted
16556
16887
  ? ` <span class="badge badge-failed">${row.proofVerdict === "refuted" ? "conflicting evidence" : "missing evidence"}</span>`
16557
16888
  : "";
16558
16889
  return (`<div class="card"><p><a href="${taskHref(row.taskId)}"><strong>${escape(row.title)}</strong></a>` +
16559
- `${row.outcome === "no-change" ? ` <span class="badge">no change needed</span>` : ""}${pr}${needsVerification}${criterionMatrixSummary(row.proofMatrix)}</p>` +
16890
+ `${row.outcome === "no-change" ? ` <span class="badge">No change needed</span>` : ""}${pr}${needsVerification}${criterionMatrixSummary(row.proofMatrix)}</p>` +
16560
16891
  `${row.handoff === null ? "" : `<p class="meta">${escape(row.handoff.length > 200 ? row.handoff.slice(0, 200) + "\u2026" : row.handoff)}</p>`}` +
16561
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 }))}`}` +
16562
16893
  ` \u00b7 <a href="${reviewHref(row.taskId)}">review \u2192</a></p></div>`);
@@ -16622,9 +16953,9 @@ function trackRow(track, all) {
16622
16953
  `<p>${trackStrip(fires)}` +
16623
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>` +
16624
16955
  (blocker !== null
16625
- ? `<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>`
16626
16957
  : latest?.instanceTaskId !== undefined && latest.instanceTaskId !== null
16627
- ? `<p class="meta">latest: <a href="${taskHref(latest.instanceTaskId)}" class="mono">${escape(latest.instanceTaskId)}</a>${latest.instanceState === null ? "" : ` (${escape(latest.instanceState)})`}</p>`
16958
+ ? `<p class="meta">Latest: <a href="${taskHref(latest.instanceTaskId)}" class="mono">${escape(latest.instanceTaskId)}</a>${latest.instanceState === null ? "" : ` (${escape(latest.instanceState)})`}</p>`
16628
16959
  : "") +
16629
16960
  `</div>`);
16630
16961
  }
@@ -16741,12 +17072,13 @@ function checkProgressHtml(progress, id = "check-progress") {
16741
17072
  : Object.values(progress.suites).every(one => one.state === "passed") ? "passed" : "unknown";
16742
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>`;
16743
17074
  }
16744
- function taskControlDetailsHtml(control, taskId, csrf, surface, inert = false) {
16745
- 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);
16746
17077
  return control.kind === "paused" || control.kind === "review-stopped" || control.kind === "stopping"
16747
17078
  ? `<details id="task-control-details"><summary>${control.kind === "paused" ? "Preserved work and resume" : "Stop details"}</summary>${html}</details>` : html;
16748
17079
  }
16749
- 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) {
16750
17082
  if (control.kind === "none")
16751
17083
  return "";
16752
17084
  const back = `<input type="hidden" name="return" value="${surface}">`;
@@ -16760,7 +17092,7 @@ function taskControlHtml(control, taskId, csrf, surface, inert = false) {
16760
17092
  if (control.kind === "stop") {
16761
17093
  return (`<section class="card task-control" id="task-control" data-task-control="stop" data-control-run="${control.run}" aria-label="stop this attempt">` +
16762
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>` +
16763
- (guarded
17095
+ (stopElsewhere ? "" : guarded
16764
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>`
16765
17097
  : `<button type="button" class="danger task-control-button" disabled>Stop</button>`) +
16766
17098
  `</section>`);
@@ -16800,7 +17132,7 @@ function resumeCeremonyPage(chrome, data) {
16800
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>`,
16801
17133
  `<p class="row">Resuming grants no new approval or publishing permission. Scope, budget, agents, risk, fallback, verification, and review limits still apply.</p>`,
16802
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>`,
16803
- data.approved ? "" : `<p class="row problem-words"><strong>the scope is not approved as it stands</strong> — resuming lifts the pause, but no worker spends until the scope is approved again</p>`,
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>`,
16804
17136
  data.gate === null ? "" : `<p class="row"><strong>Before work starts:</strong> ${escape(data.gate.summary)} — ${escape(data.gate.detail)}</p>`,
16805
17137
  `</div>`,
16806
17138
  `<form method="post" action="${taskHref(data.taskId)}/resume" class="card resume-form">`,
@@ -16876,12 +17208,23 @@ function taskChatApproval(focus, csrf) {
16876
17208
  route: focus.route,
16877
17209
  coordinator: approval.coordinator,
16878
17210
  deliverable: approval.deliverable,
16879
- 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`,
16880
17213
  notNowHref: "/chat",
16881
17214
  sticky: false,
17215
+ earlier: { active: focus.assignment?.earlierActive ?? 0, running: focus.assignment?.earlierRunning ?? 0 },
17216
+ edit: null,
16882
17217
  }) +
16883
17218
  `</section>`);
16884
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
+ }
16885
17228
  /** One live, server-derived journey from request to proof. The fragment is
16886
17229
  * safe to refresh independently, so an in-progress message is never lost. */
16887
17230
  function taskChatLiveRegion(focus, csrf, fragment = false, inert = false, approvalHref = taskChatHref(focus.id) + "#task-chat-action") {
@@ -16897,7 +17240,7 @@ function taskChatLiveRegion(focus, csrf, fragment = false, inert = false, approv
16897
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 ?? "")}">` +
16898
17241
  (focus.guide === true && focus.state !== "cancelled" ? firstTaskJourneyHtml(focus) : "") +
16899
17242
  `<div class="task-live-summary">` +
16900
- (focus.assignment !== null ? assignmentSummaryHtml(focus.assignment, { workStatus: focus.status, hideAction: focus.approval !== null && focus.dispatch?.action === "approve-scope", ...(receiptLeads ? { resultHref: chatResultHref(focus.id, focus.result.runId) } : {}) }) + (receiptLeads ? completionReceiptCard(focus.result, focus.id, "chat", focus.status, focus.assignment, false) : "") : receiptLeads ? completionReceiptCard(focus.result, focus.id, "chat", focus.status) : taskStatusCard(focus.status, focus.id, focus.dispatch, focus.liveRun?.id ?? null, focus.approval !== null && focus.dispatch?.action === "approve-scope")) +
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")) +
16901
17244
  checkProgressHtml(focus.checkProgress) +
16902
17245
  // The exact-run control (v52): the SAME component the task page
16903
17246
  // renders, refreshed with the live region — typed input in the
@@ -17008,7 +17351,7 @@ function chatFleetOverview(snapshot, projects, csrf, interactive) {
17008
17351
  return (`<section class="card chat-overview" aria-label="live portfolio overview" data-card-kind="fleet-overview">` +
17009
17352
  `<div class="chat-overview-head"><h2>${projects.length} project${projects.length === 1 ? "" : "s"}</h2>` +
17010
17353
  (interactive
17011
- ? `<form method="post" action="/chat" class="inline"><input type="hidden" name="csrf" value="${escape(csrf)}"><button type="submit" name="message" value="${escape(briefing)}" class="quiet">brief me</button></form>`
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>`
17012
17355
  : `<a href="/board?scope=all" class="chat-overview-link">open board</a>`) +
17013
17356
  `</div>` +
17014
17357
  `<div class="chat-overview-stats">` +
@@ -17043,9 +17386,9 @@ function chatProjectRail(projects, csrf, inert) {
17043
17386
  const ask = `Give me a concise status for ${one.id} (${one.label}) and recommend the next reversible action.`;
17044
17387
  return (`<div class="chat-project-card">` +
17045
17388
  `<div class="chat-project-name"><span class="mono">${escape(one.id)}</span><strong>${escape(one.label)}</strong>` +
17046
- `<span class="badge">${escape(statusOf(peek))}</span></div>` +
17389
+ `<span class="badge">${escape(sentenceCase(statusOf(peek)))}</span></div>` +
17047
17390
  (peek === null
17048
- ? `<p class="meta">pulse unavailable</p>`
17391
+ ? `<p class="meta">Pulse unavailable</p>`
17049
17392
  : `<div class="chat-project-stats">` +
17050
17393
  `<span${peek.waiting > 0 ? ' class="hot"' : ""}><b>${peek.waiting}</b> need you</span>` +
17051
17394
  `<span><b>${peek.running}</b> live</span><span><b>${peek.queued}</b> queued</span>` +
@@ -17054,13 +17397,13 @@ function chatProjectRail(projects, csrf, inert) {
17054
17397
  (inert
17055
17398
  ? ""
17056
17399
  : `<form method="post" action="/chat" class="inline"><input type="hidden" name="csrf" value="${escape(csrf)}">` +
17057
- `<button type="submit" name="message" value="${escape(ask)}" class="quiet" aria-label="ask about ${escape(one.label)}">ask</button></form>`) +
17400
+ `<button type="submit" name="message" value="${escape(ask)}" class="quiet" aria-label="ask about ${escape(one.label)}">Ask</button></form>`) +
17058
17401
  `<form method="post" action="/projects/open" class="inline"><input type="hidden" name="csrf" value="${escape(csrf)}">` +
17059
17402
  `<input type="hidden" name="path" value="${escape(one.path)}"><input type="hidden" name="return" value="/board">` +
17060
- `<button type="submit" class="quiet" aria-label="open ${escape(one.label)} board">board</button></form></div></div>`);
17403
+ `<button type="submit" class="quiet" aria-label="open ${escape(one.label)} board">Board</button></form></div></div>`);
17061
17404
  }).join("");
17062
17405
  return (`<aside class="chat-projects" id="chat-project-panel" aria-label="projects in this conversation">` +
17063
- `<div class="chat-projects-head"><h2>projects</h2><span class="badge">${projects.length}</span>` +
17406
+ `<div class="chat-projects-head"><h2>Projects</h2><span class="badge">${projects.length}</span>` +
17064
17407
  `<button type="button" class="chat-project-close quiet" aria-label="close projects">×</button></div>` +
17065
17408
  `<div class="chat-project-list">${rows}</div></aside>`);
17066
17409
  }
@@ -17073,9 +17416,9 @@ function matePromptStarters(csrf, focus = null) {
17073
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."],
17074
17417
  ]
17075
17418
  : [
17076
- ["what’s happening", "Read this task and explain its current status, what is blocking it, and what should happen next."],
17077
- ["review results", "Review the evidence recorded for this task and tell me what is proven and what is still unverified."],
17078
- ["adjust the plan", "Read this task and propose a tighter scope if that would improve the outcome. Explain why before I confirm anything."],
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."],
17079
17422
  ];
17080
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)}">` +
17081
17424
  (focus === null ? "" : `<input type="hidden" name="task" value="${escape(focus.id)}">`) +
@@ -17224,23 +17567,23 @@ function leadConfigForm(current, data) {
17224
17567
  `<form method="post" action="/chat/config" class="card">`,
17225
17568
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
17226
17569
  `<input type="hidden" name="return" value="${escape(data.returnTo)}">`,
17227
- `<label>provider<select name="provider">`,
17570
+ `<label>Provider<select name="provider">`,
17228
17571
  `<option value="codex-subscription"${current?.provider === "codex-subscription" ? " selected" : ""}>Codex membership (logged-in CLI)</option>`,
17229
17572
  `<option value="claude-subscription"${current?.provider === "claude-subscription" ? " selected" : ""}>Anthropic membership (logged-in CLI)</option>`,
17230
17573
  `<option value="anthropic-api"${current?.provider === "anthropic-api" ? " selected" : ""}>anthropic-api (direct API)</option>`,
17231
17574
  `<option value="openrouter-api"${current?.provider === "openrouter-api" ? " selected" : ""}>openrouter-api (direct API)</option>`,
17232
17575
  `</select></label>`,
17233
- `<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>` +
17234
17577
  `<input name="model" list="chat-models" value="${escape(current?.model ?? "default")}"><datalist id="chat-models">` +
17235
17578
  `${models.map(model => `<option value="${escape(model)}">${escape(labels.get(model) ?? "")}</option>`).join("")}</datalist></label>`,
17236
17579
  data.openrouterModels === null
17237
- ? `<p class="meta">with OPENROUTER_API_KEY in the serve environment, this list becomes OpenRouter's full live catalog — each model priced by the party that bills it</p>`
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>`
17238
17581
  : `<p class="meta">${data.openrouterModels.length} models live from OpenRouter's catalog; saving pins today's price — re-save to re-pin</p>`,
17239
17582
  currentSubscription
17240
- ? `<p class="meta"><strong>no dollar maximum.</strong> Membership chat uses the plan attached to the logged-in CLI; the conversation stays live until you end it, and the daily turn limit still applies.</p>`
17241
- : `<label>weekly ceiling <span class="meta">(direct API only; leave blank when choosing a membership)</span>` +
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>` +
17242
17585
  `<input type="text" name="weekly-usd" inputmode="decimal" style="width:8rem" value="${current === null ? "" : (current.weeklyCeilingMicrousd / 1_000_000).toFixed(2)}"></label>`,
17243
- `<label>daily turns <span class="meta">(default 50)</span>` +
17586
+ `<label>Daily turns <span class="meta">(default 50)</span>` +
17244
17587
  `<input type="text" name="daily-turns" inputmode="numeric" style="width:8rem" value="${current === null ? "" : String(current.dailyTurns)}"></label>`,
17245
17588
  currentSubscription
17246
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>`
@@ -17252,13 +17595,13 @@ function leadConfigForm(current, data) {
17252
17595
  : `${escape(one.provider)}: stored ${escape(one.tail ?? "")}`)
17253
17596
  .join(" · ")})</span>` +
17254
17597
  `<input type="password" name="key" placeholder="direct API only — leave empty to keep" autocomplete="off"></label>`,
17255
- `<label>your password <span class="meta">(typed again to change the provider)</span>` +
17598
+ `<label>Your password <span class="meta">(typed again to change the provider)</span>` +
17256
17599
  `<input type="password" name="token" autocomplete="current-password"></label>`,
17257
17600
  `<button type="submit">${current === null ? "turn chat on" : "save"}</button>`,
17258
17601
  `</form>`,
17259
17602
  currentSubscription
17260
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>`
17261
- : `<p class="meta">a pasted key is written once to a mode-0600 file beside the database — never INTO the database, never shown again beyond its last characters; an environment variable (` +
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 (` +
17262
17605
  `<span class="mono">ANTHROPIC_API_KEY</span> / <span class="mono">OPENROUTER_API_KEY</span>) always wins when set</p>`,
17263
17606
  ].join("\n");
17264
17607
  }
@@ -17287,7 +17630,7 @@ function chatPage(chrome, data) {
17287
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>`
17288
17631
  : code === "unpriced" || code === "no-key"
17289
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>`
17290
- : `<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>`);
17291
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 } });
17292
17635
  }
17293
17636
  const config = data.enabled.config;
@@ -17303,7 +17646,7 @@ function chatPage(chrome, data) {
17303
17646
  parts.push(data.coordinatorProposals);
17304
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 } }));
17305
17648
  for (const turn of data.latched) {
17306
- parts.push(`<div class="problem"><strong>unknown spend blocks chat.</strong> turn #${turn.id} may have cost up to ${chatMoney(turn.reservedMicrousd)} — ` +
17649
+ parts.push(`<div class="problem"><strong>Unknown spend blocks chat.</strong> turn #${turn.id} may have cost up to ${chatMoney(turn.reservedMicrousd)} — ` +
17307
17650
  `<a href="/chat/ack/${turn.id}">read and acknowledge it</a> to re-enable this credential.</div>`);
17308
17651
  }
17309
17652
  if (data.pending !== null) {
@@ -17318,7 +17661,7 @@ function chatPage(chrome, data) {
17318
17661
  }
17319
17662
  else if (last.reply !== null) {
17320
17663
  parts.push(`<div class="card" id="latest">${renderChatText(last.reply)}` +
17321
- (last.proposalsDiscarded ? `<p class="meta">a draft block in this answer was malformed and was discarded whole</p>` : "") +
17664
+ (last.proposalsDiscarded ? `<p class="meta">A draft block in this answer was malformed and was discarded whole</p>` : "") +
17322
17665
  `</div>`);
17323
17666
  }
17324
17667
  }
@@ -17326,21 +17669,21 @@ function chatPage(chrome, data) {
17326
17669
  for (const one of candidates) {
17327
17670
  const draft = one.draft;
17328
17671
  parts.push(`<div class="card">` +
17329
- `<p><strong>${draft.kind === "task" ? escape(draft.title) : escape(draft.name)}</strong> <span class="badge">draft ${escape(draft.kind)}</span></p>` +
17330
- `<p class="meta">drafted by the model from fleet context — nothing is filed; drafts do not survive a restart</p>` +
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>` +
17331
17674
  `<p style="white-space:pre-wrap">${escape(draft.goal)}</p>` +
17332
- (draft.outOfScope === null ? "" : `<p class="meta">not: ${escape(draft.outOfScope)}</p>`) +
17333
- (draft.touches.length > 0 ? `<p class="meta">touches: ${escape(draft.touches.join(", "))}</p>` : "") +
17334
- (draft.kind === "routine" ? `<p class="meta">schedule: ${escape(draft.schedule)}</p>` : "") +
17335
- `<p class="meta">repo: <span class="mono">${escape(projectName(one.repoPath))}</span></p>` +
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>` +
17336
17679
  `<form method="post" action="/chat/file/${escape(one.key)}" class="inline">` +
17337
17680
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
17338
17681
  `<input type="password" name="token" placeholder="your password" autocomplete="current-password">` +
17339
- `<button type="submit">file unapproved</button></form>` +
17682
+ `<button type="submit">File unapproved</button></form>` +
17340
17683
  `</div>`);
17341
17684
  }
17342
17685
  if (!subscription && data.canManage && data.focusTask === null) {
17343
- parts.push(`<h2>ask</h2>`, `<form method="post" action="/chat" class="card">`, `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`, `<label>message<textarea name="message" rows="3" maxlength="2000"></textarea></label>`, `<label>your password <span class="meta">(every message — chat spends)</span><input type="password" name="token" autocomplete="current-password"></label>`, `<button type="submit">ask</button>`, `</form>`);
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>`);
17344
17687
  }
17345
17688
  if (data.canManage) {
17346
17689
  parts.push(`<p class="meta" id="chat-settings"><a href="/settings/lead">Lead settings</a> · provider, model and limits</p>`);
@@ -17363,12 +17706,12 @@ function chatAckPage(chrome, turn, nonce, csrf) {
17363
17706
  `<p>Turn <span class="mono">#${turn.id}</span> on <span class="mono">${escape(turn.provider)} · ${escape(turn.model)}</span> ` +
17364
17707
  `may have started before it failed (${escape(turn.failureReason ?? "crashed")}), and its cost could not be measured.</p>`,
17365
17708
  `<p><strong>Acknowledging charges the reserved worst case, ${chatMoney(turn.reservedMicrousd)}, to the ledger and re-enables chat on this credential.</strong></p>`,
17366
- `<p class="meta">check the provider's own usage dashboard if you want the exact figure first; the ledger keeps whichever is known.</p>`,
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>`,
17367
17710
  `<form method="post" action="/chat/ack/${turn.id}">`,
17368
17711
  `<input type="hidden" name="csrf" value="${escape(csrf)}">`,
17369
17712
  `<input type="hidden" name="nonce" value="${escape(nonce)}">`,
17370
- `<label>your password<input type="password" name="token" autocomplete="current-password"></label>`,
17371
- `<button type="submit">accept the charge — re-enable chat</button>`,
17713
+ `<label>Your password<input type="password" name="token" autocomplete="current-password"></label>`,
17714
+ `<button type="submit">Accept the charge — re-enable chat</button>`,
17372
17715
  `</form>`,
17373
17716
  `</div>`,
17374
17717
  ].join("\n"), { chrome });
@@ -17575,10 +17918,10 @@ function proposalCardParts(view, csrf, inert, decision, returnTo = null) {
17575
17918
  const decisionId = typeof payload["decision"] === "number" ? payload["decision"] : 0;
17576
17919
  const pick = text("option");
17577
17920
  const options = decision === null
17578
- ? `<p class="meta">the decision is gone</p>`
17921
+ ? `<p class="meta">The decision is gone</p>`
17579
17922
  : `<ul class="answer-options">` +
17580
17923
  decision.options
17581
- .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>'}` +
17582
17925
  `${one.id === decision.recommendation ? ' <span class="meta">— the builder recommends this</span>' : ""}` +
17583
17926
  `${one.id === pick ? ' <span class="meta">— proposed</span>' : ""}` +
17584
17927
  `<p class="meta">${escape(one.consequence)}</p></li>`)
@@ -17608,13 +17951,13 @@ function proposalCardParts(view, csrf, inert, decision, returnTo = null) {
17608
17951
  : view.kind === "control"
17609
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>`)
17610
17953
  : view.kind === "cancel"
17611
- ? `<p class="meta">cancelling is armed on the task itself — <a href="${taskHref(task)}">open ${escape(task)}</a></p>` +
17612
- `<form method="post" action="${view.actionBase}/${view.id}/dismiss" class="inline"><input type="hidden" name="csrf" value="${escape(csrf)}">${returnField}<button type="submit" class="quiet">dismiss</button></form>`
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>`
17613
17956
  : `<div class="acts">` +
17614
17957
  `<form method="post" action="${view.actionBase}/${view.id}/confirm" class="inline"><input type="hidden" name="csrf" value="${escape(csrf)}">${returnField}` +
17615
17958
  (irreversible ? `<label class="arm"><input type="checkbox" name="confirm" value="yes"> I understand this cannot be undone</label>` : "") +
17616
17959
  `<button type="submit">${escape(presentation.action)}</button></form>` +
17617
- `<form method="post" action="${view.actionBase}/${view.id}/dismiss" class="inline"><input type="hidden" name="csrf" value="${escape(csrf)}">${returnField}<button type="submit" class="quiet">dismiss</button></form>` +
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>` +
17618
17961
  `</div>`;
17619
17962
  }
17620
17963
  else if (view.state === "pending") {
@@ -17645,7 +17988,7 @@ function proposalCardParts(view, csrf, inert, decision, returnTo = null) {
17645
17988
  const html = (`<article class="card proposal proposal-${escape(view.kind)} ${escape(view.state)}" data-card-kind="${escape(view.kind)}">` +
17646
17989
  `<header class="proposal-head"><span class="proposal-icon">${strokeIcon(presentation.icon)}</span>` +
17647
17990
  `<span><strong>${escape(presentation.label)}</strong><small>proposed by ${provenance}</small></span>` +
17648
- `<span class="badge ${stateClass}">${escape(view.state)}</span></header>` +
17991
+ `<span class="badge ${stateClass}">${escape(sentenceCase(view.state))}</span></header>` +
17649
17992
  `<div class="proposal-body">${what}</div>` +
17650
17993
  `<footer class="proposal-actions">${acts}</footer></article>`);
17651
17994
  // The same card as data (chat cards): one act, in the door's own words.
@@ -17693,7 +18036,7 @@ function relativeAge(iso, now) {
17693
18036
  function coordinatorProposalsSection(proposals, decisions, csrf, now, heading = true, returnTo = null) {
17694
18037
  if (proposals.length === 0)
17695
18038
  return "";
17696
- 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>` : "") +
17697
18040
  `<div class="coordinator-proposals">` +
17698
18041
  proposals.map(one => coordinatorProposalCard(one, csrf, now, decisions.get(typeof one.payload["decision"] === "number" ? one.payload["decision"] : -1) ?? null, returnTo)).join("") +
17699
18042
  `</div>`);
@@ -17817,10 +18160,10 @@ function matePage(chrome, data) {
17817
18160
  `<span>this week ${chatMoney(data.weeklySpent)} of ${chatMoney(data.config.weeklyCeilingMicrousd)}</span>`) +
17818
18161
  `</div>`,
17819
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>`,
17820
- `<form method="post" action="/chat/mate/end" class="inline"><input type="hidden" name="csrf" value="${escape(data.csrf)}"><input type="hidden" name="return" value="${escape(returnTo)}"><button type="submit" class="quiet">end the conversation and forget the thread</button></form>`,
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>`,
17821
18164
  data.recent.length === 0
17822
18165
  ? ""
17823
- : `<p class="meta">recent turns: ${data.recent
18166
+ : `<p class="meta">Recent turns: ${data.recent
17824
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)}`)
17825
18168
  .join(" · ")}</p>`,
17826
18169
  `<p class="meta"><a href="/settings/lead">Lead settings</a> · provider, model and limits</p>`,
@@ -17837,7 +18180,7 @@ function matePage(chrome, data) {
17837
18180
  if (data.problem !== null)
17838
18181
  conversation.push(`<div class="problem">${escape(data.problem)}</div>`);
17839
18182
  for (const turn of data.latched) {
17840
- conversation.push(`<div class="problem"><strong>unknown spend blocks chat.</strong> turn #${turn.id} may have cost up to ${chatMoney(turn.reservedMicrousd)} — ` +
18183
+ conversation.push(`<div class="problem"><strong>Unknown spend blocks chat.</strong> turn #${turn.id} may have cost up to ${chatMoney(turn.reservedMicrousd)} — ` +
17841
18184
  `<a href="/chat/ack/${turn.id}">read and acknowledge it</a> to re-enable this credential.</div>`);
17842
18185
  }
17843
18186
  const lastMessage = data.messages.at(-1);
@@ -17847,7 +18190,7 @@ function matePage(chrome, data) {
17847
18190
  // The New update action (package 2): hidden until a live update lands
17848
18191
  // while the reader is above the latest message; a real button, so the
17849
18192
  // keyboard reaches it. Only this act moves the reader.
17850
- `<div class="chat-new-update-holder"><button type="button" class="chat-new-update" id="chat-new-update" hidden>New update ↓</button></div>`, `<form method="post" action="/chat" class="card composer" id="${latestReply === null && data.pending === null ? "latest" : "chat-composer"}" aria-label="message the mate" data-chat-session="${data.session.id}" data-chat-task="${escape(data.focusTask?.id ?? "")}" data-chat-user="${escape(data.session.approver)}" data-chat-busy="${data.pending === null ? "0" : "1"}" data-chat-version="${mateChatVersion(data)}" data-chat-approval="${escape(data.focusTask?.approval?.digest ?? "")}">`, `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`, `<input type="hidden" name="request" value="${randomBytes(16).toString("hex")}"><input type="hidden" name="request-session" value="${data.session.id}">`, data.focusTask === null ? "" : `<input type="hidden" name="task" value="${escape(data.focusTask.id)}">`, chatProject === null ? "" : `<input type="hidden" name="project" value="${escape(chatProject)}">`, data.resultRunId == null ? "" : `<input type="hidden" name="result" value="${data.resultRunId}">`, `<label>message<textarea name="message" rows="1" maxlength="${MATE_MESSAGE_MAX_CHARS}" placeholder="${data.focusTask !== null ? "Ask about this task…" : chatProject !== null ? `Ask about ${escape(projectName(chatProject))}…` : "Describe what you want done…"}"></textarea></label>`, `<button type="submit" aria-label="${data.pending === null ? "send message" : "wait for the current reply before sending"}"${data.pending === null ? "" : " disabled"}>send</button>`, `</form>`,
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>`,
17851
18194
  // Concise pass (2026-09-13): the status line speaks only when there is
17852
18195
  // a state to report — a reply in progress here, the connection from
17853
18196
  // the continuity script — and the composer carries no second intro.
@@ -17879,25 +18222,25 @@ function routinesPage(chrome, tracks, form) {
17879
18222
  const capture = chrome.project === null
17880
18223
  ? ""
17881
18224
  : [
17882
- `<h2>file a standing order</h2>`,
18225
+ `<h2>File a standing order</h2>`,
17883
18226
  form.problem === null ? "" : `<div class="problem">${escape(form.problem)}</div>`,
17884
18227
  `<form method="post" action="/routines/add" class="card">`,
17885
18228
  `<input type="hidden" name="csrf" value="${escape(form.csrf)}">`,
17886
18229
  `<input type="hidden" name="projectRevision" value="${form.revision}">`,
17887
18230
  fill === null
17888
- ? `<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")
17889
18232
  .map(one => `<a href="/routines?template=${escape(one.name)}">${escape(one.name)}</a>`)
17890
18233
  .join(" · ")}</p>`
17891
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>`,
17892
- `<label>name <span class="meta">(lowercase-with-dashes — it names each instance)</span><input type="text" name="name" placeholder="nightly-deps" maxlength="41" value="${fill === null ? "" : escape(fill.name)}"></label>`,
17893
- `<label>goal <span class="meta">(what every firing is allowed to do)</span><textarea name="goal" rows="2">${fill === null ? "" : escape(fill.goal)}</textarea></label>`,
17894
- `<label>not this <span class="meta">(optional)</span><input type="text" name="not" value="${fill === null ? "" : escape(fill.not)}"></label>`,
17895
- `<label>touches <span class="meta">(paths, comma-separated, optional)</span><input type="text" name="touches" value="${fill === null ? "" : escape(fill.touches)}"></label>`,
17896
- `<label>acceptance <span class="meta">(required — one criterion per line: <code>statement | evidence,kinds | how</code>; evidence kinds are check, screenshot, changed-path, manual-review; id is optional and auto-numbered)</span><textarea name="acceptance" rows="3" placeholder="The full test suite passes | check">${fill === null ? "" : escape(fill.acceptance)}</textarea></label>`,
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>`,
17897
18240
  scheduleEditorHtml(fill?.schedule ?? null, form.values),
17898
- `<label>budget <span class="meta">(dollars per rolling 7 days, optional — needs a provider that reports cost)</span><input type="text" name="ceiling" inputmode="decimal" style="width:8rem" value="${escape(form.values?.get("ceiling") ?? "")}"></label>`,
17899
- `<button type="submit">file it \u2192 approve the standing order next</button>`,
17900
- `<p class="meta">filing is cheap — nothing fires until you approve the template on the next screen, password and all</p>`,
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>`,
17901
18244
  `</form>`,
17902
18245
  ].join("\n");
17903
18246
  const list = tracks.length === 0
@@ -17917,19 +18260,19 @@ function routineScreenPage(chrome, data) {
17917
18260
  const schedule = parseSchedule(routine.schedule);
17918
18261
  const scheduleSaid = schedule === null ? routine.schedule : describeSchedule(schedule);
17919
18262
  const terms = `<div class="card">` +
17920
- `<p class="meta">goal</p><p class="recap" style="margin-top:0">${escape(routine.goal)}</p>` +
17921
- `<p class="meta">not this</p><p class="recap" style="margin-top:0">${routine.outOfScope === null ? "<em>no exclusions</em>" : escape(routine.outOfScope)}</p>` +
17922
- `<p class="meta">touches · ${routine.touches.length === 0 ? "anything" : routine.touches.map(one => escape(one)).join(", ")}</p>` +
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>` +
17923
18266
  (routine.acceptance.length === 0
17924
18267
  ? ""
17925
- : `<p class="meta">acceptance</p><ul class="recap">${routine.acceptance
18268
+ : `<p class="meta">Acceptance</p><ul class="recap">${routine.acceptance
17926
18269
  .map(c => `<li><code>${escape(c.id)}</code> ${escape(c.statement)} <span class="meta">[requires: ${c.evidence.map(escape).join(", ")}]</span></li>`)
17927
18270
  .join("")}</ul>`) +
17928
- `<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>` +
17929
18272
  routineAgentsHtml(routine, approved) +
17930
- `<p class="meta">schedule · ${escape(scheduleSaid)}</p>` +
17931
- `<p class="meta">budget · ${routine.costCeilingUsd === null ? "no ceiling" : `$${routine.costCeilingUsd.toFixed(2)} per rolling 7 days`}</p>` +
17932
- `<p class="meta">one at a time — a firing skips while the previous instance is unfinished</p>` +
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>` +
17933
18276
  `</div>`;
17934
18277
  // THE CONSENT DOOR (v48): the password and the approve act exist only
17935
18278
  // where an exact, readable route can be restated — never over agents
@@ -17961,13 +18304,13 @@ function routineScreenPage(chrome, data) {
17961
18304
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
17962
18305
  `<input type="hidden" name="nonce" value="${escape(data.nonce)}">`,
17963
18306
  `<input type="hidden" name="digest" value="${escape(routine.digest)}">`,
17964
- `<p><strong>approve this standing order:</strong></p>`,
18307
+ `<p><strong>Approve this standing order:</strong></p>`,
17965
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>`,
17966
18309
  // The agents the yes freezes (v48): the same concise block every
17967
18310
  // approval shows, before the password.
17968
18311
  routineAgentsHtml(routine, false, true),
17969
- `<label>your password, typed again — a signed-in session alone cannot agree to standing work<input type="password" name="token" autocomplete="current-password"></label>` +
17970
- `<button type="submit">approve this routine</button>`,
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>`,
17971
18314
  `</form>`,
17972
18315
  ].join("\n");
17973
18316
  const verb = (name, label, danger = false) => `<form method="post" action="${routineHref(routine.id)}/${name}" class="inline">` +
@@ -17977,7 +18320,7 @@ function routineScreenPage(chrome, data) {
17977
18320
  ? `<form method="post" action="${routineHref(routine.id)}/run-now" class="inline">` +
17978
18321
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
17979
18322
  `<input type="password" name="token" autocomplete="current-password" class="inline" placeholder="your password" aria-label="password for run now" style="width:11rem"> ` +
17980
- `<button type="submit">run now</button></form>`
18323
+ `<button type="submit">Run now</button></form>`
17981
18324
  : "";
17982
18325
  const acts = `<div class="card">` +
17983
18326
  (routine.paused ? verb("resume", "resume") : verb("pause", "pause")) +
@@ -17991,7 +18334,7 @@ function routineScreenPage(chrome, data) {
17991
18334
  const said = fire.outcome === "fired"
17992
18335
  ? fire.instanceTaskId === null
17993
18336
  ? "fired"
17994
- : `<a href="${taskHref(fire.instanceTaskId)}" class="mono">${escape(fire.instanceTaskId)}</a>${fire.instanceState === null ? "" : ` <span class="badge badge-${escape(fire.instanceState)}">${escape(fire.instanceState)}</span>`}`
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>`}`
17995
18338
  : `<span class="meta">skipped — ${escape(fire.reason ?? "")}</span>`;
17996
18339
  const slot = fire.scheduledFor.replace(/^manual:/, "");
17997
18340
  return `<p class="row">${said}<span class="right meta mono">${fire.reason === "manual" ? "run now · " : ""}${whenTime(slot)}</span></p>`;
@@ -18006,14 +18349,14 @@ function routineScreenPage(chrome, data) {
18006
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>`
18007
18350
  : "",
18008
18351
  approved && !routine.paused && routine.nextFireAt !== null
18009
- ? `<p class="meta">next fire ${whenTime(routine.nextFireAt)}</p>`
18352
+ ? `<p class="meta">Next fire ${whenTime(routine.nextFireAt)}</p>`
18010
18353
  : "",
18011
- "<h2>the standing order</h2>",
18354
+ "<h2>The standing order</h2>",
18012
18355
  terms,
18013
18356
  approveForm,
18014
- "<h2>acts</h2>",
18357
+ "<h2>Acts</h2>",
18015
18358
  acts,
18016
- "<h2>firings</h2>",
18359
+ "<h2>Firings</h2>",
18017
18360
  ledger,
18018
18361
  ].join("\n"), { chrome });
18019
18362
  }
@@ -18032,21 +18375,21 @@ function homePage(chrome, data) {
18032
18375
  .map(decision => `<a class="decide-card" href="/d/${decision.id}">` +
18033
18376
  `<p class="q">${escape(decision.question)}</p>` +
18034
18377
  `<span class="mono meta">${escape(decision.taskId)}</span>` +
18035
- `${isOverdue(decision, data.now) ? ` <span class="badge badge-overdue">overdue</span>` : ""}` +
18378
+ `${isOverdue(decision, data.now) ? ` <span class="badge badge-overdue">Overdue</span>` : ""}` +
18036
18379
  `</a>`)
18037
18380
  .join("\n");
18038
18381
  const incidents = data.incidents.length === 0
18039
18382
  ? ""
18040
- : `<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>` +
18041
18384
  data.incidents
18042
18385
  .map(one => `<p class="row"><a href="${taskHref(one.taskId)}">${escape(one.taskId)}</a> — ${escape(incidentWords(one.kind))}` +
18043
18386
  `<span class="right"><form method="post" action="/i/${one.id}/resolve" class="inline">` +
18044
18387
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
18045
- `<button type="submit">resolve</button></form></span></p>`)
18388
+ `<button type="submit">Resolve</button></form></span></p>`)
18046
18389
  .join("\n");
18047
18390
  const stranded = data.stranded.length === 0
18048
18391
  ? ""
18049
- : `<h2>tasks waiting on failed work</h2><p class="hint">open a task and choose whether to try the failed work again, wait for something else, or continue without it</p>` +
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>` +
18050
18393
  data.stranded
18051
18394
  .map(one => `<p class="row"><a href="${taskHref(one.id)}">${escape(one.id)}</a> waits on ${one.blockedBy
18052
18395
  .map(blocker => `<a href="${taskHref(blocker)}">${escape(blocker)}</a>`)
@@ -18056,7 +18399,7 @@ function homePage(chrome, data) {
18056
18399
  ? ""
18057
18400
  : data.gaps.length === 0
18058
18401
  ? ""
18059
- : `<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>` +
18060
18403
  data.gaps
18061
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>`)
18062
18405
  .join("\n") +
@@ -18068,7 +18411,7 @@ function homePage(chrome, data) {
18068
18411
  // an operator actually watches this page, so it re-renders itself.
18069
18412
  const building = data.building.length === 0
18070
18413
  ? ""
18071
- : `<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">` +
18072
18415
  data.building
18073
18416
  .map(claim => `<a class="stat-card" href="${taskHref(claim.taskId)}" style="text-decoration:none">` +
18074
18417
  `<span class="k"><span class="dot dot-ok pulse"></span>${escape(claim.taskId)}</span>` +
@@ -18104,8 +18447,8 @@ function homePage(chrome, data) {
18104
18447
  : `last window: ${data.episode.built} built, ${data.episode.broke} broke \u00b7 ended ${whenTime(data.episode.endedAt)}`}</span></div>`;
18105
18448
  const fleetCards = [...runnerCards, watchCard, ...worktreeCards].filter(one => one !== "");
18106
18449
  const fleet = fleetCards.length === 0
18107
- ? `<h2>system status</h2><p class="hint">No builder is connected yet. On the machine where the project lives, open that folder and run <code>toolroll up</code>.</p>`
18108
- : `<h2>system status</h2><p class="hint">builders execute tasks in isolated temporary copies of each project</p><div class="cards">${fleetCards.join("")}</div>`;
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>`;
18109
18452
  const startHere = data.taskCount === 0
18110
18453
  ? [
18111
18454
  `<div class="card">`,
@@ -18125,10 +18468,10 @@ function homePage(chrome, data) {
18125
18468
  : `<p class="meta"><strong>${escape(projectName(data.repo))}</strong> — the last 24 hours, honestly labeled: a rolling window, whatever your hours are</p>`,
18126
18469
  startHere,
18127
18470
  ledger,
18128
- `<p class="meta">spend: ${escape(consoleSpend(summary))}</p>`,
18129
- 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>` : "",
18130
18473
  building,
18131
- `<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>`,
18132
18475
  decide,
18133
18476
  incidents,
18134
18477
  stranded,
@@ -18197,21 +18540,21 @@ function taskComposerHtml(data) {
18197
18540
  `<details class="task-options"${prefill === null ? "" : " open"}>`,
18198
18541
  `<summary><span>Edit details</span><small>optional · defaults are remembered</small></summary>`,
18199
18542
  `<div class="task-options-grid">`,
18200
- `<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>` +
18201
18544
  `<textarea name="goal" rows="3" placeholder="What success looks like">${prefill === null ? "" : escape(prefill.goal)}</textarea></label>`,
18202
- `<label class="wide">acceptance <span class="meta">— required with a goal; one per line: <code>statement | evidence,kinds | how</code></span>` +
18545
+ `<label class="wide">Acceptance <span class="meta">— required with a goal; one per line: <code>statement | evidence,kinds | how</code></span>` +
18203
18546
  `<textarea name="acceptance" rows="3" placeholder="Requests over the limit return 429 | check">${prefill === null ? "" : escape(prefill.acceptance)}</textarea></label>`,
18204
- `<label>not this <span class="meta">— optional boundary</span><input type="text" name="not" value="${prefill === null ? "" : escape(prefill.not)}"></label>`,
18205
- `<label>likely touches <span class="meta">— paths, comma-separated</span><input type="text" name="touches" value="${prefill === null ? "" : escape(prefill.touches)}"></label>`,
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>`,
18206
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>`,
18207
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>`,
18208
- `<label>task id <span class="meta">— optional</span><input type="text" name="id" value="${escape(values?.get("id") ?? "")}" placeholder="made from the request"></label>`,
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>`,
18209
18552
  candidates.length === 0
18210
18553
  ? ""
18211
- : `<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>` +
18212
18555
  candidates.map(one => `<option value="${escape(one.id)}"${one.id === after ? " selected" : ""}>${escape(one.id)} — ${escape(one.title)}</option>`).join("") +
18213
18556
  `</select></label>`,
18214
- `<fieldset class="permission-field"><legend>agent permissions</legend>${permissionModeChoices("permission-mode", values?.get("permission-mode") === "bypassPermissions" ? "bypassPermissions" : values?.get("permission-mode") === "auto" ? "auto" : data.permissionDefault)}` +
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)}` +
18215
18558
  `<p class="meta permission-note">Inherited from Settings. You can still change it on the proposed scope before approval.</p></fieldset>`,
18216
18559
  `</div>`,
18217
18560
  `</details>`,
@@ -18307,9 +18650,9 @@ function workPage(chrome, data) {
18307
18650
  project: data.multiProject || row.repo === null ? (row.repo === null ? 'Unplaced' : projectName(row.repo)) : null,
18308
18651
  age: relativeAge(row.updatedAt, data.now),
18309
18652
  status: { label: row.status.label, tone: row.status.tone, token: row.status.token },
18310
- ask: row.ask, group: row.ask ?? (row.status.rank === 1 ? 'building' : 'rest'),
18653
+ ask: row.ask, chip: row.chip, group: row.ask ?? (row.status.rank === 1 ? 'building' : 'rest'),
18311
18654
  action: actionHref === null || row.primaryAction === null ? null : { label: row.primaryAction.label, href: actionHref },
18312
- detail: needsYouDetail ? row.status.detail : null,
18655
+ detail: needsYouDetail ? row.status.detail : row.progress ?? null,
18313
18656
  problem: row.familyProblem,
18314
18657
  notes: (row.status.diagnostics ?? []).map(one => `${one.label} · ${one.detail}`),
18315
18658
  };
@@ -18337,12 +18680,12 @@ function tasksPage(chrome, tasks, state, csrf, problem, repo = null, prefill = n
18337
18680
  .join("\n");
18338
18681
  return screen("tasks", [
18339
18682
  "<h1>Tasks</h1>",
18340
- `<p class="meta">work you want done${repo === null ? "" : ` in <strong>${escape(projectName(repo))}</strong>`} \u2014 a task builds unattended only after its scope is approved; open one to write or approve its scope</p>`,
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>`,
18341
18684
  repo === null ? "" : `<p class="meta path-words"><span class="mono">${escape(repo)}</span></p>`,
18342
18685
  problem === null ? "" : `<div class="problem">${escape(problem)}</div>`,
18343
- `<p class="meta">filter: <a href="/tasks">all</a> · ${filters}</p>`,
18686
+ `<p class="meta">Filter: <a href="/tasks">all</a> · ${filters}</p>`,
18344
18687
  rows,
18345
- `<h2>add a task</h2>`,
18688
+ `<h2>Add a task</h2>`,
18346
18689
  taskComposerHtml({ csrf, project: repo, prefill, permissionDefault, qualityDefault }),
18347
18690
  ].join("\n"), { chrome });
18348
18691
  }
@@ -18352,21 +18695,21 @@ function browsePage(chrome, data) {
18352
18695
  `<form method="post" action="/projects/open" class="inline">`,
18353
18696
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
18354
18697
  `<input type="hidden" name="path" value="${escape(path)}">`,
18355
- `<button type="submit">open</button>`,
18698
+ `<button type="submit">Open</button>`,
18356
18699
  `</form>`,
18357
18700
  ].join("");
18358
18701
  return screen("projects", [
18359
18702
  `<h1>Choose a folder</h1>`,
18360
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>`,
18361
18704
  data.roots.length > 1
18362
- ? `<p class="meta">roots: ${data.roots.map(one => `<a href="/projects/browse?at=${encodeURIComponent(one)}" class="mono">${escape(projectName(one))}</a>`).join(" · ")}</p>`
18705
+ ? `<p class="meta">Roots: ${data.roots.map(one => `<a href="/projects/browse?at=${encodeURIComponent(one)}" class="mono">${escape(projectName(one))}</a>`).join(" · ")}</p>`
18363
18706
  : "",
18364
18707
  `<p class="mono meta">${escape(crumb)}</p>`,
18365
18708
  data.parent === null
18366
18709
  ? ""
18367
18710
  : `<p class="row"><a href="/projects/browse?at=${encodeURIComponent(data.parent)}">\u2190 up one level</a></p>`,
18368
18711
  data.entries.length === 0
18369
- ? `<p class="meta">no folders here</p>`
18712
+ ? `<p class="meta">No folders here</p>`
18370
18713
  : data.entries
18371
18714
  .map(one => `<p class="row">` +
18372
18715
  `<a href="/projects/browse?at=${encodeURIComponent(one.path)}"><strong>${escape(one.name)}</strong></a>` +
@@ -18384,7 +18727,7 @@ function browsePage(chrome, data) {
18384
18727
  * switching context must never depend on remembering which repo is open.
18385
18728
  */
18386
18729
  function workbenchRail(data) {
18387
- 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>`;
18388
18731
  const row = (card, reason) => `<a class="wb-row${card.taskId === data.selected ? " wb-selected" : ""}" href="/workbench?t=${encodeURIComponent(card.taskId)}"` +
18389
18732
  `${card.taskId === data.selected ? ` aria-current="true"` : ""}>` +
18390
18733
  `<span class="wb-title">${escape(card.title)}</span>` +
@@ -18394,7 +18737,7 @@ function workbenchRail(data) {
18394
18737
  (cards.length === 0 ? `<p class="meta">${empty}</p>` : cards.slice(0, 100).map(render).join("\n")) +
18395
18738
  `</section>`;
18396
18739
  const parts = [];
18397
- 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>` +
18398
18741
  `<a href="/projects">manage →</a></div>`);
18399
18742
  parts.push(group("needs you", data.attention, "Nothing needs your input.", card => row(card, escape(card.reason))));
18400
18743
  parts.push(group("in progress", data.building, "No agent is working right now.", card => {
@@ -18406,23 +18749,23 @@ function workbenchRail(data) {
18406
18749
  parts.push(group("blocked & waiting", data.waiting, "Nothing is blocked or paused.", card => row(card, escape(card.reason))));
18407
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)}`}`)));
18408
18751
  if (data.done.length > 0) {
18409
- 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>`);
18410
18753
  parts.push(data.done
18411
18754
  .map(one => `<a class="wb-row${one.taskId === data.selected ? " wb-selected" : ""}" href="/workbench?t=${encodeURIComponent(one.taskId)}">` +
18412
18755
  `<span class="wb-title">${escape(one.title)}</span>` +
18413
18756
  `<span class="wb-meta"><span class="mono meta">${escape(one.taskId)}</span>${workspace(one.repo)} ` +
18414
- `<span class="badge badge-${one.outcome === "built" || one.outcome === "no-change" ? "done" : "failed"}">${escape(one.outcome ?? "?")}</span></span></a>`)
18757
+ `<span class="badge badge-${one.outcome === "built" || one.outcome === "no-change" ? "done" : "failed"}">${escape(sentenceCase(one.outcome ?? "?"))}</span></span></a>`)
18415
18758
  .join("\n"));
18416
18759
  parts.push(`</section>`);
18417
18760
  }
18418
18761
  if (data.saturated)
18419
- parts.push(`<p class="meta">more exists — this rail is capped; the <a href="/board?scope=all">board</a> holds the rest</p>`);
18762
+ parts.push(`<p class="meta">More exists — this rail is capped; the <a href="/board?scope=all">board</a> holds the rest</p>`);
18420
18763
  return parts.join("\n");
18421
18764
  }
18422
18765
  /** The project chip every all-scope row wears: null is UNPLACED, said so. */
18423
18766
  function projectChip(repo) {
18424
18767
  return repo === null || repo === undefined
18425
- ? ` <span class="badge">unplaced</span>`
18768
+ ? ` <span class="badge">Unplaced</span>`
18426
18769
  : ` <span class="badge">${escape(projectName(repo))}</span>`;
18427
18770
  }
18428
18771
  /**
@@ -18440,11 +18783,11 @@ function decisionAnswerCard(decision, csrf, now, chip, returnTo = null) {
18440
18783
  const options = decision.options
18441
18784
  .map(option => {
18442
18785
  const recommended = option.id === decision.recommendation
18443
- ? ` <span class="badge">recommended</span>`
18786
+ ? ` <span class="badge">Recommended</span>`
18444
18787
  : "";
18445
18788
  if (!option.reversible) {
18446
18789
  return (`<p class="decide-option"><a href="/d/${decision.id}${returnTo === null ? "" : `?return=${encodeURIComponent(returnTo)}`}">${escape(option.label)}</a>` +
18447
- ` <span class="badge badge-overdue">irreversible</span>${recommended}` +
18790
+ ` <span class="badge badge-overdue">Irreversible</span>${recommended}` +
18448
18791
  ` <span class="meta">${escape(option.consequence)}</span></p>`);
18449
18792
  }
18450
18793
  return (`<form class="decide-option decide-inline" method="post" action="/d/${decision.id}/answer">` +
@@ -18459,7 +18802,7 @@ function decisionAnswerCard(decision, csrf, now, chip, returnTo = null) {
18459
18802
  `<p class="q">${escape(decision.question)}</p>` +
18460
18803
  `<details class="decision-context"><summary>Context</summary><p class="meta">${escape(decision.recap)}</p><span class="meta mono">${escape(decision.taskId)}</span></details>` +
18461
18804
  `<p class="meta">${chip ? projectChip(decision.repo) : ""}` +
18462
- `${isOverdue(decision, now) ? ` <span class="badge badge-overdue">overdue</span>` : ""}` +
18805
+ `${isOverdue(decision, now) ? ` <span class="badge badge-overdue">Overdue</span>` : ""}` +
18463
18806
  ` <a href="/d/${decision.id}${returnTo === null ? "" : `?return=${encodeURIComponent(returnTo)}`}">View details →</a></p>` +
18464
18807
  `<div class="decide-options">${options}</div></div>`);
18465
18808
  }
@@ -18540,10 +18883,10 @@ function portfolioOverview(data) {
18540
18883
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
18541
18884
  `<input type="hidden" name="path" value="${escape(one.repo)}">` +
18542
18885
  `<input type="hidden" name="return" value="/board">` +
18543
- `<button type="submit">board →</button></form>`;
18886
+ `<button type="submit">Board →</button></form>`;
18544
18887
  return (`<div class="workspace-card${one.attention > 0 ? " hot" : ""}">` +
18545
18888
  `<div class="workspace-head"><span class="workspace-name">${escape(workspace(one.repo))}</span>` +
18546
- `<span class="badge ${status.cls}">${status.word}</span>${boardForm}${one.repo === null || data.csrf === "" ? "" : `<form method="post" action="/projects/open" class="inline">${hiddenFields({ csrf: data.csrf, path: one.repo, return: "/control" })}<button>set up →</button></form>`}</div>` +
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>` +
18547
18890
  `<div class="workspace-stats">` +
18548
18891
  `<span class="pulse-stat${one.attention > 0 ? " hot" : ""}"><b>${one.attention}</b> need you</span>` +
18549
18892
  `<span class="pulse-stat"><b>${one.building}</b> live</span>` +
@@ -18595,7 +18938,7 @@ function portfolioOverview(data) {
18595
18938
  // ---- running: current live claims, project chips on ----
18596
18939
  const liveRows = data.live
18597
18940
  .map(one => `<p class="row"><a href="${taskHref(one.taskId)}">${escape(one.taskId)}</a>${projectChip(one.repo)} ` +
18598
- `<span class="badge badge-running">running</span> ` +
18941
+ `<span class="badge badge-running">Running</span> ` +
18599
18942
  `<span class="mono meta">${escape(one.runner)}${one.model === null ? "" : ` · ${escape(one.model)}`} · ` +
18600
18943
  `<time data-elapsed-since="${escape(one.claimedAt)}"></time></span></p>`)
18601
18944
  .join("\n");
@@ -18603,8 +18946,8 @@ function portfolioOverview(data) {
18603
18946
  const chipClass = (outcome) => outcome === "built" || outcome === "no-change" ? "badge-done" : outcome === "failed" ? "badge-failed" : "";
18604
18947
  const ledgerRows = data.ledger
18605
18948
  .map(one => `<p class="row"><a href="/r/${one.runId}">${escape(one.title)}</a>${projectChip(one.repo)} ` +
18606
- `<span class="badge ${chipClass(one.outcome)}">${escape(one.outcome)}</span> ` +
18607
- `${one.role === "scout" ? `<a class="badge" href="${taskHref(one.taskId)}#report">report</a> ` : ""}` +
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> ` : ""}` +
18608
18951
  `<span class="mono meta">${one.provider === null ? "" : escape(one.provider)}${one.model === null ? "" : ` · ${escape(one.model)}`}` +
18609
18952
  `${one.ranMinutes === null ? "" : ` · ${one.ranMinutes}m`}` +
18610
18953
  ` · ${escape(runCostWords({ authMode: one.authMode, costUsd: one.costUsd, tokensIn: null, tokensOut: null }))}` +
@@ -18618,11 +18961,11 @@ function portfolioOverview(data) {
18618
18961
  })()}</span></p>`)
18619
18962
  .join("\n");
18620
18963
  return [
18621
- `<div class="control-room-head"><div><h1>portfolio</h1>` +
18622
- `<p class="meta">every project and live build in one place</p></div>` +
18623
- `<div class="actions"><a class="badge" href="/tasks/new">+ new task</a><a class="badge" href="/board?scope=all">full board →</a></div></div>`,
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>`,
18624
18967
  data.saturated ? `<div class="problem">This overview reached its 200-task display cap; the task list holds the rest.</div>` : "",
18625
- `<h2>waits on you</h2>`,
18968
+ `<h2>Waits on you</h2>`,
18626
18969
  waitCount === 0
18627
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>`
18628
18971
  : [
@@ -18634,17 +18977,17 @@ function portfolioOverview(data) {
18634
18977
  `<p class="meta"><a href="/next">clear the queue → one thing at a time</a></p>`,
18635
18978
  ].filter(part => part !== "").join("\n"),
18636
18979
  data.gapsProject === null
18637
- ? `<p class="meta">requirement gaps are checked one project at a time — open a project to see and fill its gaps · <a href="/projects">open a project</a></p>`
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>`
18638
18981
  : "",
18639
- `<h2>project pulse</h2>`,
18982
+ `<h2>Project pulse</h2>`,
18640
18983
  `<p class="hint">one row per repository</p>`,
18641
18984
  pulseRows.length === 0
18642
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>`
18643
18986
  : `<div class="workspace-pulse">${workspaceRows}</div>`,
18644
- `<h2>the last 24 hours</h2>`,
18987
+ `<h2>The last 24 hours</h2>`,
18645
18988
  `<p class="hint">runs started in the last 24 hours</p>`,
18646
18989
  data.runs24.length === 0
18647
- ? `<p class="meta">no runs started in the window</p>`
18990
+ ? `<p class="meta">No runs started in the window</p>`
18648
18991
  : `<p class="row"><span class="meta">runs started</span> <span class="mono">${data.runs24.length}</span></p>` +
18649
18992
  `<p class="row"><span class="meta">outcomes</span> <span class="mono">${escape(outcomeWords)}</span></p>` +
18650
18993
  `<p class="row"><span class="meta">spend</span> <span class="mono">${escape(spendLine(summary))}</span></p>` +
@@ -18654,10 +18997,10 @@ function portfolioOverview(data) {
18654
18997
  (summary.tokens > 0
18655
18998
  ? `<p class="row"><span class="meta">tokens</span> <span class="mono">${summary.tokens.toLocaleString()}</span></p>`
18656
18999
  : ""),
18657
- `<h2>running</h2>`,
18658
- data.live.length === 0 ? `<p class="meta">no agent is working right now</p>` : liveRows,
18659
- `<h2>terminal runs started in the last 24 hours</h2>`,
18660
- data.ledger.length === 0 ? `<p class="meta">none yet</p>` : ledgerRows,
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,
18661
19004
  ].join("\n");
18662
19005
  }
18663
19006
  /** Dollars for a screen: micro-USD stated as money, unknowables in words. */
@@ -18753,7 +19096,7 @@ function contestPage(chrome, data) {
18753
19096
  const glance = summaries.length < 2
18754
19097
  ? ""
18755
19098
  : `<div class="scroll-x"><table class="contest-glance">` +
18756
- `<tr><td></td>${summaries.map(one => `<th>agent ${one.contestant.ordinal}<span class="meta"> · ${escape(one.contestant.provider)} · ${escape(one.contestant.model)}</span>${one.winner ? ` <span class="badge badge-done">picked</span>` : ""}</th>`).join("")}</tr>` +
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>` +
18757
19100
  `<tr><td class="meta">outcome</td>${summaries.map(one => `<td>${escape(one.outcome)}</td>`).join("")}</tr>` +
18758
19101
  `<tr><td class="meta">changed</td>${summaries.map(one => `<td>${escape(one.diffWords)}</td>`).join("")}</tr>` +
18759
19102
  `<tr><td class="meta">time</td>${summaries.map(one => `<td>${one.minutes === null ? "—" : `${one.minutes} min`}</td>`).join("")}</tr>` +
@@ -18766,22 +19109,22 @@ function contestPage(chrome, data) {
18766
19109
  const agent = summary.agent;
18767
19110
  const parts = [
18768
19111
  `<div class="card${winner ? " picked" : ""}">`,
18769
- `<p><strong>agent ${contestant.ordinal}</strong> <span class="meta">${escape(contestant.provider)} · ${escape(contestant.model)}</span>` +
18770
- `${winner ? ` <span class="badge badge-done">picked</span>` : ""}</p>`,
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>`,
18771
19114
  `<p class="row">${escape(outcome)}` +
18772
19115
  `${minutes === null ? "" : ` <span class="meta">· ${minutes} min</span>`}` +
18773
19116
  `${asked > 0 ? ` <span class="meta">· asked ${asked} question${asked > 1 ? "s" : ""}</span>` : ""}` +
18774
19117
  `${run === null ? "" : ` <span class="meta">· <a href="/r/${run.id}">the build</a></span>`}</p>`,
18775
- `<p class="row"><strong>cost</strong> ${escape(cost)}</p>`,
18776
- run === null || run.handoff === null ? "" : `<p><strong>its own conclusion</strong></p><p class="recap">${escape(run.handoff)}</p>`,
18777
- diff === null ? `<p class="meta">no diff was captured</p>` : terminalDiffCard(diff, run === null ? 0 : run.id),
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),
18778
19121
  ];
18779
19122
  if (picking) {
18780
19123
  if (agent.pickable) {
18781
- parts.push(`<form method="post" action="/contest/${contest.id}/arm" class="inline">`, `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`, `<input type="hidden" name="choice" value="${contestant.id}">`, `<button type="submit">pick this result…</button>`, `</form>`, `<p class="meta">picking continues to a confirmation screen — nothing happens yet</p>`);
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>`);
18782
19125
  }
18783
19126
  else {
18784
- 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>`);
18785
19128
  }
18786
19129
  }
18787
19130
  parts.push(`</div>`);
@@ -18793,11 +19136,11 @@ function contestPage(chrome, data) {
18793
19136
  `only one result will be kept as the task's outcome; the rest stay archived with their evidence</p>`,
18794
19137
  data.problem === null ? "" : `<div class="problem">${escape(data.problem)}</div>`,
18795
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>`,
18796
- 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>`,
18797
19140
  contest.kind === "comparison"
18798
- ? `<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` +
18799
19142
  `${data.anyUnknown ? ` <span class="meta">— the rest report tokens only</span>` : ""}</p>`
18800
- : `<p class="row"><strong>charged so far</strong> ${escape(contestDollars(data.totalMicrousd))}` +
19143
+ : `<p class="row"><strong>Charged so far</strong> ${escape(contestDollars(data.totalMicrousd))}` +
18801
19144
  `${data.anyUnknown ? ` <span class="meta">— includes at least one agent charged its full reservation because the exact figure was unknowable</span>` : ""}</p>`,
18802
19145
  glance,
18803
19146
  `<div class="contest-compare">${summaries.map(agentCard).join("\n")}</div>`,
@@ -18807,9 +19150,9 @@ function contestPage(chrome, data) {
18807
19150
  `<form method="post" action="/contest/${contest.id}/arm" class="inline">`,
18808
19151
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
18809
19152
  `<input type="hidden" name="act" value="abandon">`,
18810
- `<button type="submit">abandon the ${contestNoun(contest.kind)}…</button>`,
19153
+ `<button type="submit">Abandon the ${contestNoun(contest.kind)}…</button>`,
18811
19154
  `</form>`,
18812
- `<p class="meta">abandoning picks nothing: the task is marked failed (it can be re-queued), and every agent's branch and evidence is kept</p>`,
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>`,
18813
19156
  `</div>`,
18814
19157
  ].join("\n")
18815
19158
  : "",
@@ -18826,7 +19169,7 @@ function contestCeremonyPage(chrome, data) {
18826
19169
  const back = `<p class="meta"><a href="/contest/${data.contestId}">back — decide nothing</a></p>`;
18827
19170
  if (data.kind === "abandon") {
18828
19171
  return screen("tournament", [
18829
- `<h1>abandon this ${contestNoun(data.contestKind)}?</h1>`,
19172
+ `<h1>Abandon this ${contestNoun(data.contestKind)}?</h1>`,
18830
19173
  `<div class="card">`,
18831
19174
  `<p class="row">${data.agents} agents ${data.contestKind === "comparison" ? "built independently" : "raced"} on <strong>${escape(data.taskTitle)}</strong>. Abandoning picks nothing:</p>`,
18832
19175
  `<p class="row">— the task is marked <strong>failed</strong> and can be re-queued later</p>`,
@@ -18838,20 +19181,20 @@ function contestCeremonyPage(chrome, data) {
18838
19181
  `<form method="post" action="/contest/${data.contestId}/abandon" class="card">`,
18839
19182
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
18840
19183
  `<input type="hidden" name="nonce" value="${escape(data.nonceValue)}">`,
18841
- `<label>your password, typed again<input type="password" name="token" autocomplete="current-password"></label>`,
18842
- `<button type="submit" class="danger">abandon the ${contestNoun(data.contestKind)}</button>`,
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>`,
18843
19186
  `</form>`,
18844
19187
  back,
18845
19188
  ].join("\n"), { chrome });
18846
19189
  }
18847
19190
  const agent = data.chosen;
18848
19191
  if (agent === undefined || agent.run === null)
18849
- return screen("tournament", `<p class="meta">nothing to confirm</p>`, { chrome });
19192
+ return screen("tournament", `<p class="meta">Nothing to confirm</p>`, { chrome });
18850
19193
  const run = agent.run;
18851
19194
  return screen("tournament", [
18852
- `<h1>pick agent ${agent.contestant.ordinal}'s result?</h1>`,
19195
+ `<h1>Pick agent ${agent.contestant.ordinal}'s result?</h1>`,
18853
19196
  `<div class="card">`,
18854
- `<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>`,
18855
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>`,
18856
19199
  run.outcome === "no-change"
18857
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>`
@@ -18861,8 +19204,8 @@ function contestCeremonyPage(chrome, data) {
18861
19204
  ? ""
18862
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>`,
18863
19206
  data.publication === null || data.publication === undefined
18864
- ? `<p class="row"><strong>nothing is published</strong> — the branch stays local to this machine</p>`
18865
- : `<p class="row"><strong>a ${data.publication.draft ? "draft " : ""}pull request will be opened</strong> on ` +
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 ` +
18866
19209
  `<span class="mono">${escape(data.publication.githubRepo)}</span> from <span class="mono">${escape(data.publication.branch)}</span></p>`,
18867
19210
  data.contestKind === "comparison"
18868
19211
  ? `<p class="row">this agent's spend: ${agent.contestant.unknownSpend ? "unmeasured — its harness reports tokens, not dollars" : `${escape(contestDollars(agent.contestant.measuredMicrousd))} measured`}` +
@@ -18876,8 +19219,8 @@ function contestCeremonyPage(chrome, data) {
18876
19219
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
18877
19220
  `<input type="hidden" name="nonce" value="${escape(data.nonceValue)}">`,
18878
19221
  `<input type="hidden" name="choice" value="${agent.contestant.id}">`,
18879
- `<label>your password, typed again<input type="password" name="token" autocomplete="current-password"></label>`,
18880
- `<div class="sticky-actions"><button type="submit">yes — pick this result</button></div>`,
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>`,
18881
19224
  `</form>`,
18882
19225
  back,
18883
19226
  ].filter(one => one !== "").join("\n"), { chrome });
@@ -18968,18 +19311,18 @@ function githubReposPage(chrome, data) {
18968
19311
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
18969
19312
  `<input type="hidden" name="repo" value="${escape(nameWithOwner)}">`,
18970
19313
  `<input type="hidden" name="root" value="0">`,
18971
- `<button type="submit">clone here →</button>`,
19314
+ `<button type="submit">Clone here →</button>`,
18972
19315
  `</form>`,
18973
19316
  ].join("");
18974
19317
  const rows = !data.listed.ok
18975
19318
  ? `<p class="meta">${escape(data.listed.message)}</p>`
18976
19319
  : data.listed.repos.length === 0
18977
- ? `<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>`
18978
19321
  : data.listed.repos
18979
19322
  .map(repo => {
18980
19323
  const localPath = data.local.get(repo.nameWithOwner.toLowerCase()) ?? null;
18981
19324
  const action = localPath !== null && data.openProject === localPath
18982
- ? `<span class="badge badge-done">open now</span>`
19325
+ ? `<span class="badge badge-done">Open now</span>`
18983
19326
  : localPath !== null
18984
19327
  ? openForm(localPath, data.registered.has(localPath) ? "open →" : "add + open →")
18985
19328
  : data.cloneReady
@@ -18987,10 +19330,10 @@ function githubReposPage(chrome, data) {
18987
19330
  : `<span class="meta">choose a projects folder first</span>`;
18988
19331
  return [
18989
19332
  `<div class="card project-card">`,
18990
- `<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>` : ""}`,
18991
19334
  `<span class="right">${action}</span></div>`,
18992
19335
  localPath === null
18993
- ? `<p class="meta">not on this machine yet${/^\d{4}-\d{2}-\d{2}T/.test(repo.updatedAt) ? ` · pushed ${whenTime(repo.updatedAt)}` : ""}</p>`
19336
+ ? `<p class="meta">Not on this machine yet${/^\d{4}-\d{2}-\d{2}T/.test(repo.updatedAt) ? ` · pushed ${whenTime(repo.updatedAt)}` : ""}</p>`
18994
19337
  : `<p class="meta mono" style="overflow-wrap:anywhere;margin:.2rem 0">${escape(localPath)}</p>`,
18995
19338
  repo.description === "" ? "" : `<p class="meta">${escape(repo.description)}</p>`,
18996
19339
  `</div>`,
@@ -18998,8 +19341,8 @@ function githubReposPage(chrome, data) {
18998
19341
  })
18999
19342
  .join("\n");
19000
19343
  return screen("projects", [
19001
- `<h1>your GitHub repositories</h1>`,
19002
- `<p class="meta">repositories available to the GitHub account signed in on this machine — open one you already have, or clone a new one after a quick preview.</p>`,
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>`,
19003
19346
  rows,
19004
19347
  `<p class="row" style="margin-top:.6rem"><a class="badge" href="/projects">← back to projects</a></p>`,
19005
19348
  ].join("\n"), { chrome });
@@ -19014,16 +19357,16 @@ function projectsPage(chrome, recent, candidates, open, csrf, problem, _unscoped
19014
19357
  ? `<p class="meta">${escape(onboard.why)}</p>`
19015
19358
  : [
19016
19359
  `<details class="project-add-more"${onboard.record === null ? '' : ' open'}><summary>${onboard.record === null ? 'Paste a GitHub link' : 'Review repository'}</summary>`,
19017
- `<p class="meta">paste a GitHub repository — you will preview it before anything is downloaded. It goes into your saved projects folder and the builder connects automatically. Large-file (LFS) objects are not downloaded.</p>`,
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>`,
19018
19361
  onboard.record === null
19019
19362
  ? [
19020
19363
  `<form method="post" action="/projects/onboard-preview" class="card">`,
19021
19364
  `<input type="hidden" name="csrf" value="${escape(csrf)}">`,
19022
- `<label>repository <input type="text" name="repo" placeholder="owner/name or https://github.com/owner/name"></label>`,
19365
+ `<label>Repository <input type="text" name="repo" placeholder="owner/name or https://github.com/owner/name"></label>`,
19023
19366
  onboard.roots.length > 1
19024
- ? `<label>into <select name="root">${onboard.roots.map((one, index) => `<option value="${index}">${escape(one)}</option>`).join("")}</select></label>`
19025
- : `<input type="hidden" name="root" value="0"><p class="meta">into ${escape(onboard.roots[0] ?? "")}</p>`,
19026
- `<button type="submit">preview</button>`,
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>`,
19027
19370
  `</form>`,
19028
19371
  ].join("\n")
19029
19372
  : [
@@ -19035,8 +19378,8 @@ function projectsPage(chrome, recent, candidates, open, csrf, problem, _unscoped
19035
19378
  onboard.record[1].large
19036
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>`
19037
19380
  : "",
19038
- `<label>your password, typed again <input type="password" name="token" autocomplete="current-password"></label>`,
19039
- `<div class="sticky-actions"><button type="submit">clone and open</button></div>`,
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>`,
19040
19383
  `</form>`,
19041
19384
  `</div>`,
19042
19385
  ].join("\n"),
@@ -19078,7 +19421,7 @@ function projectsPage(chrome, recent, candidates, open, csrf, problem, _unscoped
19078
19421
  `<div class="row">${open !== null && open === one.path
19079
19422
  ? `<a class="project-name" href="${escape(returnTo)}"><strong>${escape(one.name)}</strong></a>`
19080
19423
  : openForm(one.path, one.name, returnTo, "project-name")}`,
19081
- `<span class="right">${open !== null && open === one.path ? `<span class="badge badge-done">open now</span>` : openForm(one.path, "Open", returnTo, "button-link")}</span></div>`,
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>`,
19082
19425
  `<p class="meta mono" style="overflow-wrap:anywhere;margin:.2rem 0">${escape(one.path)}</p>`,
19083
19426
  `<p class="row" style="gap:.35rem;flex-wrap:wrap">${peekChips(one.path, one.peek)}</p>`,
19084
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>`,
@@ -19120,13 +19463,13 @@ function projectsPage(chrome, recent, candidates, open, csrf, problem, _unscoped
19120
19463
  `<form method="post" action="/projects/open" class="card">`,
19121
19464
  `<input type="hidden" name="csrf" value="${escape(csrf)}">`,
19122
19465
  `<input type="hidden" name="return" value="${escape(returnTo)}">`,
19123
- `<label>path on this server<input type="text" name="path" placeholder="/Users/you/code/your-repo"></label>`,
19124
- `<button type="submit">open project</button>`,
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>`,
19125
19468
  `</form></details>`,
19126
19469
  ].join("\n");
19127
19470
  const addCard = [
19128
19471
  `<div class="card project-add-card">`,
19129
- `<h2 class="project-add-title">add a project</h2>`,
19472
+ `<h2 class="project-add-title">Add a project</h2>`,
19130
19473
  `<div class="project-add-actions">`,
19131
19474
  browsable
19132
19475
  ? addAction("/projects/browse", FOLDER_PATHS, "Choose a local folder", "Browse the project folders on this machine")
@@ -19147,8 +19490,8 @@ function projectsPage(chrome, recent, candidates, open, csrf, problem, _unscoped
19147
19490
  recentItems.length === 0 && candidateItems.length === 0
19148
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>`
19149
19492
  : "",
19150
- recentItems.length > 0 ? `<h2>recent</h2>${cards(recentItems)}` : "",
19151
- 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)}` : "",
19152
19495
  addCard,
19153
19496
  ].join("\n"), { chrome, workspace: { view: {
19154
19497
  kind: "projects", choosing, problem, returnTo,
@@ -19320,25 +19663,25 @@ const NAV_ICONS = {
19320
19663
  function workToolRows(scoped = false, _offersChat = false, offersCode = false) {
19321
19664
  const rows = [
19322
19665
  ...(offersCode ? [{ key: "code", href: "/code", label: "Coding sessions", hint: "Direct coding sessions and their saved results" }] : []),
19323
- { key: "inbox", href: "/inbox", label: "inbox", hint: "questions, approvals, and repairs to act on" },
19324
- { key: "board", href: "/board", label: "board", hint: "lanes by state, with the order view" },
19325
- { key: "tasks", href: "/tasks", label: "task list", hint: "everything, filterable by state" },
19326
- { key: "recipes", href: "/recipes", label: "recipes", hint: "choose, customize, and reuse a workflow" },
19327
- { key: "routines", href: "/routines", label: "routines", hint: "scheduled tracks and their firings" },
19328
- { key: "workbench", href: "/workbench", label: "portfolio", hint: "every project and live build in one place" },
19329
- { key: "ledger", href: "/ledger", label: "action ledger", hint: "who acted, what happened, and the result" },
19330
- { key: "spend", href: "/spend", label: "spend", hint: "what agent work cost, and monthly budgets" },
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" },
19331
19674
  ];
19332
19675
  return scoped ? rows.filter(row => row.key !== "workbench" && row.key !== "spend") : rows;
19333
19676
  }
19334
19677
  function settingsRows(scoped = false, offersSettings = false) {
19335
19678
  const rows = [
19336
- ...(offersSettings ? [{ key: "settings", href: "/settings", label: "settings", hint: "agent defaults, alerts, credentials" }] : []),
19337
- { key: "fleet", href: "/fleet", label: "fleet", hint: "who is working, and on what" },
19338
- { key: "caps", href: "/caps", label: "requirements", hint: "tools and credentials builds need" },
19339
- { key: "people", href: "/people", label: "people", hint: "who can sign in, and what they have done" },
19340
- { key: "mode", href: "/mode", label: "operating mode", hint: "the signed posture this repository runs under" },
19341
- { key: "system", href: "/system", label: "system", hint: "workers, providers, and grants" },
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" },
19342
19685
  ];
19343
19686
  return scoped ? rows.filter(row => row.key === "people" || row.key === "settings") : rows;
19344
19687
  }
@@ -19368,10 +19711,10 @@ function queueCard(one, csrf, revision, queueRevision, workers, column) {
19368
19711
  // and the reservation owner. Money is not in this query and is not
19369
19712
  // invented here — it stays on the task page, labeled.
19370
19713
  const state = one.taken
19371
- ? `<span class="badge">being taken — keeps its claim</span>`
19714
+ ? `<span class="badge">Being taken — keeps its claim</span>`
19372
19715
  : column === "anyone"
19373
- ? `<span class="badge">queued</span>`
19374
- : `<span class="badge">reserved for ${escape(column)}</span>`;
19716
+ ? `<span class="badge">Queued</span>`
19717
+ : `<span class="badge">Reserved for ${escape(column)}</span>`;
19375
19718
  const chips = ` ${state}` +
19376
19719
  `${one.dispatch === null || one.dispatch === undefined ? "" : ` <a class="badge" href="${taskHref(one.id)}" title="${escape(one.dispatch.summary)}">${escape(dispatchHeadline(one.dispatch))}</a>`}`;
19377
19720
  const hidden = `<input type="hidden" name="csrf" value="${escape(csrf)}">` +
@@ -19388,7 +19731,7 @@ function queueCard(one, csrf, revision, queueRevision, workers, column) {
19388
19731
  `<select name="column" aria-label="reserve for">` +
19389
19732
  `<option value="anyone"${column === "anyone" ? " selected" : ""}>anyone</option>` +
19390
19733
  workers.filter(worker => !worker.retired).map(worker => `<option value="${escape(worker.name)}"${column === worker.name ? " selected" : ""}>${escape(worker.name)}</option>`).join("") +
19391
- `</select><button type="submit">move</button></form>`;
19734
+ `</select><button type="submit">Move</button></form>`;
19392
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)}"`}>` +
19393
19736
  `<p class="row">${one.taken ? "" : `${GRIP_HANDLE}`}` +
19394
19737
  `<a href="${taskHref(one.id)}">${escape(one.title)}</a>${chips}</p>` +
@@ -19410,22 +19753,22 @@ function queueBody(tasks, workers, csrf, revision, queueRevision) {
19410
19753
  ? `<p class="meta lane-empty">${escape(empty)}</p>`
19411
19754
  : rows
19412
19755
  .map((one, index) => index === 0 && key === "anyone" && allWorkersBusy && !one.taken
19413
- ? queueCard(one, csrf, revision, queueRevision, workers, key).replace('</p>\n', "</p>").replace(`<p class="row meta">`, `<p class="meta">every worker has reserved work — this waits until a column empties</p><p class="row meta">`)
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">`)
19414
19757
  : queueCard(one, csrf, revision, queueRevision, workers, key))
19415
19758
  .join("\n")) +
19416
19759
  `</section>`;
19417
19760
  const noteForm = (worker) => worker.retired
19418
- ? `<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>`
19419
19762
  : `<p class="meta mono">${worker.building} building in this project · unattended capacity ${worker.capacity}</p>` +
19420
19763
  `<form method="post" action="/queue/note" class="row">` +
19421
19764
  `<input type="hidden" name="csrf" value="${escape(csrf)}">` +
19422
19765
  `<input type="hidden" name="projectRevision" value="${revision}">` +
19423
19766
  `<input type="hidden" name="runner" value="${escape(worker.name)}">` +
19424
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">` +
19425
- `<button type="submit">save</button></form>` +
19426
- `<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>`;
19427
19770
  return (`<div class="lanes" data-queue-revision="${queueRevision}">` +
19428
- column("shared queue", "anyone", `<p class="meta">workers take from here when their column is empty — top card first</p>`, shared, "nothing waiting — every task is reserved or running") +
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") +
19429
19772
  workers
19430
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"))
19431
19774
  .join("\n") +
@@ -19447,7 +19790,7 @@ function fleetBody(queued, building, runners, csrf, queueRevision, visibleRepo)
19447
19790
  const live = building.filter(one => one.runner === runner.name);
19448
19791
  const retired = runner.retiredAt !== null;
19449
19792
  const head = retired
19450
- ? `<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>`
19451
19794
  : `<form method="post" action="/queue/note" class="row">` +
19452
19795
  `<input type="hidden" name="csrf" value="${escape(csrf)}">` +
19453
19796
  `<input type="hidden" name="from" value="fleet">` +
@@ -19464,9 +19807,9 @@ function fleetBody(queued, building, runners, csrf, queueRevision, visibleRepo)
19464
19807
  `<p class="row">${one.taken ? "" : `${GRIP_HANDLE}`}` +
19465
19808
  `<a href="${taskHref(one.id)}">${escape(one.title)}</a></p>` +
19466
19809
  `<p class="row meta"><span class="mono">${escape(one.id)}</span>${chip(one.repo)}` +
19467
- `${one.approved ? "" : ` <span class="badge">unapproved scope</span>`}` +
19468
- `${one.blockers > 0 ? ` <span class="badge">waits for ${one.blockers}</span>` : ""}` +
19469
- `${one.taken ? ` <span class="badge">being taken</span>` : ""}</p></div>`)
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>`)
19470
19813
  .join("\n");
19471
19814
  const empty = live.length === 0 && own.length === 0
19472
19815
  ? `<p class="meta lane-empty">${retired ? "nothing left" : "idle — will take from the shared queue"}</p>`
@@ -19481,12 +19824,12 @@ function fleetBody(queued, building, runners, csrf, queueRevision, visibleRepo)
19481
19824
  `<p class="row">${one.taken ? "" : `${GRIP_HANDLE}`}` +
19482
19825
  `<a href="${taskHref(one.id)}">${escape(one.title)}</a></p>` +
19483
19826
  `<p class="row meta"><span class="mono">${escape(one.id)}</span>${chip(one.repo)}` +
19484
- `${one.approved ? "" : ` <span class="badge">unapproved scope</span>`}` +
19485
- `${one.blockers > 0 ? ` <span class="badge">waits for ${one.blockers}</span>` : ""}` +
19486
- `${one.taken ? ` <span class="badge">being taken</span>` : ""}</p></div>`)
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>`)
19487
19830
  .join("\n");
19488
- const sharedLane = `<section class="lane queue-column" data-column="anyone"><h2>shared queue</h2>` +
19489
- `<p class="meta">any free worker takes from here, top first</p>${sharedCards}` +
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}` +
19490
19833
  (shared.length === 0 ? `<p class="meta lane-empty">nothing waiting — every task is reserved or running</p>` : "") +
19491
19834
  `</section>`;
19492
19835
  return (`<div class="lanes" data-queue-revision="${queueRevision}">` +
@@ -19628,13 +19971,15 @@ function newTaskPage(chrome, project, csrf, projectRevision, problem, candidates
19628
19971
  * brief, but the console renders its known structure as a useful control
19629
19972
  * surface. Historical free-form plans keep their safe plain-text fallback. */
19630
19973
  const MILESTONE_STATE_WORDS = {
19631
- pending: "pending",
19632
- current: "in progress",
19633
- completed: "completed",
19634
- blocked: "blocked",
19974
+ pending: "Pending",
19975
+ current: "In progress",
19976
+ completed: "Completed",
19977
+ blocked: "Blocked",
19635
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);
19636
19981
  function milestoneStateWord(state) {
19637
- return MILESTONE_STATE_WORDS[state] ?? "pending";
19982
+ return MILESTONE_STATE_WORDS[state] ?? "Pending";
19638
19983
  }
19639
19984
  /** Adaptive execution plans (v44): the live milestone projection a running
19640
19985
  * (or finished) build has reported. A shared render used by BOTH the task
@@ -19693,9 +20038,9 @@ function planRevisionLedgerHtml(ledger, taskId, csrf) {
19693
20038
  `</div>`;
19694
20039
  const history = ledger.history.length <= 1
19695
20040
  ? ""
19696
- : `<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>` +
19697
20042
  ledger.history
19698
- .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> ` +
19699
20044
  `<span class="meta">${escape(one.author)} · ${whenTime(one.createdAt)}</span> — ${escape(one.reason)}</p>`)
19700
20045
  .join("\n") +
19701
20046
  `</details>`;
@@ -19814,25 +20159,24 @@ function taskBodyParts(data) {
19814
20159
  if (liveRunId === null || liveRun === undefined)
19815
20160
  return "";
19816
20161
  const minutes = Math.max(0, Math.round((data.now.getTime() - new Date(liveRun.startedAt).getTime()) / 60_000));
19817
- const head = `<p><strong>build #${liveRun.id} · ${escape(liveRun.runner)} · running ` +
20162
+ const head = `<p><strong>Build #${liveRun.id} · ${escape(liveRun.runner)} · running ` +
19818
20163
  `<time data-elapsed-since="${escape(liveRun.startedAt)}">${minutes}m</time></strong></p>`;
19819
20164
  const door = `<p class="row"><a href="/r/${liveRun.id}">full build view →</a></p>`;
19820
20165
  if (data.degraded !== undefined) {
19821
20166
  return `<div class="card attempt-live" data-live-run="${liveRun.id}">${head}` +
19822
- `<p class="meta">${data.degraded === "sensitive" ? "this page carries a password ceremony, so the live view stays on the build page" : "the live view is on the build page"}</p>${door}</div>`;
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>`;
19823
20168
  }
19824
- const peek = data.peekable === true
19825
- ? `<p class="meta">what is changing right now</p>` +
19826
- `<div id="run-peek"><p class="meta">watching\u2026 the first look lands within 15 seconds</p></div>` +
19827
- `<p class="meta" id="run-peek-stamp"></p>`
19828
- : `<p class="meta">the live file view is off \u2014 start serve with ${escape("--runner <name>")} naming this machine's worker, and it appears here</p>`;
19829
- const transcript = data.peekable !== true
19830
- ? ""
19831
- : liveRun.provider !== "claude"
19832
- ? `<p class="meta">the live transcript needs the claude harness for now \u2014 this build runs on ${escape(liveRun.provider)}</p>`
19833
- : `<p class="meta">what the agent is saying · display only \u2014 this is not evidence, and the machine running the agent could alter it</p>` +
19834
- `<pre id="live-transcript" class="mono" style="max-height:18rem;overflow:auto;white-space:pre-wrap"></pre>` +
19835
- `<p class="meta" id="live-transcript-state"></p>`;
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>`;
19836
20180
  return `<div class="card attempt-live" data-live-run="${liveRun.id}">${head}${peek}${transcript}${door}</div>`;
19837
20181
  })();
19838
20182
  // One truthful answer to the first question on a queued task: "will this
@@ -19935,7 +20279,7 @@ function taskBodyParts(data) {
19935
20279
  `<p class="meta">Saved assessment history. Current task status and checks are shown above.</p>` +
19936
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>`) +
19937
20281
  (proof.proofReasons.length === 0 ? "" : `<ul>${[...new Set(proof.proofReasons.map(plainReasonWords))].map(reason => `<li>${escape(reason)}</li>`).join("")}</ul>`) +
19938
- criterionMatrixHtml(proof.proofMatrix, { compact: true, runId: proof.runId, links: proof.proofMatrixLinks }) +
20282
+ criterionMatrixHtml(proof.proofMatrix, { compact: true, runId: proof.runId, links: proof.proofMatrixLinks, verdict: proof.proofVerdict }) +
19939
20283
  semanticCoverageHtml(proof.proofMatrix, proof.qualityMode ?? "default") + `</div></details>`;
19940
20284
  }
19941
20285
  // v50: the independent review's own card — attempt counts, what is
@@ -19987,7 +20331,7 @@ function taskBodyParts(data) {
19987
20331
  const proofDetails = proof.proofMatrix.length === 0 && machineNote === "" && chainHtml === ""
19988
20332
  ? ""
19989
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>` +
19990
- `<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>`;
19991
20335
  // The publication fact is separate from the evidence fact: a PR or
19992
20336
  // an observed merge is named from its record; nothing is "deployed".
19993
20337
  const publicationWords = (data.publication === null || data.publication === undefined ? "" : ` ${escape(receiptPublicationWords(publicationFactsOf(data.publication)))}`) +
@@ -20047,26 +20391,30 @@ function taskBodyParts(data) {
20047
20391
  : `<form method="post" action="${taskHref(task.id)}/steer" class="row">` +
20048
20392
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
20049
20393
  `<input type="text" name="note" placeholder="guidance for the next attempt" aria-label="steering note" style="width:100%;max-width:28rem">` +
20050
- `<button type="submit">steer</button></form>` +
20051
- `<p class="meta">lands when the next attempt starts — a running agent is not interrupted, and a note cannot widen the approved scope</p>`;
20052
- const steeringCard = steerRows === "" && steerForm === "" ? "" : `<h2>steering</h2>${steerRows}${steerForm}`;
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}`;
20053
20397
  const holds = data.holds.length === 0
20054
20398
  ? ""
20055
- : `<h2>holds</h2>` +
20399
+ : `<h2>Holds</h2>` +
20056
20400
  data.holds
20057
20401
  .map(hold => `<p class="row">${escape(holdOwnerWords(hold.ownerKind))} — ${escape(hold.reason)}` +
20058
20402
  `${hold.until === null ? "" : ` <span class="meta">until ${whenTime(hold.until)}</span>`}</p>`)
20059
20403
  .join("\n") +
20060
- `<p class="meta">only your hold can be lifted here — waits caused by questions, incidents, or retry delays clear on their own</p>`;
20404
+ `<p class="meta">Only your hold can be lifted here — waits caused by questions, incidents, or retry delays clear on their own</p>`;
20061
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;
20062
20410
  const scopeCard = scope === null
20063
- ? `<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>`
20064
20412
  : [
20065
20413
  `<div class="card">`,
20066
- `<p><strong>goal</strong></p><p class="recap">${escape(scope.goal)}</p>`,
20067
- scope.outOfScope === null ? "" : `<p><strong>not this</strong></p><p class="recap">${escape(scope.outOfScope)}</p>`,
20068
- scope.touches.length === 0 ? "" : `<p class="scope-paths"><strong>touches</strong> ${scope.touches.map(one => escape(one)).join(", ")}</p>`,
20069
- `<p><strong>quality</strong> ${escape(qualityModeTitle(scope.qualityMode ?? "default"))}</p>`,
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>`,
20070
20418
  // The fallback chain, on the card the yes reads (Layer F): the
20071
20419
  // digest binds the WHOLE chain, so every entry — credential
20072
20420
  // included — is said before anyone signs.
@@ -20074,7 +20422,7 @@ function taskBodyParts(data) {
20074
20422
  const chain = chainFromJson(scope.proposedChainJson ?? null);
20075
20423
  if (chain === null || chain.length < 2)
20076
20424
  return "";
20077
- 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
20078
20426
  .slice(1)
20079
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"}`)
20080
20428
  .join("; ")}</p>`;
@@ -20092,10 +20440,10 @@ function taskBodyParts(data) {
20092
20440
  // blank — the same rule as the revision brief.
20093
20441
  const reportCard = data.report === null || data.report === undefined
20094
20442
  ? data.deliverable === "report"
20095
- ? `<div class="card"><p><strong>scout task</strong> <span class="meta">delivers a report, never a branch — a read-only session investigates the goal and its report appears here when it finishes</span></p></div>`
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>`
20096
20444
  : ""
20097
20445
  : !data.report.ok
20098
- ? `<div class="card"><p><strong>the report</strong></p><p class="meta">${escape(data.report.problem)} · <a href="/r/${data.report.run}">run ${data.report.run}</a></p></div>`
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>`
20099
20447
  : [
20100
20448
  `<div class="card report">`,
20101
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>`,
@@ -20104,11 +20452,11 @@ function taskBodyParts(data) {
20104
20452
  ...(data.report.report.followUps.length === 0
20105
20453
  ? []
20106
20454
  : [
20107
- `<p><strong>follow-ups the scout proposes</strong> <span class="meta">each files as a task in this repository; its scope still needs your approval</span></p>`,
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>`,
20108
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>` +
20109
20457
  (data.csrf === "" || data.repo === null
20110
20458
  ? `<p class="meta">${data.repo === null ? "this task has no repository — file it by hand" : ""}</p>`
20111
- : `<form method="post" action="${taskHref(task.id)}/follow-up" class="inline"><input type="hidden" name="csrf" value="${escape(data.csrf)}"><input type="hidden" name="index" value="${index}"><button type="submit">file this follow-up</button></form>`) +
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>`) +
20112
20460
  `</div>`),
20113
20461
  ]),
20114
20462
  `</div>`,
@@ -20140,7 +20488,7 @@ function taskBodyParts(data) {
20140
20488
  ? data.revision != null && !("problem" in data.revision)
20141
20489
  ? `<div class="card planner-status"><span class="planner-orb" aria-hidden="true"></span><p><strong>Updating the plan</strong>` +
20142
20490
  `<span class="meta">Adding your notes to the plan. You approve any change before it builds.</span></p></div>`
20143
- : `<div class="card planner-status"><span class="planner-orb" aria-hidden="true"></span><p><strong>planning requested</strong>` +
20491
+ : `<div class="card planner-status"><span class="planner-orb" aria-hidden="true"></span><p><strong>Planning requested</strong>` +
20144
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>`
20145
20493
  : ""
20146
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">`}` +
@@ -20153,7 +20501,7 @@ function taskBodyParts(data) {
20153
20501
  ? ""
20154
20502
  : `<details class="plan-editor" id="plan-edit"><summary>Edit plan</summary><form method="post" action="${taskHref(task.id)}/plan-edit">` +
20155
20503
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}"><input type="hidden" name="saw-plan" value="${escape(data.planSha)}">` +
20156
- `<label>plan details <span class="meta">Keep the five headings. Approval locks this version for the build.</span>` +
20504
+ `<label>Plan details <span class="meta">Keep the five headings. Approval locks this version for the build.</span>` +
20157
20505
  `<textarea name="plan-document" rows="14">${escape(data.planDocument)}</textarea></label>` +
20158
20506
  `<button type="submit">Save plan</button></form></details>`) +
20159
20507
  `${approval.approved ? `</div></details>` : `</section>`}`;
@@ -20165,7 +20513,7 @@ function taskBodyParts(data) {
20165
20513
  const revisionCard = data.revision === null || data.revision === undefined
20166
20514
  ? ""
20167
20515
  : "problem" in data.revision
20168
- ? `<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>`
20169
20517
  : [
20170
20518
  `<div class="revision-card" data-revision-feedback>`,
20171
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>`,
@@ -20193,7 +20541,7 @@ function taskBodyParts(data) {
20193
20541
  : scope.profileState === "unresolved"
20194
20542
  ? `<div class="card approve-form" id="approve"><p><strong>This task is waiting on you: its scope cannot be approved yet.</strong></p>` +
20195
20543
  profileWords(scope) +
20196
- `<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>`
20197
20545
  : !consentDoor.open
20198
20546
  ? consentClosedHtml(task.id, consentDoor, "task")
20199
20547
  : approvalSheetHtml({
@@ -20216,6 +20564,11 @@ function taskBodyParts(data) {
20216
20564
  editHref: data.csrf !== "" && data.planSha != null && data.planDocument !== null ? "#plan-edit" : "#scope",
20217
20565
  notNowHref: "/work",
20218
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 },
20219
20572
  });
20220
20573
  // The attended road (Phase 2E): beside the approval, never replacing it.
20221
20574
  // The mint button leads to the CONFIRM screen where every term renders
@@ -20228,12 +20581,12 @@ function taskBodyParts(data) {
20228
20581
  : attended.open !== null
20229
20582
  ? [
20230
20583
  `<div class="card" data-attended="${escape(attended.open.id)}">`,
20231
- `<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>`,
20232
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>`,
20233
- `<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>`,
20234
20587
  `<form method="post" action="${taskHref(task.id)}/attend-revoke" class="inline">`,
20235
20588
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
20236
- `<button type="submit">revoke — stop the session</button>`,
20589
+ `<button type="submit">Revoke — stop the session</button>`,
20237
20590
  `</form>`,
20238
20591
  `</div>`,
20239
20592
  ].join("\n")
@@ -20241,36 +20594,36 @@ function taskBodyParts(data) {
20241
20594
  ? [
20242
20595
  `<form method="post" action="${taskHref(task.id)}/attend-preview" class="card">`,
20243
20596
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
20244
- `<p><strong>or run it once while you watch</strong></p>`,
20245
- `<p class="meta">no approval filed: one attempt, on this machine, only while this page is open. You will read every term before ${attended.mint?.quick === true ? "you confirm it" : "your password signs it"}.</p>`,
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>`,
20246
20599
  attended.mint === undefined || attended.mint.models.length <= 1
20247
20600
  ? ""
20248
- : `<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">` +
20249
20602
  attended.mint.models
20250
20603
  .map(model => `<option value="${escape(model)}"${model === attended.mint?.pinnedModel ? " selected" : ""}>${escape(model)}</option>`)
20251
20604
  .join("") +
20252
20605
  `</select></label>`,
20253
20606
  attended.mint === undefined
20254
20607
  ? ""
20255
- : `<label>permissions<select name="posture">` +
20608
+ : `<label>Permissions<select name="posture">` +
20256
20609
  `<option value="auto"${attended.mint.posture === "auto" ? " selected" : ""}>safe unattended — routine project commands and edits proceed</option>` +
20257
20610
  `<option value="acceptEdits"${attended.mint.posture === "acceptEdits" ? " selected" : ""}>legacy acceptEdits — commands that ask are denied unattended</option>` +
20258
20611
  `<option value="bypassPermissions"${attended.mint.posture === "bypassPermissions" ? " selected" : ""}>full permissions — nothing asks</option>` +
20259
20612
  `</select></label>`,
20260
- `<button type="submit">read the terms</button>`,
20613
+ `<button type="submit">Read the terms</button>`,
20261
20614
  `</form>`,
20262
20615
  ].join("\n")
20263
20616
  : "";
20264
20617
  const scopeForm = [
20265
- `<details${scope === null || data.scopeDraft !== undefined ? " open" : ""}><summary>${scope === null ? "write the scope" : "edit the scope"}${approval.approved ? " (editing voids the approval)" : ""}</summary>`,
20618
+ `<details${scope === null || scopeDraft !== undefined ? " open" : ""}><summary>${scope === null ? "Write the scope" : "Edit the scope"}${approval.approved ? " (editing voids the approval)" : ""}</summary>`,
20266
20619
  `<form method="post" action="${taskHref(task.id)}/scope" class="scope-editor">`,
20267
- data.scopeDraft === undefined || data.problem === null ? "" : `<div class="problem" role="alert" id="scope-error">${escape(data.problem)}</div>`,
20620
+ scopeDraft === undefined || data.problem === null ? "" : `<div class="problem" role="alert" id="scope-error">${escape(data.problem)}</div>`,
20268
20621
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
20269
- `<input type="hidden" name="sawDigest" value="${escape(data.scopeDraft?.get("sawDigest") ?? scope?.digest ?? "")}">`,
20270
- `<label>goal<textarea name="goal" rows="3"${data.scopeDraft === undefined ? "" : ' autofocus aria-describedby="scope-error"'}>${escape(data.scopeDraft?.get("goal") ?? scope?.goal ?? "")}</textarea></label>`,
20271
- `<label>not this<textarea name="not" rows="2">${escape(data.scopeDraft?.get("not") ?? scope?.outOfScope ?? "")}</textarea></label>`,
20272
- `<label>touches <span class="meta">(one per line)</span><textarea name="touches" rows="2">${escape(data.scopeDraft?.get("touches") ?? (scope?.touches ?? []).join("\n"))}</textarea></label>`,
20273
- `<label>acceptance <span class="meta">(required — one criterion per line: <code>statement | evidence,kinds | how</code>; evidence kinds are check, screenshot, changed-path, manual-review; id is optional and auto-numbered)</span><textarea name="acceptance" rows="3" placeholder="The button opens the settings panel | screenshot">${escape(data.scopeDraft?.get("acceptance") ?? acceptanceToLines(scope?.acceptance ?? []).join("\n"))}</textarea></label>`,
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>`,
20274
20627
  (() => {
20275
20628
  const defaults = data.spendDefaults ?? null;
20276
20629
  const budgetPrefill = scope?.budgetMicrousd != null
@@ -20278,8 +20631,8 @@ function taskBodyParts(data) {
20278
20631
  : defaults?.buildPerRunMicrousd != null
20279
20632
  ? (defaults.buildPerRunMicrousd / 1_000_000).toFixed(2)
20280
20633
  : "";
20281
- return `<label>agent-reported usage cap <span class="meta">(optional — leave blank for uncapped subscription work)</span>` +
20282
- `<input type="number" name="budget-usd" step="0.01" min="0.01" value="${escape(data.scopeDraft?.get("budget-usd") ?? budgetPrefill)}" placeholder="no cap"></label>` +
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>` +
20283
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>`;
20284
20637
  })(),
20285
20638
  (() => {
@@ -20290,14 +20643,14 @@ function taskBodyParts(data) {
20290
20643
  : scope?.profile?.provider === "codex" || scope?.profile?.provider === "openrouter"
20291
20644
  ? scope.profile.sandboxMode === "danger-full-access" ? "bypassPermissions" : "auto"
20292
20645
  : null;
20293
- const selected = data.scopeDraft?.get("permission-mode") === "bypassPermissions" ? "bypassPermissions" : data.scopeDraft?.get("permission-mode") === "auto" ? "auto" : data.permissionMode ?? profileMode ?? data.permissionDefault ?? "auto";
20294
- return `<fieldset class="permission-field"><legend>agent permissions</legend>` +
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>` +
20295
20648
  permissionModeChoices("permission-mode", selected) +
20296
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>`;
20297
20650
  })(),
20298
20651
  (() => {
20299
- const selected = data.scopeDraft?.get("quality-mode") === "strict" ? "strict" : data.scopeDraft?.get("quality-mode") === "default" ? "default" : scope?.qualityMode ?? data.qualityMode ?? data.qualityDefault ?? "default";
20300
- return `<fieldset class="permission-field"><legend>quality</legend>` +
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>` +
20301
20654
  qualityModeChoices("quality-mode", selected) +
20302
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>`;
20303
20656
  })(),
@@ -20308,8 +20661,8 @@ function taskBodyParts(data) {
20308
20661
  // approval above restates and covers both.
20309
20662
  const defaults = data.spendDefaults ?? null;
20310
20663
  const terms = data.raceTerms ?? null;
20311
- const selectedCount = data.scopeDraft === undefined ? terms !== null ? terms.n : defaults?.raceAgents ?? 0 : Number(data.scopeDraft.get("race-count") ?? "0");
20312
- const selectedModel = data.scopeDraft?.get("race-model") ?? terms?.agents[0]?.model ?? "claude-sonnet-5";
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";
20313
20666
  const perPrefill = terms !== null
20314
20667
  ? (terms.perAgentBudgetMicrousd / 1_000_000).toFixed(2)
20315
20668
  : defaults?.racePerAgentMicrousd != null
@@ -20321,31 +20674,31 @@ function taskBodyParts(data) {
20321
20674
  ? (defaults.raceTotalMicrousd / 1_000_000).toFixed(2)
20322
20675
  : "";
20323
20676
  return [
20324
- `<details class="more-agents"${selectedCount > 0 ? " open" : ""}><summary>more than one agent (optional)</summary>`,
20325
- `<label>how many agents compete <span class="meta">(a tournament builds the task independently N times — you compare and pick one)</span>` +
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>` +
20326
20679
  `<select name="race-count">` +
20327
20680
  `<option value=""${selectedCount === 0 ? " selected" : ""}>one agent — no tournament</option>` +
20328
20681
  [2, 3, 4].map(count => `<option value="${count}"${selectedCount === count ? " selected" : ""}>${count} agents</option>`).join("") +
20329
20682
  `</select></label>`,
20330
- `<label>competing model <select name="race-model">` +
20683
+ `<label>Competing model <select name="race-model">` +
20331
20684
  PRICED_BUILD_MODELS.map(model => `<option value="${escape(model)}"${model === selectedModel ? " selected" : ""}>${escape(model)}</option>`).join("") +
20332
20685
  `</select></label>`,
20333
- `<label>each competing agent may spend ($)<input type="number" name="race-per-usd" step="0.01" min="0.01" value="${escape(data.scopeDraft?.get("race-per-usd") ?? perPrefill)}"></label>`,
20334
- `<label>the whole tournament may spend ($)<input type="number" name="race-total-usd" step="0.01" min="0.01" value="${escape(data.scopeDraft?.get("race-total-usd") ?? totalPrefill)}"></label>`,
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>`,
20335
20688
  // The comparison lanes (Phase 3 slice B): 2-4 rows, any registered
20336
20689
  // provider, exact model required — no dollar fields exist. Blank
20337
20690
  // rows are unused; filling any row files a comparison INSTEAD of a
20338
20691
  // tournament, and mixing the two refuses in words.
20339
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>`,
20340
- ...[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}">` +
20341
20694
  `<option value="">—</option>` +
20342
- 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("") +
20343
20696
  `</select></label>` +
20344
- `<label>its exact model<input type="text" name="compare-model-${lane}" value="${escape(data.scopeDraft?.get(`compare-model-${lane}`) ?? "")}" placeholder="e.g. gemini-2.5-pro"></label></div>`),
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>`),
20345
20698
  `</details>`,
20346
20699
  ].join("\n");
20347
20700
  })(),
20348
- `<button type="submit">save scope</button>`,
20701
+ `<button type="submit">Save scope</button>`,
20349
20702
  `</form></details>`,
20350
20703
  ].join("\n");
20351
20704
  // 41_237 → "41k": token counts read at a glance; exactness lives on the run page.
@@ -20367,7 +20720,7 @@ function taskBodyParts(data) {
20367
20720
  // duration · tokens · measured-or-unmeasured cost.
20368
20721
  const runs = data.runs.length === 0
20369
20722
  ? ""
20370
- : `<h2>attempts</h2>` +
20723
+ : `<h2>Attempts</h2>` +
20371
20724
  data.runs
20372
20725
  .map(run => {
20373
20726
  const bits = [
@@ -20560,20 +20913,20 @@ function taskBodyParts(data) {
20560
20913
  })();
20561
20914
  const decisions = data.decisions.length === 0
20562
20915
  ? ""
20563
- : `<h2>decisions</h2>` +
20916
+ : `<h2>Decisions</h2>` +
20564
20917
  data.decisions
20565
20918
  .map(decision => `<p class="row"><a href="/d/${decision.id}">${escape(decision.question)}</a> ` +
20566
20919
  `<span class="meta">${escape(decision.state)}${isOverdue(decision, data.now) ? " · overdue" : ""}</span></p>`)
20567
20920
  .join("\n");
20568
20921
  const incidents = data.incidents.length === 0
20569
20922
  ? ""
20570
- : `<h2>incidents</h2>` +
20923
+ : `<h2>Incidents</h2>` +
20571
20924
  data.incidents
20572
20925
  .map(one => one.resolvedAt === null
20573
20926
  ? `<p class="row">${escape(incidentWords(one.kind))} ` +
20574
20927
  `<form method="post" action="/i/${one.id}/resolve" class="inline">` +
20575
20928
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
20576
- `<button type="submit">resolve</button></form></p>`
20929
+ `<button type="submit">Resolve</button></form></p>`
20577
20930
  : `<p class="row meta">${escape(incidentWords(one.kind))} — resolved by ${escape(one.resolvedBy ?? "?")}</p>`)
20578
20931
  .join("\n");
20579
20932
  const stalled = task.state === "failed" || data.incidents.some(one => one.resolvedAt === null);
@@ -20585,11 +20938,11 @@ function taskBodyParts(data) {
20585
20938
  const waitsFor = data.waitsFor ?? [];
20586
20939
  const candidates = (data.waitCandidates ?? []).filter(one => !waitsFor.some(existing => existing.id === one.id));
20587
20940
  const waitRows = waitsFor
20588
- .map(one => `<p class="row">${one.admitted ? `<a href="${taskHref(one.id)}" class="mono">${escape(one.id)}</a>` : `<span class="mono">${escape(one.id)}</span>`}${one.state === null ? "" : ` <span class="badge badge-${escape(one.state)}">${escape(one.state)}</span>`}` +
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>`}` +
20589
20942
  `<form method="post" action="${taskHref(task.id)}/unblock" class="inline">` +
20590
20943
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
20591
20944
  `<input type="hidden" name="on" value="${escape(one.id)}">` +
20592
- `<button type="submit">don't wait for this</button></form></p>`)
20945
+ `<button type="submit">Don't wait for this</button></form></p>`)
20593
20946
  .join("\n");
20594
20947
  const waitAdd = data.csrf === "" || candidates.length === 0
20595
20948
  ? ""
@@ -20598,11 +20951,11 @@ function taskBodyParts(data) {
20598
20951
  `<select name="on" aria-label="task to wait for">` +
20599
20952
  candidates.map(one => `<option value="${escape(one.id)}">${escape(one.id)} — ${escape(one.title)}</option>`).join("") +
20600
20953
  `</select>` +
20601
- `<button type="submit">wait for this task</button>` +
20954
+ `<button type="submit">Wait for this task</button>` +
20602
20955
  `<span class="meta"> — this task starts only after it finishes</span></form>`;
20603
20956
  const waitsForCard = waitRows === "" && waitAdd === ""
20604
20957
  ? ""
20605
- : `<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}`;
20606
20959
  // The acts bar (task page pass): every verb in one row under the title,
20607
20960
  // the one that resolves this task's state first and primary — retry on a
20608
20961
  // stalled task, build-next in the queue, plan-first with no scope. The
@@ -20664,9 +21017,10 @@ function taskBodyParts(data) {
20664
21017
  const section = (title, html, open, count, id = title.replace(/\s+/g, "-")) => {
20665
21018
  if (html === "")
20666
21019
  return "";
20667
- const inner = html.replace(`<h2>${title}</h2>`, "");
20668
- sectionParts.push({ id, title: title[0].toUpperCase() + title.slice(1), html: inner, open, count: count ?? null });
20669
- return `<details class="section" id="${id}"${open ? " open" : ""}><summary><h2>${title}${count === undefined ? "" : ` <span class="lane-count">${count}</span>`}</h2></summary>` + inner + `</details>`;
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>`;
20670
21024
  };
20671
21025
  const receiptLeads = data.assignment == null && task.state === "done" && data.completion?.receipt != null;
20672
21026
  // Exact identity stays available in Task options; failures stay in the
@@ -20678,7 +21032,7 @@ function taskBodyParts(data) {
20678
21032
  ? ` · filed by <span class="mono">${escape(data.coordinator.label)}</span>${data.coordinator.filedAgo === null ? "" : ` ${escape(data.coordinator.filedAgo)}`}`
20679
21033
  : data.filedVia === null || data.filedVia === undefined
20680
21034
  ? ""
20681
- : ` · 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>`;
20682
21036
  // A pull request that can be merged (or opened) owns the one primary action; the result stays in its section.
20683
21037
  const pullRequestActs = data.csrf !== "" && (data.pullRequest?.view?.canMerge === true || (data.pullRequest?.view == null && data.pullRequest?.offer != null));
20684
21038
  const pullRequestFact = data.pullRequest?.fact != null && data.pullRequest.view?.runId === data.assignment?.receipt?.runId ? data.pullRequest.fact : undefined;
@@ -20719,12 +21073,12 @@ function taskBodyParts(data) {
20719
21073
  : "gone from the tracker";
20720
21074
  const reopenable = mirror.remoteState === "open" && !mirror.dispatchOk && mirror.closeGeneration !== null &&
20721
21075
  mirror.syncGeneration > mirror.closeGeneration && ["cancelled", "failed", "queued"].includes(task.state) && data.csrf !== "";
20722
- return (`<div class="card"><p><strong>external work</strong> <span class="meta">${link} · ${escape(state)}</span></p>` +
21076
+ return (`<div class="card"><p><strong>External work</strong> <span class="meta">${link} · ${escape(state)}</span></p>` +
20723
21077
  (reopenable
20724
21078
  ? `<form method="post" action="${taskHref(task.id)}/reopen" class="row">` +
20725
21079
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
20726
21080
  `<input type="password" name="token" placeholder="your password" aria-label="your password" autocomplete="current-password">` +
20727
- `<button type="submit">reopen — the approved scope stands</button></form>`
21081
+ `<button type="submit">Reopen — the approved scope stands</button></form>`
20728
21082
  : "") +
20729
21083
  `</div>`);
20730
21084
  })();
@@ -20738,7 +21092,7 @@ function taskBodyParts(data) {
20738
21092
  // is as fenced as the builder on a coordinator filing, so "plan
20739
21093
  // first" would recommend a road that refuses.
20740
21094
  ? `<div class="card"><p><strong>This task is waiting on you: an agent filed it, and it has no scope.</strong></p>` +
20741
- `<p class="meta">filed by <span class="mono">${escape(data.coordinator.label)}</span> — nothing plans, claims, or runs until you write a scope below and sign it. Your signature runs their request.</p></div>`
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>`
20742
21096
  : `<div class="card task-scope-needed"><p><strong>No approved scope yet</strong></p>` +
20743
21097
  `<p class="meta"><strong>Plan first</strong> drafts it from the repository, or <a href="#scope">write it yourself</a>.</p></div>`
20744
21098
  : "";
@@ -20773,7 +21127,7 @@ function taskBodyParts(data) {
20773
21127
  attemptPanel,
20774
21128
  data.coordinatorProposals == null || data.coordinatorProposals.rows.length === 0
20775
21129
  ? ""
20776
- : section("proposals", `<h2>proposed by coordinators</h2>` +
21130
+ : section("proposals", `<h2>Proposed by coordinators</h2>` +
20777
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),
20778
21132
  section("decisions", decisions, true, data.decisions.length),
20779
21133
  section("incidents", incidents, true, data.incidents.length),
@@ -20785,13 +21139,13 @@ function taskBodyParts(data) {
20785
21139
  ? ` · CI ${data.publication.lastCheckState} at last observation`
20786
21140
  : " · no checks observed"}</span></p>`,
20787
21141
  section("report", reportCard, true),
20788
- data.assignment == null ? section("attempts", runs, true, data.runs.length) : section("Build activity", runs.replace("<h2>attempts</h2>", ""), false, data.runs.length, "attempts"),
21142
+ data.assignment == null ? section("attempts", runs, true, data.runs.length) : section("Build activity", runs.replace("<h2>Attempts</h2>", ""), false, data.runs.length, "attempts"),
20789
21143
  section("usage", spendCard, false),
20790
21144
  section("steering", steeringCard, (data.steering ?? []).length > 0, (data.steering ?? []).length),
20791
21145
  section("scope",
20792
21146
  // The recipe road rides with the scope it reuses (UI polish
20793
21147
  // 2026-09-13), off the title-to-action path.
20794
- ["<h2>scope</h2>", scopeCard, data.repo !== null && data.scope !== null ? `<p class="meta"><a href="/recipes/from-task?task=${encodeURIComponent(task.id)}">Reuse this scope as a recipe →</a></p>` : "", agentsCardHtml(task.id, data.route, data.csrf, data.canEditRoute === true), revisionInApproval ? "" : revisionCard, data.completion != null ? "" : repairChainHtml(data.repairChain ?? null), attendedCard, scopeForm].join("\n"), data.scopeDraft !== undefined || (data.plan !== "requested" && approveForm === "" && !(scope === null && canPlan))),
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))),
20795
21149
  dependencyChoiceNeeded ? "" : section("waits for", waitsForCard, (data.waitsFor ?? []).length > 0, (data.waitsFor ?? []).length),
20796
21150
  section("holds", holds, true, data.holds.length),
20797
21151
  cancelAct,
@@ -20811,7 +21165,19 @@ function taskBodyParts(data) {
20811
21165
  thread.push({ key: "filed", at: rootFiled, kind: "filed", who: "person", author: filedBy ?? "", title: "Filed the task",
20812
21166
  text: (() => { const goal = data.family?.root.goal ?? scope?.goal ?? null; return goal === null ? null : oneLineOf(goal, 280); })(), link: null, html: "", more: null });
20813
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));
20814
21178
  for (const run of familyRuns) {
21179
+ if (foldedAttempts.has(run.id))
21180
+ continue;
20815
21181
  const live = run.id === liveHistoryRunId || (run.outcome === null && run.id === liveRunId);
20816
21182
  const noun = runNoun(run);
20817
21183
  const href = { label: `${noun[0].toUpperCase()}${noun.slice(1)} #${run.id}`, href: `/r/${run.id}` };
@@ -20837,7 +21203,7 @@ function taskBodyParts(data) {
20837
21203
  thread.push({ key: `run-${run.id}`, at: run.finishedAt ?? run.startedAt, kind: "result", who: "agent", author: run.runner,
20838
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),
20839
21205
  link: href, html: "",
20840
- 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 });
20841
21207
  }
20842
21208
  // What the agent is working through now, and how its plan changed.
20843
21209
  const liveAt = liveRun?.startedAt ?? task.updatedAt;
@@ -20853,7 +21219,7 @@ function taskBodyParts(data) {
20853
21219
  const feedback = data.revision;
20854
21220
  thread.push({ key: "revision", at: task.createdAt, kind: "reply", who: "person", author: feedback.comments[0]?.author ?? "You",
20855
21221
  title: feedback.kind === "ci-repair" ? "Asked for a CI repair" : feedback.kind === "criterion-repair" ? "Asked for a repair" : "Asked for changes",
20856
- text: feedback.comments.map(one => `${one.path === null ? "" : `${one.path}${one.line === null ? "" : `:${one.line}`} — `}${one.note}`).join("\n"),
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,
20857
21223
  link: { label: `From build #${feedback.sourceRun}`, href: `/r/${feedback.sourceRun}` }, html: "", more: null });
20858
21224
  }
20859
21225
  for (const decision of data.decisions) {
@@ -20930,9 +21296,6 @@ function taskBodyParts(data) {
20930
21296
  // needs a person, then facts, then folds; the mechanics under Manage.
20931
21297
  const MANAGE = new Set(["steering", "waits-for", "holds"]);
20932
21298
  const finished = task.state === "done" || task.state === "cancelled";
20933
- // Failed: the card says what went wrong in one line, and its one act is Retry itself (the same requeue), never a link to
20934
- // this page; someone who can't retry here (a viewer, a live claim) gets no act at all.
20935
- const statusCard = data.assignment != null && assignmentOptions !== null ? assignmentCardOf(data.assignment, assignmentOptions) : null;
20936
21299
  const failedCard = statusCard?.status.headline === "Failed";
20937
21300
  // A link to this very page, with no section to open, goes nowhere: a card never offers one.
20938
21301
  const bareSelfLink = (href) => {
@@ -20945,7 +21308,33 @@ function taskBodyParts(data) {
20945
21308
  // Build again, in place, names its form after the card's own act.
20946
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"
20947
21310
  ? { action: `${taskHref(data.assignment.rootId)}/requeue` } : null;
20948
- const retry = failedCard && task.state === "failed" && !data.claimed && !stopControlsActive && data.csrf !== "" ? { action: `${taskHref(task.id)}/requeue` } : null;
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
+ };
20949
21338
  const view = {
20950
21339
  kind: "task",
20951
21340
  id: task.id,
@@ -20957,18 +21346,24 @@ function taskBodyParts(data) {
20957
21346
  { label: "Ask", href: taskChatHref(data.rootId ?? task.id), active: false },
20958
21347
  ],
20959
21348
  version: data.versionLabel == null ? null : { label: data.versionLabel, current: { label: "Current work", href: taskHref(data.rootId ?? task.id) } },
20960
- status: statusCard === null ? null : failedCard ? { ...statusCard, action: null, status: { ...statusCard.status, sentence: data.failure?.line ?? statusCard.status.sentence } }
20961
- : statusCard.action !== null && rebuild === null && bareSelfLink(statusCard.action.href) ? { ...statusCard, action: null } : statusCard,
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),
20962
21352
  statusHtml,
20963
- failure: failedCard ? data.failure ?? null : null,
21353
+ failure: failedCard && data.failure != null ? { line: data.failure.line, evidence: data.failure.evidence, suggestion: data.failure.suggestion, link: data.failure.link } : null,
20964
21354
  retry,
21355
+ runChecks,
21356
+ progress,
21357
+ stop,
21358
+ record,
21359
+ earlier,
20965
21360
  approval: approvalHtml,
20966
21361
  confirmStopped: data.csrf !== "" && data.assignment?.primaryAction?.code === "confirm-stopped" && data.assignment.primaryAction.target.runId !== null
20967
21362
  ? { action: `${taskHref(data.assignment.rootId)}/confirm-stopped`, run: data.assignment.primaryAction.target.runId, checked: needsCheck(data.assignment) } : null,
20968
21363
  rebuild,
20969
21364
  // The plan, progress and plan changes are thread entries now; the rest still needs a person here.
20970
21365
  lead: [
20971
- { 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 },
20972
21367
  { key: "pull-request", html: pullRequestCard },
20973
21368
  { key: "needs-scope", html: needsScopeCard },
20974
21369
  { key: "mirror", html: mirrorCard }, { key: "contest", html: contestCard }, { key: "attempt", html: attemptPanel },
@@ -20979,7 +21374,7 @@ function taskBodyParts(data) {
20979
21374
  sections: [
20980
21375
  ...(previousResult === null || thread.some(one => one.more !== null) ? [] : [{ id: "result", title: previousResult.title, html: previousResult.html, open: false, count: null }]),
20981
21376
  // Questions are in the thread; the scope and the ledgers fold here until asked.
20982
- ...sectionParts.filter(one => !MANAGE.has(one.id) && one.id !== "decisions").map(one => one.id === "scope" && data.scopeDraft === undefined && (finished || approval.approved) ? { ...one, open: false } : one),
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),
20983
21378
  ],
20984
21379
  manage: [
20985
21380
  ...sectionParts.filter(one => MANAGE.has(one.id)),
@@ -21425,26 +21820,27 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
21425
21820
  const intent = view.intent;
21426
21821
  let intentView = null;
21427
21822
  if (intent === null) {
21428
- parts.push(`<section class="card cockpit-section" data-cockpit-section="intent"><h3>approved scope</h3><p class="meta">No scope was filed for this task, so there is no approved goal or boundary to review.</p></section>`);
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>`);
21429
21824
  }
21430
21825
  else {
21826
+ // The time itself goes to the page as a stamp: the one formatter words it in the viewer's zone.
21431
21827
  const approvalWords = intent.approval.approved
21432
- ? `approved by ${intent.approvedBy ?? "an operator"} · ${when(intent.approval.at)}`
21828
+ ? `Approved by ${intent.approvedBy ?? "an operator"}`
21433
21829
  : intent.approval.reason === "changed"
21434
- ? "the scope changed after approval — the words below are the current text, not the signed one"
21435
- : "never approved — the result was built without a signed scope";
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.";
21436
21832
  const approval = intent.approval.approved
21437
- ? `approved by ${escape(intent.approvedBy ?? "an operator")} · ${whenTime(intent.approval.at)}`
21438
- : approvalWords;
21439
- const body = `<p class="recap" style="margin-top:.25rem"><strong>goal</strong> ${escape(intent.goal)}</p>` +
21440
- (intent.outOfScope === null ? `<p class="meta">no boundary was stated</p>` : `<p class="recap"><strong>not this</strong> ${escape(intent.outOfScope)}</p>`) +
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>`) +
21441
21837
  (intent.touches.length === 0
21442
- ? `<p class="meta">no expected paths were signed — every changed file reads as in bounds</p>`
21443
- : `<p class="row"><span class="meta">expected to touch</span> ${intent.touches.map(one => `<span class="mono">${escape(one)}</span>`).join(" ")}</p>`) +
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>`) +
21444
21840
  (view.plan === null
21445
21841
  ? ""
21446
- : `<p class="meta">plan revision ${view.plan.revision} · <span class="mono" title="${escape(view.plan.sha256)}">${escape(view.plan.sha256.slice(0, 12))}…</span>${view.plan.approach === null ? "" : ` — ${escape(view.plan.approach)}`}</p>`);
21447
- intentView = { approval: approvalWords, html: body };
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 };
21448
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>` +
21449
21845
  `<div class="cockpit-disclosure-body">` + body + `</div></details>`);
21450
21846
  }
@@ -21466,7 +21862,7 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
21466
21862
  ] },
21467
21863
  };
21468
21864
  if (run === null || view.detail === null) {
21469
- 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>` +
21470
21866
  `<p class="meta">This task has no finished build record, so there are no captured changes or checks to review.</p>` +
21471
21867
  `<p class="row"><a href="${taskHref(view.taskId)}">Open the task →</a></p></section>`);
21472
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." } };
@@ -21499,24 +21895,31 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
21499
21895
  });
21500
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>`;
21501
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;
21502
21904
  const complete = assignment?.state === "ready-to-check" && assignment.receipt !== null && canRetryReview && csrf !== ""
21503
- ? { action: `${taskHref(view.taskId)}/complete`, receipt: assignment.receipt.digest, run: run.id } : null;
21905
+ ? { action: `${taskHref(view.taskId)}/complete`, receipt: assignment.receipt.digest, run: run.id, accept: owed } : null;
21504
21906
  if (complete !== null)
21505
- parts.push(completionForm(view.taskId, run.id, complete.receipt, csrf, view.detail?.pullRequestTo ?? null));
21506
- // The one decision, after the evidence: Accept only when every requirement is met and the checks passed.
21507
- const acceptsHere = complete !== null || panel.panel.need?.accept != null;
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;
21508
21910
  const matrix = proof === null || proof.proofProblem !== null ? [] : proof.matrix;
21509
- const blocked = cantAcceptYetOf(proof?.verdict ?? null, proof?.reasons ?? [], accepted);
21510
- const words = !acceptsHere ? null : { sentence: RESULT_DECISION_SENTENCE, ...acceptWordsOf({
21511
- checks: checks === undefined ? null : checks.running != null ? "running" : checks.level === "off" && checks.status !== "passed" ? "off" : checks.status,
21512
- unmet: matrix.filter(row => row.state !== "pass" && row.state !== "manual-review").length,
21513
- yours: complete === null || accepted ? 0 : panel.panel.youCheck?.lines.length ?? 0,
21514
- action: complete !== null ? "complete" : "accept",
21515
- publishing: view.detail?.publishing ?? "other",
21516
- proof: proof !== null && proof.proof !== null && proof.proofProblem === null,
21517
- }) };
21518
- // Refuted: plain Accept stays as allowed, in outline, and the one line before the acts (acts.line) says why.
21519
- const decision = words === null || blocked === null ? words : { ...words, label: "Accept", ready: false };
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 } };
21520
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.
21521
21924
  const followUps = view.detail.followUps ?? null;
21522
21925
  const checksRunning = checks?.running != null || (followUps?.checks.some(one => one.state === "waiting" || one.state === "running") ?? false);
@@ -21526,18 +21929,27 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
21526
21929
  ? { action: `/r/${run.id}/checks`, level: followUps.full ? "full" : "quick", returnTo: `${here}&run=${run.id}&tab=checks` } : null;
21527
21930
  const need = panel.panel.need;
21528
21931
  const nextKind = selected.next?.kind;
21529
- const acts = resultActsOf({
21530
- accept: decision === null ? null : { ready: decision.ready },
21531
- runChecks: runChecks !== null,
21532
- checksRunning,
21533
- blocked,
21534
- canRequest: panel.panel.canRequest && panel.panel.request !== null,
21535
- need: need === null || need.accept != null ? null : need.rebuild != null ? "rebuild" : need.confirm !== null ? "confirm-stopped" : null,
21536
- next: nextKind === "revise" || nextKind === "draft-repair" ? nextKind : null,
21537
- });
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);
21538
21950
  // A refuted result's card lists every recorded disagreement, each tied to its lines; when the report itself doesn't
21539
21951
  // match the changes (not a failed check), that is the headline.
21540
- const mismatch = proof === null || accepted || proof.verdict !== "refuted" ? null : (() => {
21952
+ const mismatch = failure !== null || proof === null || accepted || proof.verdict !== "refuted" ? null : (() => {
21541
21953
  const patch = view.detail?.terminal?.patch ?? null;
21542
21954
  const files = patch === null || "problem" in patch ? [] : parseReviewDiff(patch.text).files;
21543
21955
  // Each file's first change: the lines it added (else every line it shows), and where that change starts (its link target).
@@ -21570,25 +21982,54 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
21570
21982
  };
21571
21983
  })();
21572
21984
  if (view.notes.length > 0) {
21573
- 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>` +
21574
21986
  view.notes.map(one => `<p class="row"><span class="meta">${escape(one.author)} · ${whenTime(one.createdAt)}</span> ${escape(one.note)}</p>`).join("\n") +
21575
21987
  `</section>`);
21576
21988
  }
21577
21989
  return {
21578
21990
  html: `<section class="cockpit-detail">${parts.join("\n")}</section>`,
21579
21991
  selected: {
21580
- ...selected, complete, decision, panel: panel.panel, contest, acts, runChecks, mismatch,
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" } }),
21581
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}` },
21582
22011
  },
21583
22012
  };
21584
22013
  }
21585
- function completionForm(taskId, runId, digest, csrf, pullRequestTo = null) {
21586
- const hidden = `<input type="hidden" name="csrf" value="${escape(csrf)}"><input type="hidden" name="receipt" value="${escape(digest)}"><input type="hidden" name="run" value="${runId}">`;
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)}">`);
21587
22028
  if (pullRequestTo !== null) {
21588
- // With pull requests set up: the PR is the primary road, a bare completion the quiet one beside it.
21589
- return `<form method="post" action="${taskHref(taskId)}/complete" class="card result-complete">${hidden}<p class="meta">After inspecting the work, mark this result complete. A pull request opens on ${escape(pullRequestTo)} from this exact commit.</p><div class="result-complete-actions"><button type="submit" name="publish" value="1" style="min-height:44px">Complete and open a pull request</button><button type="submit" class="secondary" style="min-height:44px">Complete only</button></div></form>`;
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>`;
21590
22031
  }
21591
- return `<form method="post" action="${taskHref(taskId)}/complete" class="card result-complete">${hidden}<p class="meta">After inspecting the work, mark this result complete. Checks stay unchanged; nothing is published or deployed.</p><button type="submit" style="min-height:44px">Mark complete</button></form>`;
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>`;
21592
22033
  }
21593
22034
  /** The pull request, in one card: its state and link, one line of detail, and the one action it needs — Merge
21594
22035
  * behind the password when checks passed, or Open a pull request for a result completed without one. */
@@ -21726,13 +22167,13 @@ function shortDigest(digest) {
21726
22167
  * orphaned run keeps saying what actually became of it. */
21727
22168
  function runOutcomeBadge(run, live) {
21728
22169
  if (!live && run.role === "planner" && run.reason === "plan-drafted") {
21729
- return `<span class="badge">planned</span>`;
22170
+ return `<span class="badge">Planned</span>`;
21730
22171
  }
21731
22172
  if (!live && run.reason === "interrupted")
21732
- return `<span class="badge">interrupted</span>`;
22173
+ return `<span class="badge">Interrupted</span>`;
21733
22174
  return live
21734
- ? `<span class="badge badge-running">running</span>`
21735
- : `<span class="badge badge-${escape(run.outcome ?? "cut")}">${escape(run.outcome ?? "never finished")}</span>`;
22175
+ ? `<span class="badge badge-running">Running</span>`
22176
+ : `<span class="badge badge-${escape(run.outcome ?? "cut")}">${escape(sentenceCase(run.outcome ?? "never finished"))}</span>`;
21736
22177
  }
21737
22178
  const runNoun = (run) => run.role === "planner" ? "plan" : run.role === "reviewer" ? "review" : run.role === "scout" ? "report" : "build";
21738
22179
  function runsPage(chrome, rows, liveIds, nextCursor, verdicts = new Map(), accepted = new Set()) {
@@ -21747,7 +22188,7 @@ function runsPage(chrome, rows, liveIds, nextCursor, verdicts = new Map(), accep
21747
22188
  return (`<p class="row"><a href="/r/${run.id}" class="mono">#${run.id}</a> ` +
21748
22189
  `<a href="${taskHref(run.taskId)}" class="mono">${escape(run.taskId)}</a> ` +
21749
22190
  runOutcomeBadge(run, liveIds.has(run.id)) +
21750
- `${run.qualityMode === "strict" ? ` <span class="badge">strict review</span>` : ""}` +
22191
+ `${run.qualityMode === "strict" ? ` <span class="badge">Strict review</span>` : ""}` +
21751
22192
  needsVerification +
21752
22193
  criterionMatrixSummary(verdicts.get(run.id)?.matrix ?? []) +
21753
22194
  `${run.provider === "claude" ? "" : ` <span class="meta mono">${escape(run.provider)}</span>`}` +
@@ -21756,7 +22197,7 @@ function runsPage(chrome, rows, liveIds, nextCursor, verdicts = new Map(), accep
21756
22197
  })
21757
22198
  .join("\n");
21758
22199
  const older = nextCursor === null ? "" : `<p><a href="/runs?before=${nextCursor}">older →</a></p>`;
21759
- return screen("builds", [`<h1>builds <a class="badge" href="/peek">peek at the live ones \u2192</a></h1>`, buildsViews("builds"), `<p class="hint">one build = one attempt by an agent to complete a task, on its own branch</p>`, list, older].join("\n"), { chrome });
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 });
21760
22201
  }
21761
22202
  function structuredHandoffView(artifacts, root) {
21762
22203
  const artifact = [...artifacts].reverse().find(one => one.kind === "handoff");
@@ -22111,7 +22552,7 @@ function reviewDiffHtml(patch, stat, runId, commentable, anchors = new Map()) {
22111
22552
  const parsed = parseReviewDiff(patch.text);
22112
22553
  const structured = parsed.files.some(file => file.hunks.length > 0);
22113
22554
  if (!structured) {
22114
- 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>` +
22115
22556
  `<pre class="mono" style="overflow-x:auto">${escape(patch.text)}</pre></details>` +
22116
22557
  storedDownloadLink(runId, patch.artifactId, "diff", patch.truncated));
22117
22558
  }
@@ -22185,12 +22626,12 @@ function storedDownloadLink(runId, artifactId, what, truncated) {
22185
22626
  * page's prefill script reads — a real button, keyboard-reachable, separate
22186
22627
  * from the link so the two never fight over one click (finding 4). */
22187
22628
  function terminalDiffCard(view, runId, editor = null, commentable = false) {
22188
- const parts = ["<h2>what changed</h2>"];
22629
+ const parts = ["<h2>What changed</h2>"];
22189
22630
  if (view.stat === null) {
22190
- 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>`);
22191
22632
  }
22192
22633
  else if ("problem" in view.stat) {
22193
- parts.push(`<p class="meta">stat: ${escape(view.stat.problem)}</p>`);
22634
+ parts.push(`<p class="meta">Stat: ${escape(view.stat.problem)}</p>`);
22194
22635
  }
22195
22636
  else {
22196
22637
  const s = view.stat;
@@ -22212,23 +22653,23 @@ function terminalDiffCard(view, runId, editor = null, commentable = false) {
22212
22653
  .slice(0, 40)
22213
22654
  .map(file => `<p class="row mono">${fileName(file.path)}${file.renamedFrom === undefined ? "" : ` (was ${escape(file.renamedFrom)})`} ` +
22214
22655
  `<span class="meta">${file.additions === null || file.deletions === null ? "binary" : `+${file.additions} −${file.deletions}`}</span>` +
22215
- `${commentable ? ` <button type="button" class="pick-file" data-path="${escape(file.path)}">comment</button>` : ""}</p>`)
22656
+ `${commentable ? ` <button type="button" class="pick-file" data-path="${escape(file.path)}">Comment</button>` : ""}</p>`)
22216
22657
  .join("\n") +
22217
22658
  (s.files.length > 40 ? `<p class="meta">…and ${s.files.length - 40} more file(s)</p>` : "") +
22218
22659
  `</div>`);
22219
22660
  if (editor !== null) {
22220
- parts.push(`<p class="meta">file links open in VS Code on THIS device — if the build's worktree is gone, a link opens nothing</p>`);
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>`);
22221
22662
  }
22222
22663
  }
22223
22664
  }
22224
22665
  if (view.patch === null) {
22225
- 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>`);
22226
22667
  }
22227
22668
  else if ("problem" in view.patch) {
22228
- parts.push(`<p class="meta">patch: ${escape(view.patch.problem)}</p>`);
22669
+ parts.push(`<p class="meta">Patch: ${escape(view.patch.problem)}</p>`);
22229
22670
  }
22230
22671
  else if (view.patch.text.trim() === "") {
22231
- 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>`);
22232
22673
  }
22233
22674
  else {
22234
22675
  parts.push(reviewDiffHtml(view.patch, view.stat, runId, commentable));
@@ -22247,7 +22688,7 @@ function evidenceBundleCard(view, run, publication = null, review = null) {
22247
22688
  if (view === null)
22248
22689
  return "";
22249
22690
  const runId = run.id;
22250
- const parts = ["<h2>verification and evidence</h2>"];
22691
+ const parts = ["<h2>Verification and evidence</h2>"];
22251
22692
  if (view.verdict !== null) {
22252
22693
  // The same status line every other surface renders for this run
22253
22694
  // (workspace package 1), beside the verdict's own explanation.
@@ -22257,7 +22698,7 @@ function evidenceBundleCard(view, run, publication = null, review = null) {
22257
22698
  if (view.accepted !== null) {
22258
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>`);
22259
22700
  }
22260
- parts.push(criterionMatrixHtml(view.matrix, { runId, links: view.matrixLinks }));
22701
+ parts.push(criterionMatrixHtml(view.matrix, { runId, links: view.matrixLinks, verdict: view.verdict }));
22261
22702
  parts.push(semanticCoverageHtml(view.matrix, view.qualityMode));
22262
22703
  if (reviewConflict(view.matrix, view.machineVerdict, view.verdict)) {
22263
22704
  parts.push(`<p class="meta">An independent review found conflicting evidence.</p>`);
@@ -22268,25 +22709,25 @@ function evidenceBundleCard(view, run, publication = null, review = null) {
22268
22709
  }
22269
22710
  else if (view.proof !== null) {
22270
22711
  if (view.proof.criteria.length > 0) {
22271
- parts.push(`<div class="result-section"><strong>acceptance criteria</strong><ul>` +
22712
+ parts.push(`<div class="result-section"><strong>Acceptance criteria</strong><ul>` +
22272
22713
  view.proof.criteria
22273
- .map(one => `<li><span class="badge${one.verdict === "met" ? " badge-done" : one.verdict === "not-met" ? " badge-failed" : ""}">${escape(one.verdict)}</span> ${escape(one.statement)} <span class="meta">— ${escape(one.how)}</span></li>`)
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>`)
22274
22715
  .join("") +
22275
22716
  `</ul></div>`);
22276
22717
  }
22277
22718
  if (view.proof.checks.length > 0) {
22278
- 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>` +
22279
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("") +
22280
22721
  `</ul></div>`);
22281
22722
  }
22282
22723
  if (view.proof.caveats.length > 0) {
22283
- parts.push(`<div class="result-section"><strong>caveats</strong><ul>${view.proof.caveats.map(one => `<li>${escape(one)}</li>`).join("")}</ul></div>`);
22724
+ parts.push(`<div class="result-section"><strong>Caveats</strong><ul>${view.proof.caveats.map(one => `<li>${escape(one)}</li>`).join("")}</ul></div>`);
22284
22725
  }
22285
22726
  }
22286
22727
  if (view.checkLog !== null)
22287
22728
  parts.push(checkLogHtml(view.checkLog, runId, ""));
22288
22729
  if (view.screenshots.length > 0) {
22289
- parts.push(`<div class="result-section"><strong>screenshots</strong>` +
22730
+ parts.push(`<div class="result-section"><strong>Screenshots</strong>` +
22290
22731
  view.screenshots
22291
22732
  .map(shot => `<p class="row"><a href="/r/${runId}/evidence/${shot.artifactId}">` +
22292
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> ` +
@@ -22403,7 +22844,7 @@ function completionReceiptCard(view, taskId, place, standing = receiptStatusOf(v
22403
22844
  (view.publication?.state === "failed" ? `<p class="receipt-note">${statusIconSvg("note")} Pull request couldn't open. The commit is safe locally.</p>` : "") +
22404
22845
  caveats +
22405
22846
  coverage +
22406
- `<details class="receipt-details"><summary>Result details</summary>` +
22847
+ `<details class="receipt-details"><summary>${STATUS_MORE}</summary>` +
22407
22848
  (view.summary !== null && view.summary !== conciseOutcomeOf(view.summary) ? `<p class="recap">${escape(view.summary)}</p>` : "") +
22408
22849
  `<p class="receipt-publication" data-receipt-publication="${escape(facts.publicationState)}">${escape(facts.publicationWords)}</p>` +
22409
22850
  // A review in flight is the receipt's primary status too (its chip
@@ -22534,9 +22975,10 @@ function resultPanelParts(detail, o) {
22534
22975
  items: personCheckItems(proof, patchOk ? patch.text : null, shown, runId),
22535
22976
  accept: acceptable && need?.accept == null ? { action: `${taskHref(detail.taskId)}/accept-proof`, run: run.id, returnTo: o.returnTo } : null,
22536
22977
  };
22978
+ const youCheckForm = youCheck?.accept != null && o.finishes !== true ? youCheck.accept : null;
22537
22979
  const youCheckHtml = youCheck === null ? "" :
22538
22980
  `<div class="result-you-check" data-result-you-check="${youCheck.lines.length}"><ul>${youCheck.lines.map(one => `<li>${escape(one)}</li>`).join("")}</ul>` +
22539
- (youCheck.accept === null ? "" : `<form method="post" action="${escape(youCheck.accept.action)}"><input type="hidden" name="csrf" value="${escape(o.csrf)}"><input type="hidden" name="run" value="${youCheck.accept.run}"><input type="hidden" name="return" value="${escape(youCheck.accept.returnTo)}"><button type="submit" data-accept-result>Accept</button></form>`) +
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>`) +
22540
22982
  `</div>`;
22541
22983
  const attentionHtml = attention.length === 0
22542
22984
  ? ""
@@ -22663,9 +23105,9 @@ function resultPanelParts(detail, o) {
22663
23105
  detail.files
22664
23106
  .map(file => {
22665
23107
  const counts = file.additions === null || file.deletions === null ? "binary" : `+${file.additions} −${file.deletions}`;
22666
- const flags = (file.outsideTouches ? ` <span class="badge badge-failed" data-outside-touches="1">outside touches</span>` : "") + (file.cited ? ` <span class="badge badge-done">cited</span>` : "");
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>` : "");
22667
23109
  const why = file.priority.reasons.length === 0 ? "" : ` <span class="meta">— ${file.priority.reasons.map(escape).join("; ")}</span>`;
22668
- return `<li data-file-priority="${file.priority.band}">${fileName(file)}${file.renamedFrom === null ? "" : ` <span class="meta">(was ${escape(file.renamedFrom)})</span>`} <span class="meta">${counts}</span>${flags}${detail.canAnnotate ? ` <button type="button" class="pick-file" data-path="${escape(file.path)}">comment</button>` : ""}${why}</li>`;
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>`;
22669
23111
  })
22670
23112
  .join("") +
22671
23113
  `</ol>` +
@@ -22706,14 +23148,14 @@ function resultPanelParts(detail, o) {
22706
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>`);
22707
23149
  }
22708
23150
  else {
22709
- 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 }));
22710
23152
  }
22711
23153
  checkParts.push(semanticCoverageHtml(proof.matrix, proof.qualityMode));
22712
23154
  if (reviewConflict(proof.matrix, proof.machineVerdict, proof.verdict))
22713
23155
  checkParts.push(`<p class="meta">An independent review found conflicting evidence.</p>`);
22714
23156
  checkParts.push(repairChainHtml(proof.repairChain));
22715
23157
  if (detail.reviewerFindings.length > 0) {
22716
- 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>` +
22717
23159
  reviewerNotes +
22718
23160
  `</ul></div>`);
22719
23161
  }
@@ -22742,7 +23184,7 @@ function resultPanelParts(detail, o) {
22742
23184
  // The handoff's own account of its checks — the agent's words, labeled
22743
23185
  // as such, whether or not a proof exists.
22744
23186
  if (!directAssessment && handoff !== null && handoff.verification.length > 0)
22745
- checkParts.push(`<div class="result-section" data-cockpit-source="agent-words"><strong>the agent's own account</strong><ul>${handoff.verification.map(one => `<li>${escape(one)}</li>`).join("")}</ul></div>`);
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>`);
22746
23188
  if (o.extraChecks !== undefined)
22747
23189
  checkParts.push(o.extraChecks);
22748
23190
  // ---- request changes: both feedback styles, one sealed road ------------
@@ -22787,8 +23229,8 @@ function resultPanelParts(detail, o) {
22787
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>` +
22788
23230
  `<span class="meta diff-comment-limit" id="comment-note-limit">up to ${LIMITS.note} characters</span>` +
22789
23231
  `<details class="result-pin"><summary>Attach to a file or line</summary>` +
22790
- `<div class="diff-comment-target"><label>file<input type="text" name="path" placeholder="src/…" aria-label="file" class="mono"></label>` +
22791
- `<label>line<input type="text" name="line" placeholder="—" aria-label="line" inputmode="numeric"></label></div></details>` +
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>` +
22792
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>`);
22793
23235
  }
22794
23236
  // ---- the panel ---------------------------------------------------------
@@ -22830,6 +23272,8 @@ function resultPanelParts(detail, o) {
22830
23272
  reviewHistory: REVIEW_TOKENS.has(status.token) ? status.detail : null,
22831
23273
  attention: attention.filter(one => !shortenedMaterialReason(one)).map(plainReasonWords),
22832
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),
22833
23277
  limits: attention.filter(one => shortenedMaterialReason(one)),
22834
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 })),
22835
23279
  views: [{ key: "summary", html: summaryParts.join("\n") }, { key: "changes", html: changeParts.join("\n") }, { key: "checks", html: checkParts.join("\n") }],
@@ -23024,9 +23468,9 @@ function runPage(chrome, run, taskId, artifacts, terminal = null, notes = [], cs
23024
23468
  : " · the agent is working on this";
23025
23469
  const conversation = heldTurns === null
23026
23470
  ? ""
23027
- : `<h2>conversation</h2>` +
23471
+ : `<h2>Conversation</h2>` +
23028
23472
  (heldTurns.turns.length === 0
23029
- ? `<p class="meta">nothing said yet</p>`
23473
+ ? `<p class="meta">Nothing said yet</p>`
23030
23474
  : heldTurns.turns
23031
23475
  .map(turn => `<p class="row"><span class="meta">${turn.sourceKind === "operator"
23032
23476
  ? escape(turn.author ?? "operator")
@@ -23040,11 +23484,11 @@ function runPage(chrome, run, taskId, artifacts, terminal = null, notes = [], cs
23040
23484
  ? `<form method="post" action="/r/${run.id}/turn" class="row">` +
23041
23485
  `<input type="hidden" name="csrf" value="${escape(csrf)}">` +
23042
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">` +
23043
- `<button type="submit">send</button></form>` +
23487
+ `<button type="submit">Send</button></form>` +
23044
23488
  `<p class="meta">${escape(heldTurns.state)} · ${heldTurns.turns.length}/${heldTurns.cap} messages · a waiting question must be answered before free-form messages</p>`
23045
23489
  : heldTurns.open
23046
23490
  ? ""
23047
- : `<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>`);
23048
23492
  // The live peek region (A2): the poller fills it only on a serve that
23049
23493
  // asserted its runner. Without the assertion the section still appears
23050
23494
  // for a running build and says honestly why it is empty \u2014 a page that
@@ -23053,24 +23497,24 @@ function runPage(chrome, run, taskId, artifacts, terminal = null, notes = [], cs
23053
23497
  const peek = !running
23054
23498
  ? ""
23055
23499
  : peekable
23056
- ? `<h2>what is changing right now</h2>` +
23057
- `<div id="run-peek"><p class="meta">watching\u2026 the first look lands within 15 seconds</p></div>` +
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>` +
23058
23502
  `<p class="meta" id="run-peek-stamp"></p>`
23059
- : `<h2>what is changing right now</h2>` +
23060
- `<p class="meta">the live file view is off \u2014 start serve with ${escape("--runner <name>")} naming this machine's worker, and it appears here</p>`;
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>`;
23061
23505
  // The live transcript (arc 1): the agent's own words, streamed to a file
23062
23506
  // beside the run and polled as raw text. Honesty stated on the surface:
23063
23507
  // this is display only, and the machine running the agent could alter it.
23064
23508
  const transcript = !running
23065
23509
  ? ""
23066
23510
  : !peekable
23067
- ? `<h2>what the agent is saying</h2>` +
23068
- `<p class="meta">the live transcript is off \u2014 start serve with ${escape("--runner <name>")} naming this machine's worker, and it appears here</p>`
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>`
23069
23513
  : run.provider !== "claude"
23070
- ? `<h2>what the agent is saying</h2>` +
23071
- `<p class="meta">the live transcript needs the claude harness for now \u2014 this build runs on ${escape(run.provider)}</p>`
23072
- : `<h2>what the agent is saying</h2>` +
23073
- `<p class="meta">display only \u2014 this is not evidence, and the machine running the agent could alter it</p>` +
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>` +
23074
23518
  `<pre id="live-transcript" class="mono" style="max-height:24rem;overflow:auto;white-space:pre-wrap"></pre>` +
23075
23519
  `<p class="meta" id="live-transcript-state"></p>`;
23076
23520
  const resultSummary = structuredHandoff?.conclusion ?? run.handoff;
@@ -23079,7 +23523,7 @@ function runPage(chrome, run, taskId, artifacts, terminal = null, notes = [], cs
23079
23523
  : `<div class="result-section"><strong>${escape(label)}</strong><ul>${items.map(one => `<li>${escape(one)}</li>`).join("")}</ul></div>`;
23080
23524
  const handoff = resultSummary === null
23081
23525
  ? ""
23082
- : `<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>` +
23083
23527
  (structuredHandoff === null
23084
23528
  ? ""
23085
23529
  : resultList("completed", structuredHandoff.changes) +
@@ -23115,9 +23559,9 @@ function runPage(chrome, run, taskId, artifacts, terminal = null, notes = [], cs
23115
23559
  ? ""
23116
23560
  : `<form method="post" action="/r/${run.id}/comment" class="card diff-comment-form" id="comment-form">` +
23117
23561
  `<input type="hidden" name="csrf" value="${escape(csrf)}">` +
23118
- `<div class="diff-comment-target"><label>file<input type="text" name="path" placeholder="select a line above" aria-label="file" class="mono"></label>` +
23119
- `<label>line<input type="text" name="line" placeholder="—" aria-label="line" inputmode="numeric"></label></div>` +
23120
- `<label>change requested<textarea name="note" rows="2" maxlength="${LIMITS.note}" placeholder="Explain what should change and why" aria-label="review comment" aria-describedby="comment-note-limit"${noted ? " autofocus" : ""}></textarea></label>` +
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>` +
23121
23565
  `<span class="meta diff-comment-limit" id="comment-note-limit">up to ${LIMITS.note} characters</span>` +
23122
23566
  `<button type="submit">Add annotation</button></form>`;
23123
23567
  // The device-side half of the editor-link activation (arc 6, finding 1):
@@ -23145,7 +23589,7 @@ function runPage(chrome, run, taskId, artifacts, terminal = null, notes = [], cs
23145
23589
  : `<div class="card"><p><strong>CI is failing on PR #${ciRepair.pr}</strong> <span class="meta">observed by the episode watcher</span></p>` +
23146
23590
  `<form method="post" action="/r/${run.id}/draft-repair">` +
23147
23591
  `<input type="hidden" name="csrf" value="${escape(csrf)}">` +
23148
- `<button type="submit">draft a repair task</button>` +
23592
+ `<button type="submit">Draft a repair task</button>` +
23149
23593
  `<span class="meta"> — one unapproved task; you approve its scope before anything builds</span></form></div>`;
23150
23594
  const reviewCard = (commentRows === "" && commentForm === "" ? "" : `<h2 id="review">Review and revise</h2>` +
23151
23595
  `<p class="meta">Annotate the diff above. When the batch is ready, create one scoped revision task from it.</p>` +
@@ -23161,8 +23605,8 @@ function runPage(chrome, run, taskId, artifacts, terminal = null, notes = [], cs
23161
23605
  : `<form method="post" action="/r/${run.id}/note" class="row">` +
23162
23606
  `<input type="hidden" name="csrf" value="${escape(csrf)}">` +
23163
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">` +
23164
- `<button type="submit">add note</button></form>`;
23165
- 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}`;
23166
23610
  // Continuation (Phase 2E, A4): a finished attempt offers a watched
23167
23611
  // follow-up — the text you type here enters the SIGNED terms on the
23168
23612
  // confirm screen; nothing runs until your password agrees to exactly it.
@@ -23302,7 +23746,7 @@ function capsPage(chrome, caps, gaps, repo, now) {
23302
23746
  if (caps === null) {
23303
23747
  return screen("requirements", [
23304
23748
  `<h1>Requirements</h1>`,
23305
- `<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>`,
23306
23750
  ].join("\n"), { chrome });
23307
23751
  }
23308
23752
  const list = caps.length === 0
@@ -23312,7 +23756,7 @@ function capsPage(chrome, caps, gaps, repo, now) {
23312
23756
  `${escape(describeCapability(capability, now ?? new Date()))}</p>`)
23313
23757
  .join("\n");
23314
23758
  const blocked = gaps.length === 0
23315
- ? `<p class="meta">no gaps — everything recorded is verified</p>`
23759
+ ? `<p class="meta">No gaps — everything recorded is verified</p>`
23316
23760
  : gaps
23317
23761
  .map(gap => `<div class="card"><p>${escape(gap.key)} — ${escape(gap.state)}</p>` +
23318
23762
  `<p class="meta">${gap.unblocks.length > 0
@@ -23320,16 +23764,16 @@ function capsPage(chrome, caps, gaps, repo, now) {
23320
23764
  : gap.alsoBlocks.length > 0
23321
23765
  ? `part of what holds: ${gap.alsoBlocks.map(one => escape(one)).join(", ")}`
23322
23766
  : "nothing queued needs it yet"}</p>` +
23323
- `<p class="meta">verify: ${escape(gap.verify)}</p>` +
23767
+ `<p class="meta">Verify: ${escape(gap.verify)}</p>` +
23324
23768
  `<p class="meta">${escape(gap.instructions)}</p></div>`)
23325
23769
  .join("\n");
23326
23770
  return screen("requirements", [
23327
23771
  `<h1>Requirements</h1>`,
23328
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>`,
23329
23773
  list,
23330
- `<h2>missing, ranked by what filling them frees</h2>`,
23774
+ `<h2>Missing, ranked by what filling them frees</h2>`,
23331
23775
  blocked,
23332
- `<p class="meta">read-only here: checks are shell commands you wrote, and a web button that runs shell would need its own security review</p>`,
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>`,
23333
23777
  ].join("\n"), { chrome });
23334
23778
  }
23335
23779
  /** v98: how the bot's messages reach Toolroll, in words: pushed to the public address, or asked for. */
@@ -23358,7 +23802,7 @@ function settingsPage(chrome, existing, hasEnv, csrf, problem, messaging = null,
23358
23802
  `<input type="hidden" name="csrf" value="${escape(csrf)}">` +
23359
23803
  permissionModeChoices("permission-mode", permissionDefault.mode) +
23360
23804
  `<button type="submit">Save default</button></form>`
23361
- : `<div class="card"><p><strong>${permissionDefault.mode === "bypassPermissions" ? "Full access" : "Auto"}</strong></p><p class="meta">an approver can change this default</p></div>`,
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>`,
23362
23806
  permissionDefault.updatedAt === null
23363
23807
  ? ""
23364
23808
  : `<p class="meta settings-changed">Changed ${whenTime(permissionDefault.updatedAt)}${permissionDefault.updatedBy === null ? "" : ` by ${escape(permissionDefault.updatedBy)}`}</p>`,
@@ -23372,7 +23816,7 @@ function settingsPage(chrome, existing, hasEnv, csrf, problem, messaging = null,
23372
23816
  `<input type="hidden" name="csrf" value="${escape(csrf)}">` +
23373
23817
  qualityModeChoices("quality-mode", qualityDefault.mode) +
23374
23818
  `<button type="submit">Save default</button></form>`
23375
- : `<div class="card"><p><strong>${escape(qualityModeTitle(qualityDefault.mode))}</strong></p><p class="meta">an approver can change this default</p></div>`,
23819
+ : `<div class="card"><p><strong>${escape(qualityModeTitle(qualityDefault.mode))}</strong></p><p class="meta">An approver can change this default</p></div>`,
23376
23820
  qualityDefault.updatedAt === null
23377
23821
  ? ""
23378
23822
  : `<p class="meta settings-changed">Changed ${whenTime(qualityDefault.updatedAt)}${qualityDefault.updatedBy === null ? "" : ` by ${escape(qualityDefault.updatedBy)}`}</p>`,
@@ -23762,18 +24206,18 @@ function nextPage(chrome, data) {
23762
24206
  `<p>${escape(item.approval.title)}</p>` +
23763
24207
  (data.planDocument === null
23764
24208
  ? ""
23765
- : `<div class="card"><p><strong>the plan</strong> <span class="meta">drafted by a planning session</span></p>${executionPlanHtml(data.planDocument, true)}${planContractHtml(data.planContract ?? null, "ceremony")}</div>`) +
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>`) +
23766
24210
  `<form method="post" action="${taskHref(item.approval.taskId)}/approve" class="card approve-form">` +
23767
24211
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
23768
24212
  `<input type="hidden" name="nonce" value="${escape(data.nonce)}">` +
23769
24213
  `<input type="hidden" name="digest" value="${escape(data.approvalDigest ?? item.approval.digest)}">` +
23770
24214
  `<input type="hidden" name="return" value="next">` +
23771
- `<p><strong>approve exactly this:</strong></p>` +
23772
- (data.deliverable === "report" ? `<p class="meta"><span class="badge">scout</span> a read-only session investigates this goal and delivers a report — no branch, nothing changes in the repository</p>` : "") +
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>` : "") +
23773
24217
  (scope === null || scope.profileState !== "unresolved" ? "" : profileWords(scope)) +
23774
- `<p class="meta">goal</p><p class="recap" style="margin-top:0">${escape(scope?.goal ?? item.approval.goal)}</p>` +
23775
- `<p class="meta">not this</p><p class="recap" style="margin-top:0">${scope?.outOfScope == null ? "<em>no exclusions</em>" : escape(scope.outOfScope)}</p>` +
23776
- `<p class="meta">touches · ${scope === null || scope.touches.length === 0 ? "anything" : scope.touches.map(one => escape(one)).join(", ")}</p>` +
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>` +
23777
24221
  (scope === null ? "" : acceptanceCeremonyHtml(scope.acceptance)) +
23778
24222
  (data.raceTerms === null
23779
24223
  ? ""
@@ -23783,8 +24227,8 @@ function nextPage(chrome, data) {
23783
24227
  // tap away, never in the way.
23784
24228
  agentsCeremonyHtml(data.route) +
23785
24229
  (scope === null ? "" : runtimeDetailsHtml(scope)) +
23786
- `<label>your password, typed again — a signed-in session alone cannot agree to work<input type="password" name="token" autocomplete="current-password"></label>` +
23787
- `<div class="sticky-actions"><button type="submit">approve this scope</button></div>` +
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>` +
23788
24232
  `</form>` +
23789
24233
  `<p class="meta"><a href="${taskHref(item.approval.taskId)}">open the full task</a> to edit the scope first</p>`;
23790
24234
  }
@@ -23792,22 +24236,22 @@ function nextPage(chrome, data) {
23792
24236
  card =
23793
24237
  `<h1>${escape(item.stalled.taskId)}</h1>` +
23794
24238
  `<p>${escape(item.stalled.title)}</p>` +
23795
- `<p class="meta">stopped — ${item.stalled.incidentCount} incident(s)${item.stalled.strikes > 0 ? ` after ${item.stalled.strikes} attempt(s)` : ""}</p>` +
24239
+ `<p class="meta">Stopped — ${item.stalled.incidentCount} incident(s)${item.stalled.strikes > 0 ? ` after ${item.stalled.strikes} attempt(s)` : ""}</p>` +
23796
24240
  `<form method="post" action="${taskHref(item.stalled.taskId)}/requeue" class="card">` +
23797
24241
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
23798
24242
  `<input type="hidden" name="return" value="next">` +
23799
- `<p class="meta">requeue resolves the incidents, clears the failed attempts, and puts it back in line</p>` +
23800
- `<button type="submit">retry this work</button>` +
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>` +
23801
24245
  `</form>` +
23802
24246
  `<p class="meta"><a href="${taskHref(item.stalled.taskId)}">open the full task</a> to read the runs first</p>`;
23803
24247
  }
23804
24248
  else {
23805
24249
  const { gap } = item;
23806
24250
  card =
23807
- `<h1>supply ${escape(gap.key)}</h1>` +
24251
+ `<h1>Supply ${escape(gap.key)}</h1>` +
23808
24252
  `<p class="meta">${escape(gap.state)}</p>` +
23809
24253
  `<p>Filling this starts ${gap.unblocks.length} task(s): ${gap.unblocks.map(one => `<span class="mono">${escape(one)}</span>`).join(", ")}</p>` +
23810
- `<div class="card"><p class="meta">prove it filled from the terminal:</p><pre class="recap">${escape(gap.verify)}</pre></div>`;
24254
+ `<div class="card"><p class="meta">Prove it filled from the terminal:</p><pre class="recap">${escape(gap.verify)}</pre></div>`;
23811
24255
  }
23812
24256
  return screen("next", [header, card].join("\n"), { chrome });
23813
24257
  }
@@ -23828,9 +24272,9 @@ function decisionOptionForms(decision, csrf, returnTo) {
23828
24272
  `<input type="hidden" name="choice" value="${escape(option.id)}">`,
23829
24273
  ...(returnTo === null ? [] : [`<input type="hidden" name="return" value="${escape(returnTo)}">`]),
23830
24274
  ...(option.reversible ? [] : [`<input type="hidden" name="confirm" value="yes">`]),
23831
- recommended ? `<p class="meta" style="margin:0 0 .375rem"><span class="badge">recommended</span></p>` : "",
24275
+ recommended ? `<p class="meta" style="margin:0 0 .375rem"><span class="badge">Recommended</span></p>` : "",
23832
24276
  `<p class="consequence">${escape(option.consequence)}</p>`,
23833
- `<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>`,
23834
24278
  `<input type="text" name="note" placeholder="optional note — travels with this answer" aria-label="optional note">`,
23835
24279
  `</form>`,
23836
24280
  ].join("\n");
@@ -23848,14 +24292,14 @@ function decisionPage(chrome, decision, taskId, artifacts, who, now, returnTo =
23848
24292
  : "";
23849
24293
  const evidence = artifacts.length === 0
23850
24294
  ? ""
23851
- : `<div class="evidence"><strong>evidence</strong>` +
24295
+ : `<div class="evidence"><strong>Evidence</strong>` +
23852
24296
  artifacts
23853
24297
  .map(artifact => `<a href="/d/${decision.id}/evidence/${artifact.id}">${escape(evidenceWords(artifact.kind))}` +
23854
24298
  `${artifact.truncated ? " (truncated)" : ""} · ${artifact.bytesStored} bytes</a>`)
23855
24299
  .join("\n") +
23856
24300
  "</div>";
23857
24301
  return screen(`decide \u00b7 ${taskId}`, [
23858
- `<h1>${escape(taskId)} <span class="badge badge-${escape(decision.state)}">${escape(decision.state)}</span>${isOverdue(decision, now) ? ` <span class="badge badge-overdue">overdue</span>` : ""}${decision.deadline === null ? "" : ` <span class="meta">deadline ${escape(decision.deadline)}</span>`}</h1>`,
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>`,
23859
24303
  `<div class="recap">${escape(decision.recap)}</div>`,
23860
24304
  `<div class="question">${escape(decision.question)}</div>`,
23861
24305
  decision.state === "answered" ? answered : options,