toolroll 0.9.92 → 0.9.94

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 (341) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/dist/activity-line.d.ts +7 -4
  3. package/dist/activity-line.js +9 -7
  4. package/dist/agent-fence.d.ts +4 -0
  5. package/dist/agent-fence.js +4 -1
  6. package/dist/approval-authority.d.ts +7 -1
  7. package/dist/approval-authority.js +11 -2
  8. package/dist/approval-password.d.ts +31 -0
  9. package/dist/approval-password.js +26 -0
  10. package/dist/assignment-status.d.ts +1 -1
  11. package/dist/assignment-ui.js +3 -3
  12. package/dist/assignment.js +16 -3
  13. package/dist/boot-identity.d.ts +2 -1
  14. package/dist/boot-identity.js +6 -4
  15. package/dist/bounded-spawn.d.ts +57 -0
  16. package/dist/bounded-spawn.js +220 -0
  17. package/dist/browser/terminal.js +42 -42
  18. package/dist/browser/workspace.css +2 -2
  19. package/dist/browser/workspace.js +26 -26
  20. package/dist/browser-crew.js +2 -2
  21. package/dist/browser-shell.d.ts +1 -1
  22. package/dist/browser-shell.js +2 -2
  23. package/dist/browser-workspace.d.ts +48 -7
  24. package/dist/browser-workspace.js +3 -3
  25. package/dist/builder-restart.d.ts +21 -0
  26. package/dist/builder-restart.js +50 -0
  27. package/dist/builder.d.ts +4 -0
  28. package/dist/builder.js +73 -25
  29. package/dist/chat-acceptance.js +1 -1
  30. package/dist/chat-actions.d.ts +12 -2
  31. package/dist/chat-actions.js +25 -2
  32. package/dist/chat-channel.js +11 -0
  33. package/dist/chat-decide.d.ts +7 -2
  34. package/dist/chat-decide.js +37 -9
  35. package/dist/chat-quiet.js +3 -3
  36. package/dist/chat-voice.d.ts +1 -1
  37. package/dist/chat-voice.js +2 -2
  38. package/dist/check-facts.d.ts +126 -0
  39. package/dist/check-facts.js +318 -0
  40. package/dist/check-failure.d.ts +22 -3
  41. package/dist/check-failure.js +57 -13
  42. package/dist/check-levels-ui.d.ts +4 -0
  43. package/dist/check-levels-ui.js +10 -1
  44. package/dist/check-load.d.ts +85 -0
  45. package/dist/check-load.js +147 -0
  46. package/dist/check-only-standing.d.ts +36 -0
  47. package/dist/check-only-standing.js +39 -0
  48. package/dist/check-only.d.ts +15 -0
  49. package/dist/check-only.js +99 -0
  50. package/dist/check-report.d.ts +7 -0
  51. package/dist/check-report.js +4 -3
  52. package/dist/check-stamp.d.ts +46 -0
  53. package/dist/check-stamp.js +93 -0
  54. package/dist/checkout-cleanup.d.ts +36 -5
  55. package/dist/checkout-cleanup.js +166 -14
  56. package/dist/cli-help.d.ts +4 -1
  57. package/dist/cli-help.js +47 -30
  58. package/dist/cli.js +16 -1
  59. package/dist/code-extract.d.ts +44 -0
  60. package/dist/code-extract.js +88 -0
  61. package/dist/code-map-acts.d.ts +24 -0
  62. package/dist/code-map-acts.js +53 -0
  63. package/dist/code-map-admission.d.ts +52 -0
  64. package/dist/code-map-admission.js +295 -0
  65. package/dist/code-map-build.d.ts +63 -0
  66. package/dist/code-map-build.js +415 -0
  67. package/dist/code-map-cli.d.ts +153 -0
  68. package/dist/code-map-cli.js +103 -0
  69. package/dist/code-map-main.d.ts +1 -0
  70. package/dist/code-map-main.js +48 -0
  71. package/dist/code-map-read.d.ts +106 -0
  72. package/dist/code-map-read.js +171 -0
  73. package/dist/code-map-report.d.ts +37 -0
  74. package/dist/code-map-report.js +95 -0
  75. package/dist/code-map-shape.d.ts +172 -0
  76. package/dist/code-map-shape.js +327 -0
  77. package/dist/code-map-store.d.ts +136 -0
  78. package/dist/code-map-store.js +233 -0
  79. package/dist/coding-provider.d.ts +2 -0
  80. package/dist/coding-provider.js +3 -2
  81. package/dist/coding-update.js +3 -2
  82. package/dist/coding-workspace.js +4 -4
  83. package/dist/console-chrome.js +2 -2
  84. package/dist/containment.d.ts +5 -0
  85. package/dist/containment.js +14 -1
  86. package/dist/contracts/chat-actions.d.ts +9 -0
  87. package/dist/contracts/chat-actions.js +2 -0
  88. package/dist/contracts/cli.d.ts +52 -0
  89. package/dist/contracts/cli.js +4 -0
  90. package/dist/contracts/console-api.d.ts +12 -5
  91. package/dist/contracts/console-api.js +12 -5
  92. package/dist/contracts/gateway-tools.d.ts +17 -0
  93. package/dist/contracts/gateway-tools.js +3 -1
  94. package/dist/contracts/lead-tools.d.ts +61 -0
  95. package/dist/contracts/lead-tools.js +10 -1
  96. package/dist/contracts/project-knowledge.d.ts +38 -5
  97. package/dist/contracts/project-knowledge.js +11 -5
  98. package/dist/contracts/project-memory.d.ts +3 -1
  99. package/dist/contracts/project-memory.js +3 -2
  100. package/dist/contracts/release-check.d.ts +25 -1
  101. package/dist/contracts/release-check.js +19 -0
  102. package/dist/contracts/storage.d.ts +14 -0
  103. package/dist/contracts/storage.js +3 -1
  104. package/dist/contracts/store-json.d.ts +2 -0
  105. package/dist/contracts/youiyoux-request.d.ts +1 -1
  106. package/dist/contracts/youiyoux-request.js +1 -1
  107. package/dist/desktop-access.js +6 -2
  108. package/dist/desktop-update.d.ts +2 -2
  109. package/dist/desktop-update.js +13 -9
  110. package/dist/dispatch.js +18 -15
  111. package/dist/dispose.js +8 -2
  112. package/dist/easy-start.js +2 -3
  113. package/dist/eval-hooks.d.ts +55 -0
  114. package/dist/eval-hooks.js +227 -4
  115. package/dist/evals.js +2 -1
  116. package/dist/fast-start.d.ts +136 -0
  117. package/dist/fast-start.js +431 -0
  118. package/dist/fast-terms.d.ts +9 -0
  119. package/dist/fast-terms.js +23 -0
  120. package/dist/fenced-check-coverage.d.ts +62 -0
  121. package/dist/fenced-check-coverage.js +54 -0
  122. package/dist/first-run.js +1 -1
  123. package/dist/flaky-ci.d.ts +31 -0
  124. package/dist/flaky-ci.js +149 -0
  125. package/dist/flaky-cli.d.ts +60 -0
  126. package/dist/flaky-cli.js +60 -0
  127. package/dist/flaky.d.ts +83 -0
  128. package/dist/flaky.js +264 -0
  129. package/dist/flow-card-task.js +3 -0
  130. package/dist/flow-triggers.d.ts +1 -1
  131. package/dist/flow-triggers.js +1 -1
  132. package/dist/github-import.d.ts +62 -0
  133. package/dist/github-import.js +146 -0
  134. package/dist/guides.js +15 -10
  135. package/dist/invoke.d.ts +5 -1
  136. package/dist/invoke.js +49 -14
  137. package/dist/job-object-helper.ps1 +10 -3
  138. package/dist/keymap.d.ts +41 -0
  139. package/dist/keymap.js +51 -0
  140. package/dist/knowledge-cli.js +1 -1
  141. package/dist/knowledge-ui.d.ts +21 -4
  142. package/dist/knowledge-ui.js +75 -7
  143. package/dist/laptop-background.d.ts +16 -0
  144. package/dist/laptop-background.js +108 -2
  145. package/dist/lead-context.js +1 -1
  146. package/dist/lead-contract.d.ts +1 -1
  147. package/dist/lead-contract.js +2 -2
  148. package/dist/lead-progress.js +3 -0
  149. package/dist/lead-status.d.ts +2 -0
  150. package/dist/lead-status.js +6 -2
  151. package/dist/lead-tools.js +51 -2
  152. package/dist/lead.js +1 -1
  153. package/dist/lesson-notes.d.ts +81 -0
  154. package/dist/lesson-notes.js +336 -0
  155. package/dist/limits-ui.js +1 -1
  156. package/dist/local-owner.d.ts +1 -1
  157. package/dist/local-owner.js +11 -0
  158. package/dist/machine-check.d.ts +16 -0
  159. package/dist/machine-check.js +26 -0
  160. package/dist/machine-identity.d.ts +75 -0
  161. package/dist/machine-identity.js +170 -0
  162. package/dist/mcp-core.js +11 -1
  163. package/dist/memory-cli.d.ts +300 -1
  164. package/dist/memory-cli.js +62 -9
  165. package/dist/memory-pass.d.ts +16 -0
  166. package/dist/memory-pass.js +67 -3
  167. package/dist/memory-tidy.d.ts +181 -0
  168. package/dist/memory-tidy.js +523 -0
  169. package/dist/memory-use.d.ts +35 -0
  170. package/dist/memory-use.js +109 -0
  171. package/dist/merge-checklist.d.ts +55 -0
  172. package/dist/merge-checklist.js +182 -0
  173. package/dist/needs-you.d.ts +32 -13
  174. package/dist/needs-you.js +108 -49
  175. package/dist/nightly-github.d.ts +2 -0
  176. package/dist/nightly-github.js +4 -4
  177. package/dist/operate-remote-arguments.js +6 -1
  178. package/dist/operate-remote.js +3 -1
  179. package/dist/operate.d.ts +6 -1
  180. package/dist/operate.js +540 -70
  181. package/dist/phase-routing.js +2 -2
  182. package/dist/plain-schema.d.ts +23 -0
  183. package/dist/plain-schema.js +94 -0
  184. package/dist/plain-words.d.ts +20 -0
  185. package/dist/plain-words.js +66 -0
  186. package/dist/planner.js +2 -1
  187. package/dist/process-liveness.d.ts +2 -0
  188. package/dist/process-liveness.js +61 -17
  189. package/dist/project-delete-ui.d.ts +7 -2
  190. package/dist/project-delete-ui.js +12 -1
  191. package/dist/project-delete.js +13 -2
  192. package/dist/project-knowledge.d.ts +18 -11
  193. package/dist/project-knowledge.js +39 -9
  194. package/dist/project-learning.d.ts +2 -1
  195. package/dist/project-learning.js +15 -45
  196. package/dist/project-memory.d.ts +5 -0
  197. package/dist/project-memory.js +22 -2
  198. package/dist/proof.js +30 -5
  199. package/dist/provider-auth.js +1 -1
  200. package/dist/publish.js +9 -1
  201. package/dist/pull-request-flow.js +5 -0
  202. package/dist/push.d.ts +26 -3
  203. package/dist/push.js +55 -15
  204. package/dist/release-adapters.d.ts +9 -0
  205. package/dist/release-adapters.js +87 -1
  206. package/dist/release-approval.d.ts +191 -0
  207. package/dist/release-approval.js +382 -0
  208. package/dist/release-auto.js +26 -17
  209. package/dist/release-card.d.ts +74 -6
  210. package/dist/release-card.js +179 -23
  211. package/dist/release-carry.d.ts +7 -2
  212. package/dist/release-carry.js +39 -9
  213. package/dist/release-cli.d.ts +4 -0
  214. package/dist/release-cli.js +38 -23
  215. package/dist/release-coverage.js +1 -1
  216. package/dist/release-next.d.ts +48 -0
  217. package/dist/release-next.js +161 -0
  218. package/dist/release.d.ts +50 -1
  219. package/dist/release.js +118 -11
  220. package/dist/remote-destination.d.ts +36 -0
  221. package/dist/remote-destination.js +217 -0
  222. package/dist/remote-main.d.ts +48 -0
  223. package/dist/remote-main.js +84 -5
  224. package/dist/repository-context.d.ts +3 -2
  225. package/dist/repository-context.js +112 -70
  226. package/dist/restart-certification.d.ts +1 -0
  227. package/dist/restart-certification.js +8 -6
  228. package/dist/result-acts.d.ts +29 -21
  229. package/dist/result-acts.js +36 -25
  230. package/dist/result-follow-ups.d.ts +2 -2
  231. package/dist/result-follow-ups.js +18 -4
  232. package/dist/result-review.d.ts +1 -1
  233. package/dist/route-change-check.d.ts +4 -0
  234. package/dist/route-change-check.js +13 -0
  235. package/dist/route-change.d.ts +22 -0
  236. package/dist/route-change.js +89 -0
  237. package/dist/route-hooks.js +3 -2
  238. package/dist/route-results.d.ts +6 -1
  239. package/dist/route-results.js +14 -5
  240. package/dist/run-closeout.d.ts +1 -1
  241. package/dist/run-closeout.js +7 -5
  242. package/dist/runner-process.js +3 -2
  243. package/dist/runner.js +2 -1
  244. package/dist/scope.d.ts +6 -1
  245. package/dist/scope.js +22 -9
  246. package/dist/serve.d.ts +1 -1
  247. package/dist/serve.js +24 -18
  248. package/dist/server/chat.js +14 -12
  249. package/dist/server/chrome.d.ts +1 -1
  250. package/dist/server/chrome.js +26 -35
  251. package/dist/server/console.css +23 -0
  252. package/dist/server/guards.js +4 -0
  253. package/dist/server/home.d.ts +6 -0
  254. package/dist/server/home.js +21 -13
  255. package/dist/server/http.js +1 -1
  256. package/dist/server/kit.js +1 -1
  257. package/dist/server/merge-ready-view.d.ts +10 -0
  258. package/dist/server/merge-ready-view.js +30 -0
  259. package/dist/server/pages-assets.d.ts +1 -0
  260. package/dist/server/pages-assets.js +8 -5
  261. package/dist/server/pages.js +103 -82
  262. package/dist/server/render-chat.js +2 -2
  263. package/dist/server/render-code-map.d.ts +30 -0
  264. package/dist/server/render-code-map.js +137 -0
  265. package/dist/server/render-flaky.d.ts +20 -0
  266. package/dist/server/render-flaky.js +59 -0
  267. package/dist/server/render-history.js +1 -1
  268. package/dist/server/render-new-task.d.ts +26 -1
  269. package/dist/server/render-new-task.js +30 -5
  270. package/dist/server/render-pages.d.ts +1 -175
  271. package/dist/server/render-pages.js +38 -313
  272. package/dist/server/render-projects.d.ts +2 -1
  273. package/dist/server/render-projects.js +9 -7
  274. package/dist/server/render-release.d.ts +19 -0
  275. package/dist/server/render-release.js +84 -0
  276. package/dist/server/render-results.d.ts +2 -2
  277. package/dist/server/render-results.js +71 -73
  278. package/dist/server/render-tasks.d.ts +13 -2
  279. package/dist/server/render-tasks.js +156 -139
  280. package/dist/server/retired-routes.d.ts +17 -0
  281. package/dist/server/retired-routes.js +53 -0
  282. package/dist/server/route-table.d.ts +4 -1
  283. package/dist/server/route-table.js +52 -13
  284. package/dist/server/settings.js +129 -22
  285. package/dist/server/task-views.js +61 -26
  286. package/dist/server/tasks.d.ts +17 -6
  287. package/dist/server/tasks.js +273 -338
  288. package/dist/server/welcome.js +1 -1
  289. package/dist/settings-groups-ui.js +1 -1
  290. package/dist/setup-guide.d.ts +2 -2
  291. package/dist/stop-proof.d.ts +67 -0
  292. package/dist/stop-proof.js +295 -0
  293. package/dist/storage-sweep-process.js +1 -1
  294. package/dist/storage-sweep.d.ts +5 -3
  295. package/dist/storage-sweep.js +30 -9
  296. package/dist/storage-ui.js +28 -17
  297. package/dist/storage.d.ts +27 -2
  298. package/dist/storage.js +39 -2
  299. package/dist/store.d.ts +148 -34
  300. package/dist/store.js +626 -177
  301. package/dist/surface.js +46 -5
  302. package/dist/sweep-quarantine.d.ts +1 -0
  303. package/dist/sweep-quarantine.js +6 -4
  304. package/dist/tamper-scan.d.ts +4 -0
  305. package/dist/tamper-scan.js +90 -0
  306. package/dist/task-aliases.d.ts +5 -0
  307. package/dist/task-aliases.js +9 -0
  308. package/dist/task-control.js +4 -1
  309. package/dist/task-outcome-cli.d.ts +2 -0
  310. package/dist/task-outcome-cli.js +36 -17
  311. package/dist/task-sizing.d.ts +5 -0
  312. package/dist/task-sizing.js +2 -0
  313. package/dist/task-source.d.ts +7 -0
  314. package/dist/task-source.js +14 -0
  315. package/dist/task-status.d.ts +9 -0
  316. package/dist/task-status.js +40 -49
  317. package/dist/teardown-git.d.ts +16 -0
  318. package/dist/teardown-git.js +63 -5
  319. package/dist/telegram-progress.js +5 -3
  320. package/dist/telegram-status.js +6 -4
  321. package/dist/test-infra.d.ts +8 -0
  322. package/dist/test-infra.js +18 -0
  323. package/dist/tidy.js +1 -1
  324. package/dist/toolroll-update-ui.js +2 -2
  325. package/dist/toolroll-update.js +18 -5
  326. package/dist/tui-contract.d.ts +24 -0
  327. package/dist/tui-source.js +67 -8
  328. package/dist/tui.js +51 -51
  329. package/dist/vanished-retry.d.ts +30 -0
  330. package/dist/vanished-retry.js +174 -0
  331. package/dist/work-index.d.ts +2 -0
  332. package/dist/work-index.js +82 -70
  333. package/dist/work-summary.d.ts +2 -2
  334. package/dist/work-summary.js +2 -0
  335. package/dist/workspace-ui.d.ts +4 -2
  336. package/dist/workspace-ui.js +28 -22
  337. package/dist/worktree.d.ts +12 -3
  338. package/dist/worktree.js +68 -13
  339. package/package.json +1 -1
  340. package/dist/server/render-next.d.ts +0 -63
  341. package/dist/server/render-next.js +0 -109
@@ -0,0 +1,54 @@
1
+ /** Cases that may stay skipped with no CI case behind them, each with why. Product code, reviewed like it: never a file a
2
+ * release's commits could carry. Empty until a measured run gives a reason. */
3
+ export const FENCED_SKIP_EXEMPTIONS = Object.freeze([]);
4
+ export const unitCaseIdentity = (file, name) => `unit:${file}::${name}`;
5
+ /**
6
+ * Every case in the macOS CI's reports, by the identity the release check's own reporter gives it (test/release-reporter.ts:
7
+ * `suite > test`; Vitest's JSON `fullName` joins with spaces, so it can't be matched as it stands). A name twice in a file
8
+ * passes only if every one of them did. `root` is the checkout the reports name files under.
9
+ */
10
+ export function macosCases(reports, root) {
11
+ const cases = new Map();
12
+ for (const report of reports) {
13
+ for (const file of report.testResults ?? []) {
14
+ if (typeof file.name !== "string" || !file.name.startsWith(`${root}/`))
15
+ continue;
16
+ const relative = file.name.slice(root.length + 1);
17
+ for (const one of file.assertionResults ?? []) {
18
+ if (typeof one.title !== "string")
19
+ continue;
20
+ const ancestors = Array.isArray(one.ancestorTitles) ? one.ancestorTitles.filter((title) => typeof title === "string" && title !== "") : [];
21
+ const identity = unitCaseIdentity(relative, [...ancestors, one.title].join(" > "));
22
+ cases.set(identity, (cases.get(identity) ?? true) && one.status === "passed");
23
+ }
24
+ }
25
+ }
26
+ return cases;
27
+ }
28
+ /** Every case a check's report says it skipped: unit cases and journeys alike. */
29
+ export function skippedCases(report) {
30
+ const units = (report.units?.identities ?? []).filter(one => one.state === "skipped").map(one => unitCaseIdentity(one.file, one.name));
31
+ const journeys = report.journeys.flatMap(run => run.groups.flatMap(group => group.journeys.filter(one => one.state === "skipped").map(one => `journey:${run.script}:${run.kind}:${group.group}::${one.name}`)));
32
+ return [...new Set([...units, ...journeys])].sort();
33
+ }
34
+ /** Each skipped case: passed in that commit's macOS CI, reviewed as exempt, or not covered. */
35
+ export function skipCoverage(skipped, macos, exemptions = FENCED_SKIP_EXEMPTIONS) {
36
+ const reviewed = new Set(exemptions.map(one => one.identity));
37
+ const result = { covered: [], exempt: [], uncovered: [] };
38
+ for (const identity of skipped)
39
+ (macos.get(identity) === true ? result.covered : reviewed.has(identity) ? result.exempt : result.uncovered).push(identity);
40
+ return result;
41
+ }
42
+ /** The narrowest level a measured check fully covers. No level covered: `unsandboxed`, as today, with a person's yes. */
43
+ export function measuredLevel(measurements) {
44
+ for (const level of ["sandboxed", "sign-ins"])
45
+ if (measurements.some(one => one.level === level && one.passed && one.uncovered === 0))
46
+ return level;
47
+ return "unsandboxed";
48
+ }
49
+ /** Why a sandboxed check can't stand for this release, in words a person can act on. */
50
+ export function uncoveredWords(uncovered) {
51
+ const named = uncovered.slice(0, 5).map(one => one.replace(/^unit:/, "")).join("; ");
52
+ return `${uncovered.length} ${uncovered.length === 1 ? "case" : "cases"} skipped inside the sandbox ${uncovered.length === 1 ? "isn't" : "aren't"} covered by this commit's macOS CI (${named}${uncovered.length > 5 ? "; and more" : ""}). `
53
+ + "Widen the project's check access (`toolroll project check-access`, which takes a password) and approve the release again at that level";
54
+ }
package/dist/first-run.js CHANGED
@@ -145,7 +145,7 @@ export function firstTaskJourney(read, approved, built = false) {
145
145
  : read.stage === "checking" || read.stage === "reviewing" ? { index: 3, stuck: false }
146
146
  : read.stage === "building" ? { index: 2, stuck: false }
147
147
  // Waiting on nothing a person does: the build finished and is settling.
148
- : read.stage === "waiting" ? { index: built ? 3 : 2, stuck: false }
148
+ : read.stage === "waiting" ? { index: built ? 3 : approved ? 2 : 0, stuck: false }
149
149
  : read.stage === "needs-you" && read.need === "approval" ? { index: 1, stuck: false }
150
150
  : read.stage === "planning" ? { index: 0, stuck: false }
151
151
  : read.stage === "queued" ? { index: approved ? 2 : 0, stuck: false }
@@ -0,0 +1,31 @@
1
+ import { type GhRead } from "./nightly-github.js";
2
+ import type { Store } from "./store.js";
3
+ /** How often a project is read at most. */
4
+ export declare const CI_READ_EVERY_MS: number;
5
+ /** The service cursor a project's reads keep (a cursor key can't hold a path). */
6
+ export declare const ciCursorKey: (repo: string) => string;
7
+ /** The GitHub repositories this project's records name: the nightly's opt-in and its failed-checks triggers. */
8
+ export declare function ciRepositoriesOf(store: Store, repo: string): string[];
9
+ export type CiPassResult = {
10
+ read: number;
11
+ observed: number;
12
+ rejected: number;
13
+ };
14
+ /** Pair one run's attempts n-1 and n. A pair already decided isn't read again. */
15
+ export declare function observeCiRun(store: Store, input: {
16
+ repo: string;
17
+ slug: string;
18
+ run: number;
19
+ sha: string | null;
20
+ gh: GhRead;
21
+ now: Date;
22
+ }): Promise<{
23
+ observed: number;
24
+ rejected: number;
25
+ }>;
26
+ /** One project's pass over its known runs. A read that fails records nothing and is tried on the next pass. */
27
+ export declare function observeCiAttempts(store: Store, repo: string, gh: GhRead, now: Date): Promise<CiPassResult>;
28
+ /** The storage sweep's own step: CI rows older than the retention window go; nothing else deletes them. */
29
+ export declare function pruneCiObservations(store: Store, now: Date): number;
30
+ /** Pairings refused since `since` for these repositories. */
31
+ export declare function ciNotPaired(store: Store, repositories: readonly string[], since: string): number;
@@ -0,0 +1,149 @@
1
+ /**
2
+ * GitHub CI attempts as mixed outcomes (docs/plans/learning-loops.md, "A CI observation"): read-only, lane level, never
3
+ * mixed with local pairs, no test name (logs aren't fetched). Only workflow runs Toolroll already knows by id are read:
4
+ * the watched branch's push runs (`branch_check_run`) and the nightly's runs (`github_nightly_watch`). PR runs aren't
5
+ * read: Toolroll records no run id for them.
6
+ *
7
+ * For a run on attempt n > 1, attempts n-1 and n are read with the exact-attempt reader and paired per lane. A pairing
8
+ * needs exactly one job per lane in each attempt, both attempts read whole, the same run and commit, an earlier
9
+ * `failure` that got past setup and a later `success`. Anything else is rejected and counted, never guessed.
10
+ *
11
+ * The only writer of `ci_flaky_observation` and `ci_pairing_rejected`. Nothing here reruns anything or changes a check,
12
+ * a verdict or an approval. A policy path (release-auto.ts EVAL_POLICY).
13
+ */
14
+ import { createHash } from "node:crypto";
15
+ import { failedBeforeTests } from "./github-jobs.js";
16
+ import { githubNightlyOf, laneIdOf, readAttemptJobs, readRun, resolveLane } from "./nightly-github.js";
17
+ /** How old a known run may be and still be read, how many runs one pass reads, and each read's limit. */
18
+ const RECENT_MS = 14 * 86_400_000;
19
+ const RUNS_PER_PASS = 30;
20
+ const READ_MS = 15_000;
21
+ /** How often a project is read at most. */
22
+ export const CI_READ_EVERY_MS = 30 * 60_000;
23
+ const RETAIN_MS = 180 * 86_400_000;
24
+ /** The service cursor a project's reads keep (a cursor key can't hold a path). */
25
+ export const ciCursorKey = (repo) => `flaky-ci:${createHash("sha256").update(repo).digest("hex").slice(0, 32)}`;
26
+ /** The GitHub repositories this project's records name: the nightly's opt-in and its failed-checks triggers. */
27
+ export function ciRepositoriesOf(store, repo) {
28
+ const slugs = new Set();
29
+ const nightly = githubNightlyOf(store, repo);
30
+ if (nightly !== null)
31
+ slugs.add(nightly.slug);
32
+ for (const row of store.handle.prepare("SELECT t.config_json FROM flow_trigger t JOIN flow f ON f.id = t.flow WHERE f.repo = ?").all(repo)) {
33
+ try {
34
+ const config = JSON.parse(String(row["config_json"]));
35
+ if (config.kind === "github" && config.watch === "checks" && typeof config.repo === "string")
36
+ slugs.add(config.repo);
37
+ }
38
+ catch { /* an unreadable trigger names nothing */ }
39
+ }
40
+ return [...slugs];
41
+ }
42
+ /** The runs Toolroll knows by id for this project and repository, newest first. */
43
+ function knownRuns(store, repo, slug, now) {
44
+ const since = new Date(now.getTime() - RECENT_MS).toISOString();
45
+ const branch = store.handle.prepare("SELECT run_id, head_sha FROM branch_check_run WHERE lower(repo) = lower(?) AND run_created_at >= ? ORDER BY run_created_at DESC, run_id DESC LIMIT ?").all(slug, since, RUNS_PER_PASS)
46
+ .map(row => ({ id: Number(row["run_id"]), sha: String(row["head_sha"]) }));
47
+ const nightly = githubNightlyOf(store, repo)?.slug.toLowerCase() === slug.toLowerCase()
48
+ ? store.handle.prepare("SELECT gh_run FROM github_nightly_watch WHERE repo = ? AND created_at >= ? ORDER BY created_at DESC, gh_run DESC LIMIT ?").all(repo, since, RUNS_PER_PASS).map(row => ({ id: Number(row["gh_run"]), sha: null }))
49
+ : [];
50
+ const seen = new Set();
51
+ return [...branch, ...nightly].filter(one => !seen.has(one.id) && seen.add(one.id) !== undefined).slice(0, RUNS_PER_PASS);
52
+ }
53
+ /** Pair one run's attempts n-1 and n. A pair already decided isn't read again. */
54
+ export async function observeCiRun(store, input) {
55
+ const { repo, slug, now } = input;
56
+ const get = (path) => input.gh(path, READ_MS);
57
+ const run = await readRun(get, slug, input.run);
58
+ const at = now.toISOString();
59
+ const rejectedRows = [];
60
+ const pairs = [];
61
+ const reject = (attempt, lane, why) => { rejectedRows.push({ attempt, lane, why: lane === null ? why : `${why}: ${lane}` }); };
62
+ if (run === null)
63
+ return { observed: 0, rejected: 0 };
64
+ if (run.runAttempt <= 1 || run.status !== "completed")
65
+ return { observed: 0, rejected: 0 };
66
+ const later = run.runAttempt, earlier = later - 1;
67
+ const decided = store.handle.prepare("SELECT 1 FROM ci_flaky_observation WHERE provider = 'github' AND repository = ? AND workflow_run_id = ? AND passed_attempt = ? UNION ALL SELECT 1 FROM ci_pairing_rejected WHERE provider = 'github' AND repository = ? AND workflow_run_id = ? AND attempt = ? LIMIT 1")
68
+ .get(slug, input.run, later, slug, input.run, later);
69
+ if (decided !== undefined)
70
+ return { observed: 0, rejected: 0 };
71
+ if (input.sha !== null && input.sha !== run.headSha)
72
+ reject(later, null, "the run's commit isn't the one recorded");
73
+ else {
74
+ const before = await readAttemptJobs(get, slug, run.id, earlier, run.headSha);
75
+ const after = await readAttemptJobs(get, slug, run.id, later, run.headSha);
76
+ if (before === null || after === null)
77
+ reject(later, null, "an attempt couldn't be read whole");
78
+ else {
79
+ const laneOf = (job) => resolveLane(store, repo, run.path, job.name) ?? laneIdOf(run.path, job.name);
80
+ const group = (jobs) => { const lanes = new Map(); for (const job of jobs)
81
+ lanes.set(laneOf(job), [...(lanes.get(laneOf(job)) ?? []), job]); return lanes; };
82
+ const was = group(before), now2 = group(after);
83
+ for (const lane of new Set([...was.keys(), ...now2.keys()])) {
84
+ const a = was.get(lane) ?? [], b = now2.get(lane) ?? [];
85
+ if (a.length > 1 || b.length > 1) {
86
+ reject(later, lane, "more than one job with this name");
87
+ continue;
88
+ }
89
+ if (a.length === 0 || b.length === 0) {
90
+ reject(later, lane, "the job isn't in both attempts");
91
+ continue;
92
+ }
93
+ const first = a[0], second = b[0];
94
+ // A lane that passed both times is the ordinary case, not a pairing refused.
95
+ if (first.conclusion === "success" && second.conclusion === "success")
96
+ continue;
97
+ if (first.conclusion !== "failure" || second.conclusion !== "success") {
98
+ reject(later, lane, `not failure then success (${first.conclusion ?? "no conclusion"}, ${second.conclusion ?? "no conclusion"})`);
99
+ continue;
100
+ }
101
+ if (first.steps === null || failedBeforeTests({ id: first.id, status: first.status, conclusion: first.conclusion, steps: first.steps })) {
102
+ reject(later, lane, "it failed before its tests ran");
103
+ continue;
104
+ }
105
+ pairs.push({ lane, name: first.name, workflow: run.path, head: run.headSha, failed: first, passed: second, failedAttempt: earlier, passedAttempt: later });
106
+ }
107
+ }
108
+ }
109
+ let observed = 0, rejected = 0;
110
+ store.transact(() => {
111
+ for (const one of pairs) {
112
+ observed += Number(store.handle.prepare(`INSERT INTO ci_flaky_observation (repo, provider, repository, workflow, lane_id, lane_name, workflow_run_id, head_sha, failed_attempt, failed_job_id, failed_conclusion,
113
+ passed_attempt, passed_job_id, passed_conclusion, observed_at) VALUES (?, 'github', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
114
+ ON CONFLICT (provider, repository, workflow_run_id, lane_id, failed_attempt, passed_attempt) DO NOTHING`)
115
+ .run(repo, slug, one.workflow, one.lane, one.name, input.run, one.head, one.failedAttempt, one.failed.id, one.failed.conclusion, one.passedAttempt, one.passed.id, one.passed.conclusion, at).changes);
116
+ }
117
+ for (const one of rejectedRows) {
118
+ rejected += Number(store.handle.prepare(`INSERT INTO ci_pairing_rejected (provider, repository, workflow_run_id, attempt, lane_id, why, at) VALUES ('github', ?, ?, ?, ?, ?, ?)
119
+ ON CONFLICT (provider, repository, workflow_run_id, attempt, why) DO NOTHING`).run(slug, input.run, one.attempt, one.lane, one.why.slice(0, 400), at).changes);
120
+ }
121
+ });
122
+ return { observed, rejected };
123
+ }
124
+ /** One project's pass over its known runs. A read that fails records nothing and is tried on the next pass. */
125
+ export async function observeCiAttempts(store, repo, gh, now) {
126
+ const result = { read: 0, observed: 0, rejected: 0 };
127
+ for (const slug of ciRepositoriesOf(store, repo)) {
128
+ for (const known of knownRuns(store, repo, slug, now)) {
129
+ const done = await observeCiRun(store, { repo, slug, run: known.id, sha: known.sha, gh, now });
130
+ result.read++;
131
+ result.observed += done.observed;
132
+ result.rejected += done.rejected;
133
+ }
134
+ }
135
+ return result;
136
+ }
137
+ /** The storage sweep's own step: CI rows older than the retention window go; nothing else deletes them. */
138
+ export function pruneCiObservations(store, now) {
139
+ const cut = new Date(now.getTime() - RETAIN_MS).toISOString();
140
+ const observed = store.handle.prepare("DELETE FROM ci_flaky_observation WHERE observed_at < ?").run(cut);
141
+ const rejected = store.handle.prepare("DELETE FROM ci_pairing_rejected WHERE at < ?").run(cut);
142
+ return Number(observed.changes) + Number(rejected.changes);
143
+ }
144
+ /** Pairings refused since `since` for these repositories. */
145
+ export function ciNotPaired(store, repositories, since) {
146
+ if (repositories.length === 0)
147
+ return 0;
148
+ return Number(store.handle.prepare("SELECT COUNT(*) AS n FROM ci_pairing_rejected WHERE at >= ? AND repository IN (SELECT value FROM json_each(?))").get(since, JSON.stringify(repositories))?.["n"] ?? 0);
149
+ }
@@ -0,0 +1,60 @@
1
+ import { type FlakyList, type FlakyRow } from "./flaky.js";
2
+ import type { Store } from "./store.js";
3
+ export declare const FLAKY_DESCRIPTORS: readonly [{
4
+ readonly action: "list";
5
+ readonly synopsis: "tests with mixed outcomes under recorded conditions (read only)";
6
+ readonly mutation: "none";
7
+ readonly takesQuery: false;
8
+ readonly flags: readonly [{
9
+ readonly name: "repo";
10
+ readonly takesValue: true;
11
+ readonly meaning: "admitted project checkout root";
12
+ }, {
13
+ readonly name: "days";
14
+ readonly takesValue: true;
15
+ readonly meaning: "how far back to look (default 90)";
16
+ }, {
17
+ readonly name: "json";
18
+ readonly takesValue: false;
19
+ readonly meaning: "versioned structured response";
20
+ }, {
21
+ readonly name: "db";
22
+ readonly takesValue: true;
23
+ readonly meaning: "local Toolroll database";
24
+ }];
25
+ }, {
26
+ readonly action: "show";
27
+ readonly synopsis: "one row's history, and the task that would fix it: flaky show <identity> (read only)";
28
+ readonly mutation: "none";
29
+ readonly takesQuery: true;
30
+ readonly flags: readonly [{
31
+ readonly name: "repo";
32
+ readonly takesValue: true;
33
+ readonly meaning: "admitted project checkout root";
34
+ }, {
35
+ readonly name: "days";
36
+ readonly takesValue: true;
37
+ readonly meaning: "how far back to look (default 90)";
38
+ }, {
39
+ readonly name: "json";
40
+ readonly takesValue: false;
41
+ readonly meaning: "versioned structured response";
42
+ }, {
43
+ readonly name: "db";
44
+ readonly takesValue: true;
45
+ readonly meaning: "local Toolroll database";
46
+ }];
47
+ }];
48
+ export type FlakyCliContext = {
49
+ store: Store;
50
+ write: (line: string) => void;
51
+ json: boolean;
52
+ now: Date;
53
+ actor: string | null;
54
+ repos: readonly string[];
55
+ };
56
+ /** A row's one line of status, the same words the page says. */
57
+ export declare function flakyStatusWords(row: FlakyRow): string;
58
+ /** How much of the window had per-test reporting, said plainly. */
59
+ export declare function flakyCoverageWords(list: FlakyList): string;
60
+ export declare function runFlakyCommand(positional: readonly string[], options: Map<string, string | true>, context: FlakyCliContext): number;
@@ -0,0 +1,60 @@
1
+ /** `toolroll flaky list|show`: the flaky tests list from the terminal, read only. Rerunning stays `task regate`; fixing
2
+ * one is an ordinary task a person approves (`show` prints it). Nothing here starts a model or changes anything. */
3
+ import { envelopeJson } from "./envelope.js";
4
+ import { FLAKY_WINDOW_DAYS, flakyList, flakyShow } from "./flaky.js";
5
+ const flags = [
6
+ { name: "repo", takesValue: true, meaning: "admitted project checkout root" },
7
+ { name: "days", takesValue: true, meaning: `how far back to look (default ${FLAKY_WINDOW_DAYS})` },
8
+ { name: "json", takesValue: false, meaning: "versioned structured response" },
9
+ { name: "db", takesValue: true, meaning: "local Toolroll database" },
10
+ ];
11
+ export const FLAKY_DESCRIPTORS = [
12
+ { action: "list", synopsis: "tests with mixed outcomes under recorded conditions (read only)", mutation: "none", takesQuery: false, flags },
13
+ { action: "show", synopsis: "one row's history, and the task that would fix it: flaky show <identity> (read only)", mutation: "none", takesQuery: true, flags },
14
+ ];
15
+ const day = (iso) => iso.slice(0, 10);
16
+ /** A row's one line of status, the same words the page says. */
17
+ export function flakyStatusWords(row) {
18
+ return row.status === "mixed outcomes" ? "Mixed outcomes" : row.status === "no failures since its file changed" ? "No failures since its file changed" : "Not enough recorded history";
19
+ }
20
+ /** How much of the window had per-test reporting, said plainly. */
21
+ export function flakyCoverageWords(list) {
22
+ const checks = list.reported + list.unreported;
23
+ return `Reported by ${list.reported} of ${checks} check${checks === 1 ? "" : "s"}${list.unreported === 0 ? "" : `; ${list.unreported} had no per-test reporting`}.`;
24
+ }
25
+ const rowLine = (row) => `${row.name}\n ${flakyStatusWords(row)} · ${row.observations} in window · last seen ${day(row.lastSeen)} · ${row.conditions.join(", ") || "conditions not recorded"} · ${row.hardFailures} hard failure${row.hardFailures === 1 ? "" : "s"} · ${row.sources.join(", ") || "no source recorded"}${row.fileChanged === null ? "" : ` · file changed ${day(row.fileChanged)}`}\n ${row.key}`;
26
+ export function runFlakyCommand(positional, options, context) {
27
+ const action = positional[0];
28
+ const command = action ? `flaky ${action}` : "flaky";
29
+ const fail = (reason, message) => { context.write(context.json ? envelopeJson({ ok: false, command, reason, message }) : message); return reason === "usage" ? 2 : 1; };
30
+ const ok = (result, lines) => { context.write(context.json ? envelopeJson({ ok: true, command, ...result }) : lines.join("\n")); return 0; };
31
+ if (!FLAKY_DESCRIPTORS.some(one => one.action === action))
32
+ return fail("usage", "Use flaky list | show <identity> [--repo PATH] [--days N].");
33
+ if (context.actor === null)
34
+ return fail("unauthenticated", "Sign in first: the remembered local login drives flaky commands.");
35
+ const given = options.get("repo");
36
+ const repo = typeof given === "string" ? given : context.repos.length === 1 ? context.repos[0] : null;
37
+ if (repo === null)
38
+ return fail("usage", "Use --repo with the project checkout root.");
39
+ if (!context.repos.includes(repo))
40
+ return fail("not-found", "That project is outside your access.");
41
+ const daysGiven = options.get("days");
42
+ const days = typeof daysGiven === "string" ? Number(daysGiven) : FLAKY_WINDOW_DAYS;
43
+ if (!Number.isSafeInteger(days) || days < 1 || days > 730)
44
+ return fail("usage", "--days takes a whole number from 1 to 730.");
45
+ if (action === "list") {
46
+ const list = flakyList(context.store, repo, context.now, days);
47
+ const history = list.complete ? [] : [`Not enough recorded history: test results are kept for 180 days, so ${days} days can't be counted in full.`];
48
+ if (list.rows.length === 0)
49
+ return ok({ list }, [...history, ...(list.complete ? ["No mixed outcomes recorded."] : []), flakyCoverageWords(list)]);
50
+ return ok({ list }, ["Mixed outcomes under recorded conditions", ...history, ...list.rows.map(rowLine), flakyCoverageWords(list), ...(list.notPaired === 0 ? [] : [`${list.notPaired} GitHub attempt pairing${list.notPaired === 1 ? "" : "s"} not paired.`]), "Not recorded: network, other processes, time of day."]);
51
+ }
52
+ const key = positional.slice(1).join(" ").trim();
53
+ if (key === "")
54
+ return fail("usage", "flaky show <identity from flaky list>");
55
+ const found = flakyShow(context.store, repo, key, context.now, days);
56
+ if (found === null)
57
+ return fail("not-found", "Nothing is recorded for that test in this project.");
58
+ return ok(found, [rowLine(found.row), ...found.history.map(one => ` ${day(one.at)} ${one.failed} failed, ${one.passed} passed · ${one.commit ?? "commit not recorded"} · ${one.conditions}`),
59
+ `Make a task to fix it: toolroll task add "${found.fix.title.replaceAll('"', "'")}" --goal "${found.fix.goal.replaceAll('"', "'")}" --repo ${repo}`]);
60
+ }
@@ -0,0 +1,83 @@
1
+ import type { Store } from "./store.js";
2
+ /** Track an identity a local observation named (one row per observation; first write wins, never rewritten). */
3
+ export declare function watchObservation(store: Store, input: {
4
+ repo: string;
5
+ identity: string;
6
+ observation: number;
7
+ now: Date;
8
+ }): boolean;
9
+ /** How far back the list looks, how long the pruned rows are kept, and how many runs clear a row (the plan's defaults). */
10
+ export declare const FLAKY_WINDOW_DAYS = 90;
11
+ export declare const FLAKY_RETAIN_DAYS = 180;
12
+ export declare const FLAKY_CLEAR_RUNS = 20;
13
+ /** What can be observed at all: a test case, a journey or its group, or a file, never `part:`, `upgrade:`, `check` or
14
+ * `unexplained`, and never a real-model journey (a model's behaviour isn't a test's flakiness). */
15
+ export declare function observableIdentity(identity: string): boolean;
16
+ /**
17
+ * Every observation one settled check adds, in the transaction that records its results (check-facts.ts): an in-check
18
+ * retry, and each identity that failed in one check and explicitly passed in another with equal tree, command, access,
19
+ * harness and conditions. A `file:` identity is observed only as "failed, then the whole check passed", and only when
20
+ * no case in that file names the failure. Idempotent: racing dispositions on one pair leave one row.
21
+ */
22
+ export declare function recordObservations(store: Store, runId: number, now: Date): number[];
23
+ /** The storage sweep's own step: observations older than the retention window go; nothing else deletes them. */
24
+ export declare function pruneObservations(store: Store, now: Date): number;
25
+ export type FlakyStatus = "mixed outcomes" | "no failures since its file changed" | "not enough recorded history";
26
+ export type FlakyRow = {
27
+ /** The stable handle `flaky show` takes: the identity, or `github:<lane>`. */
28
+ key: string;
29
+ kind: "test" | "file" | "lane";
30
+ /** What the row says, as recorded: untrusted test text, escaped wherever it's shown. */
31
+ name: string;
32
+ observations: number;
33
+ lastSeen: string;
34
+ /** `<platform>-<arch>-node<major>`, or GitHub for a lane. */
35
+ conditions: string[];
36
+ /** Failures under recorded conditions that have no recorded pass. */
37
+ hardFailures: number;
38
+ sources: string[];
39
+ /** When the test's file last changed, as far as the recorded checks show; null when they don't. */
40
+ fileChanged: string | null;
41
+ status: FlakyStatus;
42
+ };
43
+ export type FlakyList = {
44
+ repo: string;
45
+ days: number;
46
+ rows: FlakyRow[];
47
+ /** False when the window reaches past what is kept, so a count there could be short. */
48
+ complete: boolean;
49
+ /** CI pairings refused, and how many checks in the window had no per-test reporting. */
50
+ notPaired: number;
51
+ unreported: number;
52
+ reported: number;
53
+ };
54
+ export type FlakyObservation = {
55
+ kind: "regate" | "same-tree" | "in-check-retry" | "github";
56
+ at: string;
57
+ failed: string;
58
+ passed: string;
59
+ commit: string | null;
60
+ conditions: string;
61
+ };
62
+ /** The test file an identity names, when it names one. */
63
+ export declare function identityFile(identity: string): string | null;
64
+ /** A readable name: `<file> › <test>`; a journey's group and name; a file with its limit said. */
65
+ export declare function identityWords(identity: string): string;
66
+ /** Every row for a project: reads only. `days` past the kept window is answered with `complete: false`, never a zero. */
67
+ export declare function flakyList(store: Store, repo: string, now: Date, days?: number): FlakyList;
68
+ /** How many rows the list has now, cheaply: no tree is read. */
69
+ export declare function flakyCount(store: Store, repo: string, now: Date): number;
70
+ /** One row's history, newest first: each observation with both checks (or the CI run and attempts). Null: no such row. */
71
+ export declare function flakyShow(store: Store, repo: string, key: string, now: Date, days?: number): {
72
+ row: FlakyRow;
73
+ history: FlakyObservation[];
74
+ fix: {
75
+ title: string;
76
+ goal: string;
77
+ };
78
+ } | null;
79
+ /** The ordinary task "Make a task to fix it" files; a person approves it as usual. Never to skip or delete the test. */
80
+ export declare function fixTaskOf(row: Pick<FlakyRow, "key" | "name" | "observations" | "conditions" | "kind">): {
81
+ title: string;
82
+ goal: string;
83
+ };