toolroll 0.9.99 → 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 (103) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/accent-colors.js +1 -1
  3. package/dist/acp-check.js +4 -2
  4. package/dist/activity-line.d.ts +1 -1
  5. package/dist/activity-line.js +1 -1
  6. package/dist/agent-state.js +2 -1
  7. package/dist/agents-board.js +3 -2
  8. package/dist/agents-live.js +3 -2
  9. package/dist/approval-authority.d.ts +2 -1
  10. package/dist/approval-authority.js +2 -1
  11. package/dist/browser/terminal.css +1 -1
  12. package/dist/browser/terminal.js +42 -42
  13. package/dist/browser/workspace.css +2 -2
  14. package/dist/browser/workspace.js +32 -32
  15. package/dist/browser-workspace.d.ts +31 -2
  16. package/dist/chat-ask.js +4 -2
  17. package/dist/chat-core.js +4 -2
  18. package/dist/chat-decide.js +1 -2
  19. package/dist/chat-delivery-state.d.ts +29 -6
  20. package/dist/chat-delivery-state.js +43 -13
  21. package/dist/chat-delivery.js +12 -5
  22. package/dist/chat-flow.js +15 -13
  23. package/dist/chat-live.js +5 -2
  24. package/dist/chat-message-states.d.ts +75 -0
  25. package/dist/chat-message-states.js +74 -0
  26. package/dist/chat-messages.d.ts +10 -4
  27. package/dist/chat-messages.js +12 -7
  28. package/dist/check-failure.js +2 -1
  29. package/dist/check-report.js +1 -1
  30. package/dist/claim.d.ts +40 -2
  31. package/dist/claim.js +92 -19
  32. package/dist/contracts/chat-tables.js +3 -0
  33. package/dist/coordinator.js +3 -3
  34. package/dist/demo.js +7 -2
  35. package/dist/design/kit.css +94 -2
  36. package/dist/design/kit.d.ts +12 -4
  37. package/dist/design/kit.js +8 -2
  38. package/dist/design/tokens.css +5 -5
  39. package/dist/desktop-update.d.ts +1 -1
  40. package/dist/discord-chat.js +2 -3
  41. package/dist/dispatch.js +2 -1
  42. package/dist/dispose.d.ts +4 -1
  43. package/dist/dispose.js +2 -1
  44. package/dist/fallback.js +3 -2
  45. package/dist/fast-start.js +2 -2
  46. package/dist/first-run.d.ts +10 -6
  47. package/dist/first-run.js +19 -18
  48. package/dist/guides.js +1 -1
  49. package/dist/monitoring.js +2 -1
  50. package/dist/operate.js +77 -36
  51. package/dist/plane-review.js +2 -1
  52. package/dist/project-knowledge.d.ts +3 -3
  53. package/dist/release-authority.d.ts +9 -0
  54. package/dist/release-authority.js +13 -0
  55. package/dist/release-auto.js +2 -3
  56. package/dist/release-gate.js +1 -1
  57. package/dist/release-view.js +2 -1
  58. package/dist/release.js +18 -30
  59. package/dist/result-shots.js +1 -1
  60. package/dist/route-results.js +2 -1
  61. package/dist/run-ending.d.ts +21 -0
  62. package/dist/run-ending.js +19 -0
  63. package/dist/scope.d.ts +5 -0
  64. package/dist/scope.js +10 -3
  65. package/dist/server/chrome.d.ts +1 -1
  66. package/dist/server/chrome.js +1 -1
  67. package/dist/server/console.css +46 -17
  68. package/dist/server/kit.d.ts +36 -3
  69. package/dist/server/kit.js +33 -4
  70. package/dist/server/render-chat.d.ts +2 -0
  71. package/dist/server/render-chat.js +1 -0
  72. package/dist/server/render-pages.js +1 -1
  73. package/dist/server/render-results.js +6 -5
  74. package/dist/server/render-tasks.d.ts +10 -0
  75. package/dist/server/render-tasks.js +234 -64
  76. package/dist/server/task-views.js +5 -1
  77. package/dist/settings-groups-ui.js +1 -1
  78. package/dist/setup-guide.d.ts +2 -2
  79. package/dist/slack-chat.js +2 -3
  80. package/dist/store.d.ts +61 -2
  81. package/dist/store.js +141 -80
  82. package/dist/subagent-question.js +8 -6
  83. package/dist/surface.js +2 -2
  84. package/dist/task-archive.d.ts +3 -0
  85. package/dist/task-archive.js +5 -0
  86. package/dist/task-moves.d.ts +90 -0
  87. package/dist/task-moves.js +68 -0
  88. package/dist/task-status.d.ts +2 -2
  89. package/dist/task-status.js +3 -3
  90. package/dist/teams-chat.js +2 -2
  91. package/dist/telegram-flow.d.ts +1 -1
  92. package/dist/telegram-flow.js +1 -1
  93. package/dist/telegram-lead.d.ts +1 -2
  94. package/dist/telegram-lead.js +18 -23
  95. package/dist/telegram-progress.js +2 -1
  96. package/dist/telegram.d.ts +0 -1
  97. package/dist/telegram.js +6 -7
  98. package/dist/tui.js +1 -1
  99. package/dist/vanished-retry.js +3 -2
  100. package/dist/workspace-ui.d.ts +69 -0
  101. package/dist/workspace-ui.js +105 -1
  102. package/dist/worktree.js +3 -2
  103. package/package.json +1 -1
@@ -1,9 +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
- import { chainTermsLines, liveChainFromJson, releaseAsksAgainWords, releaseTermsSentence } from "../scope.js";
6
+ import { wasInterrupted } from "../run-ending.js";
7
+ import { chainTermsLines, liveChainFromJson, releaseAsksAgainWords, releaseCheckSentence, releaseTermsSentence } from "../scope.js";
7
8
  import { SUBSCRIPTION_CAPABLE } from "../keys.js";
8
9
  import { RELEASE_CI_RUNNING_WORDS } from "../release-ci-hold.js";
9
10
  import { writesWords } from "../writes-mode.js";
@@ -46,10 +47,10 @@ import {} from "../store.js";
46
47
  import { runCostWords } from "../summary.js";
47
48
  import {} from "../task-control.js";
48
49
  import { assignmentStageOf, demoChecksOf, stageOfCode, stageOfDispatch, STATUS_MORE, taskStatusOf } from "../task-status.js";
49
- 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";
50
51
  import { createHash, randomUUID } from "node:crypto";
51
52
  import { TEXT_LIMITS } from "../text-limits.js";
52
- 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";
53
54
  import { steerDestinationWords, steerStandingParts } from "../steer-status.js";
54
55
  import { reviewHref, taskChatHref, taskHref } from "./http.js";
55
56
  import { coordinatorProposalsSection } from "./render-chat.js";
@@ -312,6 +313,9 @@ export function approvalSheetParts(input) {
312
313
  // What the yes allows, in plain words, right above it: the agent's
313
314
  // permissions, the per-attempt limit, and an earlier version still running.
314
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 = [];
315
319
  const allowed = permissionPlainWords(scope.profile);
316
320
  if (allowed !== null)
317
321
  allowing.push(allowed);
@@ -322,11 +326,16 @@ export function approvalSheetParts(input) {
322
326
  if (fallbacks.length > 0) {
323
327
  const paid = fallbacks.filter(one => one.authMode === "api-key" || SUBSCRIPTION_CAPABLE[one.profile.provider] !== true);
324
328
  // Each agent named once, the paid one said where it stands in the order.
325
- 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);
326
333
  }
327
334
  const earlier = earlierVersionsWords(input.earlier.active, input.earlier.running);
328
- if (earlier !== null)
335
+ if (earlier !== null) {
329
336
  allowing.push(earlier);
337
+ warnings.push(earlier);
338
+ }
330
339
  const yoursToCheck = scope.acceptance.filter(one => one.evidence.includes("manual-review")).map(one => one.statement.trim().replace(/[.\s]+$/, ""));
331
340
  const consent = joinHtml([
332
341
  allowing.length === 0 ? "" : html `<p class="approval-allowing" data-approval-allowing>You’re allowing: ${allowing.join(" · ")}</p>`,
@@ -341,13 +350,14 @@ export function approvalSheetParts(input) {
341
350
  const submit = rebuild !== null ? html `Approve and retry` : html `Approve`;
342
351
  // Everything else the digest binds, one tap away.
343
352
  const detail = (title, body, attrs = html ``) => isEmptyHtml(body) ? html `` : html `<section class="approval-detail"${attrs}><h3>${title}</h3>${body}</section>`;
344
- const revisionDetail = input.revision === null ? "" : detail("Earlier build", joinHtml([
353
+ const revisionBody = input.revision === null ? html `` : joinHtml([
345
354
  html `<div class="revision-card" data-revision-feedback>`,
346
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>`,
347
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>`,
348
357
  revisionLineageHtml(input.revision.lineage),
349
358
  html `</div>`,
350
- ]));
359
+ ]);
360
+ const revisionDetail = input.revision === null ? "" : detail("Earlier build", revisionBody);
351
361
  const contract = input.planContract;
352
362
  const amendment = approvalAmendmentHtml(contract);
353
363
  const contractDetail = input.planDocument === null || !isEmptyHtml(amendment) ? html ``
@@ -374,7 +384,8 @@ export function approvalSheetParts(input) {
374
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>`,
375
385
  ]);
376
386
  const details = joinHtml([
377
- 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>`,
378
389
  revisionDetail,
379
390
  repairText === null ? "" : detail("Full goal", html `<p>${scope.goal}</p>`),
380
391
  detail("Plan record", contractDetail),
@@ -444,12 +455,21 @@ export function approvalSheetParts(input) {
444
455
  input.deliverable === "report" ? html `<p class="approval-note">Read-only: it reports back and changes nothing in the repository.</p>` : "",
445
456
  originNote(input.origin) === null ? "" : html `<p class="approval-note" data-approval-origin>${originNote(input.origin)}</p>`,
446
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
+ ]);
447
466
  const rows = joinHtml([
448
467
  html `<dl class="approval-rows">`,
449
468
  row("Goal", html `<p class="approval-goal">${goalShown}</p>`),
450
469
  row("Changes", changes),
451
470
  row("Won’t touch", html `<p>${scope.outOfScope === null ? "Nothing is ruled out." : scope.outOfScope}</p>`),
452
471
  row("Done when", doneWhen),
472
+ releaseBody === "" ? "" : html `<div class="approval-row" data-approval-release><dt>Release</dt><dd>${releaseBody}</dd></div>`,
453
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>`),
454
474
  html `</dl>`,
455
475
  amendment,
@@ -459,8 +479,74 @@ export function approvalSheetParts(input) {
459
479
  routeEvidenceHtml(route, "approval-evidence"),
460
480
  consent,
461
481
  ]);
462
- 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;
463
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" };
464
550
  /** Earlier versions of a task still in flight, in plain words: queued
465
551
  * while they only wait, running once one has started; null when none. */
466
552
  export function earlierVersionsWords(active, running) {
@@ -834,6 +920,8 @@ export function approveRefusalWords(reason) {
834
920
  return "not approved: only a person can approve this. Approve it on the task page or from your paired chat";
835
921
  if (reason === "archived")
836
922
  return "not approved: this task is archived. Restore it first";
923
+ if (reason === "cancelled")
924
+ return "not approved: this task was cancelled";
837
925
  if (reason === "unrouted")
838
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";
839
927
  if (reason === "profile-unresolved")
@@ -1215,7 +1303,7 @@ export function planRevisionLedgerHtml(ledger, taskId, csrf) {
1215
1303
  html `</details>`,
1216
1304
  ]);
1217
1305
  return joinHtml([
1218
- 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>`,
1219
1307
  html `<p class="meta">${current.reason}${current.evidenceLink === null ? "" : ` · evidence: ${current.evidenceLink}`}</p>`,
1220
1308
  pending,
1221
1309
  history,
@@ -1339,10 +1427,8 @@ export function taskBodyParts(data) {
1339
1427
  if (data.peekable !== true)
1340
1428
  return "";
1341
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>`;
1342
- const transcript = liveRun.provider !== "claude"
1343
- ? html `<p class="meta">The live transcript needs the claude harness for now — this build runs on ${liveRun.provider}</p>`
1344
- : 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>`;
1345
- 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>`;
1346
1432
  })();
1347
1433
  // One truthful answer to the first question on a queued task: "will this
1348
1434
  // run?" The badge alone cannot distinguish approval, dependency,
@@ -1557,6 +1643,8 @@ export function taskBodyParts(data) {
1557
1643
  // its draft; the full scope form below stays as it was.
1558
1644
  const inlineScopeDraft = data.scopeDraft?.has("requirement-new") === true;
1559
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>` : ""}`;
1560
1648
  const scopeCard = scope === null
1561
1649
  ? html `<p class="meta">No plan yet. Nothing builds until one is approved.</p>`
1562
1650
  : joinHtml([
@@ -1566,7 +1654,7 @@ export function taskBodyParts(data) {
1566
1654
  scope.touches.length === 0 ? "" : html `<p class="scope-paths"><strong>Files</strong> ${scope.touches.join(", ")}</p>`,
1567
1655
  html `<p><strong>Quality</strong> ${qualityModeTitle(scope.qualityMode ?? "default")}</p>`,
1568
1656
  acceptanceCeremonyHtml(scope.acceptance),
1569
- 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,
1570
1658
  approval.approved && data.autoRelease != null
1571
1659
  ? html `<p class="meta scope-seal">${autoReleaseWords(data.autoRelease)}</p>${planVersionDetailsHtml(scope.digest)}`
1572
1660
  : approval.approved && data.release?.kind === "carried"
@@ -1628,6 +1716,14 @@ export function taskBodyParts(data) {
1628
1716
  // card does not repeat it (one #contract-amendment on the page).
1629
1717
  const sheetStatesContract = scope !== null && !approval.approved && data.plan === "drafted" && data.dispatch?.action !== "repair-dependency" &&
1630
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>`;
1631
1727
  const planCard = data.planDocument === null
1632
1728
  ? data.plan === "requested"
1633
1729
  // A request the planner cannot be given waits on a person; the status card above already says what to shorten.
@@ -1644,13 +1740,7 @@ export function taskBodyParts(data) {
1644
1740
  html `</div><span class="plan-lock">${planStanding}</span>${approval.approved ? html `</summary><div class="planner-plan-body">` : html `</div>`}`,
1645
1741
  executionPlanHtml(displayedPlanDocument ?? data.planDocument),
1646
1742
  approval.approved || sheetStatesContract ? "" : planContractHtml(data.planContract ?? null, "full"),
1647
- data.csrf === "" || data.planSha == null || approval.approved
1648
- ? ""
1649
- : html `<details class="plan-editor" id="plan-edit"><summary>Edit plan</summary>${postForm(`${taskHref(task.id)}/plan-edit`, joinHtml([
1650
- html `<label>Plan details <span class="meta">Keep the five headings. Approval locks this version for the build.</span>`,
1651
- html `<textarea name="plan-document" rows="14">${data.planDocument}</textarea></label>`,
1652
- html `<button type="submit">Save plan</button>`,
1653
- ]), { hidden: { "saw-plan": data.planSha } })}</details>`,
1743
+ planEditorHtml,
1654
1744
  approval.approved ? html `</div></details>` : html `</section>`,
1655
1745
  ]);
1656
1746
  const progressCard = milestoneProgressHtml(data.milestoneProgress);
@@ -1686,7 +1776,8 @@ export function taskBodyParts(data) {
1686
1776
  (data.assignment?.primaryAction?.code === "approve-scope" && data.assignment.primaryAction.target.taskId === task.id);
1687
1777
  // The sheet's terms, when this person can approve them here: the task page's kit approval box (its Plan tab shows
1688
1778
  // the rows) and the legacy sheet both read these.
1689
- 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) ||
1690
1781
  scope.profileState === "unresolved" || !consentDoor.open ? null : {
1691
1782
  surface: "task",
1692
1783
  action: `${taskHref(task.id)}/approve`,
@@ -1706,6 +1797,7 @@ export function taskBodyParts(data) {
1706
1797
  coordinator: data.coordinator ?? null,
1707
1798
  origin: data.origin ?? null,
1708
1799
  deliverable: data.deliverable ?? "branch",
1800
+ releaseCiRunning: data.releaseCiRunning === true,
1709
1801
  rebuild: data.assignment?.need != null && "key" in data.assignment.need && data.assignment.need.key === "replan" ? data.assignment.need.build : null,
1710
1802
  editHref: data.csrf !== "" && data.planSha != null && data.planDocument !== null ? "#plan-edit" : "#scope",
1711
1803
  notNowHref: "/tasks",
@@ -1715,7 +1807,7 @@ export function taskBodyParts(data) {
1715
1807
  : { action: `${taskHref(task.id)}/scope`, draft: inlineScopeDraft ? data.scopeDraft ?? null : null, problem: inlineScopeDraft ? data.problem : null,
1716
1808
  open: data.editPlan === true, stepsHref: data.planSha != null && data.planDocument !== null ? "#plan-edit" : null },
1717
1809
  };
1718
- const approveForm = scope === null || approval.approved || data.plan === "requested"
1810
+ const approveForm = scope === null || approval.approved || task.state === "cancelled" || data.plan === "requested"
1719
1811
  ? ""
1720
1812
  : data.revision !== null && data.revision !== undefined && "problem" in data.revision
1721
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>`
@@ -1862,7 +1954,7 @@ export function taskBodyParts(data) {
1862
1954
  const workerRow = liveRun !== undefined
1863
1955
  ? prop("builder", html `${liveRun.runner} · <a href="/r/${liveRun.id}">build #${liveRun.id}</a> running`)
1864
1956
  : data.runs[0] !== undefined
1865
- ? prop("last attempt", html `<a href="/r/${data.runs[0].id}">${runNoun(data.runs[0])} #${data.runs[0].id}</a> · ${data.runs[0].role === "planner" && data.runs[0].reason === "plan-drafted" ? "planned" : data.runs[0].reason === "interrupted" ? "interrupted" : data.runs[0].id === liveHistoryRunId ? "running" : data.runs[0].outcome ?? "never finished"} · ${data.runs[0].runner}`)
1957
+ ? prop("last attempt", html `<a href="/r/${data.runs[0].id}">${runNoun(data.runs[0])} #${data.runs[0].id}</a> · ${data.runs[0].role === "planner" && data.runs[0].reason === "plan-drafted" ? "planned" : wasInterrupted(data.runs[0]) ? "interrupted" : data.runs[0].id === liveHistoryRunId ? "running" : data.runs[0].outcome ?? "never finished"} · ${data.runs[0].runner}`)
1866
1958
  : "";
1867
1959
  const queueRow = data.position !== null && data.position !== undefined && task.state === "queued"
1868
1960
  ? prop("queue", html `${data.position.position} of ${data.position.total}${data.position.column === null ? " in the shared queue" : html ` in ${data.position.column}'s queue`} · <a href="/tasks?view=order">reorder</a>`)
@@ -1885,7 +1977,7 @@ export function taskBodyParts(data) {
1885
1977
  facts.push({ label: "Builder", parts: [`${liveRun.runner} · `, { label: `build #${liveRun.id}`, href: `/r/${liveRun.id}` }, " running"] });
1886
1978
  else if (lastRun !== undefined)
1887
1979
  facts.push({ label: "Last attempt", parts: [{ label: `${runNoun(lastRun)} #${lastRun.id}`, href: `/r/${lastRun.id}` },
1888
- ` · ${lastRun.role === "planner" && lastRun.reason === "plan-drafted" ? "planned" : lastRun.reason === "interrupted" ? "interrupted" : lastRun.id === liveHistoryRunId ? "running" : lastRun.outcome ?? "never finished"} · ${lastRun.runner}`] });
1980
+ ` · ${lastRun.role === "planner" && lastRun.reason === "plan-drafted" ? "planned" : wasInterrupted(lastRun) ? "interrupted" : lastRun.id === liveHistoryRunId ? "running" : lastRun.outcome ?? "never finished"} · ${lastRun.runner}`] });
1889
1981
  if (data.position !== null && data.position !== undefined && task.state === "queued") {
1890
1982
  facts.push({ label: "Queue", parts: [`${data.position.position} of ${data.position.total}${data.position.column === null ? " in the shared queue" : ` in ${data.position.column}'s queue`} · `, { label: "Reorder", href: "/tasks?view=order" }] });
1891
1983
  }
@@ -2132,30 +2224,10 @@ export function taskBodyParts(data) {
2132
2224
  : html `<details class="task-secondary-approval"><summary>Updated approval terms</summary>${approveForm}</details>`;
2133
2225
  const optionsHtml = html `${identity}${dispatchStatus}${dependencyChoiceNeeded ? "" : actsBar}`;
2134
2226
  const optionsOpen = data.assignment?.primaryAction?.code === "unhold" || data.assignment?.primaryAction?.code === "retry-task";
2135
- const body = joinHtml([
2136
- // The title is bare; the receipt or shared task status leads once.
2137
- 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>`,
2138
- data.versionLabel == null ? "" : html `<p class="meta">${data.versionLabel} · <a href="${taskHref(data.rootId ?? task.id)}">Current work</a></p>`,
2139
- data.archived == null ? "" : html `<div class="task-archived" data-archived><p>${archivedWords(data.archived)} · ${whenUtc(data.archived.at)}</p>${data.canPutAway === true && data.csrf !== ""
2140
- ? postForm(`${taskHref(task.id)}/restore`, html `<button type="submit" class="secondary">Restore</button>`, { hidden: { return: taskHref(task.id) }, attrs: { class: "inline" } }) : ""}</div>`,
2141
- (data.history ?? ""),
2142
- // A put-away task's banner is its status: no Needs you card, no act, no approval form (each would be refused).
2143
- data.archived == null ? statusHtml : "",
2144
- controlHtml,
2145
- previousResult === null ? "" : html `<details class="task-previous-result"><summary>${previousResult.title}</summary>${previousResult.html}</details>`,
2146
- progressCard,
2147
- revisionLedgerCard,
2148
- planCard,
2149
- mirrorCard,
2150
- problemHtml,
2151
- needsScopeCard,
2152
- data.archived == null ? approvalHtml : "",
2153
- html `<details class="task-status-details" id="task-diagnostics"${optionsOpen ? html ` open` : ""}><summary>Task options</summary>${optionsHtml}</details>`,
2154
- // Evidence-first (M5.5): what needs you, then what happened — decisions
2155
- // and incidents above the attempt ledger and spend, the mechanics
2156
- // (scope, holds, acts) after. Only trustworthy facts moved up. The rail
2157
- // (slice 1c) rides beside the main column on wide screens and above it
2158
- // 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([
2159
2231
  html `<div class="task-layout"><div class="task-main">`,
2160
2232
  attemptPanel,
2161
2233
  data.coordinatorProposals == null || data.coordinatorProposals.rows.length === 0
@@ -2181,12 +2253,41 @@ export function taskBodyParts(data) {
2181
2253
  section("plan",
2182
2254
  // The recipe road rides with the scope it reuses (UI polish
2183
2255
  // 2026-09-13), off the title-to-action path.
2184
- 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"),
2185
2257
  dependencyChoiceNeeded ? "" : section("waits for", waitsForCard, (data.waitsFor ?? []).length > 0, (data.waitsFor ?? []).length),
2186
2258
  section("holds", holds, true, data.holds.length),
2187
2259
  cancelAct,
2188
2260
  html `</div><aside class="task-rail">${rail}</aside></div>`,
2189
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");
2190
2291
  // Console v2: one thread in time order (the plan, the agent's notes and
2191
2292
  // results, its questions, the person's replies) and the metadata grouped
2192
2293
  // for a Details panel. Every form below keeps its own route and fields.
@@ -2243,13 +2344,19 @@ export function taskBodyParts(data) {
2243
2344
  }
2244
2345
  // What the agent is working through now, and how its plan changed.
2245
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.
2246
2348
  if (planCard !== "")
2247
2349
  thread.push({ key: "plan", at: familyRuns.filter(run => run.role === "planner").map(run => run.finishedAt ?? run.startedAt).sort().at(-1) ?? task.updatedAt,
2248
- 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 });
2249
2351
  if (!isEmptyHtml(progressCard))
2250
2352
  thread.push({ key: "progress", at: liveAt, kind: "progress", who: "agent", author: "", title: "Progress", text: null, link: null, html: htmlString(progressCard), more: null });
2251
- if (!isEmptyHtml(revisionLedgerCard))
2252
- 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
+ }
2253
2360
  // The change a person asked for: the revision's own brief.
2254
2361
  if (data.revision != null && !("problem" in data.revision)) {
2255
2362
  const feedback = data.revision;
@@ -2273,10 +2380,12 @@ export function taskBodyParts(data) {
2273
2380
  text: [chosen, decision.note].filter((one) => one !== null && one !== "").join(" — ") || null, link: null, html: "", more: null });
2274
2381
  }
2275
2382
  }
2276
- // 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.
2277
2385
  if (decisionRail !== "")
2278
2386
  thread.push({ key: "questions", at: openDecisions.map(one => one.createdAt).sort()[0] ?? task.updatedAt, kind: "question", who: "agent", author: "",
2279
- 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 });
2280
2389
  for (const note of data.steering ?? []) {
2281
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 });
2282
2391
  }
@@ -2402,6 +2511,29 @@ export function taskBodyParts(data) {
2402
2511
  { group: "share", label: "Copy link", kind: "copy" },
2403
2512
  ...(cancelForm !== "" ? [{ group: "danger", label: "Cancel task", kind: "goto", tab: "plan", target: "cancel-task" }] : []),
2404
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])) };
2405
2537
  const view = {
2406
2538
  kind: "task",
2407
2539
  id: task.id,
@@ -2422,9 +2554,24 @@ export function taskBodyParts(data) {
2422
2554
  failure: failedCard && data.failure != null ? { line: data.failure.line, evidence: data.failure.evidence, suggestion: data.failure.suggestion, link: data.failure.link } : null,
2423
2555
  retry,
2424
2556
  runChecks,
2425
- progress,
2557
+ progress: progress === null ? null : { ...progress, steps: (data.milestoneProgress ?? []).map(one => ({ title: one.description, state: one.state })) },
2558
+ plan: planView,
2426
2559
  // One quiet line under the step: what the agent did last, and when.
2427
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
+ })(),
2428
2575
  stop,
2429
2576
  pause,
2430
2577
  steering: data.csrf !== "" ? data.steerComposer ?? null : null,
@@ -2441,22 +2588,30 @@ export function taskBodyParts(data) {
2441
2588
  recheckStopped: data.csrf !== "" && data.assignment?.primaryAction?.code === "recheck-stopped" && data.assignment.primaryAction.target.runId !== null
2442
2589
  ? { action: `${taskHref(data.assignment.rootId)}/recheck-stopped`, run: data.assignment.primaryAction.target.runId } : null,
2443
2590
  rebuild,
2444
- // 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.
2445
2593
  lead: [
2446
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) },
2447
2596
  { key: "route-change", html: data.routeChange == null ? "" : htmlString(html `<div class="card"><p>${data.routeChange}</p></div>`) },
2448
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>`) },
2449
2598
  { key: "pull-request", html: fragmentString(pullRequestCard) },
2450
2599
  { key: "needs-scope", html: fragmentString(needsScopeCard) },
2451
2600
  { key: "mirror", html: fragmentString(mirrorCard) }, { key: "attempt", html: fragmentString(attemptPanel) },
2452
2601
  ].filter(one => one.html !== ""),
2453
- // Open questions are answered in the thread.
2602
+ // Open questions are a lead block (key "questions"), so they sit in the step that waits.
2454
2603
  questions: "",
2455
2604
  facts,
2456
2605
  sections: [
2457
2606
  ...(previousResult === null || thread.some(one => one.more !== null) ? [] : [{ id: "result", title: previousResult.title, html: htmlString(previousResult.html), open: false, count: null }]),
2458
2607
  // Questions are in the thread; the scope and the ledgers fold here until asked.
2459
- ...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
+ }),
2460
2615
  ],
2461
2616
  manage: [
2462
2617
  ...sectionParts.filter(one => MANAGE.has(one.id)),
@@ -2482,7 +2637,22 @@ export function taskBodyParts(data) {
2482
2637
  return { href: `/settings/flows?repo=${encodeURIComponent(data.repo)}&starter=${starter.id}#starter-${starter.id}`, starter: starter.name };
2483
2638
  })(),
2484
2639
  };
2485
- 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 };
2486
2656
  }
2487
2657
  export function taskBody(data) {
2488
2658
  return taskBodyParts(data).html;
@@ -2504,7 +2674,6 @@ export function taskPage(chrome, data) {
2504
2674
  const liveRun = liveRunId === null ? undefined : data.runs.find(one => one.id === liveRunId);
2505
2675
  const script = (liveRun === undefined ? "" : regionScript("check-progress", "check", `/r/${liveRun.id}`)) +
2506
2676
  (liveRun !== undefined && data.peekable === true ? regionScript("run-peek", "peek", `/r/${liveRun.id}`) : "") +
2507
- (liveRun !== undefined && data.peekable === true && liveRun.provider === "claude" ? transcriptScript(`/r/${liveRun.id}`) : "") +
2508
2677
  (data.csrf !== "" && data.decisions.some(one => one.state === "open" || one.state === "expired") ? decisionAnswerScript() : "");
2509
2678
  return screen(`task \u00b7 ${data.task.id}`, first.html, {
2510
2679
  chrome,
@@ -2609,7 +2778,8 @@ export function approveKitOf(input, freshSince) {
2609
2778
  rememberedUntil: until,
2610
2779
  // Easy-start C5: the owner approving on this computer needs no password, and the line says so (not "Passwordless").
2611
2780
  ...(input.localOwner?.eligible === true ? { termsWords: "No password needed on this computer." } : {}),
2612
- 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])),
2613
2783
  };
2614
2784
  }
2615
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>`;