toolroll 0.9.86 → 0.9.88

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 (173) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/dist/acp-check.d.ts +9 -0
  3. package/dist/acp-check.js +40 -28
  4. package/dist/agents-board.d.ts +12 -0
  5. package/dist/agents-board.js +26 -0
  6. package/dist/approval-rules-ui.d.ts +4 -1
  7. package/dist/approval-rules-ui.js +31 -15
  8. package/dist/assignment-ui.js +1 -1
  9. package/dist/backup-ui.d.ts +5 -4
  10. package/dist/backup-ui.js +65 -32
  11. package/dist/browser/terminal.css +1 -1
  12. package/dist/browser/terminal.js +57 -54
  13. package/dist/browser/workspace.css +2 -2
  14. package/dist/browser/workspace.js +35 -70
  15. package/dist/browser-crew.js +1 -1
  16. package/dist/browser-workspace.d.ts +329 -183
  17. package/dist/browser-workspace.js +10 -9
  18. package/dist/chat-controls.d.ts +1 -1
  19. package/dist/chat-controls.js +1 -1
  20. package/dist/chat-settings-page.d.ts +29 -9
  21. package/dist/chat-settings-page.js +59 -28
  22. package/dist/check-levels-ui.d.ts +2 -1
  23. package/dist/check-levels-ui.js +67 -58
  24. package/dist/contracts/cli.d.ts +43 -0
  25. package/dist/contracts/cli.js +4 -0
  26. package/dist/contracts/console-api.d.ts +1 -1
  27. package/dist/contracts/console-api.js +1 -1
  28. package/dist/credentials-ui.d.ts +5 -3
  29. package/dist/credentials-ui.js +58 -31
  30. package/dist/design/kit.css +722 -0
  31. package/dist/design/kit.d.ts +208 -0
  32. package/dist/design/kit.js +177 -0
  33. package/dist/design/tokens.css +31 -1
  34. package/dist/discord-settings.js +8 -4
  35. package/dist/export-ui.d.ts +6 -4
  36. package/dist/export-ui.js +18 -8
  37. package/dist/flow-changed.d.ts +14 -0
  38. package/dist/flow-changed.js +53 -0
  39. package/dist/flow-gallery-ui.d.ts +41 -4
  40. package/dist/flow-gallery-ui.js +95 -63
  41. package/dist/flow-starters-ui.d.ts +3 -9
  42. package/dist/flow-starters-ui.js +12 -23
  43. package/dist/flow-unattended-words.d.ts +20 -0
  44. package/dist/flow-unattended-words.js +24 -0
  45. package/dist/flow-unattended.d.ts +9 -5
  46. package/dist/flow-unattended.js +45 -23
  47. package/dist/flows-cli.js +7 -3
  48. package/dist/flows-ui.d.ts +12 -6
  49. package/dist/flows-ui.js +141 -30
  50. package/dist/integrations-ui.d.ts +6 -4
  51. package/dist/integrations-ui.js +34 -40
  52. package/dist/integrations.d.ts +2 -2
  53. package/dist/integrations.js +4 -4
  54. package/dist/kits-ui.d.ts +8 -10
  55. package/dist/kits-ui.js +29 -33
  56. package/dist/kits.js +1 -1
  57. package/dist/knowledge-ui.js +76 -19
  58. package/dist/ledger-view.d.ts +2 -0
  59. package/dist/ledger-view.js +41 -38
  60. package/dist/models-ui.d.ts +8 -3
  61. package/dist/models-ui.js +82 -59
  62. package/dist/monitoring-ui.d.ts +4 -2
  63. package/dist/monitoring-ui.js +44 -13
  64. package/dist/needs-you.d.ts +1 -1
  65. package/dist/needs-you.js +8 -0
  66. package/dist/operate-remote-arguments.js +1 -0
  67. package/dist/operate-remote.js +1 -1
  68. package/dist/operate.d.ts +1 -1
  69. package/dist/operate.js +38 -4
  70. package/dist/passwordless.d.ts +1 -1
  71. package/dist/passwordless.js +1 -1
  72. package/dist/people-audit-ui.d.ts +2 -1
  73. package/dist/people-audit-ui.js +25 -23
  74. package/dist/policy-ui.d.ts +3 -2
  75. package/dist/policy-ui.js +27 -21
  76. package/dist/policy.d.ts +3 -0
  77. package/dist/policy.js +14 -1
  78. package/dist/project-delete-ui.d.ts +4 -2
  79. package/dist/project-delete-ui.js +135 -126
  80. package/dist/project-knowledge.d.ts +10 -10
  81. package/dist/publish.d.ts +11 -0
  82. package/dist/publish.js +16 -3
  83. package/dist/pull-request-ui.d.ts +2 -1
  84. package/dist/pull-request-ui.js +39 -28
  85. package/dist/recipe-ui.d.ts +4 -2
  86. package/dist/recipe-ui.js +14 -4
  87. package/dist/release-adapters.d.ts +3 -0
  88. package/dist/release-adapters.js +22 -2
  89. package/dist/release-cli.js +81 -9
  90. package/dist/release.d.ts +23 -0
  91. package/dist/release.js +46 -4
  92. package/dist/releases.js +1 -1
  93. package/dist/request-budget-ui.d.ts +4 -2
  94. package/dist/request-budget-ui.js +30 -19
  95. package/dist/retention-ui.d.ts +4 -3
  96. package/dist/retention-ui.js +15 -12
  97. package/dist/scope.d.ts +3 -0
  98. package/dist/scope.js +6 -0
  99. package/dist/serve.d.ts +0 -1
  100. package/dist/serve.js +51 -7
  101. package/dist/server/agents-computers.d.ts +9 -0
  102. package/dist/server/agents-computers.js +67 -0
  103. package/dist/server/chat.js +16 -16
  104. package/dist/server/chrome.d.ts +1 -1
  105. package/dist/server/chrome.js +19 -9
  106. package/dist/server/console.css +24 -12
  107. package/dist/server/flows.js +64 -36
  108. package/dist/server/home.d.ts +35 -0
  109. package/dist/server/home.js +104 -0
  110. package/dist/server/http.d.ts +3 -0
  111. package/dist/server/http.js +5 -0
  112. package/dist/server/kit-standalone.d.ts +22 -0
  113. package/dist/server/kit-standalone.js +70 -0
  114. package/dist/server/kit.d.ts +419 -0
  115. package/dist/server/kit.js +369 -0
  116. package/dist/server/pages.d.ts +2 -1
  117. package/dist/server/pages.js +213 -174
  118. package/dist/server/people-tokens.js +26 -85
  119. package/dist/server/render-history.d.ts +51 -0
  120. package/dist/server/render-history.js +228 -0
  121. package/dist/server/render-new-task.d.ts +29 -0
  122. package/dist/server/render-new-task.js +96 -0
  123. package/dist/server/render-next.d.ts +63 -0
  124. package/dist/server/render-next.js +109 -0
  125. package/dist/server/render-pages.d.ts +22 -190
  126. package/dist/server/render-pages.js +123 -575
  127. package/dist/server/render-people.d.ts +24 -3
  128. package/dist/server/render-people.js +93 -72
  129. package/dist/server/render-projects.d.ts +103 -0
  130. package/dist/server/render-projects.js +247 -0
  131. package/dist/server/render-results.d.ts +5 -24
  132. package/dist/server/render-results.js +48 -185
  133. package/dist/server/render-settings.d.ts +5 -74
  134. package/dist/server/render-settings.js +102 -493
  135. package/dist/server/render-tasks.d.ts +35 -5
  136. package/dist/server/render-tasks.js +114 -85
  137. package/dist/server/route-table.js +20 -5
  138. package/dist/server/settings.js +276 -255
  139. package/dist/server/task-views.d.ts +8 -0
  140. package/dist/server/task-views.js +85 -2
  141. package/dist/server/tasks.d.ts +8 -0
  142. package/dist/server/tasks.js +95 -40
  143. package/dist/server/terminals.js +1 -1
  144. package/dist/server/welcome.d.ts +22 -0
  145. package/dist/server/welcome.js +69 -0
  146. package/dist/settings-groups-ui.d.ts +279 -0
  147. package/dist/settings-groups-ui.js +615 -0
  148. package/dist/settings-kit.d.ts +42 -6
  149. package/dist/settings-kit.js +69 -7
  150. package/dist/setup-guide.d.ts +2 -2
  151. package/dist/skills-ui.js +59 -7
  152. package/dist/slack-settings.js +12 -5
  153. package/dist/spend-ui.d.ts +45 -26
  154. package/dist/spend-ui.js +124 -56
  155. package/dist/sso-ui.d.ts +5 -2
  156. package/dist/sso-ui.js +63 -31
  157. package/dist/storage-ui.d.ts +3 -2
  158. package/dist/storage-ui.js +56 -50
  159. package/dist/store.d.ts +112 -3
  160. package/dist/store.js +292 -20
  161. package/dist/surface.js +5 -1
  162. package/dist/teams-settings.d.ts +2 -1
  163. package/dist/teams-settings.js +24 -41
  164. package/dist/telegram-settings.d.ts +0 -4
  165. package/dist/telegram-settings.js +52 -25
  166. package/dist/toolroll-update-ui.d.ts +8 -4
  167. package/dist/toolroll-update-ui.js +55 -45
  168. package/dist/tools-ui.d.ts +5 -3
  169. package/dist/tools-ui.js +107 -42
  170. package/dist/work-index.js +11 -7
  171. package/dist/workspace-ui.d.ts +1 -1
  172. package/dist/workspace-ui.js +7 -4
  173. package/package.json +1 -1
@@ -10,6 +10,7 @@ import { requestLimitsHtml, tokenLimitWords } from "../request-budget-ui.js";
10
10
  import { authenticateAccount } from "../scope.js";
11
11
  import { SSO_CALLBACK } from "../sso-settings.js";
12
12
  import { personChip, refuse, screen } from "./chrome.js";
13
+ import { inviteLinkHtml, ownAccessHtml, peopleHtml, settingsSubPage } from "../settings-groups-ui.js";
13
14
  import { redirect, respond, sayTo } from "./http.js";
14
15
  /** people handlers, moved without changing their route bodies. */
15
16
  import { randomBytes } from "node:crypto";
@@ -23,6 +24,7 @@ import { cursorOf as auditCursorOf, remoteAudit } from "../remote-audit.js";
23
24
  import { credentialProof, hashPassword, proveAccountNow } from "../scope.js";
24
25
  import { form, HANDOFF_STYLE, loginHref, loginReturn, page, SIGN_IN_COOKIE, signInSpent, startedHere } from "./http.js";
25
26
  import { joinDeadPage, joinFormPage, loginPage, SIGNUP_NAME, SIGNUP_PASSWORD_MIN, signupPage } from "./render-people.js";
27
+ import { liveInviteTerms } from "../store.js";
26
28
  import { SESSION_COOKIE, SIGN_IN_LINK_PATH } from "./session.js";
27
29
  import { setupCodeMatches, signupCookie, signupFromHere } from "./signup-gate.js";
28
30
  /** Clock skew allowed between the provider and this server when reading auth_time — forward only (a provider clock
@@ -51,7 +53,7 @@ export function createPeopleHandlers(runtime) {
51
53
  const view = { who: who.name, everyone, canSeeEveryone: store.isInstanceOperator(who.name), now: Date.now(),
52
54
  sessions: store.webSessions(everyone ? null : who.name).map(one => ({ ...one, here: one.idHash === here })), tokens: store.apiTokens(everyone ? null : who.name),
53
55
  limits: { note: tokenLimitWords.bind(null, store.handle), section: store.isInstanceOperator(who.name) ? requestLimitsHtml(store.handle, store.apiTokens(null), Date.now()) : html `` } };
54
- return sendScreen(response, 200, screen("Sessions & tokens", html `<p><a href="/settings">Settings</a></p><h1>Sessions &amp; tokens</h1>${credentialsHtml(view, { said: url.searchParams.get("said"), problem: url.searchParams.get("problem") })}`, { chrome: chromeFor(project, "settings") }));
56
+ return sendScreen(response, 200, screen("Sessions & tokens", settingsSubPage("access", "Sessions and tokens", html `${credentialsHtml(view, { said: url.searchParams.get("said"), problem: url.searchParams.get("problem") })}`), { chrome: chromeFor(project, "settings") }));
55
57
  }
56
58
  async function peoplePage(ctx) {
57
59
  const { url, who, response, now, project } = ctx;
@@ -74,7 +76,7 @@ export function createPeopleHandlers(runtime) {
74
76
  // the process admission); a viewer sees exactly themselves (U3).
75
77
  if (restricted()) {
76
78
  const projects = admissionList() ?? [];
77
- return sendScreen(response, 200, screen("people", html `<h1>Your access</h1><p>${personChip(who.name)} · ${who.role === "approver" ? "operator" : "viewer"}</p><p>${who.role === "approver" ? "You can create, manage, and approve work in these projects." : "You can read work in these projects."}</p><ul>${projects.map(repo => html `<li>${projectName(repo)}</li>`)}</ul><p class="meta">An instance operator manages invitations and project access.</p><p><a href="${personHref(who.name)}">Your remote activity</a></p>`, { chrome: chromeFor(project, "people") }));
79
+ return sendScreen(response, 200, screen("Your access", ownAccessHtml({ name: who.name, approver: who.role === "approver", projects: projects.map(repo => projectName(repo)), activity: personHref(who.name) }), { chrome: chromeFor(project, "people") }));
78
80
  }
79
81
  const approverView = store.isInstanceOperator(who.name);
80
82
  const accounts = store.accountFacts().filter(one => approverView || one.name === who.name);
@@ -87,79 +89,25 @@ export function createPeopleHandlers(runtime) {
87
89
  return seen;
88
90
  };
89
91
  const projectChoices = consoleProjects();
90
- const accessFields = (selected) => html `<label>Project access<select name="access"><option value="selected"${selected !== null ? " selected" : ""}>Selected projects</option><option value="all"${selected === null ? " selected" : ""}>All projects</option></select></label>\
91
- <p class="meta">Operators with all-project access also manage the instance.</p><fieldset><legend>Projects</legend>${projectChoices.length === 0 ? html `<p class="meta">Add a project before granting selected access.</p>` : projectChoices.map(repo => html `<label class="row"><input type="checkbox" name="projects" value="${repo}"${selected?.includes(repo) ? " checked" : ""}>${projectName(repo)}</label>`)}</fieldset>`;
92
- const cards = accounts.map(one => {
92
+ const people = accounts.map(one => {
93
93
  const seen = lastSeenOf(one.name);
94
- const acts = store.recentActsOf(one.name);
95
- const standing = one.revokedAt !== null
96
- ? html `revoked ${one.revokedAt.slice(0, 16).replace("T", " ")} by ${personChip(one.revokedBy ?? "?")}`
97
- : one.role === "approver"
98
- ? "approves"
99
- : "watches";
100
- return joinHtml([
101
- html `<div class="card">`,
102
- html `<h2 style="margin-top:0">${personChip(one.name)} <span class="meta">${standing}</span></h2>`,
103
- html `<p class="meta">${one.projects === null ? "All projects" : one.projects.length === 0 ? "No project access" : one.projects.map(repo => projectName(repo)).join(", ")}</p>`,
104
- approverView && one.revokedAt === null ? html `<details><summary>Edit project access</summary>${postForm("/people/projects", html `${accessFields(one.projects)}<label>Your password<input type="password" name="token" autocomplete="current-password" required></label><p class="meta">Changing access signs this person out and ends their derived sessions and modes. Previously approved work stays recorded.</p><button type="submit">Save project access</button>`, { hidden: { name: one.name } })}</details>` : "",
105
- html `<p class="meta">${seen === null ? "not signed in right now" : `signed in \u2014 active ${new Date(seen).toISOString().slice(11, 16)} UTC`} \u00b7 joined ${one.addedAt.slice(0, 10)}</p>`,
106
- html `<p><a href="${personHref(one.name)}">Tokens and remote activity</a></p>`,
107
- acts.length === 0
108
- ? html `<p class="meta">No recorded acts yet</p>`
109
- : joinHtml(acts.map(act => html `<p class="row meta">${act.kind} ${act.subject} \u00b7 ${act.at.slice(0, 16).replace("T", " ")}</p>`), "\n"),
110
- approverView && one.revokedAt === null && one.name !== who.name
111
- ? postForm("/people/revoke", html `\
112
- ${reauth(html `<label>Your password<input type="password" name="token" autocomplete="current-password"></label>`)}\
113
- <button type="submit">Remove ${one.name}'s sign-in</button>\
114
- <span class="meta">ends their access and everything it signed \u2014 history stays</span>`, { attrs: { class: "row" }, hidden: { name: one.name } })
115
- : "",
116
- html `</div>`,
117
- ], "\n");
94
+ return {
95
+ name: one.name, you: one.name === who.name, approver: one.role === "approver", projects: one.projects,
96
+ projectNames: one.projects === null ? "All projects" : one.projects.length === 0 ? "No project access" : one.projects.map(repo => projectName(repo)).join(", "),
97
+ revoked: one.revokedAt === null ? null : `Removed ${one.revokedAt.slice(0, 16).replace("T", " ")} by ${one.revokedBy ?? "someone"}`,
98
+ seen: seen === null ? `Joined ${one.addedAt.slice(0, 10)}` : now.getTime() - seen < 5 * 60_000 ? "Signed in now" : `Active ${new Date(seen).toISOString().slice(11, 16)} UTC`,
99
+ joined: one.addedAt.slice(0, 10), activity: personHref(one.name),
100
+ acts: store.recentActsOf(one.name).map(act => `${act.kind} ${act.subject} ${act.at.slice(0, 16).replace("T", " ")}`),
101
+ };
118
102
  });
119
103
  const invites = approverView ? store.openInvites(now) : [];
120
- const inviteRows = invites.map(one => html `<p class="row">${one.role} invite · ${one.projects === null ? "all projects" : one.projects.map(repo => projectName(repo)).join(", ")} \u00b7 from ${personChip(one.mintedBy)} \u00b7 expires ${one.expiresAt.slice(0, 16).replace("T", " ")}${one.attempts > 0 ? ` \u00b7 ${one.attempts} failed attempt${one.attempts === 1 ? "" : "s"}` : ""}\
121
- ${postForm("/people/invite-revoke", html `\
122
- ${reauth(html `<label>Password <input type="password" name="token" autocomplete="current-password" style="width:8rem"></label>`)}\
123
- <button type="submit">Cancel it</button>`, { attrs: { style: "display:inline" }, hidden: { id: one.id } })}</p>`);
124
- const inviteCard = !approverView
125
- ? ""
126
- : joinHtml([
127
- html `<div class="card">`,
128
- html `<h2 style="margin-top:0">Invite someone</h2>`,
129
- inviteRows.length === 0 ? html `<p class="meta">No open invites</p>` : joinHtml(inviteRows, "\n"),
130
- postForm("/people/invite", joinHtml([
131
- "",
132
- html `<label>They can<select name="role"><option value="viewer">Viewer — read work</option><option value="approver">Operator — create, manage and approve work</option></select></label>`,
133
- accessFields(project === null ? [] : [project]),
134
- html `<label>Your password<input type="password" name="token" autocomplete="current-password"></label>`,
135
- html `<button type="submit">Make an invite link</button>`,
136
- html `<span class="meta">single-use, expires in 72 hours</span>`,
137
- ], "\n"), { attrs: { class: "row" } }),
138
- html `</div>`,
139
- ], "\n");
140
- // Coordinators (MCP spec v6): the machine principals beside the
141
- // human ones — name, immutable fingerprint, what they may file
142
- // into, their rate, and the revoke road. Approver eyes only.
143
- const coordinatorCard = !approverView
144
- ? ""
145
- : (() => {
146
- const rows = listCoordinators(store);
147
- if (rows.length === 0)
148
- return "";
149
- return joinHtml([
150
- html `<div class="card">`,
151
- html `<h2 style="margin-top:0">Agent filers</h2>`,
152
- html `<p class="meta">Agents that can propose tasks. You approve what runs.</p>`,
153
- ...rows.map(one => html `<p class="row"><span class="mono">${`${one.name}#${one.cid.slice(0, 4)}`}</span> ${one.revokedAt !== null
154
- ? html `<span class="meta">revoked ${one.revokedAt.slice(0, 10)}</span>`
155
- : html `<span class="meta">${one.perHour}/hour · files into ${one.repos.join(", ")} · ${one.expiresAt === null ? "no expiry (made before expiry)" : `expires ${one.expiresAt.slice(0, 10)}`} · last filed ${one.lastFiledAt === null ? "never" : one.lastFiledAt.slice(0, 16).replace("T", " ")}</span>`}</p>`),
156
- html `<details class="settings-more"><summary>Revoke an agent filer</summary><p class="meta">Run <code>toolroll coordinator revoke &lt;cid&gt; --as you</code> on this computer.</p></details>`,
157
- html `</div>`,
158
- ], "\n");
159
- })();
160
- return sendScreen(response, 200, screen("people", joinHtml([html `<h1>People</h1>`, ...cards, coordinatorCard, inviteCard], "\n"), {
161
- chrome: chromeFor(project, "people"),
162
- }));
104
+ const filers = !approverView ? [] : listCoordinators(store).map(one => ({ name: `${one.name}#${one.cid.slice(0, 4)}`, words: one.revokedAt !== null ? `Revoked ${one.revokedAt.slice(0, 10)}`
105
+ : `${one.perHour} an hour · files into ${one.repos.join(", ")} · ${one.expiresAt === null ? "no expiry (made before expiry)" : `expires ${one.expiresAt.slice(0, 10)}`} · last filed ${one.lastFiledAt === null ? "never" : one.lastFiledAt.slice(0, 16).replace("T", " ")}` }));
106
+ return sendScreen(response, 200, screen("People", peopleHtml({
107
+ operator: approverView, you: who.name, said: url.searchParams.get("said"), problem: url.searchParams.get("problem"), people,
108
+ invites: invites.map(one => ({ id: one.id, role: one.role, projects: one.projects === null ? "All projects" : one.projects.map(repo => projectName(repo)).join(", "), from: one.mintedBy, expires: `${one.expiresAt.slice(0, 16).replace("T", " ")} UTC`, attempts: one.attempts })),
109
+ projects: projectChoices.map(repo => ({ repo, name: projectName(repo) })), openProject: project !== null && projectChoices.includes(project) ? project : null, filers,
110
+ }), { chrome: chromeFor(project, "people") }));
163
111
  }
164
112
  async function sessionsSend(ctx) {
165
113
  const { who, request, response, now, posted } = ctx;
@@ -258,15 +206,7 @@ ${reauth(html `<label>Password <input type="password" name="token" autocomplete=
258
206
  return refuse(response, who, 400, "Select at least one project for this invitation.", "/people");
259
207
  const minted = store.mintInvite(role, who.name, now, undefined, access.projects);
260
208
  const origin = options.publicUrl !== undefined ? options.publicUrl.replace(/\/$/, "") : `http://${request.headers.host ?? "this-console"}`;
261
- const linkScreen = screen("people", joinHtml([
262
- html `<h1>Invite link</h1>`,
263
- html `<div class="card">`,
264
- html `<p>Shown once, so copy it now.</p>`,
265
- html `<p class="mono secret-value">${`${origin}/join/${minted.token}`}</p>`,
266
- html `<p class="meta">Send it to one person. It works once, lets them ${role === "approver" ? "approve and act" : "read work"} in ${access.projects === null ? "all projects" : access.projects.map(repo => projectName(repo)).join(", ")}, and expires ${minted.expiresAt.slice(0, 16).replace("T", " ")} UTC. Cancel it any time on People.</p>`,
267
- html `</div>`,
268
- html `<p class="meta"><a href="/people">Back to People</a></p>`,
269
- ], "\n"),
209
+ const linkScreen = screen("Invite link", inviteLinkHtml({ link: `${origin}/join/${minted.token}`, approver: role === "approver", projects: access.projects === null ? "all projects" : access.projects.map(repo => projectName(repo)).join(", "), expires: `${minted.expiresAt.slice(0, 16).replace("T", " ")} UTC` }),
270
210
  // A one-time secret on screen: no script of any kind rides along.
271
211
  { chrome: chromeFor(projectOf(who, request) ?? null, "people"), forceSensitive: true });
272
212
  return sendScreen(response, 200, linkScreen);
@@ -578,7 +518,8 @@ ${reauth(html `<label>Password <input type="password" name="token" autocomplete=
578
518
  async function joinPage(ctx) {
579
519
  const { url, response } = ctx;
580
520
  const joinToken = joinCodeOf(url);
581
- return page(response, 200, store.inviteIsLive(joinToken, clock()) ? joinFormPage(joinToken, null, "") : joinDeadPage());
521
+ const terms = liveInviteTerms(store, joinToken, clock());
522
+ return page(response, 200, terms !== null ? joinFormPage(joinToken, null, "", terms) : joinDeadPage());
582
523
  }
583
524
  async function joinSend(ctx) {
584
525
  const { url, request, response } = ctx;
@@ -612,15 +553,15 @@ ${reauth(html `<label>Password <input type="password" name="token" autocomplete=
612
553
  // Possession of a LIVE token earns real words (D6's disclosure
613
554
  // ruling) — the attempt is already spent either way (E3).
614
555
  if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,39}$/.test(name)) {
615
- return page(response, 400, joinFormPage(joinToken, "names are 1\u201340 characters \u2014 letters, digits, dots, dashes, underscores", name));
556
+ return page(response, 400, joinFormPage(joinToken, { field: "name", text: "Use 1\u201340 letters, digits, dots, dashes or underscores, starting with a letter or digit." }, name, liveInviteTerms(store, joinToken, clock())));
616
557
  }
617
558
  if (password.length < 8) {
618
- return page(response, 400, joinFormPage(joinToken, "the password needs at least 8 characters", name));
559
+ return page(response, 400, joinFormPage(joinToken, { field: "password", text: "The password needs at least 8 characters." }, name, liveInviteTerms(store, joinToken, clock())));
619
560
  }
620
561
  const made = store.consumeInviteAndCreateAccount({ tokenValue: joinToken, name, credentialHash: hashPassword(password) }, clock());
621
562
  if (!made.ok) {
622
563
  if (made.reason === "name-taken") {
623
- return page(response, 400, joinFormPage(joinToken, "that name is taken \u2014 pick another", name));
564
+ return page(response, 400, joinFormPage(joinToken, { field: "name", text: "That name is taken. Choose another." }, name, liveInviteTerms(store, joinToken, clock())));
624
565
  }
625
566
  return page(response, 200, joinDeadPage());
626
567
  }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * History pages on the revamp kit (UI revamp wave B): one attempt (/r/<id>), a task's full record (/t/<id>/evidence),
3
+ * an agent's question (/d/<id>). Reading pages: the phone header, then one `doc` column. Rules: src/design/KIT.md.
4
+ */
5
+ import type { EvidencePack } from "../evidence-pack.js";
6
+ import { type Html, type HtmlValue } from "../html.js";
7
+ import type { RouteStamp } from "../phase-routing.js";
8
+ import { type ResultTab } from "../result-review.js";
9
+ import type { StructuredHandoffView, TerminalDiffView } from "../result-evidence-readers.js";
10
+ import type { Artifact, Decision, DiffComment, Publication, Run } from "../store.js";
11
+ import type { ReviewFacts } from "../workspace-ui.js";
12
+ import { type Chrome, type Screen } from "./chrome.js";
13
+ import { type ProofBundleView, type ResultDetail } from "./render-results.js";
14
+ /** A section of a reading page: its heading and body (kit `k-sec`). */
15
+ export declare const sec: (title: string, body: HtmlValue, attrs?: Html) => Html;
16
+ export declare function runPage(chrome: Chrome, run: Run, taskId: string, artifacts: Artifact[], terminal?: TerminalDiffView | null, notes?: {
17
+ id: number;
18
+ author: string;
19
+ note: string;
20
+ createdAt: string;
21
+ }[], csrf?: string, comments?: DiffComment[], ciRepair?: {
22
+ pr: number;
23
+ } | null, liveScript?: string, peekable?: boolean, running?: boolean, editor?: {
24
+ worktree: string;
25
+ } | null, editorToggle?: {
26
+ on: boolean;
27
+ } | null, noted?: boolean, structuredHandoff?: StructuredHandoffView | null, proofBundle?: ProofBundleView | null, route?: RouteStamp | null, publicationRow?: Publication | null, review?: ReviewFacts | null, result?: {
28
+ detail: ResultDetail;
29
+ tab: ResultTab;
30
+ user: string;
31
+ requestToken: string;
32
+ } | null, sourceDigest?: string | null): Screen;
33
+ export type QuestionPlace = {
34
+ title: string | null;
35
+ repo: string | null;
36
+ run: Pick<Run, "id" | "role"> | null;
37
+ };
38
+ /**
39
+ * The question an agent asked, on top: what it asked and why, the options as radio cards with its recommendation
40
+ * marked, an optional note, and one Answer (kind none: one click). An irreversible option asks for a tick first; the
41
+ * answer route checks it again. A refusal shows in the box with nothing changed; an answer replaces the box.
42
+ */
43
+ export declare function decisionPage(chrome: Chrome, decision: Decision, taskId: string, artifacts: Artifact[], now: Date, place: QuestionPlace, returnTo?: string | null, refusal?: string | null,
44
+ /** What a refused answer carried: the choice and the note stay in the box. */
45
+ draft?: {
46
+ choice: string | null;
47
+ note: string | null;
48
+ } | null): Screen;
49
+ /** A task's full record, read-only: what was asked and approved, every agent that ran, the result, and its sealed audit
50
+ * log entries, with the JSON download and Print. The same facts as the pack's JSON (evidence-pack.ts). */
51
+ export declare function evidenceRecordPage(chrome: Chrome, pack: EvidencePack): Screen;
@@ -0,0 +1,228 @@
1
+ import { closedQuestionReason, LIMITS } from "../decision.js";
2
+ import { html, joinHtml, postForm } from "../html.js";
3
+ import { projectName } from "../project.js";
4
+ import { RESULT_REVIEW_SCRIPT } from "../result-review.js";
5
+ import { isOverdue, screen, sentenceCase, whenTime } from "./chrome.js";
6
+ import { taskHref } from "./http.js";
7
+ import { btn, chip, disclose, doc, docHeader, dock, facts, note, notice, phoneHeader, radios, slots, status } from "./kit.js";
8
+ import { editorFileHref, evidenceBundleCard, evidenceWords, humanBytes, prefillScript, resultPanelHtml, revisionSealFields, runFactsRows, runNoun, terminalDiffCard, } from "./render-results.js";
9
+ import { publicationFactsOf } from "./render-tasks.js";
10
+ /** A section of a reading page: its heading and body (kit `k-sec`). */
11
+ export const sec = (title, body, attrs = html ``) => html `<section class="k-sec"${attrs}><h3>${title}</h3>${body}</section>`;
12
+ /** The root every kit server page carries, so the workspace frames it as a kit page (no padding, panel or desk header). */
13
+ const kitPage = (attrs, ...parts) => html `<div class="h-page" data-kit-page${attrs}>${parts}</div>`;
14
+ // ---- /r/<id>: one attempt ----------------------------------------------------------------------------------------
15
+ export function runPage(chrome, run, taskId, artifacts, terminal = null, notes = [], csrf = "", comments = [], ciRepair = null, liveScript, peekable = false, running = false, editor = null, editorToggle = null, noted = false, structuredHandoff = null, proofBundle = null, route = null, publicationRow = null, review = null, result = null, sourceDigest = null) {
16
+ const title = `${sentenceCase(runNoun(run))} #${run.id}`;
17
+ // The facts region: a live run's is what the poller swaps; a finished run's stands still.
18
+ const summary = sec("Summary", html `<div id="run-facts">${runFactsRows(run, taskId, running, route)}</div>${running ? html `<p class="k-note" id="run-facts-stamp"></p>` : ""}`);
19
+ // The live transcript (arc 1): the agent's own words, streamed to a file beside the run and polled as raw text.
20
+ // Honesty stated on the surface: display only, and the machine running the agent could alter it.
21
+ const transcript = !running ? "" : sec("What the agent is saying", !peekable
22
+ ? note(html `The live transcript is off. Start serve with <span class="k-mono">--runner &lt;name&gt;</span> naming this machine's worker, and it appears here.`)
23
+ : run.provider !== "claude"
24
+ ? note(`The live transcript needs the claude harness for now; this build runs on ${run.provider}.`)
25
+ : html `${note("Display only: this is not evidence, and the machine running the agent could alter it.")}<pre id="live-transcript" class="k-mono h-pre h-live"></pre><p class="k-note" id="live-transcript-state"></p>`);
26
+ // The live peek region (A2): the poller fills it only on a serve that asserted its runner; otherwise the section
27
+ // says why it is empty, so the click never lands on silence.
28
+ const peek = !running ? "" : sec("What is changing right now", peekable
29
+ ? html `<div id="run-peek">${note("Watching… the first look lands within 15 seconds.")}</div><p class="k-note" id="run-peek-stamp"></p>`
30
+ : note(html `The live file view is off. Start serve with <span class="k-mono">--runner &lt;name&gt;</span> naming this machine's worker, and it appears here.`));
31
+ const resultSummary = structuredHandoff?.conclusion ?? run.handoff;
32
+ const list = (label, items) => items.length === 0 ? "" : html `<div class="h-list"><p class="k-note">${label}</p><ul>${items.map(one => html `<li>${one}</li>`)}</ul></div>`;
33
+ const handoff = resultSummary === null ? "" : sec("Result", html `<p class="h-read">${resultSummary}</p>${structuredHandoff === null ? "" : [
34
+ list("Completed", structuredHandoff.changes), list("Checks the agent reported", structuredHandoff.verification), list("Follow-up", structuredHandoff.followUps)
35
+ ]}`);
36
+ // Review comments on the immutable terminal diff (M6.8): listed, added, and sealed into ONE unapproved revision task.
37
+ // The seal only creates the unapproved task; its approval screen restates the batch.
38
+ const hasTerminalDiff = terminal !== null && terminal.patch !== null && !("problem" in terminal.patch);
39
+ const commentPath = (path, line) => {
40
+ const shown = `${path}${line === null ? "" : `:${line}`}`;
41
+ const href = editor === null ? null : editorFileHref(editor.worktree, path, line);
42
+ return href === null ? html `<span class="k-mono">${shown}</span> ` : html `<a class="k-mono k-link" href="${href}">${shown}</a> `;
43
+ };
44
+ const commentRows = comments.map(one => html `<li class="diff-comment"><span class="k-num k-muted">${whenTime(one.createdAt)}</span><span><b>${one.author}</b> ${one.path === null ? "" : commentPath(one.path, one.line)}${one.note}</span></li>`);
45
+ const commentForm = csrf === "" || !hasTerminalDiff ? null : postForm(`/r/${run.id}/comment`, html `<div class="h-pair">\
46
+ <div class="k-field"><label class="k-label" for="comment-path">File</label><input class="k-input k-mono" id="comment-path" type="text" name="path" placeholder="Choose a line above"></div>\
47
+ <div class="k-field"><label class="k-label" for="comment-line">Line</label><input class="k-input" id="comment-line" type="text" name="line" placeholder="—" inputmode="numeric"></div></div>\
48
+ <div class="k-field"><label class="k-label" for="comment-note">Change requested</label><textarea class="k-textarea" id="comment-note" name="note" rows="2" maxlength="${LIMITS.note}" placeholder="What should change, and why" aria-describedby="comment-note-limit"${noted ? html ` autofocus` : ""}></textarea>\
49
+ <p class="k-hint" id="comment-note-limit">Up to ${LIMITS.note} characters</p></div><div class="k-acts">${btn("Add annotation", { sm: true, type: "submit", attrs: { "data-k-busy": "Adding…" } })}</div>`, { attrs: { class: "h-form", id: "comment-form" } });
50
+ // The device-side half of the editor-link activation (arc 6, finding 1): only when the capability exists and this
51
+ // run belongs to this machine's runner; the person at the browser flips it.
52
+ const editorToggleForm = editorToggle === null || csrf === "" ? "" : postForm("/session/editor-links", html `${btn(editorToggle.on ? "Stop opening files in VS Code here" : "Open files in VS Code from this device", { variant: "ghost", sm: true, type: "submit" })}<p class="k-note">Only useful when this browser runs on the machine that holds the worktrees.</p>`, { attrs: { class: "h-inline" }, returnTo: `/r/${run.id}`, hidden: { on: editorToggle.on ? "0" : "1" } });
53
+ const reviseForm = csrf === "" || comments.length === 0 ? "" : postForm(`/r/${run.id}/revise`, html `${revisionSealFields(comments, sourceDigest)}${slots({
54
+ status: html `<h2>${comments.length} ${comments.length === 1 ? "annotation" : "annotations"} ready</h2>`,
55
+ text: "Creates one revision carrying this exact batch. You review its scope before anything builds.",
56
+ acts: btn("Create revision from annotations", { variant: "primary", type: "submit", attrs: { "data-k-busy": "Creating…" } }),
57
+ terms: "Nothing builds until its scope is approved.",
58
+ })}`, { attrs: { class: "revision-from-comments" } });
59
+ // CI repair, suggestion-first (M8.18): the observed red episode earns a button; the button drafts ONE unapproved
60
+ // task; a person approves it.
61
+ const repair = ciRepair === null || csrf === "" ? "" : sec(`Checks are failing on pull request #${ciRepair.pr}`, html `${note("Seen by the pull request watcher. Drafting a repair makes one unapproved task; you approve its scope before anything builds.")}${postForm(`/r/${run.id}/draft-repair`, btn("Draft a repair task", { sm: true, type: "submit", attrs: { "data-k-busy": "Drafting…" } }), { attrs: { class: "h-inline" } })}`);
62
+ const reviewSec = commentRows.length === 0 && commentForm === null ? "" : sec("Review and revise", html `${note("Annotate the diff above. When the batch is ready, create one scoped revision task from it.")}${commentRows.length === 0 ? "" : html `<ul class="h-notes diff-comments">${commentRows}</ul>`}${commentForm ?? ""}${reviseForm}${editorToggleForm}`);
63
+ const saved = artifacts.length === 0 ? "" : sec("Saved files", html `<ul class="h-files">${artifacts.map(artifact => html `<li><a class="k-link" href="/r/${run.id}/evidence/${artifact.id}">${evidenceWords(artifact.kind)}</a>\
64
+ <span class="k-num k-muted" title="${artifact.bytesStored} bytes stored${artifact.truncated ? ` of ${artifact.bytesOriginal}` : ""}">${artifact.truncated ? `${humanBytes(artifact.bytesStored)} of ${humanBytes(artifact.bytesOriginal)} · partial` : humanBytes(artifact.bytesStored)}</span></li>`)}</ul>`);
65
+ const noteRows = notes.map(one => html `<li><span class="k-num k-muted">${whenTime(one.createdAt)}</span><span><b>${one.author}</b> ${one.note}</span></li>`);
66
+ const noteForm = csrf === "" ? "" : postForm(`/r/${run.id}/note`, html `<label class="k-sr" for="run-note">Add a note</label><input class="k-input" id="run-note" type="text" name="note" placeholder="A note for whoever reads this attempt next" autocomplete="off">${btn("Add note", { sm: true, type: "submit", attrs: { "data-k-busy": "Adding…" } })}`, { attrs: { class: "h-addnote" } });
67
+ const notesSec = notes.length === 0 && noteForm === "" ? "" : sec("Notes", html `${noteRows.length === 0 ? note("No notes yet. A note stays with this attempt for whoever reads it next.") : html `<ul class="h-notes">${noteRows}</ul>`}${noteForm}`);
68
+ // A finished result (package 3) is the ONE shared panel (Summary / Changes / Checks, Request changes beside it), the
69
+ // same markup the review cockpit and the chat's result view render. Every other run keeps its record by record page.
70
+ const resultPanel = result === null ? null : resultPanelHtml(result.detail, {
71
+ place: "run", tab: result.tab, csrf, user: result.user, noted, requestToken: result.requestToken,
72
+ hrefFor: one => (one === "summary" ? `/r/${run.id}` : `/r/${run.id}?tab=${one}`), returnTo: `/r/${run.id}?record=1`, back: null,
73
+ });
74
+ const body = kitPage(html ``, phoneHeader(title, { back: taskHref(taskId) }), doc(docHeader({ title, back: [taskHref(taskId), "Task"], meta: html `Task <a class="k-mono k-link" href="${taskHref(taskId)}">${taskId}</a>${run.startedAt === null ? "" : html ` · started ${whenTime(run.startedAt)}`}` }), summary, transcript, peek, ...(resultPanel === null
75
+ ? [handoff, evidenceBundleCard(proofBundle, run, publicationFactsOf(publicationRow), review), terminal === null ? "" : terminalDiffCard(terminal, run.id, editor, commentForm !== null), reviewSec, repair]
76
+ : [html `<div class="h-panel">${resultPanel}</div>`, editorToggleForm]), saved, notesSec));
77
+ return screen(title, body, {
78
+ chrome,
79
+ // One composed functional script (arc 4 contract): the pollers when the run is live, the result panel's script or
80
+ // the comment prefill when a form exists. Neither fetches beyond the pollers.
81
+ ...(liveScript === undefined && commentForm === null && resultPanel === null
82
+ ? {}
83
+ : { functional: { script: (liveScript ?? "") + (resultPanel !== null ? RESULT_REVIEW_SCRIPT : commentForm === null ? "" : prefillScript()), fetches: liveScript !== undefined } }),
84
+ });
85
+ }
86
+ /**
87
+ * The question an agent asked, on top: what it asked and why, the options as radio cards with its recommendation
88
+ * marked, an optional note, and one Answer (kind none: one click). An irreversible option asks for a tick first; the
89
+ * answer route checks it again. A refusal shows in the box with nothing changed; an answer replaces the box.
90
+ */
91
+ export function decisionPage(chrome, decision, taskId, artifacts, now, place, returnTo = null, refusal = null,
92
+ /** What a refused answer carried: the choice and the note stay in the box. */
93
+ draft = null) {
94
+ const back = returnTo === null ? ["/", "Home"] : [returnTo, "Back to the chat"];
95
+ const runWords = place.run === null ? null : `${runNoun(place.run)} #${place.run.id}`;
96
+ const project = place.repo === null ? null : projectName(place.repo);
97
+ const meta = html `${project === null ? "" : html `${project} · `}<span class="k-mono">${taskId}</span>${runWords === null ? "" : ` · ${runWords}`} asked ${whenTime(decision.createdAt)}${decision.deadline === null || decision.state !== "open" ? "" : html ` · answer by ${whenTime(decision.deadline)}`}`;
98
+ const open = decision.state !== "answered";
99
+ const irreversible = decision.options.filter(option => !option.reversible);
100
+ let box;
101
+ if (decision.supersededReason !== null) {
102
+ box = slots({ status: html `${chip("Closed")}<h2>${decision.question}</h2>`, text: closedQuestionReason(decision.supersededReason), acts: btn("Open task", { href: taskHref(taskId) }), terms: "Nothing waits on this question now." });
103
+ }
104
+ else if (!open) {
105
+ const chosen = decision.options.find(option => option.id === decision.choice);
106
+ box = slots({
107
+ status: html `${chip("Answered", "done")}<h2>${decision.question}</h2>`,
108
+ text: html `${chosen?.label ?? decision.choice ?? ""} · by ${decision.answeredBy ?? "someone"}${decision.answeredAt === null ? "" : html ` at ${whenTime(decision.answeredAt)}`}${decision.note === null ? "" : ` · “${decision.note}”`}`,
109
+ acts: btn("Open task", { href: taskHref(taskId) }),
110
+ terms: chosen === undefined ? "" : chosen.consequence,
111
+ });
112
+ }
113
+ else {
114
+ const options = radios({
115
+ name: "choice", legend: "Choose one",
116
+ current: draft?.choice !== null && draft?.choice !== undefined && decision.options.some(option => option.id === draft.choice) ? draft.choice : decision.recommendation,
117
+ items: decision.options.map(option => ({
118
+ value: option.id, label: option.label, text: option.consequence,
119
+ tag: html `${option.id === decision.recommendation ? chip("Recommended", "", { dot: false }) : ""}${option.reversible ? "" : chip("Can't be undone", "failed", { dot: false })}`,
120
+ })),
121
+ });
122
+ const noteField = html `<div class="k-field"><label class="k-label" for="answer-note">Note for the agent <span class="k-muted">(optional)</span></label><textarea class="k-textarea" id="answer-note" name="note" rows="2" maxlength="${LIMITS.note}" placeholder="Anything it should know with your answer">${draft?.note ?? ""}</textarea></div>`;
123
+ const confirm = irreversible.length === 0 ? "" : html `<label class="h-check"><input type="checkbox" name="confirm" value="yes"><span>I understand ${irreversible.length === 1 ? `“${irreversible[0].label}” can't be undone` : "the options marked so can't be undone"}.</span></label>`;
124
+ const overdue = isOverdue(decision, now);
125
+ box = postForm(`/d/${decision.id}/answer`, slots({
126
+ wait: true,
127
+ status: html `${status("needs")}<h2>${decision.question}</h2>`,
128
+ text: decision.recap,
129
+ body: html `${options}${noteField}${confirm}`,
130
+ acts: btn("Answer", { variant: "primary", type: "submit", docked: true, attrs: { "data-k-busy": "Answering…" } }),
131
+ terms: html `Answering needs no password.${overdue ? " The answer was due; the build still waits for it." : ""}`,
132
+ err: refusal === null ? undefined : `${sentenceCase(refusal)}. Nothing changed.`,
133
+ }), { attrs: { class: "h-question", id: "answer-form" }, returnTo });
134
+ }
135
+ const looked = artifacts.length === 0 ? "" : sec("What the agent looked at", html `<ul class="h-files">${artifacts.map(artifact => html `<li><a class="k-link" href="/d/${decision.id}/evidence/${artifact.id}">${evidenceWords(artifact.kind)}</a><span class="k-num k-muted">${humanBytes(artifact.bytesStored)}${artifact.truncated ? " · partial" : ""}</span></li>`)}</ul>`);
136
+ const stopped = !open || place.run === null ? "" : sec("Where it stopped", html `<p class="h-read k-muted">${sentenceCase(runWords ?? "")} asked this and holds its work until you answer. <a class="k-link" href="/r/${place.run.id}?record=1">See the attempt</a></p>`);
137
+ const answering = open && decision.supersededReason === null;
138
+ const body = kitPage(html ``, phoneHeader(project ?? "Question", { back: back[0] }),
139
+ // The shell's phone header already says this page's title (the screen title), so the h1 is drawn once.
140
+ doc(docHeader({ title: place.title ?? taskId, back, meta, same: true }), box, looked, stopped),
141
+ // On a phone the one primary lives in the dock; it submits the same form.
142
+ answering ? dock(btn("Answer", { variant: "primary", type: "submit", attrs: { form: "answer-form", "data-k-busy": "Answering…" } })) : "");
143
+ return screen(place.title ?? `Question · ${taskId}`, body, { chrome });
144
+ }
145
+ // ---- /t/<id>/evidence: the full record ----------------------------------------------------------------------------
146
+ const at = (iso) => iso === null ? "—" : whenTime(iso);
147
+ const shortHash = (hash) => hash === null ? "—" : html `<span class="k-mono" title="${hash}">${hash.slice(0, 12)}</span>`;
148
+ const usd = (value) => value === null ? "—" : `$${value.toFixed(value < 1 && value > 0 ? 4 : 2)}`;
149
+ const webLink = (url) => /^https:\/\/[^\s"'<>]+$/.test(url) ? html `<a class="k-link" href="${url}">${url}</a>` : html `${url}`;
150
+ const filerWords = (who) => who === null || who.name === null ? "automation" : who.kind === "person" ? who.name : `${who.name} (${who.kind})`;
151
+ const basisWords = {
152
+ person: "", mode: " (operating mode)", flow: " (the flow runs on its own)", lead: " (the lead, not a person)", schedule: " (standing order)",
153
+ carried: " (carried from their release approval)", unknown: " (no person's proof on record)",
154
+ };
155
+ function stateChip(state) {
156
+ return state === "done" ? status("finished") : state === "failed" ? status("failed") : state === "running" ? status("running")
157
+ : state === "queued" ? status("queued") : chip(sentenceCase(state));
158
+ }
159
+ /** A task's full record, read-only: what was asked and approved, every agent that ran, the result, and its sealed audit
160
+ * log entries, with the JSON download and Print. The same facts as the pack's JSON (evidence-pack.ts). */
161
+ export function evidenceRecordPage(chrome, pack) {
162
+ const task = pack.task;
163
+ const href = `/t/${encodeURIComponent(task.id)}`;
164
+ const chain = pack.ledger.chain;
165
+ const chainWords = chain.ok
166
+ ? html `<span data-evidence-chain="ok">Chain verified · ${chain.entries} entries · head ${shortHash(chain.head)}${pack.ledger.checkpoint === null ? "" : ` · last checkpoint at entry #${pack.ledger.checkpoint.through}`}</span>`
167
+ : html `<span data-evidence-chain="broken">Chain broken: ${chain.problem ?? "it doesn't verify"}</span>`;
168
+ const rules = pack.rules === null ? "None: every plan asks a person" : html `${sentenceCase(pack.rules.now)}${pack.rules.changes.length === 0 ? "" : disclose(`${pack.rules.changes.length} ${pack.rules.changes.length === 1 ? "change" : "changes"}`, html `<ul class="h-notes">${pack.rules.changes.slice(-5).map(one => html `<li><span class="k-num k-muted">${at(one.at)}</span><span>${one.by}: ${one.change ?? ""}</span></li>`)}</ul>`)}`;
169
+ const summary = sec("Summary", facts([
170
+ ["Task", html `<span class="k-mono">${task.id}</span>${task.project === null ? "" : ` · ${task.project}`}`],
171
+ ["Status", stateChip(task.state)],
172
+ ["Approval rules", rules],
173
+ ["Agents", pack.totals.runs === 0 ? "None ran" : `${pack.totals.runs} ${pack.totals.runs === 1 ? "run" : "runs"} · ${usd(pack.totals.costUsd)}`],
174
+ ["Prepared", html `${at(pack.generatedAt)} for ${pack.generatedBy}`],
175
+ ["Audit log", chainWords],
176
+ ]));
177
+ const broken = chain.ok ? "" : notice(html `The audit log's chain doesn't verify: ${chain.problem ?? "an entry changed"}. The record below is what the log holds now.`);
178
+ const versions = pack.versions.map((version, index) => {
179
+ const scope = version.scope;
180
+ const approved = scope?.approved ?? null;
181
+ const approvals = scope === null ? [] : scope.approvals.filter(one => one.digest === (approved?.digest ?? scope.digest));
182
+ const rows = [["Filed", html `${at(version.filedAt)} by ${filerWords(version.filedBy)}`]];
183
+ if (scope === null)
184
+ rows.push(["Plan", "None written"]);
185
+ else {
186
+ rows.push(["Goal", scope.goal]);
187
+ if (scope.outOfScope)
188
+ rows.push(["Not in scope", scope.outOfScope]);
189
+ if (scope.touches.length)
190
+ rows.push(["Touches", joinHtml(scope.touches.map(one => html `<span class="k-mono">${one}</span>`), ", ")]);
191
+ if (scope.acceptance.length)
192
+ rows.push(["Requirements", html `<ol class="h-ol">${scope.acceptance.map(one => html `<li>${one.statement}</li>`)}</ol>`]);
193
+ if (scope.budgetUsd !== null)
194
+ rows.push(["Budget", usd(scope.budgetUsd)]);
195
+ if (scope.writtenBy.length)
196
+ rows.push(["Plan written by", [...new Set(scope.writtenBy.map(one => one.author))].join(", ")]);
197
+ rows.push(["Approved", approved === null ? "Not approved" : html `${approved.by}${basisWords[approved.basis]} · ${at(approved.at)}${approved.current ? "" : " · the plan changed after this"}`]);
198
+ if (approvals.length > 1)
199
+ rows.push(["Approvals", joinHtml(approvals.map(one => html `${one.approver} · ${at(one.at)}`), html `<br>`)]);
200
+ rows.push(["Plan version", shortHash(scope.digest)]);
201
+ }
202
+ const heading = pack.versions.length === 1 ? "Request and approval" : index === 0 ? "Request and approval" : `Revision ${index}`;
203
+ return sec(heading, html `${pack.versions.length === 1 ? "" : html `<p class="k-note k-mono">${version.id}</p>`}${facts(rows)}`, html ` data-evidence-version="${version.id}"`);
204
+ });
205
+ const runs = pack.versions.flatMap(version => version.runs);
206
+ const agents = sec("Agents", runs.length === 0 ? note("No agent has run for this task yet.") : html `<div class="h-table" role="table" aria-label="Agents">\
207
+ <div class="h-tr h-th" role="row"><span role="columnheader">Run</span><span role="columnheader">Role</span><span role="columnheader">Agent</span><span role="columnheader">Outcome</span><span role="columnheader">Spend</span><span role="columnheader">Finished</span><span role="columnheader">Commit</span></div>\
208
+ ${runs.map(run => html `<div class="h-tr" role="row"><span role="cell"><a class="k-mono k-link" href="/r/${run.id}?record=1">#${run.id}</a></span><span role="cell">${run.role}</span>\
209
+ <span role="cell">${run.agent}${run.model ? html ` <span class="k-mono">${run.model}</span>` : ""}</span><span role="cell">${run.outcome === null ? "Running" : sentenceCase(run.outcome)}</span>\
210
+ <span role="cell" class="k-num">${usd(run.costUsd)}</span><span role="cell">${at(run.finishedAt)}</span><span role="cell">${shortHash(run.head)}</span></div>`)}</div>${runs.some(run => run.role === "builder" || run.role === "repair") ? disclose("Changed files", html `<div class="h-changes">${runs.filter(run => run.role === "builder" || run.role === "repair").map(run => html `<p class="k-note">Run #${run.id}${run.diff === null ? "" : html ` · diff ${shortHash(run.diff.sha256)}${run.diff.complete ? "" : " (partial)"}`}</p>${run.changedFiles === null ? note("Its changed files couldn't be read.") : run.changedFiles.length === 0 ? note("No files changed.") : html `<ul class="h-files">${run.changedFiles.map(file => html `<li><span class="k-mono">${file}</span></li>`)}</ul>`}`)}</div>`) : ""}`);
211
+ const result = pack.result;
212
+ const outcome = sec("Result", result === null ? note("No result yet.") : facts([
213
+ ["Checks", html `${result.checks.detail}${result.checks.command ? html ` <span class="k-mono">${result.checks.command}</span>` : ""}`],
214
+ ["Completed", result.completedBy === null ? "Not yet" : html `${result.completedBy} · ${at(result.completedAt)}`],
215
+ ["Published", result.publication === null ? "Not published" : html `${sentenceCase(result.publication.state)}${result.publication.prUrl ? html ` · ${webLink(result.publication.prUrl)}` : ""}${result.publication.remoteState ? ` · ${result.publication.remoteState.toLowerCase()}` : ""}`],
216
+ ["Commit", html `${shortHash(result.base)} → ${shortHash(result.head)}`],
217
+ ...(result.caveats.length ? [["Caveats", joinHtml(result.caveats, html `<br>`)]] : []),
218
+ ]), html ` data-evidence-result`);
219
+ const entries = pack.ledger.entries.filter(one => one.source !== "request");
220
+ const requests = pack.ledger.entries.length - entries.length;
221
+ const log = sec("Audit log", html `${entries.length === 0 ? note("No entries.") : html `<ul class="h-log">${[...entries].reverse().map(one => html `<li data-ledger-id="${one.id}">\
222
+ <span class="k-num k-muted">${at(one.at)}</span><span><b>${one.actor}</b> ${one.action}${one.detail ? html `<span class="k-muted"> · ${one.detail}</span>` : ""}</span>\
223
+ <span class="k-muted">${one.outcome}</span><span class="h-seal">${shortHash(one.seal?.hash ?? null)}</span></li>`)}</ul>`}${requests > 0 ? note(`Plus ${requests} ${requests === 1 ? "request" : "requests"} made through the console (accepted or refused), listed in the JSON.`) : ""}${pack.ledger.truncated ? note(`This record holds ${entries.length} of the task's entries and the newest ${requests} requests; the audit log export has the rest.`) : ""}`);
224
+ const json = `${href}/evidence?format=json`;
225
+ const tools = html `<span class="k-acts h-tools">${btn("Download JSON", { sm: true, href: json, attrs: { download: true } })}${btn("Print", { sm: true, variant: "ghost", attrs: { "data-k-print": true } })}</span>`;
226
+ const body = kitPage(html ` data-evidence-pack="${task.id}"`, phoneHeader("Full record", { back: href }), doc(docHeader({ title: "Full record", back: [href, "Task"], meta: html `${task.title}${task.project === null ? "" : ` · ${task.project}`} · <span class="k-mono">${task.id}</span>`, right: tools }), html `<p class="k-note h-lead">Read-only. Everything below is sealed in the audit log; nothing here can be changed.</p>`, broken, summary, versions, agents, outcome, log, html `<p class="k-note h-foot">Record digest <span class="k-mono">${pack.digest}</span>. Each entry's seal can be rechecked from the JSON: <span class="k-mono">${pack.ledger.recipe}</span>.</p>`, html `<div class="h-phone-tools">${btn("Download JSON", { href: json, attrs: { download: true } })}</div>`));
227
+ return screen(`Full record · ${task.title}`, body, { chrome });
228
+ }
@@ -0,0 +1,29 @@
1
+ import type { QualityMode } from "../quality.js";
2
+ import type { UnattendedPermissionMode } from "../scope.js";
3
+ import { type Chrome, type Screen } from "./chrome.js";
4
+ export type TaskComposerPrefill = {
5
+ title: string;
6
+ goal: string;
7
+ not: string;
8
+ touches: string;
9
+ acceptance: string;
10
+ values?: URLSearchParams;
11
+ };
12
+ export declare function newTaskPage(chrome: Chrome, project: string | null,
13
+ /** undefined: keep the revision the refused form posted (its draft), so a project switch since still refuses. */
14
+ projectRevision: number | undefined, problem: string | null, candidates?: {
15
+ id: string;
16
+ title: string;
17
+ }[], permissionDefault?: UnattendedPermissionMode, qualityDefault?: QualityMode, projects?: {
18
+ path: string;
19
+ name: string;
20
+ }[],
21
+ /** A shipped template's fields, or a refused filing's own words, back in the form. */
22
+ prefill?: TaskComposerPrefill | null,
23
+ /** Words an idea's link put in the box (GET ?text=). */
24
+ draft?: string | null,
25
+ /** Ideas for the chosen project: one tap drafts the words into the box. */
26
+ suggestions?: ReadonlyArray<{
27
+ label: string;
28
+ draft: string;
29
+ }>): Screen;