toolroll 0.9.86 → 0.9.87

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 (171) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/dist/agents-board.d.ts +12 -0
  3. package/dist/agents-board.js +26 -0
  4. package/dist/approval-rules-ui.d.ts +4 -1
  5. package/dist/approval-rules-ui.js +31 -15
  6. package/dist/assignment-ui.js +1 -1
  7. package/dist/backup-ui.d.ts +5 -4
  8. package/dist/backup-ui.js +65 -32
  9. package/dist/browser/terminal.css +1 -1
  10. package/dist/browser/terminal.js +57 -54
  11. package/dist/browser/workspace.css +2 -2
  12. package/dist/browser/workspace.js +35 -70
  13. package/dist/browser-crew.js +1 -1
  14. package/dist/browser-workspace.d.ts +329 -183
  15. package/dist/browser-workspace.js +10 -9
  16. package/dist/chat-controls.d.ts +1 -1
  17. package/dist/chat-controls.js +1 -1
  18. package/dist/chat-settings-page.d.ts +29 -9
  19. package/dist/chat-settings-page.js +59 -28
  20. package/dist/check-levels-ui.d.ts +2 -1
  21. package/dist/check-levels-ui.js +67 -58
  22. package/dist/contracts/cli.d.ts +43 -0
  23. package/dist/contracts/cli.js +4 -0
  24. package/dist/contracts/console-api.d.ts +1 -1
  25. package/dist/contracts/console-api.js +1 -1
  26. package/dist/credentials-ui.d.ts +5 -3
  27. package/dist/credentials-ui.js +58 -31
  28. package/dist/design/kit.css +685 -0
  29. package/dist/design/kit.d.ts +208 -0
  30. package/dist/design/kit.js +177 -0
  31. package/dist/design/tokens.css +19 -1
  32. package/dist/discord-settings.js +8 -4
  33. package/dist/export-ui.d.ts +6 -4
  34. package/dist/export-ui.js +18 -8
  35. package/dist/flow-changed.d.ts +14 -0
  36. package/dist/flow-changed.js +53 -0
  37. package/dist/flow-gallery-ui.d.ts +41 -4
  38. package/dist/flow-gallery-ui.js +95 -63
  39. package/dist/flow-starters-ui.d.ts +3 -9
  40. package/dist/flow-starters-ui.js +12 -23
  41. package/dist/flow-unattended-words.d.ts +20 -0
  42. package/dist/flow-unattended-words.js +24 -0
  43. package/dist/flow-unattended.d.ts +9 -5
  44. package/dist/flow-unattended.js +45 -23
  45. package/dist/flows-cli.js +7 -3
  46. package/dist/flows-ui.d.ts +12 -6
  47. package/dist/flows-ui.js +141 -30
  48. package/dist/integrations-ui.d.ts +6 -4
  49. package/dist/integrations-ui.js +34 -40
  50. package/dist/integrations.d.ts +2 -2
  51. package/dist/integrations.js +4 -4
  52. package/dist/kits-ui.d.ts +8 -10
  53. package/dist/kits-ui.js +29 -33
  54. package/dist/kits.js +1 -1
  55. package/dist/knowledge-ui.js +76 -19
  56. package/dist/ledger-view.d.ts +2 -0
  57. package/dist/ledger-view.js +41 -38
  58. package/dist/models-ui.d.ts +8 -3
  59. package/dist/models-ui.js +82 -59
  60. package/dist/monitoring-ui.d.ts +4 -2
  61. package/dist/monitoring-ui.js +44 -13
  62. package/dist/needs-you.d.ts +1 -1
  63. package/dist/needs-you.js +8 -0
  64. package/dist/operate-remote-arguments.js +1 -0
  65. package/dist/operate-remote.js +1 -1
  66. package/dist/operate.d.ts +1 -1
  67. package/dist/operate.js +38 -4
  68. package/dist/passwordless.d.ts +1 -1
  69. package/dist/passwordless.js +1 -1
  70. package/dist/people-audit-ui.d.ts +2 -1
  71. package/dist/people-audit-ui.js +25 -23
  72. package/dist/policy-ui.d.ts +3 -2
  73. package/dist/policy-ui.js +27 -21
  74. package/dist/policy.d.ts +3 -0
  75. package/dist/policy.js +14 -1
  76. package/dist/project-delete-ui.d.ts +4 -2
  77. package/dist/project-delete-ui.js +135 -126
  78. package/dist/project-knowledge.d.ts +10 -10
  79. package/dist/publish.d.ts +11 -0
  80. package/dist/publish.js +16 -3
  81. package/dist/pull-request-ui.d.ts +2 -1
  82. package/dist/pull-request-ui.js +39 -28
  83. package/dist/recipe-ui.d.ts +4 -2
  84. package/dist/recipe-ui.js +14 -4
  85. package/dist/release-adapters.d.ts +3 -0
  86. package/dist/release-adapters.js +22 -2
  87. package/dist/release-cli.js +81 -9
  88. package/dist/release.d.ts +21 -0
  89. package/dist/release.js +40 -4
  90. package/dist/releases.js +1 -1
  91. package/dist/request-budget-ui.d.ts +4 -2
  92. package/dist/request-budget-ui.js +30 -19
  93. package/dist/retention-ui.d.ts +4 -3
  94. package/dist/retention-ui.js +15 -12
  95. package/dist/scope.d.ts +3 -0
  96. package/dist/scope.js +6 -0
  97. package/dist/serve.d.ts +0 -1
  98. package/dist/serve.js +51 -7
  99. package/dist/server/agents-computers.d.ts +9 -0
  100. package/dist/server/agents-computers.js +67 -0
  101. package/dist/server/chat.js +16 -16
  102. package/dist/server/chrome.d.ts +1 -1
  103. package/dist/server/chrome.js +19 -9
  104. package/dist/server/console.css +2 -2
  105. package/dist/server/flows.js +64 -36
  106. package/dist/server/home.d.ts +35 -0
  107. package/dist/server/home.js +104 -0
  108. package/dist/server/http.d.ts +3 -0
  109. package/dist/server/http.js +5 -0
  110. package/dist/server/kit-standalone.d.ts +22 -0
  111. package/dist/server/kit-standalone.js +69 -0
  112. package/dist/server/kit.d.ts +405 -0
  113. package/dist/server/kit.js +361 -0
  114. package/dist/server/pages.d.ts +2 -1
  115. package/dist/server/pages.js +213 -174
  116. package/dist/server/people-tokens.js +26 -85
  117. package/dist/server/render-history.d.ts +51 -0
  118. package/dist/server/render-history.js +228 -0
  119. package/dist/server/render-new-task.d.ts +29 -0
  120. package/dist/server/render-new-task.js +96 -0
  121. package/dist/server/render-next.d.ts +63 -0
  122. package/dist/server/render-next.js +109 -0
  123. package/dist/server/render-pages.d.ts +22 -190
  124. package/dist/server/render-pages.js +123 -575
  125. package/dist/server/render-people.d.ts +24 -3
  126. package/dist/server/render-people.js +93 -72
  127. package/dist/server/render-projects.d.ts +103 -0
  128. package/dist/server/render-projects.js +247 -0
  129. package/dist/server/render-results.d.ts +5 -24
  130. package/dist/server/render-results.js +48 -185
  131. package/dist/server/render-settings.d.ts +5 -74
  132. package/dist/server/render-settings.js +102 -493
  133. package/dist/server/render-tasks.d.ts +35 -5
  134. package/dist/server/render-tasks.js +114 -85
  135. package/dist/server/route-table.js +20 -5
  136. package/dist/server/settings.js +276 -255
  137. package/dist/server/task-views.d.ts +8 -0
  138. package/dist/server/task-views.js +85 -2
  139. package/dist/server/tasks.d.ts +8 -0
  140. package/dist/server/tasks.js +95 -40
  141. package/dist/server/terminals.js +1 -1
  142. package/dist/server/welcome.d.ts +22 -0
  143. package/dist/server/welcome.js +69 -0
  144. package/dist/settings-groups-ui.d.ts +279 -0
  145. package/dist/settings-groups-ui.js +615 -0
  146. package/dist/settings-kit.d.ts +42 -6
  147. package/dist/settings-kit.js +69 -7
  148. package/dist/setup-guide.d.ts +2 -2
  149. package/dist/skills-ui.js +59 -7
  150. package/dist/slack-settings.js +12 -5
  151. package/dist/spend-ui.d.ts +45 -26
  152. package/dist/spend-ui.js +124 -56
  153. package/dist/sso-ui.d.ts +5 -2
  154. package/dist/sso-ui.js +63 -31
  155. package/dist/storage-ui.d.ts +3 -2
  156. package/dist/storage-ui.js +56 -50
  157. package/dist/store.d.ts +112 -3
  158. package/dist/store.js +292 -20
  159. package/dist/surface.js +5 -1
  160. package/dist/teams-settings.d.ts +2 -1
  161. package/dist/teams-settings.js +24 -41
  162. package/dist/telegram-settings.d.ts +0 -4
  163. package/dist/telegram-settings.js +52 -25
  164. package/dist/toolroll-update-ui.d.ts +8 -4
  165. package/dist/toolroll-update-ui.js +55 -45
  166. package/dist/tools-ui.d.ts +5 -3
  167. package/dist/tools-ui.js +107 -42
  168. package/dist/work-index.js +11 -7
  169. package/dist/workspace-ui.d.ts +1 -1
  170. package/dist/workspace-ui.js +7 -4
  171. package/package.json +1 -1
@@ -22,19 +22,19 @@ export type KnowledgeView = ReturnType<typeof knowledgeView>;
22
22
  export declare function knowledgeVersion(store: Store, repo: string, actor: string, revision: number): Knowledge;
23
23
  /** Chat reads only an admitted project. Keep the full source in the project UI. */
24
24
  export declare function conversationKnowledge(store: Store, repo: string, actor: string, reference?: string, decision?: number): {
25
- title?: never;
26
- content?: never;
25
+ history?: never;
27
26
  revision: number;
28
27
  stale: boolean;
29
28
  notice: string;
30
29
  decision?: never;
31
- history?: never;
30
+ title?: never;
31
+ content?: never;
32
32
  instructions?: never;
33
33
  references?: never;
34
34
  decisions?: never;
35
35
  } | {
36
- title?: never;
37
- content?: never;
36
+ revision?: never;
37
+ history?: never;
38
38
  stale?: never;
39
39
  decision: {
40
40
  repo: string;
@@ -51,27 +51,27 @@ export declare function conversationKnowledge(store: Store, repo: string, actor:
51
51
  id: number;
52
52
  };
53
53
  notice: string;
54
- revision?: never;
55
- history?: never;
54
+ title?: never;
55
+ content?: never;
56
56
  instructions?: never;
57
57
  references?: never;
58
58
  decisions?: never;
59
59
  } | {
60
+ history?: never;
60
61
  stale?: never;
61
62
  decision?: never;
62
63
  revision: number;
63
64
  title: string;
64
65
  content: string;
65
66
  notice: string;
66
- history?: never;
67
67
  instructions?: never;
68
68
  references?: never;
69
69
  decisions?: never;
70
70
  } | {
71
- title?: never;
72
- content?: never;
73
71
  stale?: never;
74
72
  decision?: never;
73
+ title?: never;
74
+ content?: never;
75
75
  revision: number;
76
76
  history: {
77
77
  revision: number;
package/dist/publish.d.ts CHANGED
@@ -131,3 +131,14 @@ export declare function sweepMerges(store: Store, options: {
131
131
  clock?: () => Date;
132
132
  exec?: PublishExec;
133
133
  }): Promise<MergeReport>;
134
+ /** A conditional settle (store.settleObservation) and, only when it won, that head's checks' own timing (v146 ci_check),
135
+ * in one transaction: a stale, losing answer neither settles nor records a check. */
136
+ export declare function settleWithChecks(store: Store, observation: {
137
+ githubRepo: string;
138
+ prNumber: number;
139
+ headSha: string;
140
+ state: "passing" | "failing" | "running" | "none";
141
+ generation: number;
142
+ }, rollup: unknown, now: Date): {
143
+ won: boolean;
144
+ };
package/dist/publish.js CHANGED
@@ -26,7 +26,7 @@ import { tmpdir } from "node:os";
26
26
  import { join } from "node:path";
27
27
  import { run as execRun } from "./exec.js";
28
28
  import { summarizeChecks } from "./pulls.js";
29
- import { releasePublication } from "./store.js";
29
+ import { parseCiRollup, recordCiChecks, releasePublication } from "./store.js";
30
30
  import { headWithin } from "./names.js";
31
31
  export const BODY_TEMPLATE_VERSION = 2;
32
32
  /** After this many failed attempts a publication stops retrying and pages instead. */
@@ -340,6 +340,8 @@ export async function observeChecks(store, options) {
340
340
  if (!settled.won)
341
341
  return;
342
342
  won = true;
343
+ // The checks' own timing (v146), only with the observation that won.
344
+ recordCiChecks(store, publication.githubRepo, publication.prNumber, headOid, parseCiRollup(payload.statusCheckRollup), clock());
343
345
  store.recordPublicationRemoteState(publication.id, remoteState, clock());
344
346
  report.resolved += store.resolveCiEpisodes(publication.githubRepo, publication.prNumber, null, clock());
345
347
  // A closed PR moots its merge blocker and supersedes its intent —
@@ -361,7 +363,8 @@ export async function observeChecks(store, options) {
361
363
  continue;
362
364
  }
363
365
  const state = summarizeChecks(payload.statusCheckRollup);
364
- const settled = store.settleObservation({ githubRepo: publication.githubRepo, prNumber: publication.prNumber, headSha: headOid, state, generation }, clock());
366
+ // Settled and, when it won, the checks' own timing recorded (v146) in one transaction: a losing generation writes nothing.
367
+ const settled = settleWithChecks(store, { githubRepo: publication.githubRepo, prNumber: publication.prNumber, headSha: headOid, state, generation }, payload.statusCheckRollup, clock());
365
368
  if (!settled.won) {
366
369
  // An out-of-order response: a newer observation already spoke.
367
370
  // Nothing persists from this one (round-4 findings 24/25).
@@ -465,7 +468,7 @@ async function observeOne(store, exec, clock, githubRepo, prNumber) {
465
468
  if (String(payload.state ?? "").toUpperCase() !== "OPEN")
466
469
  return { ok: false, why: "pr-" + String(payload.state ?? "gone").toLowerCase() };
467
470
  const state = summarizeChecks(payload.statusCheckRollup);
468
- const settled = store.settleObservation({ githubRepo, prNumber, headSha, state, generation }, clock());
471
+ const settled = settleWithChecks(store, { githubRepo, prNumber, headSha, state, generation }, payload.statusCheckRollup, clock());
469
472
  // A losing settle is NO observation (round-4 finding 24).
470
473
  if (!settled.won)
471
474
  return { ok: false, why: "stale-observation" };
@@ -707,3 +710,13 @@ export async function sweepMerges(store, options) {
707
710
  }
708
711
  return report;
709
712
  }
713
+ /** A conditional settle (store.settleObservation) and, only when it won, that head's checks' own timing (v146 ci_check),
714
+ * in one transaction: a stale, losing answer neither settles nor records a check. */
715
+ export function settleWithChecks(store, observation, rollup, now) {
716
+ return store.transact(() => {
717
+ const settled = store.settleObservation(observation, now);
718
+ if (settled.won)
719
+ recordCiChecks(store, observation.githubRepo, observation.prNumber, observation.headSha, parseCiRollup(rollup), now);
720
+ return settled;
721
+ });
722
+ }
@@ -21,4 +21,5 @@ export type PullRequestSettingsView = {
21
21
  problem: string | null;
22
22
  };
23
23
  export declare function pullRequestSettingsHtml(view: PullRequestSettingsView): Html;
24
- export declare const PULL_REQUEST_SETTINGS_CSS = "\n .pr-settings { display: grid; gap: .6rem; max-width: 40rem; }\n .pr-settings p, .pr-settings h2 { margin: 0; }\n .pr-settings label { display: grid; gap: .25rem; }\n .pr-settings label input[type=password], .pr-settings select { min-height: 44px; box-sizing: border-box; width: 100%; }\n .pr-settings .pr-check { display: flex; align-items: center; gap: .5rem; min-height: 44px; }\n .pr-settings button { min-height: 44px; justify-self: start; }\n .pr-terms { margin: 0; padding-left: 1.1rem; display: grid; gap: .25rem; }\n @media (max-width: 480px) { .pr-settings button { justify-self: stretch; } }\n";
24
+ /** Kept for the console's style bundle (server/chrome.ts); the page itself is kit markup. */
25
+ export declare const PULL_REQUEST_SETTINGS_CSS = "";
@@ -1,44 +1,55 @@
1
1
  /** Settings → Projects → Pull requests: one project's pull-request setup, in plain words. Off shows what setup
2
2
  * found (or what to fix) and one Turn on; on shows where pull requests go and how they merge. Every change takes
3
3
  * the password; turning on restates the exact repository and branch that were checked. */
4
- import { html, postForm, reauth } from "./html.js";
4
+ import { html, joinHtml, postForm } from "./html.js";
5
+ import { acts, btn, disclose, formRow, infoLine, note, notice, select, settingsSection, switchInput } from "./server/kit.js";
6
+ import { kitSectionForm } from "./settings-kit.js";
5
7
  const METHOD_WORDS = { squash: "Squash and merge", merge: "Merge commit", rebase: "Rebase and merge" };
8
+ const mono = (text) => html `<span class="k-mono">${text}</span>`;
6
9
  export function pullRequestSettingsHtml(view) {
7
- const post = (act, body, hidden = {}) => postForm("/settings/pull-requests", body, { attrs: { class: "card pr-settings" }, hidden: { repo: view.repo, act, ...hidden } });
8
- const password = html `${reauth(html `<label>Password<input type="password" name="password" autocomplete="current-password" required></label>`)}`;
9
- const notes = [
10
- view.said === null ? "" : html `<p class="notice" role="status">${view.said}</p>`,
11
- view.problem === null ? "" : html `<p class="problem" role="alert">${view.problem}</p>`,
12
- ];
13
- const head = html `<p class="meta">${view.name} · <span class="mono">${view.repo}</span></p>${notes}`;
10
+ const head = joinHtml([
11
+ view.problem === null ? "" : notice(view.problem),
12
+ view.said === null ? "" : infoLine(view.said),
13
+ note(html `${view.name} · ${mono(view.repo)}`),
14
+ ]);
14
15
  const publishing = view.publishing;
15
16
  if (publishing.on) {
16
- const status = html `<section class="card pr-settings"><p><strong>On</strong> · <span class="mono">${publishing.githubRepo}</span> into <span class="mono">${publishing.base}</span></p><p class="meta">${publishing.legacy ? "Every finished build opens a pull request (set up with publish grant)." : "Complete offers “Complete and open a pull request”."} Set up by ${publishing.grantedBy}.</p></section>`;
17
+ const status = settingsSection({ id: "pull-requests", title: "Pull requests", state: " · On",
18
+ lead: html `${mono(publishing.githubRepo)} into ${mono(publishing.base)}`,
19
+ body: note(`${publishing.legacy ? "Every finished build opens a pull request (set up with publish grant)." : "Complete offers “Complete and open a pull request”."} Set up by ${publishing.grantedBy}.`) });
17
20
  if (!view.canChange)
18
- return html `${head}${status}`;
19
- const methods = ["squash", "merge", "rebase"].map(method => html `<option value="${method}"${publishing.mergeMethod === method ? html ` selected` : ""}>${METHOD_WORDS[method]}${method === "squash" ? " (default)" : ""}</option>`);
20
- return html `${head}${status}${post("settings", html `<h2>Merging</h2><label>Merge method<select name="method">${methods}</select></label><label class="row pr-check"><input type="checkbox" name="when-green" value="1"${publishing.mergeWhenGreen ? html ` checked` : ""}> Merge when checks pass</label><p class="meta">Off by default. When on, a pull request merges by itself once its checks pass on the completed commit. Branches are deleted after merging.</p>${password}<button type="submit">Save</button>`)}<details class="settings-more"><summary>Turn off pull requests</summary>${post("off", html `<p class="meta">Nothing more is pushed. Open pull requests stay on GitHub.</p>${password}<button type="submit" class="danger">Turn off</button>`)}</details>`;
21
+ return joinHtml([head, status]);
22
+ const merging = settingsSection({ id: "merging", title: "Merging",
23
+ body: kitSectionForm({ proof: "pull-requests", action: "/settings/pull-requests", id: "merging-form", gate: true, hidden: { repo: view.repo, act: "settings" },
24
+ body: joinHtml([
25
+ formRow("Merge method", null, select({ id: "pr-method", name: "method", label: "Merge method", current: publishing.mergeMethod,
26
+ items: ["squash", "merge", "rebase"].map(method => [method, `${METHOD_WORDS[method]}${method === "squash" ? " (default)" : ""}`]) })),
27
+ formRow("Merge when checks pass", "Off by default. When on, a pull request merges by itself once its checks pass on the completed commit. Branches are deleted after merging.", switchInput({ id: "pr-when-green", name: "when-green", label: publishing.mergeWhenGreen ? "On" : "Off", on: publishing.mergeWhenGreen, attrs: { value: "1", "aria-label": "Merge when checks pass" } }), "inline"),
28
+ ]) }) });
29
+ const off = disclose("Turn off pull requests", kitSectionForm({ proof: "pull-requests", action: "/settings/pull-requests", id: "pr-off-form", hidden: { repo: view.repo, act: "off" },
30
+ save: { label: "Turn off", variant: "danger" }, confirm: "Turn off pull requests", body: note("Nothing more is pushed. Open pull requests stay on GitHub.") }));
31
+ return joinHtml([head, status, merging, off]);
21
32
  }
22
33
  const check = view.check;
23
34
  if (check === null)
24
- return html `${head}<section class="card pr-settings"><p><strong>Off</strong></p><p class="meta">An approver can turn on pull requests.</p></section>`;
35
+ return joinHtml([head, settingsSection({ id: "pull-requests", title: "Pull requests", state: " · Off", body: note("An approver can turn on pull requests.") })]);
25
36
  // The checks ask git and GitHub, so they run when asked, never on opening the page (docs/plans/get-side-effects.md).
26
- const checkButton = (label) => postForm("/settings/pull-requests/check", html `<button type="submit"${label === "Check again" ? html ` class="secondary"` : ""}>${label}</button>`, { hidden: { repo: view.repo } });
27
- if (check === "unchecked")
28
- return html `${head}<section class="card pr-settings"><p><strong>Off</strong></p><p class="meta">Check this project's GitHub setup to see what turning on would do.</p>${checkButton("Check setup")}</section>`;
37
+ const checkButton = (label, variant) => postForm("/settings/pull-requests/check", acts(btn(label, { sm: true, type: "submit", variant })), { hidden: { repo: view.repo } });
38
+ if (check === "unchecked") {
39
+ return joinHtml([head, settingsSection({ id: "pull-requests", title: "Pull requests", state: " · Off",
40
+ lead: "Check this project's GitHub setup to see what turning on would do.", body: checkButton("Check setup", "primary") })]);
41
+ }
29
42
  if (!check.ok) {
30
- return html `${head}<section class="card pr-settings"><p><strong>Can’t turn on yet</strong></p><p>${check.message}</p>${checkButton("Check again")}</section>`;
43
+ return joinHtml([head, settingsSection({ id: "pull-requests", title: "Pull requests", state: " · Can’t turn on yet",
44
+ body: joinHtml([notice(check.message), checkButton("Check again", "outline")]) })]);
31
45
  }
32
46
  const plan = check.plan;
33
- return html `${head}${post("on", html `<p><strong>Ready to turn on</strong></p><ul class="pr-terms"><li>Pull requests open on <span class="mono">${plan.githubRepo}</span> into <span class="mono">${plan.base}</span>${plan.account === null ? "" : `, as ${plan.account}`}.</li><li>Complete offers “Complete and open a pull request”, from the exact completed commit. A flow's Pull request zone can open one too.</li><li>Merging squashes and deletes the branch. A person merges with their password; a flow merges only when its zone says so, after a person approved the card.</li></ul>${password}<button type="submit">Turn on pull requests</button>`, { github: plan.githubRepo, base: plan.base })}`;
47
+ return joinHtml([head, settingsSection({ id: "pull-requests", title: "Pull requests", state: " · Ready to turn on",
48
+ body: kitSectionForm({ proof: "pull-requests-on", action: "/settings/pull-requests", id: "pr-on-form", hidden: { repo: view.repo, act: "on", github: plan.githubRepo, base: plan.base },
49
+ save: { label: "Turn on pull requests" }, confirm: "Turn on pull requests",
50
+ body: html `<ul class="k-note"><li>Pull requests open on ${mono(plan.githubRepo)} into ${mono(plan.base)}${plan.account === null ? "" : `, as ${plan.account}`}.</li>\
51
+ <li>Complete offers “Complete and open a pull request”, from the exact completed commit. A flow's Pull request zone can open one too.</li>\
52
+ <li>Merging squashes and deletes the branch. A person merges with their password; a flow merges only when its zone says so, after a person approved the card.</li></ul>` }) })]);
34
53
  }
35
- export const PULL_REQUEST_SETTINGS_CSS = `
36
- .pr-settings { display: grid; gap: .6rem; max-width: 40rem; }
37
- .pr-settings p, .pr-settings h2 { margin: 0; }
38
- .pr-settings label { display: grid; gap: .25rem; }
39
- .pr-settings label input[type=password], .pr-settings select { min-height: 44px; box-sizing: border-box; width: 100%; }
40
- .pr-settings .pr-check { display: flex; align-items: center; gap: .5rem; min-height: 44px; }
41
- .pr-settings button { min-height: 44px; justify-self: start; }
42
- .pr-terms { margin: 0; padding-left: 1.1rem; display: grid; gap: .25rem; }
43
- @media (max-width: 480px) { .pr-settings button { justify-self: stretch; } }
44
- `;
54
+ /** Kept for the console's style bundle (server/chrome.ts); the page itself is kit markup. */
55
+ export const PULL_REQUEST_SETTINGS_CSS = "";
@@ -1,7 +1,9 @@
1
1
  import { type Recipe, type RecipeDocument, type WorkflowPreview } from "./recipes.js";
2
2
  import { type Html } from "./html.js";
3
3
  export declare function recipeFromForm(body: URLSearchParams): RecipeDocument;
4
- export declare function recipeLibraryHtml(starters: Recipe[], saved: Recipe[], project: string | null, revision: number, recent: {
4
+ /** "One-time work" (New flow): the open project's recipes and the starters as rows, each with one "Use this"; then
5
+ * Create a recipe, importing a shared one, and the work recipes made lately. */
6
+ export declare function recipeRowsHtml(starters: Recipe[], saved: Recipe[], project: string | null, projectName: string | null, revision: number, recent: {
5
7
  name: string;
6
8
  href: string;
7
9
  state: string;
@@ -16,4 +18,4 @@ export declare function recipeDefinitionPreviewHtml(preview: WorkflowPreview, re
16
18
  export declare function recipeRunHtml(recipe: Recipe, repo: string, revision: number, problem?: string | null, values?: URLSearchParams, saved?: boolean): Html;
17
19
  export declare function recipeAnswersFromForm(body: URLSearchParams): Map<string, string>;
18
20
  export declare function recipeScript(): string;
19
- export declare const RECIPE_CSS = "\n.recipe-heading { margin: .5rem 0 1.75rem; max-width: 46rem; }\n.recipe-heading h1 { font-size: clamp(1.65rem,3vw,2.5rem); letter-spacing:-.035em; margin:.35rem 0 .6rem; }\n.recipe-kicker { font-size:.7rem; text-transform:uppercase; letter-spacing:.12em; color:var(--muted-foreground); }\n.recipe-grid { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:.8rem; margin:1rem 0 2rem; }\n.recipe-card { border:1px solid var(--border); border-radius:var(--radius); padding:1.1rem; background:var(--card); display:flex; flex-direction:column; gap:.5rem; }\n.recipe-card h3 { font-size:1.05rem; margin:0; letter-spacing:-.015em; }\n.recipe-card p { color:var(--muted-foreground); margin:.2rem 0; font-size:.85rem; line-height:1.5; }\n.recipe-card-link { margin-top:auto; padding-top:.75rem; display:flex; justify-content:space-between; }\n.recipe-editor { max-width:52rem; }.recipe-editor h2 { margin-top:0; }\n.recipe-criterion { border:1px solid var(--border); border-radius:calc(var(--radius) - 3px); padding:.8rem; margin:.8rem 0; }\n.recipe-evidence { display:flex; flex-wrap:wrap; gap:.5rem 1rem; }.recipe-evidence label { display:flex; align-items:center; gap:.4rem; font-size:.8rem; }.recipe-evidence input { width:auto; margin:0; }\n.recipe-options { display:grid; grid-template-columns:1fr 1fr; gap:1rem; }\n.recipe-preview-grid { display:grid; grid-template-columns:minmax(0,1.7fr) minmax(16rem,1fr); gap:1rem; }\n.recipe-steps { padding-left:1.4rem; }.recipe-steps li { padding:.4rem 0 .6rem .3rem; }.recipe-steps p { margin:.3rem 0 0; font-size:.85rem; color:var(--muted-foreground); }\n.recipe-launch { display:grid; grid-template-columns:1fr 1fr; gap:1rem; }.recipe-launch form { margin:0 0 .6rem; }\n.recipe-history a { display:flex; justify-content:space-between; gap:1rem; padding:.6rem 0; border-bottom:1px solid var(--border); }.recipe-history span { color:var(--muted-foreground); }\n.recipe-error { border-color:var(--destructive); }\n.recipe-card[hidden] { display:none; }.recipe-search { max-width:28rem; }.recipe-question { border:1px solid var(--border); border-radius:calc(var(--radius) - 3px); margin:1rem 0; padding:.8rem; min-width:0; }.recipe-run { max-width:42rem; }.recipe-run button { margin-top:1rem; }\n@media(max-width:1000px){.recipe-grid { grid-template-columns:repeat(2,minmax(0,1fr)); }}\n@media(max-width:760px){.recipe-grid,.recipe-options,.recipe-preview-grid,.recipe-launch { grid-template-columns:1fr; }.recipe-heading { margin-bottom:1rem; }.recipe-history a { flex-direction:column; gap:.2rem; }}\n";
21
+ export declare const RECIPE_CSS = "\n.fl-recipe-more { display:grid; gap:12px; padding-top:12px; }.fl-recipe-more .k-disclose[open] > form, .fl-recipe-more .k-disclose[open] > div { margin-top:12px; }.fl-recipe-import { display:grid; gap:12px; margin:0; }\n.fl-recipe-recent { display:grid; }.fl-recipe-recent a { display:flex; justify-content:space-between; gap:12px; min-height:40px; align-items:center; border-bottom:1px solid var(--so-line); color:var(--so-ink); text-decoration:none; font-size:13px; }\n.recipe-heading { margin: .5rem 0 1.75rem; max-width: 46rem; }\n.recipe-heading h1 { font-size: clamp(1.65rem,3vw,2.5rem); letter-spacing:-.035em; margin:.35rem 0 .6rem; }\n.recipe-kicker { font-size:.7rem; text-transform:uppercase; letter-spacing:.12em; color:var(--muted-foreground); }\n.recipe-grid { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:.8rem; margin:1rem 0 2rem; }\n.recipe-card { border:1px solid var(--border); border-radius:var(--radius); padding:1.1rem; background:var(--card); display:flex; flex-direction:column; gap:.5rem; }\n.recipe-card h3 { font-size:1.05rem; margin:0; letter-spacing:-.015em; }\n.recipe-card p { color:var(--muted-foreground); margin:.2rem 0; font-size:.85rem; line-height:1.5; }\n.recipe-card-link { margin-top:auto; padding-top:.75rem; display:flex; justify-content:space-between; }\n.recipe-editor { max-width:52rem; }.recipe-editor h2 { margin-top:0; }\n.recipe-criterion { border:1px solid var(--border); border-radius:calc(var(--radius) - 3px); padding:.8rem; margin:.8rem 0; }\n.recipe-evidence { display:flex; flex-wrap:wrap; gap:.5rem 1rem; }.recipe-evidence label { display:flex; align-items:center; gap:.4rem; font-size:.8rem; }.recipe-evidence input { width:auto; margin:0; }\n.recipe-options { display:grid; grid-template-columns:1fr 1fr; gap:1rem; }\n.recipe-preview-grid { display:grid; grid-template-columns:minmax(0,1.7fr) minmax(16rem,1fr); gap:1rem; }\n.recipe-steps { padding-left:1.4rem; }.recipe-steps li { padding:.4rem 0 .6rem .3rem; }.recipe-steps p { margin:.3rem 0 0; font-size:.85rem; color:var(--muted-foreground); }\n.recipe-launch { display:grid; grid-template-columns:1fr 1fr; gap:1rem; }.recipe-launch form { margin:0 0 .6rem; }\n.recipe-history a { display:flex; justify-content:space-between; gap:1rem; padding:.6rem 0; border-bottom:1px solid var(--border); }.recipe-history span { color:var(--muted-foreground); }\n.recipe-error { border-color:var(--destructive); }\n.recipe-card[hidden] { display:none; }.recipe-search { max-width:28rem; }.recipe-question { border:1px solid var(--border); border-radius:calc(var(--radius) - 3px); margin:1rem 0; padding:.8rem; min-width:0; }.recipe-run { max-width:42rem; }.recipe-run button { margin-top:1rem; }\n@media(max-width:1000px){.recipe-grid { grid-template-columns:repeat(2,minmax(0,1fr)); }}\n@media(max-width:760px){.recipe-grid,.recipe-options,.recipe-preview-grid,.recipe-launch { grid-template-columns:1fr; }.recipe-heading { margin-bottom:1rem; }.recipe-history a { flex-direction:column; gap:.2rem; }}\n";
package/dist/recipe-ui.js CHANGED
@@ -3,6 +3,8 @@ import { composerSchedule, scheduleEditorHtml, scheduleEditorScript } from "./ta
3
3
  import { parseRecipe, workflowSteps, recipeScheduleWords, RecipeError, RECIPE_INPUT_LIMIT } from "./recipes.js";
4
4
  import { ACCEPTANCE_LIMITS } from "./scope.js";
5
5
  import { html, joinHtml, postForm, replaceMarkup } from "./html.js";
6
+ import { templateRow } from "./flow-gallery-ui.js";
7
+ import { acts, btn, count, disclose, field, group, note, textarea } from "./server/kit.js";
6
8
  const hidden = (name, value) => html `<input type="hidden" name="${name}" value="${value}">`;
7
9
  const evidence = [["check", "Passing tests or commands"], ["changed-path", "Changed files"], ["screenshot", "Screenshot"], ["manual-review", "Reviewer assessment"]];
8
10
  export function recipeFromForm(body) {
@@ -53,10 +55,16 @@ export function recipeFromForm(body) {
53
55
  return parseRecipe({ format: "standing-orders-recipe", version: inputs.length ? 2 : 1, ...(inputs.length ? { inputs } : {}), name: body.get("name") ?? "", description: body.get("description") ?? "", goal: body.get("goal") ?? "", outOfScope: body.get("not") || null,
54
56
  touches: (body.get("touches") ?? "").split(/[\n,]/).map(one => one.trim()).filter(Boolean), acceptance, planning: body.get("planning") ?? "auto", deliverable: body.get("deliverable") ?? "branch", schedule: schedule.schedule, costCeilingUsd: ceiling === "" ? null : Number(ceiling) });
55
57
  }
56
- export function recipeLibraryHtml(starters, saved, project, revision, recent) {
57
- const cards = (recipes, reusable = false) => html `<div class="recipe-grid"${reusable && html ` data-saved-recipes`}>${recipes.map(recipe => html `<article class="recipe-card" data-recipe-card data-recipe-search="${recipe.document.name + " " + recipe.document.description}"><p class="recipe-kicker">${recipe.document.deliverable === "report" ? "Report" : recipe.document.schedule === null ? "One-time work" : "Recurring care"}${recipe.document.version === 2 ? ` · ${recipe.document.inputs.length} question${recipe.document.inputs.length === 1 ? "" : "s"}` : ""}</p><h3>${recipe.document.name}</h3><p>${recipe.document.description}</p><a class="recipe-card-link" href="/recipes/${reusable ? "run" : "start"}?recipe=${recipe.id}&revision=${recipe.revision}">${reusable ? "Use recipe" : "Customize recipe"} <span aria-hidden="true">→</span></a>${reusable && html `<a class="meta" href="/recipes/start?recipe=${recipe.id}&revision=${recipe.revision}">Edit a copy</a>`}</article>`)}</div>`;
58
- const savedSection = html `<h2>Your project’s recipes</h2>${saved.length >= 100 && html `<p class="meta">Showing 100 recipes, with recently used recipes first.</p>`}${saved.length ? html `<p class="hint">Your usual work, ready to go. Recently used recipes come first.</p><label class="recipe-search">Find a recipe<input type="search" data-recipe-search-input placeholder="Search your recipes…"></label>${cards(saved, true)}<p data-recipe-empty hidden>No recipes match. Try another search.</p>` : html `<p class="hint">Save work you do often. Subagents with project access can use the same recipe.</p>`}`;
59
- return html `<div class="recipe-heading"><p class="recipe-kicker">Workflows</p><h1>Good work, on repeat.</h1><p class="hint">Save the work you do often. Answer what changes this time. Kick it off.</p><p><a href="/recipes/new" class="new-task">Create a recipe</a> <a href="/flows">Manage scheduled work →</a></p></div>${project === null && html `<div class="card"><strong>Choose a project to get started.</strong><p>Browse recipes now; <a href="/projects">open a project</a> to customize, save, or run one.</p></div>`}${saved.length > 0 && savedSection}<h2>Start here</h2>${cards(starters)}${saved.length === 0 && savedSection}${recent.length > 0 && html `<h2>Recent workflows</h2><div class="card recipe-history">${recent.map(one => html `<a href="${one.href}"><strong>${one.name}</strong><span>${one.state} →</span></a>`)}</div>`}${project !== null && html `<details class="card"><summary>Import a shared recipe</summary><p class="hint">Paste exported recipe JSON. You’ll review it in this project before creating any work.</p>${postForm("/recipes/import", html `<label>Recipe JSON<textarea name="document" rows="7" required maxlength="32768" spellcheck="false"></textarea></label><button type="submit">Review imported recipe</button>`, { projectRevision: String(revision), hidden: { repo: project } })}</details>`}`;
58
+ /** "One-time work" (New flow): the open project's recipes and the starters as rows, each with one "Use this"; then
59
+ * Create a recipe, importing a shared one, and the work recipes made lately. */
60
+ export function recipeRowsHtml(starters, saved, project, projectName, revision, recent) {
61
+ const kind = (recipe) => `${recipe.document.deliverable === "report" ? "Report" : "Code change"} · ${recipe.document.schedule === null ? "runs once as a task" : "repeats on a schedule"}${recipe.document.version === 2 ? ` · ${recipe.document.inputs.length} question${recipe.document.inputs.length === 1 ? "" : "s"}` : ""}`;
62
+ const rowOf = (recipe, reusable) => templateRow({ name: recipe.document.name, promise: recipe.document.description, line: kind(recipe), attrs: html ` data-recipe-card data-recipe="${recipe.id}"`,
63
+ action: project === null ? "" : btn("Use this", { sm: true, href: `/recipes/${reusable ? "run" : "start"}?recipe=${recipe.id}&revision=${recipe.revision}`, attrs: { "aria-label": `Use ${recipe.document.name}` } }) });
64
+ const tools = project === null ? note("Open a project to use, save or import a recipe.")
65
+ : html `<p class="k-acts fl-recipe-acts">${btn("Create a recipe", { sm: true, href: "/recipes/new" })}</p>${disclose("Import a shared recipe", postForm("/recipes/import", html `${field({ id: "fl-recipe-json", label: "Recipe JSON", hint: "Paste exported recipe JSON. You review it in this project before any work is made.", control: textarea({ id: "fl-recipe-json", name: "document", rows: 6, attrs: { required: true, maxlength: 32768, spellcheck: "false" } }) })}${acts(btn("Review imported recipe", { type: "submit" }))}`, { projectRevision: String(revision), hidden: { repo: project }, attrs: { class: "fl-recipe-import" } }))}`;
66
+ const recentRows = recent.length === 0 ? "" : disclose(`Made from recipes lately (${recent.length})`, html `<div class="fl-recipe-recent">${recent.map(one => html `<a href="${one.href}"><span>${one.name}</span><span class="k-note">${one.state}</span></a>`)}</div>`);
67
+ return html `${group(projectName === null ? "One-time work" : `One-time work in ${projectName}`, count(saved.length + starters.length))}<div data-group="recipes">${saved.map(one => rowOf(one, true))}${starters.map(one => rowOf(one, false))}</div><div class="fl-recipe-more">${tools}${recentRows}</div>`;
60
68
  }
61
69
  function criterionHtml(index, criterion, values) {
62
70
  const prefix = `criterion-${index}-`;
@@ -137,6 +145,8 @@ export function recipeScript() {
137
145
  })();`;
138
146
  }
139
147
  export const RECIPE_CSS = `
148
+ .fl-recipe-more { display:grid; gap:12px; padding-top:12px; }.fl-recipe-more .k-disclose[open] > form, .fl-recipe-more .k-disclose[open] > div { margin-top:12px; }.fl-recipe-import { display:grid; gap:12px; margin:0; }
149
+ .fl-recipe-recent { display:grid; }.fl-recipe-recent a { display:flex; justify-content:space-between; gap:12px; min-height:40px; align-items:center; border-bottom:1px solid var(--so-line); color:var(--so-ink); text-decoration:none; font-size:13px; }
140
150
  .recipe-heading { margin: .5rem 0 1.75rem; max-width: 46rem; }
141
151
  .recipe-heading h1 { font-size: clamp(1.65rem,3vw,2.5rem); letter-spacing:-.035em; margin:.35rem 0 .6rem; }
142
152
  .recipe-kicker { font-size:.7rem; text-transform:uppercase; letter-spacing:.12em; color:var(--muted-foreground); }
@@ -189,3 +189,6 @@ export declare class ProtectionUnreadable extends Error {
189
189
  }
190
190
  /** Whether main's protection holds, as a yes/no (release-auto.ts sealAutoGreen): null when it can't be read. */
191
191
  export declare function readUpToDateRequired(run: Exec, gh: string, repo: string, branch?: string): Promise<boolean | null>;
192
+ /** The release's step record (store.ts release_step, v146) through a store opened for each entry: a failure to open or
193
+ * write is swallowed, so recording never stops or changes the release. */
194
+ export declare function releaseProgressRecorder(open: () => Store, done: (store: Store) => void): NonNullable<ReleaseAdapters["progress"]>;
@@ -19,7 +19,7 @@ import { PROTECTION_MINIMUM_CHECKS, PROTECTION_REPORT_ONLY_CHECKS } from "./rele
19
19
  import { activeUpdateWork } from "./desktop-update-gate.js";
20
20
  import { BEFORE_SWAP, gateOwner } from "./release-gate.js";
21
21
  import { findCliLinks, switchCliLinks } from "./release-links.js";
22
- import { databasePath, finishReleaseOperation, requireFreshReleaseApproval, openStore, releaseAuthorityOfTask, releaseCarries, releaseOperations, renewReleaseLease, reserveRelease, runReleaseOperation, Store, releaseIssuerGone, takeReleaseLease, } from "./store.js";
22
+ import { databasePath, finishReleaseOperation, requireFreshReleaseApproval, openStore, releaseAuthorityOfTask, releaseCarries, releaseOperations, renewReleaseLease, reserveRelease, runReleaseOperation, Store, recordReleaseStep, releaseIssuerGone, takeReleaseLease, } from "./store.js";
23
23
  import { acquireDeploymentLock } from "./deploy-lock.js";
24
24
  import { autoReleaseApprovalOf, liveReleaseOf } from "./scope.js";
25
25
  import { recheckReleaseTask, releaseCheckStanding } from "./check-failure.js";
@@ -429,7 +429,7 @@ export function releaseAdapters(options) {
429
429
  // A boundary's one bounded read of GitHub's nightly: plain exec, never the release's long retries (20 s in all).
430
430
  githubReads(run, ciGh));
431
431
  return {
432
- ...(existsSync(database) ? { release } : {}),
432
+ ...(existsSync(database) ? { release, progress: releaseProgressRecorder(() => openStore(database), store => store.close()) } : {}),
433
433
  now: options.now ?? (() => new Date()),
434
434
  sleep: options.sleep ?? sleep,
435
435
  say,
@@ -1471,3 +1471,23 @@ function mainTriggered(run) {
1471
1471
  return bases.length > 0 && bases.every(base => base === "main");
1472
1472
  return false;
1473
1473
  }
1474
+ /** The release's step record (store.ts release_step, v146) through a store opened for each entry: a failure to open or
1475
+ * write is swallowed, so recording never stops or changes the release. */
1476
+ export function releaseProgressRecorder(open, done) {
1477
+ return entry => {
1478
+ let store = null;
1479
+ try {
1480
+ store = open();
1481
+ recordReleaseStep(store, entry);
1482
+ }
1483
+ catch { /* best effort: the release goes on */ }
1484
+ finally {
1485
+ if (store !== null) {
1486
+ try {
1487
+ done(store);
1488
+ }
1489
+ catch { /* best effort */ }
1490
+ }
1491
+ }
1492
+ };
1493
+ }
@@ -16,7 +16,8 @@ import { RELEASE_LIMITS, runRelease } from "./release.js";
16
16
  import { processes, releaseAdapters } from "./release-adapters.js";
17
17
  import { journals, releaseGate } from "./release-gate.js";
18
18
  import { rescueDeployment } from "./deploy-rescue.js";
19
- import { databasePath, openStore, revokeRelease } from "./store.js";
19
+ import { databasePath, elapsedSeconds, latestReleaseStepTask, openStore, releaseProgressFor, revokeRelease } from "./store.js";
20
+ import { dur } from "./design/kit.js";
20
21
  import { carryRefusalWords, proveCarry, setAskEveryReleaseCommit, setUnsandboxedCarry } from "./release-carry.js";
21
22
  import { authenticateApprover, machineActing, personProof, proveCredential } from "./scope.js";
22
23
  import { allowAutoPublish, setReleaseApproval } from "./release-auto.js";
@@ -35,6 +36,12 @@ export const RELEASE_HELP = `toolroll release <branch> — release Toolroll itse
35
36
 
36
37
  Run the same command to resume after a stop. Interrupted deployments recover their saved stage before retrying.
37
38
 
39
+ A branch whose name is one of this command's own words (status, nightly, carry…) is named with --branch <name>
40
+ or after --: \`toolroll release --branch status\`.
41
+
42
+ toolroll release status [<task>] — the steps of a release (the newest one recorded, or the release task named): each
43
+ step's state and how long it took, and where it stopped
44
+
38
45
  toolroll release --release-gate <id> [--yes] — lift the update pause a deployment left before its swap
39
46
 
40
47
  Shows why it is safe first: the pause is that deployment's, its journal stopped before the swap, and the
@@ -98,7 +105,7 @@ A runner's lease is returned with \`toolroll worker release <lease>\`.`;
98
105
  /** A runner lease id (claim.ts mints randomUUID()). */
99
106
  export const LEASE_SHAPED = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
100
107
  export const LEASE_DEPRECATION = "`toolroll release <lease>` is now `toolroll worker release <lease>`; the old form goes in the next minor version.";
101
- const VALUE_FLAGS = new Set(["--repo", "--limit", "--tap", "--release-gate", "--to", "--request", "--as", "--token", "--schedule", "--stage", "--lane-retire"]);
108
+ const VALUE_FLAGS = new Set(["--repo", "--limit", "--tap", "--release-gate", "--to", "--request", "--as", "--token", "--schedule", "--stage", "--lane-retire", "--branch"]);
102
109
  const BARE_FLAGS = new Set(["--new", "--json", "--help", "--yes", "--no-carry", "--start", "--recover-deploy", "--github"]);
103
110
  export async function runReleaseCommand(args, write, deps = {}) {
104
111
  // The flag, not a secret's value that happens to read "--json".
@@ -112,9 +119,23 @@ export async function runReleaseCommand(args, write, deps = {}) {
112
119
  const positional = [];
113
120
  const limits = {};
114
121
  let stage, laneRetire, laneAlias;
122
+ // `--branch <name>` (or `-- <name>`) names the branch outright, so a branch called "status", "nightly" or "carry" is
123
+ // still a branch and never one of this command's own words.
124
+ let branchFlag, literal = false, branchFlags = 0;
125
+ // Only what follows `--` is literal: it never changes how the words before it read.
126
+ const literalArgs = [];
115
127
  let repo, tap, gate, to, request, as, token, scheduleFlag;
116
128
  for (let i = 0; i < args.length; i += 1) {
117
129
  const raw = args[i];
130
+ // After `--`, every argument is a branch name, never a flag or a subcommand.
131
+ if (raw === "--" && !literal) {
132
+ literal = true;
133
+ continue;
134
+ }
135
+ if (literal) {
136
+ literalArgs.push(raw);
137
+ continue;
138
+ }
118
139
  if (BARE_FLAGS.has(raw))
119
140
  continue;
120
141
  // `--lane-alias <lane-id> <new name>`: two values, neither a flag of this command.
@@ -157,6 +178,10 @@ export async function runReleaseCommand(args, write, deps = {}) {
157
178
  stage = value;
158
179
  else if (arg === "--lane-retire")
159
180
  laneRetire = value;
181
+ else if (arg === "--branch") {
182
+ branchFlag = value;
183
+ branchFlags += 1;
184
+ }
160
185
  else {
161
186
  const match = /^([a-z]+)=([0-9]+)$/.exec(value);
162
187
  if (match === null || !(match[1] in RELEASE_LIMITS) || Number(match[2]) < 1)
@@ -174,12 +199,12 @@ export async function runReleaseCommand(args, write, deps = {}) {
174
199
  return 0;
175
200
  }
176
201
  if (gate !== undefined) {
177
- if (positional.length > 0 || args.some(arg => ["--repo", "--limit", "--tap", "--new"].includes(arg)))
202
+ if (positional.length > 0 || branchFlag !== undefined || literalArgs.length > 0 || args.some(arg => ["--repo", "--limit", "--tap", "--new"].includes(arg)))
178
203
  return refuse("usage", "--release-gate <id> takes no branch or release options.", 2);
179
204
  return runGateRelease(gate, bare.has("--yes"), write, json, deps.gateWorld ?? installedGateWorld());
180
205
  }
181
206
  if (bare.has("--recover-deploy")) {
182
- if (positional.length > 0 || args.some(arg => ["--repo", "--limit", "--tap", "--new", "--release-gate"].includes(arg.split("=")[0])))
207
+ if (positional.length > 0 || branchFlag !== undefined || literalArgs.length > 0 || args.some(arg => ["--repo", "--limit", "--tap", "--new", "--release-gate"].includes(arg.split("=")[0])))
183
208
  return refuse("usage", "--recover-deploy takes only --stage <dir>, --yes and --json.", 2);
184
209
  return runRescue(stage, bare.has("--yes"), write, json, deps.rescueWorld ?? installedRescueWorld(deps.cwd ?? process.cwd()));
185
210
  }
@@ -194,15 +219,29 @@ export async function runReleaseCommand(args, write, deps = {}) {
194
219
  return refuse("usage", "Use one of --github, --lane-alias or --lane-retire, without --schedule or --start.", 2);
195
220
  return runGithubNightlyCommand({ repo: repo === undefined ? undefined : resolve(deps.cwd ?? process.cwd(), repo), as, token, alias: laneAlias, retire: laneRetire }, write, json, deps);
196
221
  }
197
- if (positional[0] === "nightly" && positional.length === 1)
222
+ const named = branchFlag !== undefined || literalArgs.length > 0;
223
+ // One branch, named one way: never a guess between two.
224
+ if (branchFlags > 1 || literalArgs.length > 1 || (branchFlag !== undefined && (positional.length > 0 || literalArgs.length > 0)) || (literalArgs.length > 0 && positional.length > 0)) {
225
+ return refuse("usage", "Name the branch once: `toolroll release <branch>`, `--branch <name>` or `-- <name>`, never two of them.", 2);
226
+ }
227
+ if (branchFlag !== undefined && branchFlag === "")
228
+ return refuse("usage", "--branch takes a branch name.", 2);
229
+ // `release status [<task>]`: the steps recorded for a release. A branch named "status" is released with
230
+ // `--branch status` (or `-- status`).
231
+ if (!named && positional[0] === "status" && positional.length <= 2) {
232
+ if (args.some(arg => VALUE_FLAGS.has(arg.split("=")[0]) || ["--new", "--no-carry", "--start"].includes(arg)))
233
+ return refuse("usage", "`toolroll release status [<task>]` takes only --json.", 2);
234
+ return runStatusCommand(positional[1], write, json, deps);
235
+ }
236
+ if (!named && positional[0] === "nightly" && positional.length === 1)
198
237
  return runNightlyCommand({ repo: repo === undefined ? undefined : resolve(deps.cwd ?? process.cwd(), repo), as, token, schedule: scheduleFlag, start: bare.has("--start") }, write, json, deps);
199
- if (["carry", "revoke", "ask-every-commit", "unsandboxed-carry", "auto-approve", "publish-ok"].includes(positional[0] ?? "") && positional.length === 2) {
238
+ if (!named && ["carry", "revoke", "ask-every-commit", "unsandboxed-carry", "auto-approve", "publish-ok"].includes(positional[0] ?? "") && positional.length === 2) {
200
239
  return runAuthorityCommand(positional[0], positional[1], { to, request, as, token, repo: repo === undefined ? undefined : resolve(deps.cwd ?? process.cwd(), repo) }, write, json, deps);
201
240
  }
202
- if (positional.length !== 1)
241
+ if (!named && positional.length !== 1)
203
242
  return refuse("usage", `Which branch? \`toolroll release <branch>\`.\n${RELEASE_HELP}`, 2);
204
- const branch = positional[0];
205
- if (LEASE_SHAPED.test(branch)) {
243
+ const branch = branchFlag ?? literalArgs[0] ?? positional[0];
244
+ if (!named && LEASE_SHAPED.test(branch)) {
206
245
  const releaseOptions = args.filter(arg => VALUE_FLAGS.has(arg) || arg === "--new");
207
246
  if (releaseOptions.length > 0)
208
247
  return refuse("ambiguous", `${branch} looks like a lease id, but ${releaseOptions[0]} is a release option. Return a lease with \`toolroll worker release <lease>\`; a branch shaped like a lease id can't be released.`, 2);
@@ -519,3 +558,36 @@ async function runGithubNightlyCommand(flags, write, json, deps) {
519
558
  store.close();
520
559
  }
521
560
  }
561
+ /** `toolroll release status [<task>]` (read-only): the release's recorded steps (store.ts release_step, v146). */
562
+ async function runStatusCommand(task, write, json, deps) {
563
+ const command = "release status";
564
+ const refuse = (reason, message, code) => { write(json ? checkedEnvelopeJson({ ok: false, command, reason, message }) : message); return code; };
565
+ const database = deps.database ?? databasePath(process.env, homedir());
566
+ if (!existsSync(database))
567
+ return refuse("no-plane", "This machine has no Toolroll database.", 1);
568
+ const store = openStore(database);
569
+ try {
570
+ const now = (deps.now ?? (() => new Date()))();
571
+ const taskId = task ?? latestReleaseStepTask(store);
572
+ if (taskId === null)
573
+ return refuse("no-release", "No release has recorded its steps yet.", 1);
574
+ const progress = releaseProgressFor(store, taskId);
575
+ if (progress === null)
576
+ return refuse("no-release", `No release steps are recorded for ${taskId}.`, 1);
577
+ const steps = progress.steps.map(one => ({ ...one, seconds: elapsedSeconds(one.startedAt, one.endedAt, now) }));
578
+ if (json) {
579
+ write(checkedEnvelopeJson({ ok: true, command, task: taskId, release: { ...progress, steps } }));
580
+ return 0;
581
+ }
582
+ const word = { running: "running", waiting: "waiting", done: "done", failed: "failed" };
583
+ write(`Release ${progress.version} (${progress.tag})${progress.pr === null ? "" : ` · pull request #${progress.pr}`} · task ${taskId}`);
584
+ for (const one of steps)
585
+ write(` ${one.step.padEnd(9)} ${word[one.state].padEnd(8)} ${one.seconds === null ? "–" : dur(one.seconds)}${one.detail === null ? "" : ` ${one.detail}`}`);
586
+ if (progress.stopped !== null)
587
+ write(`Stopped at ${progress.stopped.step}: ${progress.stopped.message}`);
588
+ return 0;
589
+ }
590
+ finally {
591
+ store.close();
592
+ }
593
+ }
package/dist/release.d.ts CHANGED
@@ -48,6 +48,13 @@ export declare const REQUIRED_PR_CHECKS_WORKFLOW: Readonly<Record<typeof REQUIRE
48
48
  export declare const PROTECTION_REPORT_ONLY_CHECKS: readonly string[];
49
49
  /** The checks main's protection must require at minimum: every required check that isn't report-only. */
50
50
  export declare const PROTECTION_MINIMUM_CHECKS: readonly string[];
51
+ /**
52
+ * Pull-request checks a release never waits for or fails on, by exact name and the workflow file that defines them:
53
+ * youiyoux's UI drift check (src/design/KIT.md), which fails a pull request that adds design drift but says nothing
54
+ * about whether a build is safe to release. Only a GitHub Actions run of that name from that workflow is left out; the
55
+ * same name from anywhere else is judged like any other check. Never a required check.
56
+ */
57
+ export declare const ADVISORY_PR_CHECKS: Readonly<Record<string, string>>;
51
58
  /**
52
59
  * Main's protection as the plane's gh reads it (release-adapters.ts readMainProtection): `effective` only when one layer
53
60
  * (classic protection or one active ruleset) requires up-to-date branches, binds every PROTECTION_MINIMUM_CHECKS check to GitHub Actions,
@@ -393,6 +400,18 @@ export type AutoReleaseAdapter = {
393
400
  export type ReleaseAdapters = {
394
401
  /** One approval per release (release-once.md); absent on a plane that can't carry. */
395
402
  release?: ReleaseAuthorityAdapter;
403
+ /** Where each step's start, end and outcome are recorded (store.ts release_step, v146) for the Release view and
404
+ * `toolroll release status`. Best effort: a failure here never stops or changes the release. */
405
+ progress?: (entry: {
406
+ taskId: string;
407
+ tag: string;
408
+ version: string;
409
+ pr: number | null;
410
+ step: ReleaseStep;
411
+ state: "running" | "done" | "failed" | "waiting";
412
+ at: Date;
413
+ detail?: string | null;
414
+ }) => void;
396
415
  now(): Date;
397
416
  sleep(ms: number): Promise<void>;
398
417
  say(line: string): void;
@@ -731,4 +750,6 @@ export declare function checkFailedWords(input: {
731
750
  }): string;
732
751
  /** Whether GitHub's workflow-run `path` names exactly `file` (it may carry the ref it ran from, after an `@`). */
733
752
  export declare function workflowIs(path: string | null | undefined, file: string): boolean;
753
+ /** How a person names this branch to `toolroll release` again. */
754
+ export declare function releaseBranchArg(branch: string): string;
734
755
  export {};