toolroll 0.9.93 → 0.9.95
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.
- package/CHANGELOG.md +79 -0
- package/dist/activity-line.d.ts +7 -4
- package/dist/activity-line.js +9 -7
- package/dist/agent-fence.d.ts +10 -0
- package/dist/agent-fence.js +6 -3
- package/dist/approval-authority.d.ts +7 -1
- package/dist/approval-authority.js +11 -2
- package/dist/approval-password.d.ts +31 -0
- package/dist/approval-password.js +26 -0
- package/dist/assignment-status.d.ts +1 -1
- package/dist/assignment-ui.js +3 -1
- package/dist/assignment.js +5 -0
- package/dist/attest.d.ts +3 -0
- package/dist/attest.js +10 -0
- package/dist/boot-identity.d.ts +2 -1
- package/dist/boot-identity.js +6 -4
- package/dist/bounded-spawn.d.ts +57 -0
- package/dist/bounded-spawn.js +220 -0
- package/dist/browser/terminal.js +42 -42
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +32 -32
- package/dist/browser-crew.js +2 -0
- package/dist/browser-shell.d.ts +1 -1
- package/dist/browser-shell.js +2 -2
- package/dist/browser-workspace.d.ts +50 -2
- package/dist/browser-workspace.js +3 -3
- package/dist/build-review.js +2 -0
- package/dist/builder-restart.d.ts +21 -0
- package/dist/builder-restart.js +50 -0
- package/dist/builder.d.ts +11 -2
- package/dist/builder.js +234 -101
- package/dist/card-catalog.d.ts +209 -0
- package/dist/card-catalog.js +133 -0
- package/dist/card-cli.d.ts +43 -0
- package/dist/card-cli.js +44 -0
- package/dist/card-render.d.ts +26 -0
- package/dist/card-render.js +223 -0
- package/dist/card-sources.d.ts +68 -0
- package/dist/card-sources.js +234 -0
- package/dist/chat-acceptance.js +1 -1
- package/dist/chat-actions.d.ts +106 -9
- package/dist/chat-actions.js +473 -46
- package/dist/chat-approval.d.ts +13 -0
- package/dist/chat-approval.js +39 -9
- package/dist/chat-card-part.d.ts +11 -0
- package/dist/chat-card-part.js +18 -0
- package/dist/chat-card.d.ts +46 -0
- package/dist/chat-card.js +99 -0
- package/dist/chat-channel.d.ts +2 -0
- package/dist/chat-channel.js +37 -14
- package/dist/chat-controls.d.ts +4 -0
- package/dist/chat-controls.js +4 -0
- package/dist/chat-decide.d.ts +10 -16
- package/dist/chat-decide.js +79 -62
- package/dist/chat-delivery.js +13 -4
- package/dist/chat-flow.d.ts +0 -27
- package/dist/chat-flow.js +31 -2
- package/dist/chat-quiet.js +4 -4
- package/dist/chat-task-actions.d.ts +3 -2
- package/dist/chat-task-actions.js +19 -5
- package/dist/chat-terms.d.ts +14 -2
- package/dist/chat-terms.js +35 -0
- package/dist/chat-voice.d.ts +1 -1
- package/dist/chat-voice.js +2 -2
- package/dist/check-facts.d.ts +3 -1
- package/dist/check-facts.js +57 -11
- package/dist/check-failure.d.ts +24 -3
- package/dist/check-failure.js +89 -13
- package/dist/check-levels-ui.d.ts +4 -0
- package/dist/check-levels-ui.js +10 -1
- package/dist/check-load.d.ts +85 -0
- package/dist/check-load.js +147 -0
- package/dist/check-only-standing.d.ts +36 -0
- package/dist/check-only-standing.js +39 -0
- package/dist/check-only.d.ts +15 -0
- package/dist/check-only.js +99 -0
- package/dist/check-repair.d.ts +47 -0
- package/dist/check-repair.js +240 -0
- package/dist/check-report.d.ts +2 -0
- package/dist/check-report.js +1 -1
- package/dist/check-stamp.d.ts +46 -0
- package/dist/check-stamp.js +93 -0
- package/dist/checkout-cleanup.d.ts +36 -5
- package/dist/checkout-cleanup.js +166 -14
- package/dist/claim.d.ts +6 -0
- package/dist/claim.js +13 -2
- package/dist/cli-help.d.ts +4 -1
- package/dist/cli-help.js +49 -30
- package/dist/cli.js +15 -1
- package/dist/code-extract.d.ts +44 -0
- package/dist/code-extract.js +88 -0
- package/dist/code-map-acts.d.ts +24 -0
- package/dist/code-map-acts.js +53 -0
- package/dist/code-map-admission.d.ts +52 -0
- package/dist/code-map-admission.js +295 -0
- package/dist/code-map-build.d.ts +63 -0
- package/dist/code-map-build.js +415 -0
- package/dist/code-map-cli.d.ts +153 -0
- package/dist/code-map-cli.js +103 -0
- package/dist/code-map-main.d.ts +1 -0
- package/dist/code-map-main.js +48 -0
- package/dist/code-map-read.d.ts +106 -0
- package/dist/code-map-read.js +171 -0
- package/dist/code-map-report.d.ts +37 -0
- package/dist/code-map-report.js +95 -0
- package/dist/code-map-shape.d.ts +172 -0
- package/dist/code-map-shape.js +327 -0
- package/dist/code-map-store.d.ts +136 -0
- package/dist/code-map-store.js +233 -0
- package/dist/coding-provider.d.ts +3 -1
- package/dist/coding-provider.js +12 -7
- package/dist/coding-update.js +3 -2
- package/dist/coding-workspace.d.ts +1 -1
- package/dist/coding-workspace.js +8 -8
- package/dist/console-chrome.js +2 -2
- package/dist/containment.d.ts +5 -0
- package/dist/containment.js +14 -1
- package/dist/contracts/chat-actions.d.ts +92 -0
- package/dist/contracts/chat-actions.js +20 -0
- package/dist/contracts/chat-card.d.ts +91 -0
- package/dist/contracts/chat-card.js +54 -0
- package/dist/contracts/chat-content.d.ts +191 -0
- package/dist/contracts/chat-content.js +4 -0
- package/dist/contracts/cli.d.ts +52 -0
- package/dist/contracts/cli.js +4 -0
- package/dist/contracts/console-api.d.ts +12 -3
- package/dist/contracts/console-api.js +12 -3
- package/dist/contracts/flow-propose.d.ts +8 -1
- package/dist/contracts/flow-propose.js +3 -1
- package/dist/contracts/flow-send.d.ts +126 -0
- package/dist/contracts/flow-send.js +3 -0
- package/dist/contracts/gateway-tools.d.ts +17 -0
- package/dist/contracts/gateway-tools.js +3 -1
- package/dist/contracts/lead-tools.d.ts +234 -0
- package/dist/contracts/lead-tools.js +25 -1
- package/dist/contracts/project-knowledge.d.ts +38 -5
- package/dist/contracts/project-knowledge.js +11 -5
- package/dist/contracts/project-memory.d.ts +3 -1
- package/dist/contracts/project-memory.js +3 -2
- package/dist/contracts/storage.d.ts +14 -0
- package/dist/contracts/storage.js +3 -1
- package/dist/contracts/store-json.d.ts +18 -0
- package/dist/contracts/youiyoux-request.d.ts +1 -1
- package/dist/contracts/youiyoux-request.js +1 -1
- package/dist/desktop-access.js +6 -2
- package/dist/discord-chat.js +14 -0
- package/dist/dispatch.d.ts +1 -1
- package/dist/dispatch.js +25 -16
- package/dist/dispose.d.ts +19 -0
- package/dist/dispose.js +55 -9
- package/dist/easy-start.js +2 -3
- package/dist/eval-oracle.js +4 -1
- package/dist/evals.js +2 -1
- package/dist/evidence-pair.d.ts +53 -0
- package/dist/evidence-pair.js +128 -0
- package/dist/exhaustion.d.ts +17 -4
- package/dist/exhaustion.js +44 -4
- package/dist/export.js +1 -1
- package/dist/failing-first.d.ts +31 -0
- package/dist/failing-first.js +44 -0
- package/dist/fallback-schema.d.ts +8 -0
- package/dist/fallback-schema.js +40 -0
- package/dist/fallback.d.ts +138 -0
- package/dist/fallback.js +605 -0
- package/dist/fast-start.d.ts +136 -0
- package/dist/fast-start.js +431 -0
- package/dist/fast-terms.d.ts +9 -0
- package/dist/fast-terms.js +23 -0
- package/dist/fenced-check-coverage.d.ts +62 -0
- package/dist/fenced-check-coverage.js +54 -0
- package/dist/findings.d.ts +224 -0
- package/dist/findings.js +449 -0
- package/dist/first-run.js +1 -1
- package/dist/flaky-ci.d.ts +31 -0
- package/dist/flaky-ci.js +149 -0
- package/dist/flaky-cli.d.ts +60 -0
- package/dist/flaky-cli.js +60 -0
- package/dist/flaky.d.ts +75 -9
- package/dist/flaky.js +255 -0
- package/dist/fleet-schema.d.ts +17 -0
- package/dist/fleet-schema.js +135 -0
- package/dist/flow-browser.js +2 -2
- package/dist/flow-card-task.js +3 -0
- package/dist/flow-code.d.ts +2 -2
- package/dist/flow-code.js +9 -5
- package/dist/flow-result-card.d.ts +22 -0
- package/dist/flow-result-card.js +55 -0
- package/dist/flow-send.js +12 -9
- package/dist/flow-steps.js +1 -1
- package/dist/flow-triggers.d.ts +1 -1
- package/dist/flow-triggers.js +2 -2
- package/dist/flow-unattended.d.ts +26 -0
- package/dist/flow-unattended.js +12 -3
- package/dist/flows-ui.js +10 -2
- package/dist/follow-through-brief.d.ts +12 -0
- package/dist/follow-through-brief.js +31 -0
- package/dist/follow-through-cli.d.ts +23 -0
- package/dist/follow-through-cli.js +153 -0
- package/dist/git-incident.d.ts +16 -0
- package/dist/git-incident.js +69 -6
- package/dist/github-import.d.ts +62 -0
- package/dist/github-import.js +146 -0
- package/dist/guides.js +15 -10
- package/dist/handover-note.d.ts +33 -0
- package/dist/handover-note.js +106 -0
- package/dist/invoke.d.ts +25 -2
- package/dist/invoke.js +157 -58
- package/dist/job-object-helper.ps1 +10 -3
- package/dist/keymap.d.ts +41 -0
- package/dist/keymap.js +51 -0
- package/dist/knowledge-cli.js +1 -1
- package/dist/knowledge-ui.d.ts +21 -4
- package/dist/knowledge-ui.js +75 -7
- package/dist/laptop-background.d.ts +16 -0
- package/dist/laptop-background.js +108 -2
- package/dist/lead-cli.js +26 -24
- package/dist/lead-context.js +1 -1
- package/dist/lead-contract.d.ts +1 -1
- package/dist/lead-contract.js +3 -2
- package/dist/lead-doors.d.ts +4 -0
- package/dist/lead-doors.js +2 -2
- package/dist/lead-progress.js +5 -0
- package/dist/lead-status.d.ts +2 -0
- package/dist/lead-status.js +6 -2
- package/dist/lead-tools.js +162 -34
- package/dist/lead.js +2 -2
- package/dist/lesson-notes.d.ts +81 -0
- package/dist/lesson-notes.js +336 -0
- package/dist/limits-ui.js +1 -1
- package/dist/local-owner.d.ts +1 -1
- package/dist/local-owner.js +16 -0
- package/dist/machine-check.d.ts +16 -0
- package/dist/machine-check.js +26 -0
- package/dist/machine-identity.d.ts +75 -0
- package/dist/machine-identity.js +170 -0
- package/dist/mcp-core.js +11 -1
- package/dist/memory-cli.d.ts +300 -1
- package/dist/memory-cli.js +62 -9
- package/dist/memory-pass.d.ts +16 -0
- package/dist/memory-pass.js +67 -3
- package/dist/memory-tidy.d.ts +181 -0
- package/dist/memory-tidy.js +523 -0
- package/dist/memory-use.d.ts +35 -0
- package/dist/memory-use.js +109 -0
- package/dist/merge-checklist.d.ts +55 -0
- package/dist/merge-checklist.js +182 -0
- package/dist/modes.d.ts +6 -6
- package/dist/modes.js +3 -1
- package/dist/needs-you.d.ts +31 -9
- package/dist/needs-you.js +110 -41
- package/dist/nightly-github.d.ts +2 -0
- package/dist/nightly-github.js +4 -4
- package/dist/operate-remote-arguments.js +7 -1
- package/dist/operate-remote.js +3 -1
- package/dist/operate.d.ts +6 -1
- package/dist/operate.js +565 -64
- package/dist/phase-routing.js +2 -2
- package/dist/plain-schema.d.ts +23 -0
- package/dist/plain-schema.js +94 -0
- package/dist/plain-words.d.ts +20 -0
- package/dist/plain-words.js +66 -0
- package/dist/plan-login.d.ts +14 -0
- package/dist/plan-login.js +58 -0
- package/dist/planner.d.ts +1 -1
- package/dist/planner.js +4 -2
- package/dist/process-liveness.d.ts +2 -0
- package/dist/process-liveness.js +61 -17
- package/dist/project-delete-ui.d.ts +19 -2
- package/dist/project-delete-ui.js +29 -1
- package/dist/project-delete.js +14 -1
- package/dist/project-knowledge.d.ts +18 -11
- package/dist/project-knowledge.js +39 -9
- package/dist/project-learning.d.ts +2 -1
- package/dist/project-learning.js +15 -45
- package/dist/project-memory.d.ts +5 -0
- package/dist/project-memory.js +22 -2
- package/dist/project-tools.d.ts +4 -4
- package/dist/project-tools.js +23 -7
- package/dist/proof.js +30 -5
- package/dist/protected-storage-look.d.ts +1 -0
- package/dist/protected-storage-look.js +13 -0
- package/dist/protected-storage.d.ts +70 -39
- package/dist/protected-storage.js +196 -147
- package/dist/provider-auth.js +1 -1
- package/dist/provider-capture.d.ts +34 -0
- package/dist/provider-capture.js +53 -0
- package/dist/publish.js +9 -1
- package/dist/pull-request-flow.js +5 -0
- package/dist/push.d.ts +18 -2
- package/dist/push.js +40 -16
- package/dist/release-adapters.d.ts +9 -0
- package/dist/release-adapters.js +76 -0
- package/dist/release-auto.js +13 -13
- package/dist/release-card.d.ts +1 -2
- package/dist/release-card.js +11 -0
- package/dist/release-carry.d.ts +7 -2
- package/dist/release-carry.js +49 -10
- package/dist/release-cli.d.ts +4 -0
- package/dist/release-cli.js +29 -22
- package/dist/release-coverage.js +1 -1
- package/dist/release-next.d.ts +48 -0
- package/dist/release-next.js +161 -0
- package/dist/release.d.ts +20 -1
- package/dist/release.js +85 -9
- package/dist/remote-destination.d.ts +36 -0
- package/dist/remote-destination.js +217 -0
- package/dist/remote-main.d.ts +48 -0
- package/dist/remote-main.js +84 -5
- package/dist/repository-context.d.ts +3 -2
- package/dist/repository-context.js +112 -70
- package/dist/restart-certification.d.ts +1 -0
- package/dist/restart-certification.js +8 -6
- package/dist/result-acts.d.ts +27 -19
- package/dist/result-acts.js +36 -25
- package/dist/result-follow-ups.d.ts +2 -2
- package/dist/result-follow-ups.js +18 -4
- package/dist/result-review.d.ts +1 -1
- package/dist/route-change-check.d.ts +4 -0
- package/dist/route-change-check.js +13 -0
- package/dist/route-change.d.ts +22 -0
- package/dist/route-change.js +89 -0
- package/dist/route-hooks.js +3 -2
- package/dist/route-results.d.ts +6 -1
- package/dist/route-results.js +14 -5
- package/dist/run-closeout.d.ts +1 -1
- package/dist/run-closeout.js +7 -5
- package/dist/runner-process.js +3 -2
- package/dist/runner.js +2 -1
- package/dist/scope.d.ts +28 -4
- package/dist/scope.js +92 -28
- package/dist/scout.d.ts +1 -1
- package/dist/scout.js +2 -1
- package/dist/serve.d.ts +1 -1
- package/dist/serve.js +26 -18
- package/dist/server/chat.js +16 -14
- package/dist/server/chrome.d.ts +1 -1
- package/dist/server/chrome.js +26 -35
- package/dist/server/console.css +73 -0
- package/dist/server/guards.js +3 -0
- package/dist/server/home.d.ts +6 -0
- package/dist/server/home.js +21 -13
- package/dist/server/http.js +1 -1
- package/dist/server/kit.js +1 -1
- package/dist/server/merge-ready-view.d.ts +10 -0
- package/dist/server/merge-ready-view.js +30 -0
- package/dist/server/pages-assets.d.ts +1 -0
- package/dist/server/pages-assets.js +8 -5
- package/dist/server/pages.js +103 -82
- package/dist/server/render-chat.js +16 -3
- package/dist/server/render-code-map.d.ts +30 -0
- package/dist/server/render-code-map.js +137 -0
- package/dist/server/render-flaky.d.ts +20 -0
- package/dist/server/render-flaky.js +59 -0
- package/dist/server/render-follow-through.d.ts +11 -0
- package/dist/server/render-follow-through.js +68 -0
- package/dist/server/render-history.js +1 -1
- package/dist/server/render-new-task.d.ts +26 -1
- package/dist/server/render-new-task.js +30 -5
- package/dist/server/render-pages.d.ts +1 -175
- package/dist/server/render-pages.js +38 -313
- package/dist/server/render-projects.d.ts +2 -1
- package/dist/server/render-projects.js +9 -7
- package/dist/server/render-release.d.ts +2 -0
- package/dist/server/render-release.js +3 -3
- package/dist/server/render-results.d.ts +2 -0
- package/dist/server/render-results.js +68 -68
- package/dist/server/render-tasks.d.ts +16 -2
- package/dist/server/render-tasks.js +177 -141
- package/dist/server/retired-routes.d.ts +17 -0
- package/dist/server/retired-routes.js +53 -0
- package/dist/server/route-table.d.ts +3 -0
- package/dist/server/route-table.js +48 -12
- package/dist/server/settings.js +166 -34
- package/dist/server/task-views.js +66 -26
- package/dist/server/tasks.d.ts +2 -6
- package/dist/server/tasks.js +250 -321
- package/dist/server/welcome.js +1 -1
- package/dist/settings-groups-ui.d.ts +1 -0
- package/dist/settings-groups-ui.js +4 -2
- package/dist/settings-kit.js +3 -2
- package/dist/settings-proofs.js +3 -1
- package/dist/slack-chat.js +14 -1
- package/dist/steer-mailbox.d.ts +8 -1
- package/dist/steer-mailbox.js +164 -6
- package/dist/stop-proof.js +6 -4
- package/dist/storage-sweep-process.js +1 -1
- package/dist/storage-sweep.d.ts +5 -3
- package/dist/storage-sweep.js +30 -9
- package/dist/storage-ui.js +28 -17
- package/dist/storage.d.ts +27 -2
- package/dist/storage.js +39 -2
- package/dist/store.d.ts +89 -7
- package/dist/store.js +386 -114
- package/dist/surface.js +67 -6
- package/dist/sweep-quarantine.d.ts +1 -0
- package/dist/sweep-quarantine.js +6 -4
- package/dist/tamper-scan.d.ts +4 -0
- package/dist/tamper-scan.js +90 -0
- package/dist/task-aliases.d.ts +5 -0
- package/dist/task-aliases.js +9 -0
- package/dist/task-control.d.ts +2 -1
- package/dist/task-control.js +4 -1
- package/dist/task-outcome-cli.d.ts +2 -0
- package/dist/task-outcome-cli.js +17 -17
- package/dist/task-sizing.d.ts +5 -0
- package/dist/task-sizing.js +2 -0
- package/dist/task-source.d.ts +7 -0
- package/dist/task-source.js +14 -0
- package/dist/task-status.d.ts +9 -0
- package/dist/task-status.js +45 -49
- package/dist/teardown-git.d.ts +18 -0
- package/dist/teardown-git.js +105 -21
- package/dist/telegram-lead.js +23 -6
- package/dist/telegram-progress.js +5 -3
- package/dist/telegram-status.js +6 -4
- package/dist/telegram.js +12 -1
- package/dist/test-infra.d.ts +8 -0
- package/dist/test-infra.js +18 -0
- package/dist/tidy.js +1 -1
- package/dist/toolroll-update.js +19 -5
- package/dist/tui-contract.d.ts +24 -0
- package/dist/tui-source.js +67 -8
- package/dist/tui.js +51 -51
- package/dist/vanished-retry.d.ts +30 -0
- package/dist/vanished-retry.js +174 -0
- package/dist/work-index.d.ts +2 -0
- package/dist/work-index.js +95 -66
- package/dist/work-summary.d.ts +1 -1
- package/dist/work-summary.js +7 -2
- package/dist/workspace-ui.d.ts +7 -3
- package/dist/workspace-ui.js +38 -23
- package/dist/worktree.d.ts +12 -3
- package/dist/worktree.js +91 -14
- package/package.json +2 -1
- package/dist/server/render-next.d.ts +0 -63
- package/dist/server/render-next.js +0 -109
package/dist/chat-voice.js
CHANGED
|
@@ -48,7 +48,7 @@ export function inSentence(summary) {
|
|
|
48
48
|
return /^\p{Lu}\p{Ll}/u.test(summary) ? summary[0].toLowerCase() + summary.slice(1) : summary;
|
|
49
49
|
}
|
|
50
50
|
/** The two buttons under a result ready for a person: the real next step, then a look first. */
|
|
51
|
-
export const READY_ACTIONS = { merge: "Merge", complete: "Accept
|
|
51
|
+
export const READY_ACTIONS = { merge: "Merge", complete: "Accept", look: "Look first" };
|
|
52
52
|
/** Whether this line asks for the result's next step, so it carries [Merge] or [Accept and finish] and [Look first]. */
|
|
53
53
|
export function asksToFinish(fact) {
|
|
54
54
|
return fact.update == null && fact.headline === "Ready for review" && !fact.report;
|
|
@@ -67,7 +67,7 @@ export function finishedLine(fact) {
|
|
|
67
67
|
if (fact.report)
|
|
68
68
|
return fact.lead === true ? `I wrote up ${lower}. The report is ready to read.` : `${name}: the report is ready to read.`;
|
|
69
69
|
if (fact.checks === "passed")
|
|
70
|
-
return `${ready}. Your tests passed. ${fact.pullRequest === true ? "Merge it?" : "Accept
|
|
70
|
+
return `${ready}. Your tests passed. ${fact.pullRequest === true ? "Merge it?" : "Accept it?"}`;
|
|
71
71
|
if (fact.checks === "off")
|
|
72
72
|
return `${ready}. Its checks run at release, so look it over first.`;
|
|
73
73
|
return `${ready}, but no tests ran. Look it over first.`;
|
package/dist/check-facts.d.ts
CHANGED
|
@@ -87,6 +87,8 @@ export type CheckFactsInput = {
|
|
|
87
87
|
* nothing reads these for a verdict, a rerun or an approval.
|
|
88
88
|
*/
|
|
89
89
|
export declare function recordCheckFacts(store: Store, root: string, input: CheckFactsInput): void;
|
|
90
|
+
/** The storage sweep's own step: results older than the retention window go; their fact rows stay. */
|
|
91
|
+
export declare function pruneCheckResults(store: Store, now: Date): number;
|
|
90
92
|
/** A check's recorded facts, or null. */
|
|
91
93
|
export type CheckFact = {
|
|
92
94
|
run: number;
|
|
@@ -117,7 +119,7 @@ export declare function checkResultsOf(store: Store, runId: number): {
|
|
|
117
119
|
* accepted note's until it ends, an observation's for 90 days — exact cases only, the most recently tracked first, at
|
|
118
120
|
* most 200. Those left out are counted (`leftOut`): a coverage gap, said, never hidden.
|
|
119
121
|
*/
|
|
120
|
-
export declare function learnRequestFor(store: Store, repo: string, now: Date): {
|
|
122
|
+
export declare function learnRequestFor(store: Store, repo: string, now: Date, runId?: number): {
|
|
121
123
|
learn: string[];
|
|
122
124
|
leftOut: number;
|
|
123
125
|
};
|
package/dist/check-facts.js
CHANGED
|
@@ -25,6 +25,8 @@ import { createHash } from "node:crypto";
|
|
|
25
25
|
import { failingTestsOf } from "./gate-failure.js";
|
|
26
26
|
import { checkContextOf, projectGitDir, treeFactsOf } from "./check-report.js";
|
|
27
27
|
import { readVerifiedArtifact } from "./evidence.js";
|
|
28
|
+
import { recordObservations } from "./flaky.js";
|
|
29
|
+
import { considerLessonNote, markTalliesPartial, revisedFailures, tallyCheck } from "./lesson-notes.js";
|
|
28
30
|
import { CHECK_REPORT_PREFIX, isExactCase, learnLineSchema, learnRequest, LEARN_MAX_BYTES, LEARN_PREFIX } from "./contracts/release-check.js";
|
|
29
31
|
import { nightlyTaskRef, releaseTaskRef } from "./store.js";
|
|
30
32
|
/** The sources a result can come from, and the states it can have. */
|
|
@@ -36,6 +38,8 @@ export const UNEXPLAINED = "unexplained";
|
|
|
36
38
|
export const MAX_RESULTS_PER_CHECK = 20_000;
|
|
37
39
|
/** How old a tracked observation may be and still be asked about (docs/plans/learning-loops.md, 90-day window). */
|
|
38
40
|
export const OBSERVATION_WATCH_MS = 90 * 24 * 60 * 60 * 1000;
|
|
41
|
+
/** How long per-identity results are kept (docs/plans/learning-loops.md, "Per table"). */
|
|
42
|
+
const RESULT_RETAIN_MS = 180 * 24 * 60 * 60 * 1000;
|
|
39
43
|
const sha256 = (value) => createHash("sha256").update(value).digest("hex");
|
|
40
44
|
/**
|
|
41
45
|
* The conditions a check's command starts under: its commit's verified tree facts, the approved command's digest, its
|
|
@@ -225,16 +229,47 @@ export function recordCheckFacts(store, root, input) {
|
|
|
225
229
|
kept.set(key, row);
|
|
226
230
|
}
|
|
227
231
|
const at = input.now.toISOString();
|
|
228
|
-
|
|
229
|
-
|
|
232
|
+
let recorded = false;
|
|
233
|
+
try {
|
|
234
|
+
store.transact(() => {
|
|
235
|
+
const inserted = store.handle.prepare(`INSERT INTO check_fact (run, repo, task_ref, head, tree, command_digest, access, harness_digest, lockfile, conditions_digest,
|
|
230
236
|
conditions_class, purpose, learn_state, learn_why, recorded_at) SELECT id, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? FROM run WHERE id = ?
|
|
231
237
|
ON CONFLICT (run) DO NOTHING`).run(input.repo, input.taskRef, conditions?.head ?? null, comparable ? conditions.tree : null, conditions?.commandDigest ?? null, conditions?.access ?? null, comparable ? conditions.harnessDigest : null, comparable ? conditions.lockfile : null, comparable ? conditions.conditionsDigest : null, comparable ? conditions.conditionsClass : null, checkPurposeOf(store, input.taskRef), learnState, learnState === "not-recorded" ? learnWhy ?? "not recorded" : null, at, input.runId);
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
+
if (Number(inserted.changes) !== 1)
|
|
239
|
+
return;
|
|
240
|
+
const insert = store.handle.prepare("INSERT INTO check_result (run, identity, state, source, recorded_at) VALUES (?, ?, ?, ?, ?)");
|
|
241
|
+
for (const row of [...kept.values()].slice(0, MAX_RESULTS_PER_CHECK))
|
|
242
|
+
insert.run(input.runId, row.identity, row.state, row.source, at);
|
|
243
|
+
// In the same transaction: what this check adds to the flaky-test list and to accepted notes' tallies, advisory like
|
|
244
|
+
// everything here.
|
|
245
|
+
const observations = recordObservations(store, input.runId, input.now);
|
|
246
|
+
if (input.repo !== null)
|
|
247
|
+
tallyCheck(store, input.runId, input.repo, observations, input.now);
|
|
248
|
+
recorded = true;
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
// Not every check was counted this month: its tallies say so.
|
|
253
|
+
if (input.repo !== null) {
|
|
254
|
+
try {
|
|
255
|
+
markTalliesPartial(store, input.repo, input.now);
|
|
256
|
+
}
|
|
257
|
+
catch { /* the note reads as it was */ }
|
|
258
|
+
}
|
|
259
|
+
throw error;
|
|
260
|
+
}
|
|
261
|
+
// A person's lesson note, when this check is what passed a revision. Advisory: nothing here reaches the check.
|
|
262
|
+
if (recorded) {
|
|
263
|
+
try {
|
|
264
|
+
considerLessonNote(store, root, input.runId, input.now);
|
|
265
|
+
}
|
|
266
|
+
catch { /* no note */ }
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/** The storage sweep's own step: results older than the retention window go; their fact rows stay. */
|
|
270
|
+
export function pruneCheckResults(store, now) {
|
|
271
|
+
const cut = new Date(now.getTime() - RESULT_RETAIN_MS).toISOString();
|
|
272
|
+
return Number(store.handle.prepare("DELETE FROM check_result WHERE recorded_at < ?").run(cut).changes);
|
|
238
273
|
}
|
|
239
274
|
export function checkFactOf(store, runId) {
|
|
240
275
|
const row = store.handle.prepare("SELECT * FROM check_fact WHERE run = ?").get(runId);
|
|
@@ -255,7 +290,7 @@ export function checkResultsOf(store, runId) {
|
|
|
255
290
|
* accepted note's until it ends, an observation's for 90 days — exact cases only, the most recently tracked first, at
|
|
256
291
|
* most 200. Those left out are counted (`leftOut`): a coverage gap, said, never hidden.
|
|
257
292
|
*/
|
|
258
|
-
export function learnRequestFor(store, repo, now) {
|
|
293
|
+
export function learnRequestFor(store, repo, now, runId) {
|
|
259
294
|
const since = new Date(now.getTime() - OBSERVATION_WATCH_MS).toISOString();
|
|
260
295
|
let tracked;
|
|
261
296
|
// Advisory: a read that fails asks about nothing, and the check runs as it would have.
|
|
@@ -267,6 +302,17 @@ export function learnRequestFor(store, repo, now) {
|
|
|
267
302
|
catch {
|
|
268
303
|
return { learn: [], leftOut: 0 };
|
|
269
304
|
}
|
|
270
|
-
|
|
271
|
-
|
|
305
|
+
// First, the failed cases of the check this run revises (lesson-notes.ts rule 4).
|
|
306
|
+
let revised = [];
|
|
307
|
+
if (runId !== undefined) {
|
|
308
|
+
try {
|
|
309
|
+
revised = revisedFailures(store, runId);
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
revised = [];
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
const all = [...new Set([...revised, ...tracked])];
|
|
316
|
+
const learn = learnRequest(all);
|
|
317
|
+
return { learn, leftOut: all.length - learn.length };
|
|
272
318
|
}
|
package/dist/check-failure.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type CheckReport } from "./contracts/release-check.js";
|
|
2
|
+
import { type CheckLoadRecord } from "./check-load.js";
|
|
2
3
|
import { INFRA_RERUNS_PER_CANDIDATE, INFRA_RERUNS_PER_INCIDENT, type FailureClassName } from "./release.js";
|
|
3
4
|
import type { Store } from "./store.js";
|
|
4
5
|
export type FailureClass = FailureClassName;
|
|
@@ -20,12 +21,16 @@ export type CheckFacts = {
|
|
|
20
21
|
failure?: string;
|
|
21
22
|
};
|
|
22
23
|
coverage?: string | undefined;
|
|
24
|
+
/** The approved limit of the command the receipt names (receipt.command.timeoutMs): a load record must be for it. */
|
|
25
|
+
grantMs?: number | undefined;
|
|
23
26
|
};
|
|
24
27
|
/**
|
|
25
|
-
* One failed check, classified. `facts` come from the plane's sealed receipt
|
|
26
|
-
* when valid) only ever picks among the classes that
|
|
28
|
+
* One failed check, classified. `facts` come from the plane's sealed receipt and `load` from the plane's own sealed load
|
|
29
|
+
* record (check-load.ts); `report` (the check's own report, sealed when valid) only ever picks among the classes that
|
|
30
|
+
* block — a timeout is excused only when the report names nothing else that failed, and only by the plane's own facts:
|
|
31
|
+
* its load readings cut the budget, and the check ran into its limit by the plane's own clock. Null when it passed.
|
|
27
32
|
*/
|
|
28
|
-
export declare function classifyCheckFailure(facts: CheckFacts, report: CheckReport | null): ClassifiedFailure | null;
|
|
33
|
+
export declare function classifyCheckFailure(facts: CheckFacts, report: CheckReport | null, load?: CheckLoadRecord | null): ClassifiedFailure | null;
|
|
29
34
|
/** What passed only on a retry inside a passing check (a real-model journey group, a unit file): counted and reported,
|
|
30
35
|
* never silently part of a pass. Null when there is no valid report to read it from: unknown, never "no retries". */
|
|
31
36
|
export declare function retriedInside(report: CheckReport | null): string[] | null;
|
|
@@ -45,6 +50,22 @@ export declare const SETUP_REPLAY_RETRY = "the whole check, rerun after the appr
|
|
|
45
50
|
* missing program; a receipt that doesn't classify it (an older one) says so, never that it was a launch failure.
|
|
46
51
|
*/
|
|
47
52
|
export declare function setupReplayWords(first: unknown): string;
|
|
53
|
+
/**
|
|
54
|
+
* An attempt interrupted before its candidate was even loaded (run 3326: the worker stopped under it — "The attempt was
|
|
55
|
+
* stopped before the prepared candidate was loaded."): it ran no check, so it is no check failure of the commit. Only
|
|
56
|
+
* the plane's own records say so, all of them at once: the run ended failed/interrupted, recorded no commit, and has no
|
|
57
|
+
* check attempt (sealed before any check command runs), no recorded check and no check log or receipt. Any attempt that
|
|
58
|
+
* got as far as its check keeps every rule below.
|
|
59
|
+
*/
|
|
60
|
+
export declare function ranNoCheck(store: Store, row: {
|
|
61
|
+
id: number;
|
|
62
|
+
outcome: unknown;
|
|
63
|
+
reason: unknown;
|
|
64
|
+
head: unknown;
|
|
65
|
+
checkRow: unknown;
|
|
66
|
+
}): boolean;
|
|
67
|
+
/** The ledger entry the build writes when it refuses a prepared candidate before any process (builder.ts). */
|
|
68
|
+
export declare const NOTHING_STARTED_ACTION = "attempt refused before anything ran";
|
|
48
69
|
/**
|
|
49
70
|
* Whether a passing check of a release task stands, given every earlier attempt on the same candidate and signed scope:
|
|
50
71
|
* each earlier failure must be confirmed infrastructure the release itself reran (at most two), and nothing in the
|
package/dist/check-failure.js
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* deterministic typecheck, build, a failing test, a scripted journey, an upgrade, a required test gone:
|
|
6
6
|
* nothing tolerated, never rerun to pass; the branch is fixed and the new commit released.
|
|
7
7
|
* infrastructure independently confirmed by the plane itself: the check never started, this worker lost the
|
|
8
|
-
* build, Toolroll's own shutdown stopped it
|
|
8
|
+
* build, Toolroll's own shutdown stopped it, it ran out of time on a machine the plane measured
|
|
9
|
+
* busy enough to cut its budget to half or less (check-load.ts) — or, on GitHub, a job that failed before any of the
|
|
9
10
|
* workflow's own steps ran. At most one rerun per incident and two per candidate, under the same
|
|
10
11
|
* approval; a pass after them counts.
|
|
11
12
|
* unknown exit 1 with no failing test named, an unexplained timeout or signal, no result at all: an error
|
|
@@ -20,10 +21,12 @@
|
|
|
20
21
|
*/
|
|
21
22
|
import { CHECK_REPORT_CAPTURE, readSealedJson } from "./check-report.js";
|
|
22
23
|
import { readCheckReport } from "./contracts/release-check.js";
|
|
24
|
+
import { busyMachineTimeout, CHECK_LOAD_CAPTURE, readCheckLoad } from "./check-load.js";
|
|
23
25
|
import { regateTask } from "./dispose.js";
|
|
24
26
|
import { INFRA_RERUNS_PER_CANDIDATE, INFRA_RERUNS_PER_INCIDENT } from "./release.js";
|
|
25
27
|
import { releaseCheckScope } from "./scope.js";
|
|
26
|
-
import { verificationEvidence } from "./verification-evidence.js";
|
|
28
|
+
import { isVerificationReceipt, verificationEvidence } from "./verification-evidence.js";
|
|
29
|
+
import { checkAttemptOf } from "./store.js";
|
|
27
30
|
/** The plane's limits for confirmed infrastructure failures (one rerun per incident, two per candidate). */
|
|
28
31
|
export { INFRA_RERUNS_PER_CANDIDATE, INFRA_RERUNS_PER_INCIDENT };
|
|
29
32
|
/** The ledger action that records each rerun the release asked for, and why. */
|
|
@@ -35,11 +38,15 @@ const PLANE_INFRA = {
|
|
|
35
38
|
"custody-lost": "this worker lost the build before the check could finish",
|
|
36
39
|
"stopped-by-toolroll": "Toolroll's own shutdown stopped the check before it finished",
|
|
37
40
|
};
|
|
41
|
+
/** How a timeout on a busy machine reads (its rerun is the release terms' "Toolroll's own machine fails"). */
|
|
42
|
+
const busyWords = (why) => `${why}: Toolroll's own machine, so this commit's check runs again`;
|
|
38
43
|
/**
|
|
39
|
-
* One failed check, classified. `facts` come from the plane's sealed receipt
|
|
40
|
-
* when valid) only ever picks among the classes that
|
|
44
|
+
* One failed check, classified. `facts` come from the plane's sealed receipt and `load` from the plane's own sealed load
|
|
45
|
+
* record (check-load.ts); `report` (the check's own report, sealed when valid) only ever picks among the classes that
|
|
46
|
+
* block — a timeout is excused only when the report names nothing else that failed, and only by the plane's own facts:
|
|
47
|
+
* its load readings cut the budget, and the check ran into its limit by the plane's own clock. Null when it passed.
|
|
41
48
|
*/
|
|
42
|
-
export function classifyCheckFailure(facts, report) {
|
|
49
|
+
export function classifyCheckFailure(facts, report, load = null) {
|
|
43
50
|
const result = facts.result;
|
|
44
51
|
const passedCoverage = facts.coverage === undefined || facts.coverage === "full" || facts.coverage === "composed";
|
|
45
52
|
if (result.configured && result.ran === true && result.exitCode === 0 && passedCoverage)
|
|
@@ -51,8 +58,12 @@ export function classifyCheckFailure(facts, report) {
|
|
|
51
58
|
const infra = PLANE_INFRA[failure];
|
|
52
59
|
if (infra !== undefined)
|
|
53
60
|
return { class: "infrastructure", why: infra };
|
|
54
|
-
if (failure === "timed-out" || failure === "retry-timed-out")
|
|
55
|
-
|
|
61
|
+
if (failure === "timed-out" || failure === "retry-timed-out") {
|
|
62
|
+
// The plane stopped it at its limit. With the plane's own record of a cut budget, the machine's fault; otherwise
|
|
63
|
+
// (a normal budget, or no record) a timeout nothing explains, as ever.
|
|
64
|
+
const busy = facts.grantMs === undefined ? null : busyMachineTimeout(load, facts.grantMs, true);
|
|
65
|
+
return busy !== null ? { class: "infrastructure", why: busyWords(busy) } : { class: "unknown", why: "the check timed out, and nothing says why" };
|
|
66
|
+
}
|
|
56
67
|
if (failure === "baseline-unverifiable")
|
|
57
68
|
return { class: "unknown", why: "the last release Toolroll recorded couldn't be verified, so what the check owed is unknown and it wasn't run" };
|
|
58
69
|
if (failure === "no-result")
|
|
@@ -65,10 +76,14 @@ export function classifyCheckFailure(facts, report) {
|
|
|
65
76
|
if (report === null)
|
|
66
77
|
return { class: "unknown", why: `the check exited ${result.exitCode ?? "nonzero"} and gave no report naming what failed` };
|
|
67
78
|
const deterministic = [], agent = [], unexplained = [];
|
|
79
|
+
// Whether everything unexplained is a part that ran out of time (never a signal, never an error no test owned).
|
|
80
|
+
let onlyTimeouts = true;
|
|
68
81
|
for (const part of report.parts) {
|
|
69
82
|
if (!part.selected || !part.ran || part.code === null)
|
|
70
83
|
continue;
|
|
71
84
|
if (part.timedOut || part.signal != null) {
|
|
85
|
+
if (!part.timedOut || part.signal != null)
|
|
86
|
+
onlyTimeouts = false;
|
|
72
87
|
unexplained.push(`${part.part} ${part.timedOut ? "timed out" : `was ended by ${part.signal}`}`);
|
|
73
88
|
continue;
|
|
74
89
|
}
|
|
@@ -78,8 +93,10 @@ export function classifyCheckFailure(facts, report) {
|
|
|
78
93
|
for (const file of report.units?.files ?? [])
|
|
79
94
|
if (file.state === "failed")
|
|
80
95
|
deterministic.push(`${file.file} failed`);
|
|
81
|
-
if ((report.units?.runErrors ?? 0) > 0 && !(report.units?.files ?? []).some(file => file.state === "failed"))
|
|
96
|
+
if ((report.units?.runErrors ?? 0) > 0 && !(report.units?.files ?? []).some(file => file.state === "failed")) {
|
|
97
|
+
onlyTimeouts = false;
|
|
82
98
|
unexplained.push(`${report.units.runErrors} unit errors no test owned`);
|
|
99
|
+
}
|
|
83
100
|
for (const run of report.journeys)
|
|
84
101
|
for (const group of run.groups) {
|
|
85
102
|
if (group.state !== "failed")
|
|
@@ -94,8 +111,14 @@ export function classifyCheckFailure(facts, report) {
|
|
|
94
111
|
return { class: "deterministic", why: list(deterministic) };
|
|
95
112
|
if (agent.length > 0)
|
|
96
113
|
return { class: "agent-behaviour", why: list(agent) };
|
|
97
|
-
if (unexplained.length > 0)
|
|
114
|
+
if (unexplained.length > 0) {
|
|
115
|
+
// Only parts that ran out of time, and the plane's own record says the machine cut the budget and the check ran
|
|
116
|
+
// into its limit: the machine's fault. The report can only withhold this (by naming anything else), never grant it.
|
|
117
|
+
const busy = onlyTimeouts && facts.grantMs !== undefined ? busyMachineTimeout(load, facts.grantMs, false) : null;
|
|
118
|
+
if (busy !== null)
|
|
119
|
+
return { class: "infrastructure", why: busyWords(`${list(unexplained)}; ${busy}`) };
|
|
98
120
|
return { class: "unknown", why: list(unexplained) };
|
|
121
|
+
}
|
|
99
122
|
return { class: "unknown", why: `the check exited ${result.exitCode ?? "nonzero"} with no failing test identified` };
|
|
100
123
|
}
|
|
101
124
|
/** What passed only on a retry inside a passing check (a real-model journey group, a unit file): counted and reported,
|
|
@@ -127,7 +150,10 @@ export function classifyRunCheck(store, root, runId) {
|
|
|
127
150
|
return { class: "unknown", why: "the check's receipt names no result" };
|
|
128
151
|
const sealed = readSealedJson(store, root, runId, CHECK_REPORT_CAPTURE);
|
|
129
152
|
const parsed = sealed === null ? null : readCheckReport(sealed.value);
|
|
130
|
-
|
|
153
|
+
// The plane's own load record (check-load.ts): absent on older runs, and then nothing is excused.
|
|
154
|
+
const loadSealed = readSealedJson(store, root, runId, CHECK_LOAD_CAPTURE);
|
|
155
|
+
const grantMs = typeof receipt.command?.timeoutMs === "number" ? receipt.command.timeoutMs : undefined;
|
|
156
|
+
return classifyCheckFailure({ result: receipt.result, coverage: receipt.coverage, grantMs }, parsed?.ok === true ? parsed.value : null, loadSealed === null ? null : readCheckLoad(loadSealed.value));
|
|
131
157
|
}
|
|
132
158
|
/**
|
|
133
159
|
* What passed only on a retry inside a run's check (eval-harness review 1, finding 3): the summary sealed into its
|
|
@@ -174,12 +200,59 @@ export function setupReplayWords(first) {
|
|
|
174
200
|
}
|
|
175
201
|
return `${SETUP_REPLAY_RETRY} (its first attempt's failure isn't classified)`;
|
|
176
202
|
}
|
|
177
|
-
/**
|
|
203
|
+
/**
|
|
204
|
+
* An attempt interrupted before its candidate was even loaded (run 3326: the worker stopped under it — "The attempt was
|
|
205
|
+
* stopped before the prepared candidate was loaded."): it ran no check, so it is no check failure of the commit. Only
|
|
206
|
+
* the plane's own records say so, all of them at once: the run ended failed/interrupted, recorded no commit, and has no
|
|
207
|
+
* check attempt (sealed before any check command runs), no recorded check and no check log or receipt. Any attempt that
|
|
208
|
+
* got as far as its check keeps every rule below.
|
|
209
|
+
*/
|
|
210
|
+
export function ranNoCheck(store, row) {
|
|
211
|
+
if (row.outcome !== "failed" || (row.reason !== "interrupted" && row.reason !== "no-op") || row.head !== null || row.checkRow !== null)
|
|
212
|
+
return false;
|
|
213
|
+
if (checkAttemptOf(store, row.id) !== null)
|
|
214
|
+
return false;
|
|
215
|
+
if (store.artifactsFor(row.id).some(artifact => artifact.kind === "check-log" || isVerificationReceipt(artifact)))
|
|
216
|
+
return false;
|
|
217
|
+
return row.reason === "interrupted" || refusedBeforeAnything(store, row.id);
|
|
218
|
+
}
|
|
219
|
+
/** The ledger entry the build writes when it refuses a prepared candidate before any process (builder.ts). */
|
|
220
|
+
export const NOTHING_STARTED_ACTION = "attempt refused before anything ran";
|
|
221
|
+
/** Every table a process, an agent or a check of this run writes. */
|
|
222
|
+
const RUN_TRACES = [["artifact", "run"], ["run_process", "run"], ["run_process_summary", "run"], ["run_tool", "run"], ["run_activity", "run"],
|
|
223
|
+
["check_stamp", "run_id"], ["release_check_attempt", "run"], ["run_note", "run"], ["agent_turn", "run"], ["run_checkpoint", "run"], ["run_decision", "run"]];
|
|
224
|
+
/**
|
|
225
|
+
* An attempt the build refused before anything ran (run 3332: Git couldn't read HEAD while it proved the candidate; the
|
|
226
|
+
* next attempt passed). A failed attempt with reason `no-op` proves nothing alone: an agent's empty result and a check's
|
|
227
|
+
* refusal end that way too. So the build's own ledger entry says so, written only where nothing has run, and the run's
|
|
228
|
+
* records agree: no base stamped, no provider start, session, tokens or commit, and not one row of any process, agent or
|
|
229
|
+
* check. All of them, or it stands as a failure, and a record that can't be read (an older database) never excuses one.
|
|
230
|
+
*/
|
|
231
|
+
function refusedBeforeAnything(store, runId) {
|
|
232
|
+
try {
|
|
233
|
+
const said = store.handle.prepare("SELECT COUNT(*) AS n FROM action_ledger WHERE run_id = ? AND action = ? AND outcome = 'refused'").get(runId, NOTHING_STARTED_ACTION)?.["n"];
|
|
234
|
+
if (Number(said) !== 1)
|
|
235
|
+
return false;
|
|
236
|
+
const run = store.handle.prepare("SELECT base_revision, head_revision, provider_started_at, session_id, tokens_in, tokens_out, cost_usd, committed FROM run WHERE id = ?").get(runId);
|
|
237
|
+
if (run === undefined || Object.values(run).some(value => value !== null))
|
|
238
|
+
return false;
|
|
239
|
+
// check_fact is a learning-loops table (eval-hooks.ts): read here in its own literal statement, never through the
|
|
240
|
+
// table list, so no SQL text that could name it is assembled outside its sole writer.
|
|
241
|
+
if (store.handle.prepare("SELECT 1 FROM check_fact WHERE run = ? LIMIT 1").get(runId) !== undefined)
|
|
242
|
+
return false;
|
|
243
|
+
return RUN_TRACES.every(([table, column]) => store.handle.prepare(`SELECT 1 FROM ${table} WHERE ${column} = ? LIMIT 1`).get(runId) === undefined);
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
/** The task's finished builder runs under `digest`, oldest first, each with whether its check passed or never ran. */
|
|
178
250
|
function builderRuns(store, taskRef, digest) {
|
|
179
|
-
return store.handle.prepare(`SELECT r.id, r.outcome, r.scope_digest, r.finished_at, c.status FROM run r LEFT JOIN run_check c ON c.run = r.id
|
|
251
|
+
return store.handle.prepare(`SELECT r.id, r.outcome, r.reason, r.head_revision, r.scope_digest, r.finished_at, c.status, c.run AS check_row FROM run r LEFT JOIN run_check c ON c.run = r.id
|
|
180
252
|
WHERE r.task_ref = ? AND r.role = 'builder' AND r.finished_at IS NOT NULL AND r.scope_digest = ? ORDER BY r.id`).all(taskRef, digest)
|
|
181
253
|
.map(row => ({ id: Number(row["id"]), outcome: row["outcome"] === null ? null : String(row["outcome"]), scopeDigest: String(row["scope_digest"]), finishedAt: String(row["finished_at"]),
|
|
182
|
-
passed: (row["outcome"] === "built" || row["outcome"] === "no-change") && row["status"] === "passed"
|
|
254
|
+
passed: (row["outcome"] === "built" || row["outcome"] === "no-change") && row["status"] === "passed",
|
|
255
|
+
noCheck: ranNoCheck(store, { id: Number(row["id"]), outcome: row["outcome"], reason: row["reason"], head: row["head_revision"] ?? null, checkRow: row["check_row"] ?? null }) }));
|
|
183
256
|
}
|
|
184
257
|
/** The reruns the release asked for on this task (each source run once): run id → its class. */
|
|
185
258
|
function reruns(store, taskId) {
|
|
@@ -208,6 +281,9 @@ export function releaseCheckStanding(store, root, input) {
|
|
|
208
281
|
for (const one of builderRuns(store, ref.id, digest)) {
|
|
209
282
|
if (one.id >= input.run || one.passed)
|
|
210
283
|
continue;
|
|
284
|
+
// Interrupted before its candidate was loaded: no check ran, so there is no check failure to stand against a pass.
|
|
285
|
+
if (one.noCheck)
|
|
286
|
+
continue;
|
|
211
287
|
const classified = one.outcome === "built" || one.outcome === "no-change" ? classifyRunCheck(store, root, one.id) : { class: "unknown", why: `its build ended ${one.outcome ?? "without an outcome"}` };
|
|
212
288
|
if (classified === null)
|
|
213
289
|
continue;
|
package/dist/check-levels-ui.js
CHANGED
|
@@ -48,7 +48,16 @@ export function checkSettingsHtml(view) {
|
|
|
48
48
|
body: view.full === null
|
|
49
49
|
? joinHtml([note("Approve one from the command line:"), codeLine(`toolroll verify set --repo … --command "…"`)])
|
|
50
50
|
: command(view.full) });
|
|
51
|
-
return joinHtml([head, level, batchHtml(view), reviewHtml(view), quick, full]);
|
|
51
|
+
return joinHtml([head, level, fastHtml(view), batchHtml(view), reviewHtml(view), quick, full]);
|
|
52
|
+
/** Small tasks: one state, one sentence, and its one change behind a disclosure (the password). */
|
|
53
|
+
function fastHtml(view) {
|
|
54
|
+
if (view.fast === undefined)
|
|
55
|
+
return "";
|
|
56
|
+
const act = view.fast.on ? "Turn off" : "Turn on";
|
|
57
|
+
return settingsSection({ id: "small-tasks", title: "Small tasks start right away", state: html `<span data-small-tasks="${view.fast.on ? "on" : "off"}"> · ${view.fast.on ? "On" : "Off"}</span>`,
|
|
58
|
+
lead: "New task shows the exact terms and Build approves them, with no plan step. It takes a person, and only small, ordinary changes.",
|
|
59
|
+
body: view.canChange ? disclose(act, form("small-tasks-form", "small-tasks", html ``, { label: act, variant: "outline" }, { on: view.fast.on ? "0" : "1" })) : "" });
|
|
60
|
+
}
|
|
52
61
|
/** Batch checks: one state and one sentence; turning it on restates exactly what happens before the password. */
|
|
53
62
|
function batchHtml(view) {
|
|
54
63
|
const batch = view.batch;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const CHECK_LOAD_CAPTURE = "release check load v1";
|
|
3
|
+
/** One reading of the machine: its cores and one-minute load (NaN where it isn't measured: Windows reports 0). */
|
|
4
|
+
export type MachineSample = {
|
|
5
|
+
cores: number;
|
|
6
|
+
load: number;
|
|
7
|
+
};
|
|
8
|
+
export declare const machineSample: (platform?: NodeJS.Platform, read?: () => number[], cores?: () => number) => MachineSample;
|
|
9
|
+
/** The check's budget on a quiet machine: three quarters of the cores, at least 2 (release-check.mjs checkBudget). */
|
|
10
|
+
export declare const normalFree: (cores: number) => number;
|
|
11
|
+
/** The check's budget under `load` (release-check.mjs checkBudget's `free`). Unmeasured load budgets from the cores. */
|
|
12
|
+
export declare const freeAt: (cores: number, load: number) => number;
|
|
13
|
+
/** Cut: the budget is at most half its normal size. Unmeasured load is never cut. */
|
|
14
|
+
export declare const budgetCut: (cores: number, load: number) => boolean;
|
|
15
|
+
/** A release check's limit never goes past this, however busy the machine. */
|
|
16
|
+
export declare const CHECK_LIMIT_CEILING_MS: number;
|
|
17
|
+
/** …nor past this many times the approved limit. */
|
|
18
|
+
export declare const CHECK_LIMIT_MAX_FACTOR = 2;
|
|
19
|
+
/**
|
|
20
|
+
* The limit a release check runs under: the approved one, unless the machine's load at its start cuts the check's
|
|
21
|
+
* budget to half or less — then the approved limit times normal/granted, at most twice it and never past the ceiling
|
|
22
|
+
* (an approved limit already at or past the ceiling is never extended). `why` says which, in words.
|
|
23
|
+
*/
|
|
24
|
+
export declare function checkLimitFor(grantMs: number, sample: MachineSample): {
|
|
25
|
+
limitMs: number;
|
|
26
|
+
why: string;
|
|
27
|
+
};
|
|
28
|
+
/** When a check ends itself before its limit (release-check.mjs checkDeadline, from its start): src/check-load.test.ts
|
|
29
|
+
* holds the two equal. A check that ran at least this long ran into its own limit. */
|
|
30
|
+
export declare const selfStopAfter: (limitMs: number) => number;
|
|
31
|
+
/** The samples read for the budget the check started with: the check waits up to ten minutes for the load to drop
|
|
32
|
+
* (release-check.mjs), so the least load in its first eleven — before its own load counts — is what it budgeted from at
|
|
33
|
+
* best. The least, so a check that found a quieter moment is never called cut. */
|
|
34
|
+
export declare const CUT_WINDOW_MS: number;
|
|
35
|
+
declare const recordSchema: z.ZodObject<{
|
|
36
|
+
version: z.ZodLiteral<1>;
|
|
37
|
+
cores: z.ZodInt;
|
|
38
|
+
grantMs: z.ZodInt;
|
|
39
|
+
limitMs: z.ZodInt;
|
|
40
|
+
why: z.ZodString;
|
|
41
|
+
startedAt: z.ZodString;
|
|
42
|
+
elapsedMs: z.ZodNullable<z.ZodInt>;
|
|
43
|
+
samples: z.ZodArray<z.ZodObject<{
|
|
44
|
+
atMs: z.ZodInt;
|
|
45
|
+
load: z.ZodNullable<z.ZodNumber>;
|
|
46
|
+
}, z.core.$strict>>;
|
|
47
|
+
}, z.core.$strict>;
|
|
48
|
+
export type CheckLoadRecord = z.infer<typeof recordSchema>;
|
|
49
|
+
/** A sealed record read back, or null when it isn't one (an older run has none: then nothing is excused). */
|
|
50
|
+
export declare function readCheckLoad(value: unknown): CheckLoadRecord | null;
|
|
51
|
+
/**
|
|
52
|
+
* Why a release check that ran out of time did so because the machine was busy — or null. Every fact is the plane's:
|
|
53
|
+
* its load readings in the first eleven minutes (the least of them) cut the check's budget to half or less, and the
|
|
54
|
+
* check's last attempt ran into its own limit (at least `selfStopAfter(limitMs)`), or the plane stopped it at the limit
|
|
55
|
+
* (`stoppedAtLimit`). A check with its normal budget that times out is never excused here: a real hang stays a failure.
|
|
56
|
+
*/
|
|
57
|
+
export declare function busyMachineTimeout(record: CheckLoadRecord | null, grantMs: number, stoppedAtLimit: boolean): string | null;
|
|
58
|
+
/**
|
|
59
|
+
* Watches one release check: the limit at its start, the load every 15 s while it runs, and how long its last attempt
|
|
60
|
+
* took. Advisory to the check itself (the limit is a plane value the check is told), and the only source of the record.
|
|
61
|
+
*/
|
|
62
|
+
export declare class CheckLoadWatch {
|
|
63
|
+
private readonly grantMs;
|
|
64
|
+
private readonly options;
|
|
65
|
+
private readonly started;
|
|
66
|
+
private readonly samples;
|
|
67
|
+
private readonly timer;
|
|
68
|
+
private readonly cores;
|
|
69
|
+
private attemptStart;
|
|
70
|
+
private elapsed;
|
|
71
|
+
readonly limitMs: number;
|
|
72
|
+
readonly why: string;
|
|
73
|
+
constructor(grantMs: number, options?: {
|
|
74
|
+
sample?: () => MachineSample;
|
|
75
|
+
clock?: () => number;
|
|
76
|
+
everyMs?: number;
|
|
77
|
+
});
|
|
78
|
+
private push;
|
|
79
|
+
/** Around each attempt of the command: the last one's time is the record's. */
|
|
80
|
+
attemptStarted(): void;
|
|
81
|
+
attemptEnded(): void;
|
|
82
|
+
/** Stops the readings and gives the record to seal. */
|
|
83
|
+
finish(): CheckLoadRecord;
|
|
84
|
+
}
|
|
85
|
+
export {};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plane's own record of the machine a release check ran on (docs/plans/heavy-jobs.md, layer 1b). The check sizes
|
|
3
|
+
* its budget from the load it finds (scripts/release-check.mjs checkBudget), so on a busy machine it runs on one or two
|
|
4
|
+
* lanes — and ran out of its approved hour twice in one evening (runs 3311 and 3315), which read as an unexplained
|
|
5
|
+
* failure a person had to re-approve.
|
|
6
|
+
*
|
|
7
|
+
* Two plane-side answers, never the check's own words:
|
|
8
|
+
* - the limit: a release check that starts with its budget cut to half its normal size or less gets more time, in
|
|
9
|
+
* proportion to the budget it lost, never more than twice the approved limit nor past a hard ceiling (two hours);
|
|
10
|
+
* - the evidence: the load the plane measured itself (when the check started and every 15 s while it ran), the limit
|
|
11
|
+
* it gave and why, and how long the check ran, sealed beside its receipt (CHECK_LOAD_CAPTURE). check-failure.ts reads
|
|
12
|
+
* only this record — never the check's output or report — to call a timeout under a cut budget the machine's own
|
|
13
|
+
* fault. Nothing the candidate writes can make a failure infrastructure.
|
|
14
|
+
*
|
|
15
|
+
* The budget arithmetic is the plane's copy of the check's (src/check-load.test.ts holds them equal), so the budget the
|
|
16
|
+
* plane judges is the one the check gives itself from the same load.
|
|
17
|
+
*/
|
|
18
|
+
import { availableParallelism, loadavg } from "node:os";
|
|
19
|
+
import { z } from "zod";
|
|
20
|
+
export const CHECK_LOAD_CAPTURE = "release check load v1";
|
|
21
|
+
export const machineSample = (platform = process.platform, read = loadavg, cores = availableParallelism) => ({ cores: cores(), load: platform === "win32" ? Number.NaN : read()[0] ?? Number.NaN });
|
|
22
|
+
/** The check's budget on a quiet machine: three quarters of the cores, at least 2 (release-check.mjs checkBudget). */
|
|
23
|
+
export const normalFree = (cores) => Math.max(2, Math.floor(cores * 0.75));
|
|
24
|
+
/** The check's budget under `load` (release-check.mjs checkBudget's `free`). Unmeasured load budgets from the cores. */
|
|
25
|
+
export const freeAt = (cores, load) => Math.max(2, normalFree(cores) - (Number.isFinite(load) && load > 0 ? Math.floor(load) : 0));
|
|
26
|
+
/** Cut: the budget is at most half its normal size. Unmeasured load is never cut. */
|
|
27
|
+
export const budgetCut = (cores, load) => Number.isFinite(load) && freeAt(cores, load) * 2 <= normalFree(cores);
|
|
28
|
+
/** A release check's limit never goes past this, however busy the machine. */
|
|
29
|
+
export const CHECK_LIMIT_CEILING_MS = 120 * 60_000;
|
|
30
|
+
/** …nor past this many times the approved limit. */
|
|
31
|
+
export const CHECK_LIMIT_MAX_FACTOR = 2;
|
|
32
|
+
const minutes = (ms) => `${Math.round(ms / 60_000)} min`;
|
|
33
|
+
/**
|
|
34
|
+
* The limit a release check runs under: the approved one, unless the machine's load at its start cuts the check's
|
|
35
|
+
* budget to half or less — then the approved limit times normal/granted, at most twice it and never past the ceiling
|
|
36
|
+
* (an approved limit already at or past the ceiling is never extended). `why` says which, in words.
|
|
37
|
+
*/
|
|
38
|
+
export function checkLimitFor(grantMs, sample) {
|
|
39
|
+
const normal = normalFree(sample.cores), free = freeAt(sample.cores, sample.load);
|
|
40
|
+
if (!budgetCut(sample.cores, sample.load)) {
|
|
41
|
+
return { limitMs: grantMs, why: `the approved ${minutes(grantMs)}: ${Number.isFinite(sample.load) ? `load ${sample.load.toFixed(1)} on ${sample.cores} cores left the check ${free} of its normal ${normal}` : "load isn't measured here"}` };
|
|
42
|
+
}
|
|
43
|
+
const ceiling = Math.max(grantMs, Math.min(grantMs * CHECK_LIMIT_MAX_FACTOR, CHECK_LIMIT_CEILING_MS));
|
|
44
|
+
const limitMs = Math.min(ceiling, Math.round(grantMs * normal / free));
|
|
45
|
+
return { limitMs, why: `the approved ${minutes(grantMs)}${limitMs > grantMs ? `, extended to ${minutes(limitMs)}` : ", not extended (it is at the ceiling)"}: load ${sample.load.toFixed(1)} on ${sample.cores} cores left the check ${free} of its normal ${normal}, so it gets time in proportion, at most ${minutes(ceiling)}` };
|
|
46
|
+
}
|
|
47
|
+
/** When a check ends itself before its limit (release-check.mjs checkDeadline, from its start): src/check-load.test.ts
|
|
48
|
+
* holds the two equal. A check that ran at least this long ran into its own limit. */
|
|
49
|
+
export const selfStopAfter = (limitMs) => limitMs - Math.min(Math.max(3 * 60_000, Math.round(limitMs / 10)), Math.round(limitMs / 2));
|
|
50
|
+
/** The samples read for the budget the check started with: the check waits up to ten minutes for the load to drop
|
|
51
|
+
* (release-check.mjs), so the least load in its first eleven — before its own load counts — is what it budgeted from at
|
|
52
|
+
* best. The least, so a check that found a quieter moment is never called cut. */
|
|
53
|
+
export const CUT_WINDOW_MS = 11 * 60_000;
|
|
54
|
+
const SAMPLE_EVERY_MS = 15_000;
|
|
55
|
+
const MAX_SAMPLES = 1000;
|
|
56
|
+
const recordSchema = z.strictObject({
|
|
57
|
+
version: z.literal(1),
|
|
58
|
+
cores: z.int().min(1).max(4096),
|
|
59
|
+
grantMs: z.int().min(1),
|
|
60
|
+
limitMs: z.int().min(1),
|
|
61
|
+
why: z.string().max(1000),
|
|
62
|
+
startedAt: z.string().max(40),
|
|
63
|
+
/** How long the check's last attempt ran, by the plane's clock (null: it never started). */
|
|
64
|
+
elapsedMs: z.int().min(0).nullable(),
|
|
65
|
+
/** Each reading, `atMs` after the check started; null where load isn't measured. */
|
|
66
|
+
samples: z.array(z.strictObject({ atMs: z.int().min(0), load: z.number().min(0).nullable() })).max(MAX_SAMPLES),
|
|
67
|
+
});
|
|
68
|
+
/** A sealed record read back, or null when it isn't one (an older run has none: then nothing is excused). */
|
|
69
|
+
export function readCheckLoad(value) {
|
|
70
|
+
const read = recordSchema.safeParse(value);
|
|
71
|
+
if (!read.success)
|
|
72
|
+
return null;
|
|
73
|
+
const record = read.data;
|
|
74
|
+
// A limit the plane could never have given is no record at all.
|
|
75
|
+
const ceiling = Math.max(record.grantMs, Math.min(record.grantMs * CHECK_LIMIT_MAX_FACTOR, CHECK_LIMIT_CEILING_MS));
|
|
76
|
+
return record.limitMs < record.grantMs || record.limitMs > ceiling ? null : record;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Why a release check that ran out of time did so because the machine was busy — or null. Every fact is the plane's:
|
|
80
|
+
* its load readings in the first eleven minutes (the least of them) cut the check's budget to half or less, and the
|
|
81
|
+
* check's last attempt ran into its own limit (at least `selfStopAfter(limitMs)`), or the plane stopped it at the limit
|
|
82
|
+
* (`stoppedAtLimit`). A check with its normal budget that times out is never excused here: a real hang stays a failure.
|
|
83
|
+
*/
|
|
84
|
+
export function busyMachineTimeout(record, grantMs, stoppedAtLimit) {
|
|
85
|
+
if (record === null || record.grantMs !== grantMs)
|
|
86
|
+
return null;
|
|
87
|
+
const early = record.samples.filter(one => one.atMs <= CUT_WINDOW_MS && one.load !== null).map(one => one.load);
|
|
88
|
+
if (early.length === 0)
|
|
89
|
+
return null;
|
|
90
|
+
const least = Math.min(...early);
|
|
91
|
+
if (!budgetCut(record.cores, least))
|
|
92
|
+
return null;
|
|
93
|
+
if (!stoppedAtLimit && (record.elapsedMs === null || record.elapsedMs < selfStopAfter(record.limitMs)))
|
|
94
|
+
return null;
|
|
95
|
+
return `the machine was busy: its load stayed at ${least.toFixed(1)} or more on ${record.cores} cores as the check started, cutting its budget to ${freeAt(record.cores, least)} of its normal ${normalFree(record.cores)} cores, and it ran out of its ${minutes(record.limitMs)}`;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Watches one release check: the limit at its start, the load every 15 s while it runs, and how long its last attempt
|
|
99
|
+
* took. Advisory to the check itself (the limit is a plane value the check is told), and the only source of the record.
|
|
100
|
+
*/
|
|
101
|
+
export class CheckLoadWatch {
|
|
102
|
+
grantMs;
|
|
103
|
+
options;
|
|
104
|
+
started;
|
|
105
|
+
samples = [];
|
|
106
|
+
timer;
|
|
107
|
+
cores;
|
|
108
|
+
attemptStart = null;
|
|
109
|
+
elapsed = null;
|
|
110
|
+
limitMs;
|
|
111
|
+
why;
|
|
112
|
+
constructor(grantMs, options = {}) {
|
|
113
|
+
this.grantMs = grantMs;
|
|
114
|
+
this.options = options;
|
|
115
|
+
const sample = options.sample ?? (() => machineSample());
|
|
116
|
+
const clock = options.clock ?? Date.now;
|
|
117
|
+
this.started = clock();
|
|
118
|
+
const first = sample();
|
|
119
|
+
this.cores = first.cores;
|
|
120
|
+
({ limitMs: this.limitMs, why: this.why } = checkLimitFor(grantMs, first));
|
|
121
|
+
this.push(0, first.load);
|
|
122
|
+
const every = options.everyMs ?? SAMPLE_EVERY_MS;
|
|
123
|
+
this.timer = every > 0 ? setInterval(() => { try {
|
|
124
|
+
this.push(clock() - this.started, sample().load);
|
|
125
|
+
}
|
|
126
|
+
catch { /* a missed reading */ } }, every) : null;
|
|
127
|
+
this.timer?.unref?.();
|
|
128
|
+
}
|
|
129
|
+
push(atMs, load) {
|
|
130
|
+
if (this.samples.length >= MAX_SAMPLES) {
|
|
131
|
+
if (this.timer !== null)
|
|
132
|
+
clearInterval(this.timer);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
this.samples.push({ atMs: Math.max(0, Math.round(atMs)), load: Number.isFinite(load) && load >= 0 ? Math.round(load * 100) / 100 : null });
|
|
136
|
+
}
|
|
137
|
+
/** Around each attempt of the command: the last one's time is the record's. */
|
|
138
|
+
attemptStarted() { this.attemptStart = (this.options.clock ?? Date.now)(); }
|
|
139
|
+
attemptEnded() { if (this.attemptStart !== null)
|
|
140
|
+
this.elapsed = Math.max(0, Math.round((this.options.clock ?? Date.now)() - this.attemptStart)); }
|
|
141
|
+
/** Stops the readings and gives the record to seal. */
|
|
142
|
+
finish() {
|
|
143
|
+
if (this.timer !== null)
|
|
144
|
+
clearInterval(this.timer);
|
|
145
|
+
return { version: 1, cores: this.cores, grantMs: this.grantMs, limitMs: this.limitMs, why: this.why, startedAt: new Date(this.started).toISOString(), elapsedMs: this.elapsed, samples: [...this.samples] };
|
|
146
|
+
}
|
|
147
|
+
}
|