toolroll 0.9.100 → 0.9.101

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/accent-colors.js +1 -1
  3. package/dist/activity-line.d.ts +1 -1
  4. package/dist/activity-line.js +1 -1
  5. package/dist/browser/terminal.css +1 -1
  6. package/dist/browser/terminal.js +42 -42
  7. package/dist/browser/workspace.css +2 -2
  8. package/dist/browser/workspace.js +32 -32
  9. package/dist/browser-workspace.d.ts +31 -2
  10. package/dist/design/kit.css +94 -2
  11. package/dist/design/kit.d.ts +12 -4
  12. package/dist/design/kit.js +8 -2
  13. package/dist/design/tokens.css +5 -5
  14. package/dist/first-run.d.ts +10 -6
  15. package/dist/first-run.js +19 -18
  16. package/dist/guides.js +1 -1
  17. package/dist/operate.js +2 -0
  18. package/dist/scope.d.ts +5 -0
  19. package/dist/scope.js +8 -1
  20. package/dist/server/chrome.d.ts +1 -1
  21. package/dist/server/chrome.js +1 -1
  22. package/dist/server/console.css +46 -17
  23. package/dist/server/kit.d.ts +36 -3
  24. package/dist/server/kit.js +33 -4
  25. package/dist/server/render-chat.d.ts +2 -0
  26. package/dist/server/render-chat.js +1 -0
  27. package/dist/server/render-pages.js +1 -1
  28. package/dist/server/render-results.js +3 -3
  29. package/dist/server/render-tasks.d.ts +10 -0
  30. package/dist/server/render-tasks.js +231 -62
  31. package/dist/server/task-views.js +5 -1
  32. package/dist/settings-groups-ui.js +1 -1
  33. package/dist/store.js +14 -3
  34. package/dist/task-archive.d.ts +3 -0
  35. package/dist/task-archive.js +5 -0
  36. package/dist/task-status.d.ts +2 -2
  37. package/dist/task-status.js +3 -3
  38. package/dist/tui.js +1 -1
  39. package/dist/workspace-ui.d.ts +69 -0
  40. package/dist/workspace-ui.js +103 -0
  41. package/package.json +1 -1
@@ -1,10 +1,10 @@
1
1
  /** Task pages: approval, agents, plans, controls, decisions and the task body. */
2
2
  import { archivedWords } from "../tidy.js";
3
- import { btn } from "./kit.js";
3
+ import { btn, stepRows } from "./kit.js";
4
4
  import { whenUtc } from "../when-html.js";
5
5
  import { networkWords } from "../egress.js";
6
6
  import { wasInterrupted } from "../run-ending.js";
7
- import { chainTermsLines, liveChainFromJson, releaseAsksAgainWords, releaseTermsSentence } from "../scope.js";
7
+ import { chainTermsLines, liveChainFromJson, releaseAsksAgainWords, releaseCheckSentence, releaseTermsSentence } from "../scope.js";
8
8
  import { SUBSCRIPTION_CAPABLE } from "../keys.js";
9
9
  import { RELEASE_CI_RUNNING_WORDS } from "../release-ci-hold.js";
10
10
  import { writesWords } from "../writes-mode.js";
@@ -47,10 +47,10 @@ import {} from "../store.js";
47
47
  import { runCostWords } from "../summary.js";
48
48
  import {} from "../task-control.js";
49
49
  import { assignmentStageOf, demoChecksOf, stageOfCode, stageOfDispatch, STATUS_MORE, taskStatusOf } from "../task-status.js";
50
- import { buildProgressOf, dispatchActionLabel, earlierAttemptsWords, receiptPublicationWords, resultStatusOf, REVIEW_TOKENS, workStatusOf } from "../workspace-ui.js";
50
+ import { buildProgressOf, dispatchActionLabel, earlierAttemptsWords, receiptPublicationWords, resultStatusOf, REVIEW_TOKENS, stepRowsOf, workStatusOf } from "../workspace-ui.js";
51
51
  import { createHash, randomUUID } from "node:crypto";
52
52
  import { TEXT_LIMITS } from "../text-limits.js";
53
- import { isOverdue, oneLineOf, projectChip, regionScript, safePrUrl, screen, SENSITIVE_INPUT, sentenceCase, shortDigest, transcriptScript, whenTime } from "./chrome.js";
53
+ import { isOverdue, oneLineOf, projectChip, regionScript, safePrUrl, screen, SENSITIVE_INPUT, sentenceCase, shortDigest, whenTime } from "./chrome.js";
54
54
  import { steerDestinationWords, steerStandingParts } from "../steer-status.js";
55
55
  import { reviewHref, taskChatHref, taskHref } from "./http.js";
56
56
  import { coordinatorProposalsSection } from "./render-chat.js";
@@ -313,6 +313,9 @@ export function approvalSheetParts(input) {
313
313
  // What the yes allows, in plain words, right above it: the agent's
314
314
  // permissions, the per-attempt limit, and an earlier version still running.
315
315
  const allowing = [];
316
+ // The clauses of `allowing` the plan sheet says under Worth knowing (a paid fallback, an earlier version still
317
+ // running): warnings, said first, never folded.
318
+ const warnings = [];
316
319
  const allowed = permissionPlainWords(scope.profile);
317
320
  if (allowed !== null)
318
321
  allowing.push(allowed);
@@ -323,11 +326,16 @@ export function approvalSheetParts(input) {
323
326
  if (fallbacks.length > 0) {
324
327
  const paid = fallbacks.filter(one => one.authMode === "api-key" || SUBSCRIPTION_CAPABLE[one.profile.provider] !== true);
325
328
  // Each agent named once, the paid one said where it stands in the order.
326
- allowing.push(`if it runs out of its plan, the work moves to ${fallbacks.map(one => `${agentNameWords(one.profile.provider, one.profile.model)}${paid.includes(one) ? " on an API key, which spends money" : ""}`).join(", then ")}`);
329
+ const clause = `if it runs out of its plan, the work moves to ${fallbacks.map(one => `${agentNameWords(one.profile.provider, one.profile.model)}${paid.includes(one) ? " on an API key, which spends money" : ""}`).join(", then ")}`;
330
+ allowing.push(clause);
331
+ if (paid.length > 0)
332
+ warnings.push(clause);
327
333
  }
328
334
  const earlier = earlierVersionsWords(input.earlier.active, input.earlier.running);
329
- if (earlier !== null)
335
+ if (earlier !== null) {
330
336
  allowing.push(earlier);
337
+ warnings.push(earlier);
338
+ }
331
339
  const yoursToCheck = scope.acceptance.filter(one => one.evidence.includes("manual-review")).map(one => one.statement.trim().replace(/[.\s]+$/, ""));
332
340
  const consent = joinHtml([
333
341
  allowing.length === 0 ? "" : html `<p class="approval-allowing" data-approval-allowing>You’re allowing: ${allowing.join(" · ")}</p>`,
@@ -342,13 +350,14 @@ export function approvalSheetParts(input) {
342
350
  const submit = rebuild !== null ? html `Approve and retry` : html `Approve`;
343
351
  // Everything else the digest binds, one tap away.
344
352
  const detail = (title, body, attrs = html ``) => isEmptyHtml(body) ? html `` : html `<section class="approval-detail"${attrs}><h3>${title}</h3>${body}</section>`;
345
- const revisionDetail = input.revision === null ? "" : detail("Earlier build", joinHtml([
353
+ const revisionBody = input.revision === null ? html `` : joinHtml([
346
354
  html `<div class="revision-card" data-revision-feedback>`,
347
355
  html `<p><a href="${input.revisionSourceHref}"${input.surface === "chat" ? html ` data-revision-source` : ""}>${input.surface === "chat" ? html `Original result: build #${input.revision.sourceRun} →` : html `build #${input.revision.sourceRun}`}</a></p>`,
348
356
  input.revision.comments.length === 0 ? "" : html `<ul>${input.revision.comments.map(one => html `<li>${revisionImageLink(one.image)}${one.path === null ? "" : html `<span class="mono">${one.path}${one.line === null ? "" : `:${one.line}`}</span> · `}${one.note} <span class="meta">— ${one.author}</span></li>`)}</ul>`,
349
357
  revisionLineageHtml(input.revision.lineage),
350
358
  html `</div>`,
351
- ]));
359
+ ]);
360
+ const revisionDetail = input.revision === null ? "" : detail("Earlier build", revisionBody);
352
361
  const contract = input.planContract;
353
362
  const amendment = approvalAmendmentHtml(contract);
354
363
  const contractDetail = input.planDocument === null || !isEmptyHtml(amendment) ? html ``
@@ -375,7 +384,8 @@ export function approvalSheetParts(input) {
375
384
  scope.budgetMicrousd === null ? "" : html `<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>`,
376
385
  ]);
377
386
  const details = joinHtml([
378
- html `<details class="approval-details"><summary>Plan details</summary>`,
387
+ // On the task page the details are open: the page without script shows every term before the yes. Chat keeps the fold.
388
+ html `<details class="approval-details"${input.surface === "task" ? html ` open` : ""}><summary>Plan details</summary>`,
379
389
  revisionDetail,
380
390
  repairText === null ? "" : detail("Full goal", html `<p>${scope.goal}</p>`),
381
391
  detail("Plan record", contractDetail),
@@ -445,12 +455,21 @@ export function approvalSheetParts(input) {
445
455
  input.deliverable === "report" ? html `<p class="approval-note">Read-only: it reports back and changes nothing in the repository.</p>` : "",
446
456
  originNote(input.origin) === null ? "" : html `<p class="approval-note" data-approval-origin>${originNote(input.origin)}</p>`,
447
457
  ]);
458
+ // A release's terms are part of what the yes agrees to (the digest signs them): said whole on every approval
459
+ // surface (the plan sheet, the page without script, chat), before Approve, with nothing folded.
460
+ const releaseBody = scope.releaseTerms == null && scope.releaseCheck == null ? "" : joinHtml([
461
+ scope.releaseTerms == null ? "" : html `<p>${releaseTermsSentence(scope.releaseTerms)}</p>`,
462
+ scope.releaseCheck == null ? "" : html `<p>${releaseCheckSentence(scope.releaseCheck)}</p>`,
463
+ input.releaseCiRunning === true ? html `<p class="meta">${RELEASE_CI_RUNNING_WORDS}</p>` : "",
464
+ scope.releaseTerms?.policy.carry !== true ? "" : html `<p class="plan-sheet-sub">What asks again</p><ul class="plan-sheet-list">${releaseAsksAgainWords(scope.releaseTerms).map(line => html `<li>${line}</li>`)}</ul>`,
465
+ ]);
448
466
  const rows = joinHtml([
449
467
  html `<dl class="approval-rows">`,
450
468
  row("Goal", html `<p class="approval-goal">${goalShown}</p>`),
451
469
  row("Changes", changes),
452
470
  row("Won’t touch", html `<p>${scope.outOfScope === null ? "Nothing is ruled out." : scope.outOfScope}</p>`),
453
471
  row("Done when", doneWhen),
472
+ releaseBody === "" ? "" : html `<div class="approval-row" data-approval-release><dt>Release</dt><dd>${releaseBody}</dd></div>`,
454
473
  chain.length === 0 ? "" : row("If it runs out", html `<p>${chain[0]}</p><ol class="approval-steps" data-approval-fallback>${chain.slice(1).map(one => html `<li>${one.replace(/^\d+\. /, "")}</li>`)}</ol>`),
455
474
  html `</dl>`,
456
475
  amendment,
@@ -460,8 +479,74 @@ export function approvalSheetParts(input) {
460
479
  routeEvidenceHtml(route, "approval-evidence"),
461
480
  consent,
462
481
  ]);
463
- return { lead, rows, editor, editorForm, details, submit, label: rebuild !== null ? "Approve and retry" : "Approve", after, allowing, passwordNote };
482
+ // The plan sheet (docs/plans/task-page-steps.md): the same parts, re-ordered and re-labelled to read top to bottom —
483
+ // one lead sentence, Steps, Done when, Will change / Won't touch, Worth knowing, the agent — with nothing a person
484
+ // signs inside a fold. Never a second reading of the scope: every term below is one of the parts above.
485
+ const parsedPlan = plan !== null && plan.ok ? plan.document : null;
486
+ const approach = parsedPlan?.approach.trim() ?? "";
487
+ const firstSentence = (text) => /^[\s\S]+?[.!?](?=\s|$)/.exec(text)?.[0] ?? text;
488
+ const sheetLead = approach !== "" ? firstSentence(approach) : goalShown.trim();
489
+ const approachRest = approach !== "" ? approach.slice(sheetLead.length).trim() : "";
490
+ const part = (title, body, attrs = html ``) => body === "" || isEmptyHtml(body) ? html `` : html `<section class="plan-sheet-part"${attrs}><h3 class="plan-sheet-h">${title}</h3>${body}</section>`;
491
+ const statementKey = (text) => text.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
492
+ const signedKeys = scope.acceptance.map(one => statementKey(one.statement));
493
+ // The plan's proof lines a requirement doesn't already say, word for word: a line that adds a condition to one stays.
494
+ const extraProof = (parsedPlan?.proof ?? []).filter(line => { const key = statementKey(line); return key !== "" && !signedKeys.includes(key); });
495
+ const doneWhenSheet = joinHtml([
496
+ scope.acceptance.length === 0 ? html `<p>You decide when you review the result.</p>` : html `<ul class="plan-sheet-done">${scope.acceptance.map(one => html `<li><span>${one.statement}</span><span class="plan-sheet-how">${one.evidence.map(kind => EVIDENCE_SHEET[kind] ?? kind).join(" · ")}</span></li>`)}</ul>`,
497
+ extraProof.length === 0 ? "" : html `<ul class="plan-sheet-done plan-sheet-proof">${extraProof.map(line => html `<li><span>${line}</span><span class="plan-sheet-how">From the plan</span></li>`)}</ul>`,
498
+ ]);
499
+ const reportNote = input.deliverable === "report" ? html `<p class="approval-note">Read-only: it reports back and changes nothing in the repository.</p>` : "";
500
+ const notAllowingWarnings = allowing.filter(one => !warnings.includes(one));
501
+ const sheet = joinHtml([
502
+ html `<article class="plan-sheet" data-plan-sheet aria-label="${input.deliverable === "report" ? "The investigation" : "The plan"}">`,
503
+ sentence === null ? "" : html `<p class="approval-revision">${sentence}</p>`,
504
+ input.coordinator === null ? "" : html `<p class="approval-note">An agent filed this: <span class="mono">${input.coordinator.label}</span>${input.coordinator.filedAgo === null ? "" : html `, ${input.coordinator.filedAgo}`}. Nothing runs until you approve, and approving runs its request.</p>`,
505
+ reportNote,
506
+ originNote(input.origin) === null ? "" : html `<p class="approval-note" data-plan-origin>${originNote(input.origin)}</p>`,
507
+ html `<p class="${sheetLead.length > 180 ? "plan-sheet-lead plan-sheet-lead-long" : "plan-sheet-lead"}">${sheetLead}</p>`,
508
+ approachRest === "" ? "" : html `<p class="plan-sheet-text">${approachRest}</p>`,
509
+ // An older plan that doesn't parse keeps its text, as written.
510
+ plan !== null && !plan.ok ? html `<pre class="recap plan-doc">${input.planDocument ?? ""}</pre>` : "",
511
+ goalShown.trim() === sheetLead ? "" : part("Goal", html `<p class="approval-goal">${goalShown}</p>`),
512
+ repairText === null ? "" : part("Full goal", html `<p>${scope.goal}</p>`),
513
+ input.revision === null ? "" : part("Asked for changes", revisionBody),
514
+ releaseBody === "" ? "" : part("Release", releaseBody, html ` data-plan-release`),
515
+ milestones.length === 0 ? "" : part("Steps", html `<ol class="plan-sheet-steps">${milestones.map(one => html `<li><span>${one}</span></li>`)}</ol>`),
516
+ part("Done when", doneWhenSheet),
517
+ html `<div class="plan-sheet-pair">`,
518
+ part("Will change", scope.touches.length > 0 ? approvalPathsHtml(scope.touches) : html `<p>Any file in the project.</p>`),
519
+ part("Won’t touch", html `<p>${scope.outOfScope === null ? "Nothing is ruled out." : scope.outOfScope}</p>`),
520
+ html `</div>`,
521
+ part("Worth knowing", joinHtml([
522
+ amendment,
523
+ ...warnings.map(one => html `<p class="plan-sheet-warn" data-plan-warning>${one.charAt(0).toUpperCase()}${one.slice(1)}.</p>`),
524
+ (parsedPlan?.risks.length ?? 0) === 0 ? "" : html `<ul class="plan-sheet-list">${parsedPlan.risks.map(one => html `<li>${one}</li>`)}</ul>`,
525
+ (parsedPlan?.dependencies.length ?? 0) === 0 ? "" : html `<p class="plan-sheet-sub">Depends on</p><ul class="plan-sheet-list">${parsedPlan.dependencies.map(one => html `<li>${one}</li>`)}</ul>`,
526
+ ])),
527
+ part("The agent", joinHtml([
528
+ projection === null ? "" : sizeLineHtml(projection),
529
+ who.length === 0 ? "" : html `<p class="approval-who">${who.join(" · ")}</p>`,
530
+ routeEvidenceHtml(route, "approval-evidence"),
531
+ agentsDetail,
532
+ limits,
533
+ notAllowingWarnings.length === 0 ? "" : html `<p class="approval-allowing" data-plan-allowing>You’re allowing: ${notAllowingWarnings.join(" · ")}</p>`,
534
+ yoursToCheck.length === 0 ? "" : html `<p class="approval-you-check" data-plan-you-check>You’ll check: ${yoursToCheck.join("; ")}</p>`,
535
+ chain.length === 0 ? "" : html `<p>${chain[0]}</p><ol class="plan-sheet-list" data-plan-fallback>${chain.slice(1).map(one => html `<li>${one.replace(/^\d+\. /, "")}</li>`)}</ol>`,
536
+ ])),
537
+ part("Plan record", contractDetail),
538
+ scope.candidate ? part("Saved commit", html `<p><code class="approval-commit">${scope.candidate}</code></p>`) : "",
539
+ part("Plan version", html `<p class="meta">Approving agrees to exactly this plan. <span class="seal mono" title="${planVersionWords(scope.digest)}">${planVersionWords(scope.digest)}</span></p>`),
540
+ html `</article>`,
541
+ ]);
542
+ return { lead, rows, editor, editorForm, details, submit, label: rebuild !== null ? "Approve and retry" : "Approve", after, allowing, passwordNote, sheet, sheetLead };
543
+ }
544
+ /** The plan sheet: what an approval signs, read top to bottom (approvalSheetParts' own parts, re-ordered). */
545
+ export function planSheetHtml(input) {
546
+ return approvalSheetParts(input).sheet;
464
547
  }
548
+ /** Evidence kinds on the plan sheet: how each requirement is proven, in plain words. */
549
+ export const EVIDENCE_SHEET = { check: "Runs automatically", screenshot: "Shown in a screenshot", "changed-path": "Seen in the changed files", "manual-review": "You review it" };
465
550
  /** Earlier versions of a task still in flight, in plain words: queued
466
551
  * while they only wait, running once one has started; null when none. */
467
552
  export function earlierVersionsWords(active, running) {
@@ -835,6 +920,8 @@ export function approveRefusalWords(reason) {
835
920
  return "not approved: only a person can approve this. Approve it on the task page or from your paired chat";
836
921
  if (reason === "archived")
837
922
  return "not approved: this task is archived. Restore it first";
923
+ if (reason === "cancelled")
924
+ return "not approved: this task was cancelled";
838
925
  if (reason === "unrouted")
839
926
  return "not approved: this plan was made before agents were chosen per task, so its old approval no longer stands. Edit and save the plan to choose today’s agents, then approve it";
840
927
  if (reason === "profile-unresolved")
@@ -1216,7 +1303,7 @@ export function planRevisionLedgerHtml(ledger, taskId, csrf) {
1216
1303
  html `</details>`,
1217
1304
  ]);
1218
1305
  return joinHtml([
1219
- html `<div class="card plan-revision"><span class="eyebrow">plan updated</span><h2>Using plan revision ${current.revision}</h2>`,
1306
+ html `<div class="card plan-revision" id="plan-revisions"><span class="eyebrow">plan updated</span><h2>Using plan revision ${current.revision}</h2>`,
1220
1307
  html `<p class="meta">${current.reason}${current.evidenceLink === null ? "" : ` · evidence: ${current.evidenceLink}`}</p>`,
1221
1308
  pending,
1222
1309
  history,
@@ -1340,10 +1427,8 @@ export function taskBodyParts(data) {
1340
1427
  if (data.peekable !== true)
1341
1428
  return "";
1342
1429
  const peek = html `<p class="meta">What is changing right now</p><div id="run-peek"><p class="meta">Watching… the first look lands within 15 seconds</p></div><p class="meta" id="run-peek-stamp"></p>`;
1343
- const transcript = liveRun.provider !== "claude"
1344
- ? html `<p class="meta">The live transcript needs the claude harness for now — this build runs on ${liveRun.provider}</p>`
1345
- : html `<p class="meta">What the agent is saying</p><pre id="live-transcript" class="mono" style="max-height:18rem;overflow:auto;white-space:pre-wrap"></pre><p class="meta" id="live-transcript-state"></p>`;
1346
- return html `<div class="card attempt-live" data-live-run="${liveRun.id}">${head}${peek}${transcript}${door}</div>`;
1430
+ // What the agent is saying is the work panel's (Watch the work): the same records, read by the workspace.
1431
+ return html `<div class="card attempt-live" data-live-run="${liveRun.id}">${head}${peek}${door}</div>`;
1347
1432
  })();
1348
1433
  // One truthful answer to the first question on a queued task: "will this
1349
1434
  // run?" The badge alone cannot distinguish approval, dependency,
@@ -1558,6 +1643,8 @@ export function taskBodyParts(data) {
1558
1643
  // its draft; the full scope form below stays as it was.
1559
1644
  const inlineScopeDraft = data.scopeDraft?.has("requirement-new") === true;
1560
1645
  const scopeDraft = inlineScopeDraft ? undefined : data.scopeDraft;
1646
+ // A release's terms in the scope card (the page without script); the plan sheet says them whole (approvalSheetParts).
1647
+ const releaseTermsHtml = scope == null || (scope.releaseTerms == null && scope.releaseCheck == null) ? "" : html `<p><strong>Release</strong></p>${scope.releaseTerms == null ? "" : html `<p class="recap">${releaseTermsSentence(scope.releaseTerms)}</p>`}${scope.releaseCheck == null ? "" : html `<p class="recap">${releaseCheckSentence(scope.releaseCheck)}</p>`}${data.releaseCiRunning === true ? html `<p class="meta">${RELEASE_CI_RUNNING_WORDS}</p>` : ""}${scope.releaseTerms?.policy.carry === true ? html `<p><strong>What asks again</strong></p><ul>${joinHtml(releaseAsksAgainWords(scope.releaseTerms).map(line => html `<li>${line}</li>`), "")}</ul>` : ""}`;
1561
1648
  const scopeCard = scope === null
1562
1649
  ? html `<p class="meta">No plan yet. Nothing builds until one is approved.</p>`
1563
1650
  : joinHtml([
@@ -1567,7 +1654,7 @@ export function taskBodyParts(data) {
1567
1654
  scope.touches.length === 0 ? "" : html `<p class="scope-paths"><strong>Files</strong> ${scope.touches.join(", ")}</p>`,
1568
1655
  html `<p><strong>Quality</strong> ${qualityModeTitle(scope.qualityMode ?? "default")}</p>`,
1569
1656
  acceptanceCeremonyHtml(scope.acceptance),
1570
- scope.releaseTerms == null ? "" : html `<p><strong>Release</strong></p><p class="recap">${releaseTermsSentence(scope.releaseTerms)}</p>${data.releaseCiRunning === true ? html `<p class="meta">${RELEASE_CI_RUNNING_WORDS}</p>` : ""}${scope.releaseTerms.policy.carry ? html `<details><summary>What asks again</summary><ul>${joinHtml(releaseAsksAgainWords(scope.releaseTerms).map(line => html `<li>${line}</li>`), "")}</ul></details>` : ""}`,
1657
+ releaseTermsHtml,
1571
1658
  approval.approved && data.autoRelease != null
1572
1659
  ? html `<p class="meta scope-seal">${autoReleaseWords(data.autoRelease)}</p>${planVersionDetailsHtml(scope.digest)}`
1573
1660
  : approval.approved && data.release?.kind === "carried"
@@ -1629,6 +1716,14 @@ export function taskBodyParts(data) {
1629
1716
  // card does not repeat it (one #contract-amendment on the page).
1630
1717
  const sheetStatesContract = scope !== null && !approval.approved && data.plan === "drafted" && data.dispatch?.action !== "repair-dependency" &&
1631
1718
  !(data.revision != null && "problem" in data.revision) && scope.profileState !== "unresolved" && consentDoorOf(scope, data.route).open;
1719
+ // The plan document's own editor (#plan-edit): the plan card holds it, and the task page's Plan row.
1720
+ const planEditorHtml = data.planDocument === null || data.csrf === "" || data.planSha == null || approval.approved
1721
+ ? ""
1722
+ : html `<details class="plan-editor" id="plan-edit"><summary>Edit plan</summary>${postForm(`${taskHref(task.id)}/plan-edit`, joinHtml([
1723
+ html `<label>Plan details <span class="meta">Keep the five headings. Approval locks this version for the build.</span>`,
1724
+ html `<textarea name="plan-document" rows="14">${data.planDocument}</textarea></label>`,
1725
+ html `<button type="submit">Save plan</button>`,
1726
+ ]), { hidden: { "saw-plan": data.planSha } })}</details>`;
1632
1727
  const planCard = data.planDocument === null
1633
1728
  ? data.plan === "requested"
1634
1729
  // A request the planner cannot be given waits on a person; the status card above already says what to shorten.
@@ -1645,13 +1740,7 @@ export function taskBodyParts(data) {
1645
1740
  html `</div><span class="plan-lock">${planStanding}</span>${approval.approved ? html `</summary><div class="planner-plan-body">` : html `</div>`}`,
1646
1741
  executionPlanHtml(displayedPlanDocument ?? data.planDocument),
1647
1742
  approval.approved || sheetStatesContract ? "" : planContractHtml(data.planContract ?? null, "full"),
1648
- data.csrf === "" || data.planSha == null || approval.approved
1649
- ? ""
1650
- : html `<details class="plan-editor" id="plan-edit"><summary>Edit plan</summary>${postForm(`${taskHref(task.id)}/plan-edit`, joinHtml([
1651
- html `<label>Plan details <span class="meta">Keep the five headings. Approval locks this version for the build.</span>`,
1652
- html `<textarea name="plan-document" rows="14">${data.planDocument}</textarea></label>`,
1653
- html `<button type="submit">Save plan</button>`,
1654
- ]), { hidden: { "saw-plan": data.planSha } })}</details>`,
1743
+ planEditorHtml,
1655
1744
  approval.approved ? html `</div></details>` : html `</section>`,
1656
1745
  ]);
1657
1746
  const progressCard = milestoneProgressHtml(data.milestoneProgress);
@@ -1687,7 +1776,8 @@ export function taskBodyParts(data) {
1687
1776
  (data.assignment?.primaryAction?.code === "approve-scope" && data.assignment.primaryAction.target.taskId === task.id);
1688
1777
  // The sheet's terms, when this person can approve them here: the task page's kit approval box (its Plan tab shows
1689
1778
  // the rows) and the legacy sheet both read these.
1690
- const sheetInput = scope === null || approval.approved || data.plan === "requested" || (data.revision != null && "problem" in data.revision) ||
1779
+ // A cancelled task asks for no approval: nothing would start, so its page offers no Approve (reopening asks again).
1780
+ const sheetInput = scope === null || approval.approved || task.state === "cancelled" || data.plan === "requested" || (data.revision != null && "problem" in data.revision) ||
1691
1781
  scope.profileState === "unresolved" || !consentDoor.open ? null : {
1692
1782
  surface: "task",
1693
1783
  action: `${taskHref(task.id)}/approve`,
@@ -1707,6 +1797,7 @@ export function taskBodyParts(data) {
1707
1797
  coordinator: data.coordinator ?? null,
1708
1798
  origin: data.origin ?? null,
1709
1799
  deliverable: data.deliverable ?? "branch",
1800
+ releaseCiRunning: data.releaseCiRunning === true,
1710
1801
  rebuild: data.assignment?.need != null && "key" in data.assignment.need && data.assignment.need.key === "replan" ? data.assignment.need.build : null,
1711
1802
  editHref: data.csrf !== "" && data.planSha != null && data.planDocument !== null ? "#plan-edit" : "#scope",
1712
1803
  notNowHref: "/tasks",
@@ -1716,7 +1807,7 @@ export function taskBodyParts(data) {
1716
1807
  : { action: `${taskHref(task.id)}/scope`, draft: inlineScopeDraft ? data.scopeDraft ?? null : null, problem: inlineScopeDraft ? data.problem : null,
1717
1808
  open: data.editPlan === true, stepsHref: data.planSha != null && data.planDocument !== null ? "#plan-edit" : null },
1718
1809
  };
1719
- const approveForm = scope === null || approval.approved || data.plan === "requested"
1810
+ const approveForm = scope === null || approval.approved || task.state === "cancelled" || data.plan === "requested"
1720
1811
  ? ""
1721
1812
  : data.revision !== null && data.revision !== undefined && "problem" in data.revision
1722
1813
  ? html `<div class="card approve-form" id="approve"><p><strong>This task is waiting on you: approval is blocked.</strong></p><p class="meta">${data.revision.problem}. A revision can be approved only when its saved notes can be read.</p></div>`
@@ -2133,30 +2224,10 @@ export function taskBodyParts(data) {
2133
2224
  : html `<details class="task-secondary-approval"><summary>Updated approval terms</summary>${approveForm}</details>`;
2134
2225
  const optionsHtml = html `${identity}${dispatchStatus}${dependencyChoiceNeeded ? "" : actsBar}`;
2135
2226
  const optionsOpen = data.assignment?.primaryAction?.code === "unhold" || data.assignment?.primaryAction?.code === "retry-task";
2136
- const body = joinHtml([
2137
- // The title is bare; the receipt or shared task status leads once.
2138
- html `<div class="task-title-row"><h1 class="task-main-title">${data.rootTitle ?? task.title}</h1>${data.csrf === "" ? "" : taskViewSwitch(data.rootId ?? task.id, "overview")}</div>`,
2139
- data.versionLabel == null ? "" : html `<p class="meta">${data.versionLabel} · <a href="${taskHref(data.rootId ?? task.id)}">Current work</a></p>`,
2140
- data.archived == null ? "" : html `<div class="task-archived" data-archived><p>${archivedWords(data.archived)} · ${whenUtc(data.archived.at)}</p>${data.canPutAway === true && data.csrf !== ""
2141
- ? postForm(`${taskHref(task.id)}/restore`, html `<button type="submit" class="secondary">Restore</button>`, { hidden: { return: taskHref(task.id) }, attrs: { class: "inline" } }) : ""}</div>`,
2142
- (data.history ?? ""),
2143
- // A put-away task's banner is its status: no Needs you card, no act, no approval form (each would be refused).
2144
- data.archived == null ? statusHtml : "",
2145
- controlHtml,
2146
- previousResult === null ? "" : html `<details class="task-previous-result"><summary>${previousResult.title}</summary>${previousResult.html}</details>`,
2147
- progressCard,
2148
- revisionLedgerCard,
2149
- planCard,
2150
- mirrorCard,
2151
- problemHtml,
2152
- needsScopeCard,
2153
- data.archived == null ? approvalHtml : "",
2154
- html `<details class="task-status-details" id="task-diagnostics"${optionsOpen ? html ` open` : ""}><summary>Task options</summary>${optionsHtml}</details>`,
2155
- // Evidence-first (M5.5): what needs you, then what happened — decisions
2156
- // and incidents above the attempt ledger and spend, the mechanics
2157
- // (scope, holds, acts) after. Only trustworthy facts moved up. The rail
2158
- // (slice 1c) rides beside the main column on wide screens and above it
2159
- // on narrow ones.
2227
+ // The Plan section after its scope card: the release, the recipe and audit links, the agents, the edit form.
2228
+ const scopeRest = [releaseCardHtml(task.id, data.release ?? null, data.canStopRelease === true), html `<p class="meta">${data.repo !== null && data.scope !== null ? html `<a href="/recipes/from-task?task=${encodeURIComponent(task.id)}">Reuse this plan as a recipe →</a> · ` : ""}<a href="${taskHref(data.rootId ?? task.id)}/evidence">Audit record →</a></p>`, agentsCardHtml(task.id, data.route, data.canEditRoute === true), revisionInApproval ? "" : revisionCard, data.completion != null ? "" : repairChainHtml(data.repairChain ?? null), scopeForm];
2229
+ // The page's sections, built once and in order (each `section` also records its part for the rebuilt page).
2230
+ const layoutTail = joinHtml([
2160
2231
  html `<div class="task-layout"><div class="task-main">`,
2161
2232
  attemptPanel,
2162
2233
  data.coordinatorProposals == null || data.coordinatorProposals.rows.length === 0
@@ -2182,12 +2253,41 @@ export function taskBodyParts(data) {
2182
2253
  section("plan",
2183
2254
  // The recipe road rides with the scope it reuses (UI polish
2184
2255
  // 2026-09-13), off the title-to-action path.
2185
- joinHtml([html `<h2>Plan</h2>`, scopeCard, releaseCardHtml(task.id, data.release ?? null, data.canStopRelease === true), html `<p class="meta">${data.repo !== null && data.scope !== null ? html `<a href="/recipes/from-task?task=${encodeURIComponent(task.id)}">Reuse this plan as a recipe →</a> · ` : ""}<a href="${taskHref(data.rootId ?? task.id)}/evidence">Audit record →</a></p>`, agentsCardHtml(task.id, data.route, data.canEditRoute === true), revisionInApproval ? "" : revisionCard, data.completion != null ? "" : repairChainHtml(data.repairChain ?? null), scopeForm], "\n"), scopeDraft !== undefined || (data.plan !== "requested" && approveForm === "" && !(scope === null && canPlan)), undefined, "scope"),
2256
+ joinHtml([html `<h2>Plan</h2>`, scopeCard, ...scopeRest], "\n"), scopeDraft !== undefined || (data.plan !== "requested" && approveForm === "" && !(scope === null && canPlan)), undefined, "scope"),
2186
2257
  dependencyChoiceNeeded ? "" : section("waits for", waitsForCard, (data.waitsFor ?? []).length > 0, (data.waitsFor ?? []).length),
2187
2258
  section("holds", holds, true, data.holds.length),
2188
2259
  cancelAct,
2189
2260
  html `</div><aside class="task-rail">${rail}</aside></div>`,
2190
2261
  ], "\n");
2262
+ // `rows`: the task's step rows, every one open (the page without script, docs/plans/task-page-steps.md): the status,
2263
+ // the approval, the plan, the build's progress and the previous result move into their rows; everything else stays.
2264
+ const nativeBody = (rows) => joinHtml([
2265
+ // The title is bare; the receipt or shared task status leads once.
2266
+ html `<div class="task-title-row"><h1 class="task-main-title">${data.rootTitle ?? task.title}</h1>${data.csrf === "" ? "" : taskViewSwitch(data.rootId ?? task.id, "overview")}</div>`,
2267
+ data.versionLabel == null ? "" : html `<p class="meta">${data.versionLabel} · <a href="${taskHref(data.rootId ?? task.id)}">Current work</a></p>`,
2268
+ data.archived == null ? "" : html `<div class="task-archived" data-archived><p>${archivedWords(data.archived)} · ${whenUtc(data.archived.at)}</p>${data.canPutAway === true && data.csrf !== ""
2269
+ ? postForm(`${taskHref(task.id)}/restore`, html `<button type="submit" class="secondary">Restore</button>`, { hidden: { return: taskHref(task.id) }, attrs: { class: "inline" } }) : ""}</div>`,
2270
+ (data.history ?? ""),
2271
+ rows ?? "",
2272
+ // A put-away task's banner is its status: no Needs you card, no act, no approval form (each would be refused).
2273
+ rows === null && data.archived == null ? statusHtml : "",
2274
+ rows === null ? controlHtml : "",
2275
+ rows !== null || previousResult === null ? "" : html `<details class="task-previous-result"><summary>${previousResult.title}</summary>${previousResult.html}</details>`,
2276
+ rows === null ? progressCard : "",
2277
+ rows === null ? revisionLedgerCard : "",
2278
+ rows === null ? planCard : "",
2279
+ mirrorCard,
2280
+ problemHtml,
2281
+ needsScopeCard,
2282
+ rows === null && data.archived == null ? approvalHtml : "",
2283
+ html `<details class="task-status-details" id="task-diagnostics"${optionsOpen ? html ` open` : ""}><summary>Task options</summary>${optionsHtml}</details>`,
2284
+ // Evidence-first (M5.5): what needs you, then what happened — decisions
2285
+ // and incidents above the attempt ledger and spend, the mechanics
2286
+ // (scope, holds, acts) after. Only trustworthy facts moved up. The rail
2287
+ // (slice 1c) rides beside the main column on wide screens and above it
2288
+ // on narrow ones.
2289
+ layoutTail,
2290
+ ], "\n");
2191
2291
  // Console v2: one thread in time order (the plan, the agent's notes and
2192
2292
  // results, its questions, the person's replies) and the metadata grouped
2193
2293
  // for a Details panel. Every form below keeps its own route and fields.
@@ -2244,13 +2344,19 @@ export function taskBodyParts(data) {
2244
2344
  }
2245
2345
  // What the agent is working through now, and how its plan changed.
2246
2346
  const liveAt = liveRun?.startedAt ?? task.updatedAt;
2347
+ // The plan itself is the page's Plan row: the thread keeps one line saying it was written.
2247
2348
  if (planCard !== "")
2248
2349
  thread.push({ key: "plan", at: familyRuns.filter(run => run.role === "planner").map(run => run.finishedAt ?? run.startedAt).sort().at(-1) ?? task.updatedAt,
2249
- kind: "plan", who: "agent", author: "", title: "The plan", text: null, link: null, html: htmlString(planCard), more: null });
2350
+ kind: "plan", who: "agent", author: "", title: data.planDocument !== null ? "Plan written" : data.revision != null && !("problem" in data.revision) ? "Updating the plan" : "Planning", text: null, link: null, html: "", more: null });
2250
2351
  if (!isEmptyHtml(progressCard))
2251
2352
  thread.push({ key: "progress", at: liveAt, kind: "progress", who: "agent", author: "", title: "Progress", text: null, link: null, html: htmlString(progressCard), more: null });
2252
- if (!isEmptyHtml(revisionLedgerCard))
2253
- thread.push({ key: "plan-revisions", at: liveAt, kind: "progress", who: "agent", author: "", title: "Plan changes", text: null, link: null, html: htmlString(revisionLedgerCard), more: null });
2353
+ // A plan change is decided on the page (the Build row holds its card): the thread, which the work panel shows, keeps
2354
+ // one line and a link there. Nothing in the thread takes a decision.
2355
+ if (!isEmptyHtml(revisionLedgerCard)) {
2356
+ const waiting = data.planRevisions?.pending ?? null;
2357
+ thread.push({ key: "plan-revisions", at: liveAt, kind: "progress", who: "agent", author: "", title: waiting === null ? "Plan changes" : "Recommends a plan change",
2358
+ text: waiting === null ? null : oneLineOf(waiting.reason, 280), link: { label: waiting === null ? "See the plan changes" : "Decide on the task page", href: "#plan-revisions" }, html: "", more: null });
2359
+ }
2254
2360
  // The change a person asked for: the revision's own brief.
2255
2361
  if (data.revision != null && !("problem" in data.revision)) {
2256
2362
  const feedback = data.revision;
@@ -2274,10 +2380,12 @@ export function taskBodyParts(data) {
2274
2380
  text: [chosen, decision.note].filter((one) => one !== null && one !== "").join(" — ") || null, link: null, html: "", more: null });
2275
2381
  }
2276
2382
  }
2277
- // Open questions keep their answer card (and its #task-questions anchor).
2383
+ // Open questions are answered on the page (their cards sit in the step that waits, at #task-questions): the thread
2384
+ // says what was asked and links there.
2278
2385
  if (decisionRail !== "")
2279
2386
  thread.push({ key: "questions", at: openDecisions.map(one => one.createdAt).sort()[0] ?? task.updatedAt, kind: "question", who: "agent", author: "",
2280
- title: openDecisions.length === 1 ? "Asked you a question" : `Asked you ${openDecisions.length} questions`, text: null, link: null, html: htmlString(decisionRail), more: null });
2387
+ title: openDecisions.length === 1 ? "Asked you a question" : `Asked you ${openDecisions.length} questions`, text: openDecisions.map(one => oneLineOf(one.question, 280)).join("\n"),
2388
+ link: { label: openDecisions.length === 1 ? "Answer it on the task page" : "Answer them on the task page", href: "#task-questions" }, html: "", more: null });
2281
2389
  for (const note of data.steering ?? []) {
2282
2390
  thread.push({ key: `steer-${note.id}`, at: note.createdAt, kind: "reply", who: "person", author: note.author, title: "Note to the agent", text: note.note, link: null, html: "", more: null });
2283
2391
  }
@@ -2403,6 +2511,29 @@ export function taskBodyParts(data) {
2403
2511
  { group: "share", label: "Copy link", kind: "copy" },
2404
2512
  ...(cancelForm !== "" ? [{ group: "danger", label: "Cancel task", kind: "goto", tab: "plan", target: "cancel-task" }] : []),
2405
2513
  ];
2514
+ // The Plan row (docs/plans/task-page-steps.md): the plan sheet of the terms on file, built from the same parts as
2515
+ // the approval's (approvalSheetParts), and the plan's own editor. While the approval waits here the sheet is the
2516
+ // approval's own (`approve.planHtml`), so this row carries only the editor: the plan is shown once.
2517
+ const plannerRun = familyRuns.filter(run => run.role === "planner" && run.outcome !== null).sort((a, b) => b.id - a.id)[0];
2518
+ const sheetShown = scope === null ? null : sheetInput ?? {
2519
+ surface: "task", action: "", csrf: "", nonce: "", digest: scope.digest, returnTo: null, scope,
2520
+ planDocument: data.planDocument ?? null, planContract: data.planContract ?? null,
2521
+ revision: data.revision == null || "problem" in data.revision ? null : data.revision,
2522
+ revisionSourceHref: data.revision == null || "problem" in data.revision ? "" : `/r/${data.revision.sourceRun}`,
2523
+ repairChain: data.repairChain ?? null, route: data.route, coordinator: data.coordinator ?? null, origin: data.origin ?? null,
2524
+ deliverable: data.deliverable ?? "branch", releaseCiRunning: data.releaseCiRunning === true, editHref: "#scope", notNowHref: "/tasks", sticky: false,
2525
+ earlier: { active: data.assignment?.earlierActive ?? 0, running: data.assignment?.earlierRunning ?? 0 }, edit: null,
2526
+ };
2527
+ const sheetParts = sheetShown === null ? null : approvalSheetParts(sheetShown);
2528
+ const approvalWaitsHere = data.archived == null && sheetInput !== null && !dependencyChoiceNeeded && data.csrf !== "";
2529
+ // A revision's plan being updated with the notes: the planner's card says so above the plan it starts from.
2530
+ const planUpdating = data.planDocument === null && data.plan === "requested" && data.revision != null && !("problem" in data.revision) && planCard !== "";
2531
+ const planView = scope === null
2532
+ ? (planCard === "" ? null : { lead: null, by: null, at: null, html: htmlString(planCard), planning: data.plan === "requested", updating: planUpdating, key: "card" })
2533
+ : { lead: oneLineOf(sheetParts.sheetLead, 160), by: plannerRun === undefined ? null : agentNameWords(plannerRun.provider, plannerRun.model),
2534
+ at: plannerRun?.finishedAt ?? scope.proposedAt ?? null, planning: planUpdating, updating: planUpdating,
2535
+ key: `${planUpdating ? "updating:" : ""}${approvalWaitsHere ? "editor" : `sheet:${scope.digest}`}`,
2536
+ html: htmlString(joinHtml([planUpdating ? planCard : "", approvalWaitsHere ? "" : sheetParts.sheet, planEditorHtml])) };
2406
2537
  const view = {
2407
2538
  kind: "task",
2408
2539
  id: task.id,
@@ -2423,9 +2554,24 @@ export function taskBodyParts(data) {
2423
2554
  failure: failedCard && data.failure != null ? { line: data.failure.line, evidence: data.failure.evidence, suggestion: data.failure.suggestion, link: data.failure.link } : null,
2424
2555
  retry,
2425
2556
  runChecks,
2426
- progress,
2557
+ progress: progress === null ? null : { ...progress, steps: (data.milestoneProgress ?? []).map(one => ({ title: one.description, state: one.state })) },
2558
+ plan: planView,
2427
2559
  // One quiet line under the step: what the agent did last, and when.
2428
2560
  activity: liveRun === undefined ? null : data.activity ?? null,
2561
+ // The work panel's run: the live build, else this version's latest build or report. Its records are read through the
2562
+ // run page's own guarded window; a page asking for a password names no window at all.
2563
+ work: (() => {
2564
+ const recorded = (run) => run.role === "builder" || run.role === "scout";
2565
+ // Running: unfinished under the task's current claim (liveRun is the one run proved live; a builder whose repair
2566
+ // child is the live run shares that claim and is still this attempt).
2567
+ const running = (run) => liveRun !== undefined && run.outcome === null && run.finishedAt === null && run.leaseId === liveRun.leaseId;
2568
+ const builds = data.runs.filter(recorded).sort((a, b) => b.id - a.id);
2569
+ const run = builds.find(running) ?? builds[0];
2570
+ if (run === undefined)
2571
+ return null;
2572
+ const live = running(run);
2573
+ return { run: run.id, live, agent: agentNameWords(run.provider, run.model), transcript: degraded ? null : `/r/${run.id}?fragment=transcript&shape=records` };
2574
+ })(),
2429
2575
  stop,
2430
2576
  pause,
2431
2577
  steering: data.csrf !== "" ? data.steerComposer ?? null : null,
@@ -2442,22 +2588,30 @@ export function taskBodyParts(data) {
2442
2588
  recheckStopped: data.csrf !== "" && data.assignment?.primaryAction?.code === "recheck-stopped" && data.assignment.primaryAction.target.runId !== null
2443
2589
  ? { action: `${taskHref(data.assignment.rootId)}/recheck-stopped`, run: data.assignment.primaryAction.target.runId } : null,
2444
2590
  rebuild,
2445
- // The plan, progress and plan changes are thread entries now; the rest still needs a person here.
2591
+ // What needs a person on the page itself. Open questions and a plan change are answered here, in their step's row:
2592
+ // the thread (the work panel) only links to them.
2446
2593
  lead: [
2447
2594
  { key: "history", html: data.history === undefined ? "" : htmlString(data.history) }, { key: "control", html: htmlString(stop === null ? controlHtml : taskControlDetailsHtml(data.control, task.id, data.csrf, "task", false, true)) }, { key: "problem", html: fragmentString(problemHtml) },
2595
+ { key: "questions", html: decisionRail === "" ? "" : htmlString(decisionRail) }, { key: "plan-revisions", html: isEmptyHtml(revisionLedgerCard) ? "" : htmlString(revisionLedgerCard) },
2448
2596
  { key: "route-change", html: data.routeChange == null ? "" : htmlString(html `<div class="card"><p>${data.routeChange}</p></div>`) },
2449
2597
  { key: "fast-grown", html: data.fastGrown == null || data.csrf === "" ? "" : htmlString(html `<div class="card"><p>${data.fastGrown}</p>${postForm(`${taskHref(task.id)}/keep-grown`, btn("Approve it", { variant: "primary", type: "submit" }))}</div>`) },
2450
2598
  { key: "pull-request", html: fragmentString(pullRequestCard) },
2451
2599
  { key: "needs-scope", html: fragmentString(needsScopeCard) },
2452
2600
  { key: "mirror", html: fragmentString(mirrorCard) }, { key: "attempt", html: fragmentString(attemptPanel) },
2453
2601
  ].filter(one => one.html !== ""),
2454
- // Open questions are answered in the thread.
2602
+ // Open questions are a lead block (key "questions"), so they sit in the step that waits.
2455
2603
  questions: "",
2456
2604
  facts,
2457
2605
  sections: [
2458
2606
  ...(previousResult === null || thread.some(one => one.more !== null) ? [] : [{ id: "result", title: previousResult.title, html: htmlString(previousResult.html), open: false, count: null }]),
2459
2607
  // Questions are in the thread; the scope and the ledgers fold here until asked.
2460
- ...sectionParts.filter(one => !MANAGE.has(one.id) && one.id !== "decisions").map(one => one.id === "scope" && scopeDraft === undefined && (finished || approval.approved) ? { ...one, open: false } : one),
2608
+ // The Plan and Approval rows hold the plan sheet (a release's terms included), so this page's Plan section keeps
2609
+ // only what the sheet leaves out (the links, the agents, the edit form): the plan is never printed a second time.
2610
+ ...sectionParts.filter(one => !MANAGE.has(one.id) && one.id !== "decisions").map(one => one.id !== "scope" ? one : {
2611
+ ...one,
2612
+ ...(scope === null ? {} : { html: htmlString(joinHtml(scopeRest, "\n")) }),
2613
+ ...(scopeDraft === undefined && (finished || approval.approved) ? { open: false } : {}),
2614
+ }),
2461
2615
  ],
2462
2616
  manage: [
2463
2617
  ...sectionParts.filter(one => MANAGE.has(one.id)),
@@ -2483,7 +2637,22 @@ export function taskBodyParts(data) {
2483
2637
  return { href: `/settings/flows?repo=${encodeURIComponent(data.repo)}&starter=${starter.id}#starter-${starter.id}`, starter: starter.name };
2484
2638
  })(),
2485
2639
  };
2486
- return { html: body, view };
2640
+ // Without script the steps are the page too: every row open, in order, each holding its own part.
2641
+ const rowFacts = data.archived != null ? null : stepRowsOf(view);
2642
+ const rowsHtml = rowFacts === null ? null : (() => {
2643
+ const here = rowFacts.find(row => row.state === "current" || row.state === "stuck") ?? rowFacts.find(row => row.open) ?? rowFacts.at(-1);
2644
+ const bodyOf = (key) => joinHtml([
2645
+ key === here.key ? statusHtml : "",
2646
+ key === "plan" ? planCard : "",
2647
+ // The shared approval sheet, its Plan details open here: nothing the yes agrees to is folded on this page.
2648
+ key === "approve" ? (approvalHtml !== "" ? approvalHtml : view.approved == null ? "" : html `<p class="meta">${view.approved.automatic ? `Approved automatically by ${view.approved.rule ?? "a rule"}` : `Approved by ${view.approved.by}`} · ${whenTime(view.approved.at)}</p>`) : "",
2649
+ key === "build" ? joinHtml([controlHtml, progressCard, revisionLedgerCard]) : "",
2650
+ key === "ready" && previousResult !== null ? html `<section class="task-previous-result" aria-label="${previousResult.title}">${previousResult.html}</section>` : "",
2651
+ ]);
2652
+ return stepRows(rowFacts.map(row => ({ key: row.key, label: row.label, mark: row.mark, sentence: row.sentence, needsYou: row.needsYou, current: row.state === "current" || row.state === "stuck",
2653
+ at: row.at, when: row.at === null ? "" : whenText(row.at), body: bodyOf(row.key), open: true })));
2654
+ })();
2655
+ return { html: nativeBody(rowsHtml), view };
2487
2656
  }
2488
2657
  export function taskBody(data) {
2489
2658
  return taskBodyParts(data).html;
@@ -2505,7 +2674,6 @@ export function taskPage(chrome, data) {
2505
2674
  const liveRun = liveRunId === null ? undefined : data.runs.find(one => one.id === liveRunId);
2506
2675
  const script = (liveRun === undefined ? "" : regionScript("check-progress", "check", `/r/${liveRun.id}`)) +
2507
2676
  (liveRun !== undefined && data.peekable === true ? regionScript("run-peek", "peek", `/r/${liveRun.id}`) : "") +
2508
- (liveRun !== undefined && data.peekable === true && liveRun.provider === "claude" ? transcriptScript(`/r/${liveRun.id}`) : "") +
2509
2677
  (data.csrf !== "" && data.decisions.some(one => one.state === "open" || one.state === "expired") ? decisionAnswerScript() : "");
2510
2678
  return screen(`task \u00b7 ${data.task.id}`, first.html, {
2511
2679
  chrome,
@@ -2610,7 +2778,8 @@ export function approveKitOf(input, freshSince) {
2610
2778
  rememberedUntil: until,
2611
2779
  // Easy-start C5: the owner approving on this computer needs no password, and the line says so (not "Passwordless").
2612
2780
  ...(input.localOwner?.eligible === true ? { termsWords: "No password needed on this computer." } : {}),
2613
- planHtml: htmlString(joinHtml([parts.lead, parts.rows, parts.editor ?? "", parts.details, parts.editorForm])),
2781
+ // What it signs: the plan sheet (the same parts as the chat's sheet, re-ordered), then the in-place editor.
2782
+ planHtml: htmlString(joinHtml([parts.sheet, parts.editor ?? "", parts.editorForm])),
2614
2783
  };
2615
2784
  }
2616
2785
  /** How an approved plan was approved, for the status box's terms line: by whom and how, or the rule that approved it. */
@@ -37,7 +37,7 @@ import { runActivityOf } from "../task-activity.js";
37
37
  import { taskFingerprint } from "../task-live.js";
38
38
  import { pullRequestFactOf } from '../task-status.js';
39
39
  import { openWorkDecisionOf } from "../work-summary.js";
40
- import { learningHtml } from "../workspace-ui.js";
40
+ import { learningHtml, stepRowsOf } from "../workspace-ui.js";
41
41
  import { createHash, randomBytes } from "node:crypto";
42
42
  import {} from "node:http";
43
43
  import { agentChoicesFor, routeOfTask } from "../agentconfig.js";
@@ -661,6 +661,7 @@ export function createTaskViews(runtime) {
661
661
  coordinator: view.coordinator ?? null,
662
662
  origin: view.origin ?? null,
663
663
  repairChain: view.repairChain ?? null,
664
+ releaseCiRunning: view.releaseCiRunning === true,
664
665
  },
665
666
  decisions: (view?.decisions ?? runtime.store.decisionsForTask(ref.id))
666
667
  .filter(one => one.state === "open" || one.state === "expired")
@@ -886,6 +887,9 @@ export function createTaskViews(runtime) {
886
887
  if (oneTaskView.release != null)
887
888
  oneTaskView.journey = null;
888
889
  }
890
+ // The step rows read the view as the page will show it (its result, its release, its checks words).
891
+ if (oneTaskView?.kind === "task")
892
+ oneTaskView.steps = stepRowsOf(oneTaskView);
889
893
  // The Result tab's no-script page, under the task's own (the same HTML its result page serves), and its script.
890
894
  if (extras.result != null) {
891
895
  page.body = html `${page.body}<section class="task-result-tab" id="task-result" aria-label="Result">${extras.result.html}</section>`;
@@ -148,7 +148,7 @@ export function generalGroup(facts, flags) {
148
148
  formRow("Theme", "Light, dark, or whatever this device uses.", postForm("/settings/appearance", html `<div class="k-seg" role="group" aria-label="Theme">${themeButton("system", "Match device")}${themeButton("light", "Light")}${themeButton("dark", "Dark")}</div>`, { returnTo: "/settings/general" })),
149
149
  kitSectionForm({ proof: "appearance", action: "/settings/appearance", id: "accent", gate: true, hidden: { return: "/settings/general" },
150
150
  body: joinHtml([
151
- formRow("Accent", "Buttons and focus. Amber always means “needs you” and can’t be changed.", select({ name: "accent", label: "Accent", current: facts.accent, items: [...ACCENT_CHOICES.map(one => [one.hex, one.id === "ink" ? "Ink (default)" : one.name]), ...(custom === null ? [] : [[custom, `Your own colour (${custom})`]])] })),
151
+ formRow("Accent", "Buttons, focus and what needs you.", select({ name: "accent", label: "Accent", current: facts.accent, items: [...ACCENT_CHOICES.map(one => [one.hex, one.id === "ink" ? "Ink (default)" : one.name]), ...(custom === null ? [] : [[custom, `Your own colour (${custom})`]])] })),
152
152
  accentNoteWords === null ? "" : note(accentNoteWords),
153
153
  ]) }),
154
154
  disclose("Use your own colour", kitSectionForm({ proof: "appearance", action: "/settings/appearance", id: "accent-own", gate: true, hidden: { return: "/settings/general" },