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
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { CheckAccess } from "./check-access.js";
|
|
2
|
+
import type { Store } from "./store.js";
|
|
3
|
+
export type CheckAuthorization = {
|
|
4
|
+
id: number;
|
|
5
|
+
taskRef: number;
|
|
6
|
+
run: number;
|
|
7
|
+
candidate: string;
|
|
8
|
+
commandDigest: string;
|
|
9
|
+
level: "quick" | "full";
|
|
10
|
+
access: CheckAccess;
|
|
11
|
+
scopeDigest: string;
|
|
12
|
+
expiresAt: string;
|
|
13
|
+
usedRequest: number | null;
|
|
14
|
+
};
|
|
15
|
+
export declare function readAuthorization(row: Record<string, unknown>): CheckAuthorization;
|
|
16
|
+
/** The approval an authorization stands on: the plan's seal, on these exact bytes, still standing as dispatch reads it. */
|
|
17
|
+
export declare function approvedScopeStands(store: Store, taskRef: number, scopeDigest: string, now: Date): boolean;
|
|
18
|
+
/** The plain reason an authorization doesn't stand, or null: this exact commit and command, on the result it was
|
|
19
|
+
* written for, under the same standing approval, not put away. */
|
|
20
|
+
export declare function authorizationProblem(store: Store, auth: CheckAuthorization, input: {
|
|
21
|
+
head: string;
|
|
22
|
+
digest: string;
|
|
23
|
+
}, now: Date): string | null;
|
|
24
|
+
/** At the run: the used authorization this request claimed. */
|
|
25
|
+
export declare function recheckStanding(store: Store, input: {
|
|
26
|
+
request: number;
|
|
27
|
+
runId: number;
|
|
28
|
+
head: string;
|
|
29
|
+
digest: string;
|
|
30
|
+
}): {
|
|
31
|
+
ok: true;
|
|
32
|
+
access: CheckAccess;
|
|
33
|
+
} | {
|
|
34
|
+
ok: false;
|
|
35
|
+
problem: string;
|
|
36
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** Whether a check-only authorization stands for one automatic recheck (check-only.ts), read at every step. Kept apart
|
|
2
|
+
* from the sweep so the follow-up runner reads it without loading the road that asks. */
|
|
3
|
+
import { authorityStandsSql } from "./approval-authority.js";
|
|
4
|
+
import { checkAccessOf } from "./check-access.js";
|
|
5
|
+
import { refArchived } from "./task-archive.js";
|
|
6
|
+
export function readAuthorization(row) {
|
|
7
|
+
return { id: Number(row["id"]), taskRef: Number(row["task_ref"]), run: Number(row["run"]), candidate: String(row["candidate"]), commandDigest: String(row["command_digest"]),
|
|
8
|
+
level: row["level"] === "quick" ? "quick" : "full", access: checkAccessOf(row["check_access"]), scopeDigest: String(row["scope_digest"]), expiresAt: String(row["expires_at"]),
|
|
9
|
+
usedRequest: row["used_request"] === null || row["used_request"] === undefined ? null : Number(row["used_request"]) };
|
|
10
|
+
}
|
|
11
|
+
/** The approval an authorization stands on: the plan's seal, on these exact bytes, still standing as dispatch reads it. */
|
|
12
|
+
export function approvedScopeStands(store, taskRef, scopeDigest, now) {
|
|
13
|
+
const stamp = now.toISOString();
|
|
14
|
+
return store.handle.prepare(`SELECT 1 AS hit FROM task_scope JOIN task_ref ON task_ref.id = ? AND task_scope.task_id = task_ref.external_id
|
|
15
|
+
WHERE task_scope.digest = ? AND task_scope.approved_digest = task_scope.digest AND task_scope.approved_at IS NOT NULL
|
|
16
|
+
AND ${authorityStandsSql("task_scope", "task_ref.repo", "?")}`).get(taskRef, scopeDigest, stamp, stamp, stamp, stamp) !== undefined;
|
|
17
|
+
}
|
|
18
|
+
/** The plain reason an authorization doesn't stand, or null: this exact commit and command, on the result it was
|
|
19
|
+
* written for, under the same standing approval, not put away. */
|
|
20
|
+
export function authorizationProblem(store, auth, input, now) {
|
|
21
|
+
if (auth.candidate !== input.head)
|
|
22
|
+
return "The commit changed.";
|
|
23
|
+
if (auth.commandDigest !== input.digest)
|
|
24
|
+
return "The approved check changed.";
|
|
25
|
+
if (refArchived(store, auth.taskRef))
|
|
26
|
+
return "This task is put away.";
|
|
27
|
+
if (!approvedScopeStands(store, auth.taskRef, auth.scopeDigest, now))
|
|
28
|
+
return "Its approval no longer stands.";
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
/** At the run: the used authorization this request claimed. */
|
|
32
|
+
export function recheckStanding(store, input) {
|
|
33
|
+
const row = store.handle.prepare("SELECT * FROM check_authorization WHERE used_request = ? AND run = ?").get(input.request, input.runId);
|
|
34
|
+
if (row === undefined)
|
|
35
|
+
return { ok: false, problem: "Its authorization couldn't be read." };
|
|
36
|
+
const auth = readAuthorization(row);
|
|
37
|
+
const problem = authorizationProblem(store, auth, input, new Date());
|
|
38
|
+
return problem === null ? { ok: true, access: auth.access } : { ok: false, problem };
|
|
39
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Store } from "./store.js";
|
|
2
|
+
/** How long a recheck may still start after its build finished. */
|
|
3
|
+
export declare const CHECK_ONLY_WINDOW_MS: number;
|
|
4
|
+
export declare const RECHECK_ACTOR = "Toolroll";
|
|
5
|
+
/**
|
|
6
|
+
* Written only from the existing approval of this exact commit: the seal this run was admitted under, still standing,
|
|
7
|
+
* on the bytes this run built. Null when anything is missing, and the person's Run checks stays the only road.
|
|
8
|
+
*/
|
|
9
|
+
export declare function authorizeCheckOnly(store: Store, runId: number, now: Date): number | null;
|
|
10
|
+
/** Claim one authorization and ask for its check, in one transaction. Null: nothing was asked. */
|
|
11
|
+
export declare function claimRecheck(store: Store, authorization: number, now: Date): {
|
|
12
|
+
request: number;
|
|
13
|
+
} | null;
|
|
14
|
+
/** Each sweep: a standing authorization whose result has no readable verdict asks for its one check. */
|
|
15
|
+
export declare function requestMissingRechecks(store: Store, root: string, now: Date): number;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Automatic recheck under a check-only authorization (docs/plans/plain-toolroll.md P2.2).
|
|
3
|
+
*
|
|
4
|
+
* When a build finishes, the approval that built its commit writes one authorization: this task, this exact commit, the
|
|
5
|
+
* approved check command's digest, the access the build's own check had, one automatic run, and an expiry. It never
|
|
6
|
+
* seals anything and starts no build and no agent: it asks the follow-up check road (result-follow-ups.ts), which checks
|
|
7
|
+
* the commit out in a fresh folder and runs the approved command, nothing else.
|
|
8
|
+
*
|
|
9
|
+
* The sweep uses it only when the result's verdict is missing or unreadable. A failed, passed or running check needs
|
|
10
|
+
* nothing, and closes it. The claim (used_at, compare-and-swap in the transaction that asks) is the cap: a second worker
|
|
11
|
+
* or a crash after the claim finds it spent, and the need stays with the person. A changed command is a different
|
|
12
|
+
* digest, so the old authorization covers nothing: Run checks on the new command is the person's act.
|
|
13
|
+
*/
|
|
14
|
+
import { assignmentChecksForRun } from "./assignment.js";
|
|
15
|
+
import { runCheckLevel, repoOfRun, checkCommandFor } from "./check-levels.js";
|
|
16
|
+
import { approvedScopeStands, authorizationProblem, readAuthorization } from "./check-only-standing.js";
|
|
17
|
+
import { requestFollowUpChecks } from "./result-follow-ups.js";
|
|
18
|
+
import { releaseCheckScope } from "./scope.js";
|
|
19
|
+
import { refArchived } from "./task-archive.js";
|
|
20
|
+
import { approvedCheckStateOf } from "./task-status.js";
|
|
21
|
+
/** How long a recheck may still start after its build finished. */
|
|
22
|
+
export const CHECK_ONLY_WINDOW_MS = 24 * 60 * 60_000;
|
|
23
|
+
export const RECHECK_ACTOR = "Toolroll";
|
|
24
|
+
/**
|
|
25
|
+
* Written only from the existing approval of this exact commit: the seal this run was admitted under, still standing,
|
|
26
|
+
* on the bytes this run built. Null when anything is missing, and the person's Run checks stays the only road.
|
|
27
|
+
*/
|
|
28
|
+
export function authorizeCheckOnly(store, runId, now) {
|
|
29
|
+
return store.transact(() => {
|
|
30
|
+
const run = store.getRun(runId);
|
|
31
|
+
if (run === null || run.role !== "builder" || run.outcome !== "built" || run.headRevision === null || !/^[a-f0-9]{40}$/.test(run.headRevision))
|
|
32
|
+
return null;
|
|
33
|
+
const ref = store.refById(run.taskRef);
|
|
34
|
+
if (ref === null || ref.repo === null || refArchived(store, ref.id))
|
|
35
|
+
return null;
|
|
36
|
+
const scope = store.getScope(ref.externalId);
|
|
37
|
+
if (scope === null || scope.approvedDigest !== scope.digest || run.scopeDigest !== scope.digest)
|
|
38
|
+
return null;
|
|
39
|
+
// A release's check has its own road (check-failure.ts), and the nightly's its own.
|
|
40
|
+
if (releaseCheckScope(scope) || scope.releaseTerms != null)
|
|
41
|
+
return null;
|
|
42
|
+
if (!approvedScopeStands(store, ref.id, scope.digest, now))
|
|
43
|
+
return null;
|
|
44
|
+
const level = runCheckLevel(store, runId);
|
|
45
|
+
if (level === "off")
|
|
46
|
+
return null;
|
|
47
|
+
const chosen = checkCommandFor(store, ref.repo, level ?? "full");
|
|
48
|
+
if (chosen.command === null || chosen.level === "off")
|
|
49
|
+
return null;
|
|
50
|
+
const written = store.handle.prepare(`INSERT INTO check_authorization (task_ref, run, candidate, command_digest, level, check_access, scope_digest, written_at, expires_at)
|
|
51
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT (task_ref, candidate, command_digest) DO NOTHING`)
|
|
52
|
+
.run(ref.id, runId, run.headRevision, chosen.command.digest, chosen.level, store.runCheckAccess(runId), scope.digest, now.toISOString(), new Date(now.getTime() + CHECK_ONLY_WINDOW_MS).toISOString());
|
|
53
|
+
return Number(written.changes) > 0 ? Number(written.lastInsertRowid) : null;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
/** Claim one authorization and ask for its check, in one transaction. Null: nothing was asked. */
|
|
57
|
+
export function claimRecheck(store, authorization, now) {
|
|
58
|
+
return store.transact(() => {
|
|
59
|
+
const row = store.handle.prepare("SELECT * FROM check_authorization WHERE id = ? AND used_at IS NULL AND closed_at IS NULL AND expires_at > ?").get(authorization, now.toISOString());
|
|
60
|
+
if (row === undefined)
|
|
61
|
+
return null;
|
|
62
|
+
const auth = readAuthorization(row);
|
|
63
|
+
const run = store.getRun(auth.run);
|
|
64
|
+
const repo = repoOfRun(store, auth.run);
|
|
65
|
+
if (run === null || repo === null || store.lastBuilderRun(auth.taskRef)?.id !== auth.run || store.hasLiveClaim(auth.taskRef, now))
|
|
66
|
+
return null;
|
|
67
|
+
const live = checkCommandFor(store, repo, auth.level).command;
|
|
68
|
+
if (live === null || authorizationProblem(store, auth, { head: run.headRevision ?? "", digest: live.digest }, now) !== null)
|
|
69
|
+
return null;
|
|
70
|
+
const asked = requestFollowUpChecks(store, { runId: auth.run, level: auth.level, actor: RECHECK_ACTOR, why: "recheck" }, now);
|
|
71
|
+
// An existing request is the person's own: it stands, and this authorization isn't spent on it.
|
|
72
|
+
if (!asked.ok || asked.existing)
|
|
73
|
+
return null;
|
|
74
|
+
const spent = store.handle.prepare("UPDATE check_authorization SET used_at = ?, used_request = ? WHERE id = ? AND used_at IS NULL").run(now.toISOString(), asked.request, auth.id);
|
|
75
|
+
if (Number(spent.changes) !== 1)
|
|
76
|
+
throw new Error("the recheck's authorization moved inside its own transaction");
|
|
77
|
+
return { request: asked.request };
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/** Each sweep: a standing authorization whose result has no readable verdict asks for its one check. */
|
|
81
|
+
export function requestMissingRechecks(store, root, now) {
|
|
82
|
+
const open = store.handle.prepare("SELECT * FROM check_authorization WHERE used_at IS NULL AND closed_at IS NULL AND expires_at > ? ORDER BY id").all(now.toISOString());
|
|
83
|
+
let asked = 0;
|
|
84
|
+
for (const row of open) {
|
|
85
|
+
const auth = readAuthorization(row);
|
|
86
|
+
const run = store.getRun(auth.run);
|
|
87
|
+
if (run === null || run.finishedAt === null)
|
|
88
|
+
continue;
|
|
89
|
+
const state = approvedCheckStateOf(assignmentChecksForRun(store, root, auth.run, now));
|
|
90
|
+
if (state === "not-run" || state === "unreadable") {
|
|
91
|
+
if (claimRecheck(store, auth.id, now) !== null)
|
|
92
|
+
asked++;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (state !== "running")
|
|
96
|
+
store.handle.prepare("UPDATE check_authorization SET closed_at = ? WHERE id = ? AND used_at IS NULL").run(now.toISOString(), auth.id);
|
|
97
|
+
}
|
|
98
|
+
return asked;
|
|
99
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Store } from "./store.js";
|
|
2
|
+
export type CheckRepairState = "waiting" | "repairing" | "rerunning" | "with-person" | "parked" | "fixed" | "closed";
|
|
3
|
+
export type CheckRepair = {
|
|
4
|
+
id: number;
|
|
5
|
+
familyRoot: string;
|
|
6
|
+
ownerTask: string;
|
|
7
|
+
repo: string;
|
|
8
|
+
revision: string;
|
|
9
|
+
sourceRun: number;
|
|
10
|
+
state: CheckRepairState;
|
|
11
|
+
why: string;
|
|
12
|
+
repairTask: string | null;
|
|
13
|
+
attempt: number | null;
|
|
14
|
+
createdAt: string;
|
|
15
|
+
updatedAt: string;
|
|
16
|
+
};
|
|
17
|
+
export declare function checkRepairsOfTask(store: Store, taskId: string): CheckRepair[];
|
|
18
|
+
export declare function checkRepairOfRun(store: Store, runId: number): CheckRepair | null;
|
|
19
|
+
/** What makes this failure this failure: the command, how it ended, and which files failed. Never the log's own bytes,
|
|
20
|
+
* which differ on every run. */
|
|
21
|
+
export declare function failureKeyOf(receipt: string, log: string): string;
|
|
22
|
+
/**
|
|
23
|
+
* Route one failed check. Runs at disposition. A failure already on record for this family and revision changes nothing,
|
|
24
|
+
* unless it was the one rerun of an unrelated failure, which now goes to a person through the ledger's own rerun stop.
|
|
25
|
+
*/
|
|
26
|
+
export declare function routeFailedCheck(store: Store, repo: string, root: string, runId: number, now: Date): CheckRepair | null;
|
|
27
|
+
export type StartRepair = {
|
|
28
|
+
ok: true;
|
|
29
|
+
draft: string;
|
|
30
|
+
attempt: number;
|
|
31
|
+
approved: boolean;
|
|
32
|
+
} | {
|
|
33
|
+
ok: false;
|
|
34
|
+
reason: "unknown" | "refused";
|
|
35
|
+
message: string;
|
|
36
|
+
};
|
|
37
|
+
/** A person asks for the repair. The draft takes the normal plan approval: this is not that approval. */
|
|
38
|
+
export declare function startCheckRepair(store: Store, root: string, runId: number, now: Date): StartRepair;
|
|
39
|
+
/** A passing result in the family ends its failed-check rows. Rows the person already holds end too: the code passes now. */
|
|
40
|
+
export declare function settleCheckRepairs(store: Store, taskId: string, verdict: string, now: Date): void;
|
|
41
|
+
/** Each sweep: a parked failure whose task is free again takes its next step, unless a newer attempt replaced it. */
|
|
42
|
+
export declare function resumeParkedRepairs(store: Store, root: string, now: Date): number;
|
|
43
|
+
/** A build was accepted as finished: count what it answered, then route its check if it failed. Bookkeeping that never
|
|
44
|
+
* costs the build its result: a problem here leaves the finding open and the failed check on the result. */
|
|
45
|
+
export declare function followBuiltRun(store: Store, repo: string, root: string, runId: number, worktree: string, now: Date): void;
|
|
46
|
+
/** How a failed check stands, for a person, in one line. */
|
|
47
|
+
export declare function checkRepairWords(row: CheckRepair): string;
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A failed check gets one owner and a bounded repair (docs/plans/fleet-follow-through.md, part 2).
|
|
3
|
+
*
|
|
4
|
+
* One `check_repair` row per task family, revision and failure: the UNIQUE key is what keeps the same failure on the
|
|
5
|
+
* same revision from starting a second repair, however many times it is seen. The row's owner is the task that built
|
|
6
|
+
* the revision. The repair itself is the existing ledger's (`maybeTriggerRepair`, `repair_chain`), so it is an ordinary
|
|
7
|
+
* revision: the source scope verbatim, the ordinary rails, the ordinary approval.
|
|
8
|
+
*
|
|
9
|
+
* Who says yes doesn't change here. A mode a person signed with automatic repair and a limit drafts and approves through
|
|
10
|
+
* the seal that already exists. Without that signature nothing is drafted and nothing spends: the row waits for a person,
|
|
11
|
+
* and a person's repair is a draft that takes the normal plan approval. A model never moves a row past that.
|
|
12
|
+
*
|
|
13
|
+
* Failure text never reaches this file's decisions: the failure key comes from the sealed receipt and the log's failing
|
|
14
|
+
* file names, and it only ever matches rows, it grants nothing.
|
|
15
|
+
*/
|
|
16
|
+
import { createHash } from "node:crypto";
|
|
17
|
+
import { maybeTriggerRepair } from "./dispose.js";
|
|
18
|
+
import { readingFleet } from "./fleet-schema.js";
|
|
19
|
+
import { familyRootOf, settleFindingAnswers } from "./findings.js";
|
|
20
|
+
import { failingTestsOf } from "./gate-failure.js";
|
|
21
|
+
import { CHECK_REPAIR_LIMIT, modeTermsFromJson } from "./modes.js";
|
|
22
|
+
import { releaseCheckScope } from "./scope.js";
|
|
23
|
+
import { readVerificationView } from "./contracts/verification-view.js";
|
|
24
|
+
import { failedVerificationEvidence } from "./verification-evidence.js";
|
|
25
|
+
import { saveEvidencePair } from "./evidence-pair.js";
|
|
26
|
+
const repairOf = (row) => ({
|
|
27
|
+
id: Number(row["id"]), familyRoot: String(row["family_root"]), ownerTask: String(row["owner_task"]), repo: String(row["repo"]), revision: String(row["revision"]),
|
|
28
|
+
sourceRun: Number(row["source_run"]), state: String(row["state"]), why: String(row["why"]), repairTask: row["repair_task"] === null ? null : String(row["repair_task"]),
|
|
29
|
+
attempt: row["attempt"] === null ? null : Number(row["attempt"]), createdAt: String(row["created_at"]), updatedAt: String(row["updated_at"]),
|
|
30
|
+
});
|
|
31
|
+
export function checkRepairsOfTask(store, taskId) {
|
|
32
|
+
return readingFleet(() => store.handle.prepare("SELECT * FROM check_repair WHERE family_root = ? ORDER BY id DESC").all(familyRootOf(store, taskId)).map(repairOf), []);
|
|
33
|
+
}
|
|
34
|
+
export function checkRepairOfRun(store, runId) {
|
|
35
|
+
return readingFleet(() => {
|
|
36
|
+
const row = store.handle.prepare("SELECT * FROM check_repair WHERE source_run = ? ORDER BY id DESC LIMIT 1").get(runId);
|
|
37
|
+
return row === undefined ? null : repairOf(row);
|
|
38
|
+
}, null);
|
|
39
|
+
}
|
|
40
|
+
/** What makes this failure this failure: the command, how it ended, and which files failed. Never the log's own bytes,
|
|
41
|
+
* which differ on every run. */
|
|
42
|
+
export function failureKeyOf(receipt, log) {
|
|
43
|
+
const view = readVerificationView(receipt);
|
|
44
|
+
const facts = view.ok ? view.value : null;
|
|
45
|
+
const failing = failingTestsOf(log)?.map(one => one.file).sort() ?? [];
|
|
46
|
+
const result = facts === null ? null : facts.result;
|
|
47
|
+
return createHash("sha256").update(JSON.stringify(["toolroll:check-failure", facts?.command.digest ?? null, result?.exitCode ?? null, result?.failure ?? null, failing])).digest("hex").slice(0, 32);
|
|
48
|
+
}
|
|
49
|
+
/** Parked work stays quiet: a person already has this task, or it is stopped. */
|
|
50
|
+
function parkedWhy(store, taskRef, runId, now) {
|
|
51
|
+
if (store.activeHolds(taskRef, now).length > 0)
|
|
52
|
+
return "This task is on hold.";
|
|
53
|
+
if (store.applicableStopFor(runId) !== null)
|
|
54
|
+
return "This attempt was stopped.";
|
|
55
|
+
if (store.decisionsForTask(taskRef).some(one => one.state === "open" || one.state === "expired"))
|
|
56
|
+
return "This task is waiting for an answer.";
|
|
57
|
+
if (store.pendingSteerCount(taskRef) > 0)
|
|
58
|
+
return "This task has a note waiting for its next attempt.";
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
/** Why an approved plan can't cover a repair of this result, or null when it can. */
|
|
62
|
+
function notCovered(store, taskId, runId) {
|
|
63
|
+
const scope = store.getScope(taskId);
|
|
64
|
+
if (scope === null)
|
|
65
|
+
return "This task has no approved plan to repair under.";
|
|
66
|
+
if (scope.releaseTerms != null || releaseCheckScope(scope) || (scope.candidate ?? null) !== null)
|
|
67
|
+
return "Its plan only covers checking this commit. A fix needs a task of its own.";
|
|
68
|
+
if (store.lookupRef(taskId)?.deliverable === "report")
|
|
69
|
+
return "This task delivers a report: there's no code to repair.";
|
|
70
|
+
const publication = store.publicationForRun(runId);
|
|
71
|
+
if (publication !== null && !(publication.state === "intended" && publication.attempts === 0))
|
|
72
|
+
return "This result is published. Fix it through its pull request.";
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
/** The attempts this family's repair chain has spent. */
|
|
76
|
+
const attemptsUsed = (store, taskId) => store.repairLineageOf(taskId).attempts.reduce((highest, row) => Math.max(highest, row.attempt), 0);
|
|
77
|
+
function move(store, id, state, why, now, draft) {
|
|
78
|
+
store.handle.prepare("UPDATE check_repair SET state = ?, why = ?, updated_at = ?, repair_task = COALESCE(?, repair_task), attempt = COALESCE(?, attempt) WHERE id = ?")
|
|
79
|
+
.run(state, why, now.toISOString(), draft?.task ?? null, draft?.attempt ?? null, id);
|
|
80
|
+
store.bumpWake();
|
|
81
|
+
}
|
|
82
|
+
function askPerson(store, row, subject, body, now) {
|
|
83
|
+
store.enqueueNotification({ source: { run: row.sourceRun }, dedupeKey: `check-repair:${row.id}:${subject}`, kind: "check-repair", pushClass: "attention", subject, body, link: `/t/${encodeURIComponent(row.ownerTask)}` }, now);
|
|
84
|
+
}
|
|
85
|
+
/** Take the row one step, from where it stands now. A person's yes is never decided here: `person` only says a person asked for the draft. */
|
|
86
|
+
function advance(store, repo, root, row, now, person) {
|
|
87
|
+
const run = store.getRun(row.sourceRun);
|
|
88
|
+
const ref = run === null ? null : store.refById(run.taskRef);
|
|
89
|
+
const verdict = store.proofVerdictFor(row.sourceRun);
|
|
90
|
+
if (run === null || ref === null || verdict === null) {
|
|
91
|
+
move(store, row.id, "with-person", "The failed attempt's records can't be read.", now);
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
const parked = person ? null : parkedWhy(store, run.taskRef, run.id, now);
|
|
95
|
+
if (parked !== null) {
|
|
96
|
+
move(store, row.id, "parked", parked, now);
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
const uncovered = notCovered(store, row.ownerTask, run.id);
|
|
100
|
+
if (uncovered !== null) {
|
|
101
|
+
move(store, row.id, "with-person", uncovered, now);
|
|
102
|
+
// A release's check speaks for itself (release-adapters.ts): one voice for one failure.
|
|
103
|
+
if (store.getScope(row.ownerTask)?.releaseTerms == null)
|
|
104
|
+
askPerson(store, row, "A failed check needs you", `The check failed on ${row.revision.slice(0, 7)}, and a repair can't run under this plan. ${uncovered}`, now);
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
const mode = store.activeMode(repo, now);
|
|
108
|
+
const terms = mode === null ? null : modeTermsFromJson(mode.termsJson);
|
|
109
|
+
const auto = terms?.repairAuto === true;
|
|
110
|
+
const limit = auto ? terms.repairMaxAttempts : CHECK_REPAIR_LIMIT;
|
|
111
|
+
const used = attemptsUsed(store, row.ownerTask);
|
|
112
|
+
if (used >= limit) {
|
|
113
|
+
const why = `${used} repair${used === 1 ? "" : "s"} tried, which is the limit. It's yours to decide.`;
|
|
114
|
+
move(store, row.id, "with-person", why, now);
|
|
115
|
+
askPerson(store, row, "Repairs ran out on a failed check", `The check still fails on ${row.revision.slice(0, 7)} after ${used} repair${used === 1 ? "" : "s"}. The failed check's log is on the result.`, now);
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
if (!auto && !person) {
|
|
119
|
+
move(store, row.id, "waiting", "Waiting for a person to start a repair.", now);
|
|
120
|
+
store.enqueueNotification({ source: { run: run.id }, dedupeKey: `check-repair:${row.id}:waiting`, kind: "check-repair", subject: "A check failed", body: `The check failed on ${row.revision.slice(0, 7)}. Start a repair on the task page.`, link: `/t/${encodeURIComponent(row.ownerTask)}` }, now);
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
const trigger = maybeTriggerRepair(store, repo, root, run.id, verdict.verdict, now, "verification", !person);
|
|
124
|
+
if (trigger.kind === "drafted") {
|
|
125
|
+
move(store, row.id, "repairing", trigger.approved ? `Repair ${trigger.draftTaskId} is approved under the signed mode and will build next.` : `Repair ${trigger.draftTaskId} is drafted and waits for its plan to be approved.`, now, { task: trigger.draftTaskId, attempt: trigger.attempt });
|
|
126
|
+
saveEvidencePair(store, root, { taskId: row.ownerTask, runId: run.id, by: "Toolroll" }, now);
|
|
127
|
+
}
|
|
128
|
+
else if (trigger.kind === "rerun") {
|
|
129
|
+
move(store, row.id, "rerunning", "The failure looks unrelated to this change, so the check runs once more on the same commit.", now);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
const why = trigger.kind === "stopped" ? stopWords(trigger.reason) : "A repair can't start from this result. It's yours to decide.";
|
|
133
|
+
move(store, row.id, "with-person", why, now);
|
|
134
|
+
if (trigger.kind === "stopped")
|
|
135
|
+
askPerson(store, row, "A repair stopped", why, now);
|
|
136
|
+
}
|
|
137
|
+
return trigger;
|
|
138
|
+
}
|
|
139
|
+
const stopWords = (reason) => ({
|
|
140
|
+
"repair-attempts-spent": "The repairs allowed were used up.",
|
|
141
|
+
"repair-no-progress": "Two repairs in a row didn't reduce what's failing, so Toolroll stopped.",
|
|
142
|
+
"repair-refused-integrity": "The result disagrees with its approved terms, so a person reads it first.",
|
|
143
|
+
}[reason]);
|
|
144
|
+
/**
|
|
145
|
+
* Route one failed check. Runs at disposition. A failure already on record for this family and revision changes nothing,
|
|
146
|
+
* unless it was the one rerun of an unrelated failure, which now goes to a person through the ledger's own rerun stop.
|
|
147
|
+
*/
|
|
148
|
+
export function routeFailedCheck(store, repo, root, runId, now) {
|
|
149
|
+
const run = store.getRun(runId);
|
|
150
|
+
if (run === null || run.role !== "builder" || run.finishedAt === null || !run.headRevision)
|
|
151
|
+
return null;
|
|
152
|
+
const taskId = store.externalIdFor(run.taskRef);
|
|
153
|
+
if (taskId === null)
|
|
154
|
+
return null;
|
|
155
|
+
const failure = failedVerificationEvidence(store, root, runId);
|
|
156
|
+
if (failure.kind !== "failed")
|
|
157
|
+
return null;
|
|
158
|
+
const familyRoot = familyRootOf(store, taskId);
|
|
159
|
+
const key = failureKeyOf(failure.receipt, failure.log);
|
|
160
|
+
return store.transact(() => {
|
|
161
|
+
const earlier = store.handle.prepare("SELECT * FROM check_repair WHERE family_root = ? AND revision = ? AND failure_key = ?").get(familyRoot, run.headRevision, key);
|
|
162
|
+
if (earlier !== undefined) {
|
|
163
|
+
const row = repairOf(earlier);
|
|
164
|
+
if (row.state === "rerunning") {
|
|
165
|
+
store.handle.prepare("UPDATE check_repair SET source_run = ? WHERE id = ?").run(runId, row.id);
|
|
166
|
+
maybeTriggerRepair(store, repo, root, runId, store.proofVerdictFor(runId)?.verdict ?? "refuted", now, "verification", true);
|
|
167
|
+
move(store, row.id, "with-person", "It failed again on the same commit. It's yours to decide.", now);
|
|
168
|
+
}
|
|
169
|
+
return repairOf(store.handle.prepare("SELECT * FROM check_repair WHERE id = ?").get(row.id));
|
|
170
|
+
}
|
|
171
|
+
// A newer failure replaces an older one nobody took up.
|
|
172
|
+
store.handle.prepare("UPDATE check_repair SET state = 'closed', why = 'A newer attempt replaced this result.', updated_at = ? WHERE family_root = ? AND state IN ('waiting', 'parked', 'rerunning')").run(now.toISOString(), familyRoot);
|
|
173
|
+
const made = store.handle.prepare(`INSERT INTO check_repair (family_root, owner_task, repo, revision, failure_key, source_run, state, why, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, 'waiting', '', ?, ?)`)
|
|
174
|
+
.run(familyRoot, taskId, repo, run.headRevision, key, runId, now.toISOString(), now.toISOString());
|
|
175
|
+
const row = repairOf(store.handle.prepare("SELECT * FROM check_repair WHERE id = ?").get(Number(made.lastInsertRowid)));
|
|
176
|
+
advance(store, repo, root, row, now, false);
|
|
177
|
+
return repairOf(store.handle.prepare("SELECT * FROM check_repair WHERE id = ?").get(row.id));
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
/** A person asks for the repair. The draft takes the normal plan approval: this is not that approval. */
|
|
181
|
+
export function startCheckRepair(store, root, runId, now) {
|
|
182
|
+
const row = checkRepairOfRun(store, runId);
|
|
183
|
+
if (row === null)
|
|
184
|
+
return { ok: false, reason: "unknown", message: `Run ${runId} has no failed check to repair.` };
|
|
185
|
+
if (row.state !== "waiting" && row.state !== "parked" && row.state !== "with-person")
|
|
186
|
+
return { ok: false, reason: "refused", message: row.repairTask !== null ? `Repair ${row.repairTask} already exists for this failure.` : row.why };
|
|
187
|
+
return store.transact(() => {
|
|
188
|
+
const trigger = advance(store, row.repo, root, row, now, true);
|
|
189
|
+
if (trigger?.kind === "drafted")
|
|
190
|
+
return { ok: true, draft: trigger.draftTaskId, attempt: trigger.attempt, approved: trigger.approved };
|
|
191
|
+
return { ok: false, reason: "refused", message: repairOf(store.handle.prepare("SELECT * FROM check_repair WHERE id = ?").get(row.id)).why };
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
/** A passing result in the family ends its failed-check rows. Rows the person already holds end too: the code passes now. */
|
|
195
|
+
export function settleCheckRepairs(store, taskId, verdict, now) {
|
|
196
|
+
if (verdict !== "verified" && verdict !== "attested")
|
|
197
|
+
return;
|
|
198
|
+
readingFleet(() => store.handle.prepare("UPDATE check_repair SET state = 'fixed', why = 'A later result passed its check.', updated_at = ? WHERE family_root = ? AND state NOT IN ('fixed', 'closed')").run(now.toISOString(), familyRootOf(store, taskId)), null);
|
|
199
|
+
}
|
|
200
|
+
/** Each sweep: a parked failure whose task is free again takes its next step, unless a newer attempt replaced it. */
|
|
201
|
+
export function resumeParkedRepairs(store, root, now) {
|
|
202
|
+
return readingFleet(() => {
|
|
203
|
+
let moved = 0;
|
|
204
|
+
for (const raw of store.handle.prepare("SELECT * FROM check_repair WHERE state = 'parked'").all()) {
|
|
205
|
+
const row = repairOf(raw);
|
|
206
|
+
const run = store.getRun(row.sourceRun);
|
|
207
|
+
if (run === null)
|
|
208
|
+
continue;
|
|
209
|
+
if (parkedWhy(store, run.taskRef, run.id, now) !== null)
|
|
210
|
+
continue;
|
|
211
|
+
store.transact(() => {
|
|
212
|
+
if (store.lastBuilderRun(run.taskRef)?.id !== run.id)
|
|
213
|
+
move(store, row.id, "closed", "A newer attempt replaced this result.", now);
|
|
214
|
+
else
|
|
215
|
+
advance(store, row.repo, root, row, now, false);
|
|
216
|
+
});
|
|
217
|
+
moved++;
|
|
218
|
+
}
|
|
219
|
+
return moved;
|
|
220
|
+
}, 0);
|
|
221
|
+
}
|
|
222
|
+
/** A build was accepted as finished: count what it answered, then route its check if it failed. Bookkeeping that never
|
|
223
|
+
* costs the build its result: a problem here leaves the finding open and the failed check on the result. */
|
|
224
|
+
export function followBuiltRun(store, repo, root, runId, worktree, now) {
|
|
225
|
+
try {
|
|
226
|
+
settleFindingAnswers(store, runId, worktree, now);
|
|
227
|
+
}
|
|
228
|
+
catch { /* the answers stay saved */ }
|
|
229
|
+
try {
|
|
230
|
+
store.transact(() => store.savepoint(() => routeFailedCheck(store, repo, root, runId, now)));
|
|
231
|
+
}
|
|
232
|
+
catch { /* the failure stays on the result */ }
|
|
233
|
+
}
|
|
234
|
+
/** How a failed check stands, for a person, in one line. */
|
|
235
|
+
export function checkRepairWords(row) {
|
|
236
|
+
const head = `Check failed on ${row.revision.slice(0, 7)} (build #${row.sourceRun})`;
|
|
237
|
+
if (row.state === "with-person")
|
|
238
|
+
return `${head}, needs you. ${row.why}`;
|
|
239
|
+
return row.state === "parked" ? `${head}. Paused: ${row.why}` : `${head}. ${row.why}`;
|
|
240
|
+
}
|
package/dist/check-report.d.ts
CHANGED
|
@@ -117,6 +117,8 @@ export declare function prepareCheckAttempt(store: Store, root: string, input: {
|
|
|
117
117
|
/** Exact cases asked about for learning only (docs/plans/learning-loops.md): a separate handshake field, at most 200,
|
|
118
118
|
* that never joins `watch`, the inventory or anything the check owes. */
|
|
119
119
|
learn?: readonly string[];
|
|
120
|
+
/** The limit the check runs under when the plane gave it more than the approved one (check-load.ts checkLimitFor). */
|
|
121
|
+
limitMs?: number | undefined;
|
|
120
122
|
}, now: Date): PreparedAttempt | null;
|
|
121
123
|
/** Seal one JSON payload as a structured-output artifact under its capture label. */
|
|
122
124
|
export declare function sealJson(store: Store, root: string, runId: number, name: string, value: unknown, capture: string, now: Date): number;
|
package/dist/check-report.js
CHANGED
|
@@ -220,7 +220,7 @@ export function prepareCheckAttempt(store, root, input, now) {
|
|
|
220
220
|
// `reportBytes`: the largest report this plane reads back once sealed. A plane that doesn't say (0.9.77 and older) reads
|
|
221
221
|
// only 64 KiB, so the check gives it no larger report rather than one that poisons the receipt (scripts/release-check.mjs).
|
|
222
222
|
const learn = learnRequest(input.learn ?? []);
|
|
223
|
-
const stdin = JSON.stringify({ nonce, mode, plan, watch: [...(input.watch ?? [])], limitMs: input.command.timeoutMs, reportBytes: CHECK_ARTIFACT_CAP, ...(plan === null ? { owed, ciSubstitute } : {}), ...(learn.length > 0 ? { learn } : {}) });
|
|
223
|
+
const stdin = JSON.stringify({ nonce, mode, plan, watch: [...(input.watch ?? [])], limitMs: Number.isSafeInteger(input.limitMs) && input.limitMs >= input.command.timeoutMs ? input.limitMs : input.command.timeoutMs, reportBytes: CHECK_ARTIFACT_CAP, ...(plan === null ? { owed, ciSubstitute } : {}), ...(learn.length > 0 ? { learn } : {}) });
|
|
224
224
|
return { runId: input.runId, nonce, stdin, mode, inventory, context, plan, snapshot, watch: [...(input.watch ?? [])], learn };
|
|
225
225
|
}
|
|
226
226
|
/** Seal one JSON payload as a structured-output artifact under its capture label. */
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type CheckAccess } from "./check-access.js";
|
|
2
|
+
export declare const CHECK_STAMP_VERSION = 1;
|
|
3
|
+
/** Every path: the launch had no write or read fence of that kind. */
|
|
4
|
+
export declare const EVERYWHERE = "*";
|
|
5
|
+
/** What the launch's network was: the machine's own, or the fence's, with exactly the hosts it allowed. */
|
|
6
|
+
export type StampNetwork = {
|
|
7
|
+
mode: "machine";
|
|
8
|
+
} | {
|
|
9
|
+
mode: "open" | "allowlist";
|
|
10
|
+
hosts: string[];
|
|
11
|
+
};
|
|
12
|
+
export type CheckStamp = {
|
|
13
|
+
version: typeof CHECK_STAMP_VERSION;
|
|
14
|
+
/** The level the commands ran at, whatever was sealed. */
|
|
15
|
+
level: CheckAccess;
|
|
16
|
+
/** The digest of the whole fence as given to the launch (invoke.ts fenceDigest); null with no fence. */
|
|
17
|
+
policyDigest: string | null;
|
|
18
|
+
/** Where it could write: exact grants, or [EVERYWHERE]. */
|
|
19
|
+
writable: string[];
|
|
20
|
+
/** What it was given to read beyond the fence's denies, or [EVERYWHERE]. */
|
|
21
|
+
readable: string[];
|
|
22
|
+
network: StampNetwork;
|
|
23
|
+
};
|
|
24
|
+
export type StampReading = {
|
|
25
|
+
state: "current";
|
|
26
|
+
stamp: CheckStamp;
|
|
27
|
+
} | {
|
|
28
|
+
state: "unknown";
|
|
29
|
+
why: "missing" | "old" | "unreadable";
|
|
30
|
+
};
|
|
31
|
+
/** A launch with no fence at all. */
|
|
32
|
+
export declare const unfencedStamp: () => CheckStamp;
|
|
33
|
+
export declare const stampDigest: (stamp: CheckStamp) => string;
|
|
34
|
+
/** One saved stamp, exactly its shape or unknown. */
|
|
35
|
+
export declare function readStamp(text: unknown): StampReading;
|
|
36
|
+
/**
|
|
37
|
+
* A run's whole account of its checks: every launch it stamped. Any it can't read makes the whole unknown (one launch of
|
|
38
|
+
* unknown reach is reach unknown); otherwise the widest stands, so a narrow command never hides a wider one. None: unknown.
|
|
39
|
+
*/
|
|
40
|
+
export declare function readStamps(texts: readonly unknown[]): StampReading;
|
|
41
|
+
/** Why relying on `reading` for a check that had to stay within `sealed` isn't possible, or null when it can be. */
|
|
42
|
+
export declare function stampRefusal(reading: StampReading, sealed: CheckAccess): string | null;
|
|
43
|
+
/** How a check ran, as a person reads it after the fact; null for a current sandboxed one, which needs no line. */
|
|
44
|
+
export declare function ranWords(reading: StampReading): string | null;
|
|
45
|
+
/** v154: one row per distinct launch of a run's commands (a run's repeats of one launch are one row). */
|
|
46
|
+
export declare const V154_SCHEMA = "\nCREATE TABLE IF NOT EXISTS check_stamp (\n id INTEGER PRIMARY KEY,\n run_id INTEGER NOT NULL,\n digest TEXT NOT NULL,\n stamp TEXT NOT NULL,\n at TEXT NOT NULL,\n UNIQUE (run_id, digest)\n);\n";
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A check's execution stamp (docs/plans/trusted-release-reports.md, H2): what actually ran, written at launch by the code
|
|
3
|
+
* that launched it. Before this, a run's `sandbox_access` was all there was, and a missing or unreadable value read as
|
|
4
|
+
* "sandboxed". That reading is right for one thing only, choosing how a later check may run (narrowest), and wrong for
|
|
5
|
+
* saying how a check DID run. Anything that relies on how a check ran reads a stamp here: a missing, old or damaged one
|
|
6
|
+
* is `unknown`, which refuses where it matters and is never said to be sandboxed.
|
|
7
|
+
*
|
|
8
|
+
* Reading a stamp is strict, and a new shape takes a new version: an older build's stamp is `unknown` to a newer one.
|
|
9
|
+
*/
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
11
|
+
import { canonicalJson } from "./canonical-json.js";
|
|
12
|
+
import { isCheckAccess, widerThan } from "./check-access.js";
|
|
13
|
+
export const CHECK_STAMP_VERSION = 1;
|
|
14
|
+
/** Every path: the launch had no write or read fence of that kind. */
|
|
15
|
+
export const EVERYWHERE = "*";
|
|
16
|
+
/** A launch with no fence at all. */
|
|
17
|
+
export const unfencedStamp = () => ({ version: CHECK_STAMP_VERSION, level: "unsandboxed", policyDigest: null, writable: [EVERYWHERE], readable: [EVERYWHERE], network: { mode: "machine" } });
|
|
18
|
+
export const stampDigest = (stamp) => createHash("sha256").update(`toolroll:check-stamp:${canonicalJson(stamp)}`).digest("hex").slice(0, 32);
|
|
19
|
+
const paths = (value) => Array.isArray(value) && value.every(one => typeof one === "string" && one !== "");
|
|
20
|
+
const networkIs = (value) => {
|
|
21
|
+
if (value === null || typeof value !== "object")
|
|
22
|
+
return false;
|
|
23
|
+
const one = value;
|
|
24
|
+
if (one["mode"] === "machine")
|
|
25
|
+
return Object.keys(one).length === 1;
|
|
26
|
+
return (one["mode"] === "open" || one["mode"] === "allowlist") && paths(one["hosts"]) && Object.keys(one).length === 2;
|
|
27
|
+
};
|
|
28
|
+
/** One saved stamp, exactly its shape or unknown. */
|
|
29
|
+
export function readStamp(text) {
|
|
30
|
+
if (typeof text !== "string")
|
|
31
|
+
return { state: "unknown", why: "missing" };
|
|
32
|
+
let value;
|
|
33
|
+
try {
|
|
34
|
+
value = JSON.parse(text);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return { state: "unknown", why: "unreadable" };
|
|
38
|
+
}
|
|
39
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
40
|
+
return { state: "unknown", why: "unreadable" };
|
|
41
|
+
const one = value;
|
|
42
|
+
if (one["version"] !== CHECK_STAMP_VERSION)
|
|
43
|
+
return { state: "unknown", why: typeof one["version"] === "number" ? "old" : "unreadable" };
|
|
44
|
+
const digest = one["policyDigest"];
|
|
45
|
+
const whole = isCheckAccess(one["level"]) && (digest === null || (typeof digest === "string" && /^[0-9a-f]{32}$/.test(digest)))
|
|
46
|
+
&& paths(one["writable"]) && paths(one["readable"]) && networkIs(one["network"]) && Object.keys(one).length === 6;
|
|
47
|
+
if (!whole)
|
|
48
|
+
return { state: "unknown", why: "unreadable" };
|
|
49
|
+
// No fence and a narrow level can't both be true: a stamp that says so is damaged.
|
|
50
|
+
if (digest === null && one["level"] !== "unsandboxed")
|
|
51
|
+
return { state: "unknown", why: "unreadable" };
|
|
52
|
+
return { state: "current", stamp: value };
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* A run's whole account of its checks: every launch it stamped. Any it can't read makes the whole unknown (one launch of
|
|
56
|
+
* unknown reach is reach unknown); otherwise the widest stands, so a narrow command never hides a wider one. None: unknown.
|
|
57
|
+
*/
|
|
58
|
+
export function readStamps(texts) {
|
|
59
|
+
if (texts.length === 0)
|
|
60
|
+
return { state: "unknown", why: "missing" };
|
|
61
|
+
let widest = null;
|
|
62
|
+
for (const text of texts) {
|
|
63
|
+
const one = readStamp(text);
|
|
64
|
+
if (one.state === "unknown")
|
|
65
|
+
return one;
|
|
66
|
+
if (widest === null || widerThan(one.stamp.level, widest.level))
|
|
67
|
+
widest = one.stamp;
|
|
68
|
+
}
|
|
69
|
+
return { state: "current", stamp: widest };
|
|
70
|
+
}
|
|
71
|
+
/** Why relying on `reading` for a check that had to stay within `sealed` isn't possible, or null when it can be. */
|
|
72
|
+
export function stampRefusal(reading, sealed) {
|
|
73
|
+
if (reading.state === "unknown")
|
|
74
|
+
return reading.why === "old" ? "its check's record is from an older Toolroll, so how it ran can't be shown" : "how its check ran wasn't recorded";
|
|
75
|
+
return widerThan(reading.stamp.level, sealed) ? `its check ran ${reading.stamp.level}, wider than the ${sealed} it was approved for` : null;
|
|
76
|
+
}
|
|
77
|
+
/** How a check ran, as a person reads it after the fact; null for a current sandboxed one, which needs no line. */
|
|
78
|
+
export function ranWords(reading) {
|
|
79
|
+
if (reading.state === "unknown")
|
|
80
|
+
return "How these checks ran wasn't recorded";
|
|
81
|
+
return reading.stamp.level === "sandboxed" ? null : reading.stamp.level === "unsandboxed" ? "Checks ran without a sandbox" : "Checks ran with model and GitHub sign-ins";
|
|
82
|
+
}
|
|
83
|
+
/** v154: one row per distinct launch of a run's commands (a run's repeats of one launch are one row). */
|
|
84
|
+
export const V154_SCHEMA = `
|
|
85
|
+
CREATE TABLE IF NOT EXISTS check_stamp (
|
|
86
|
+
id INTEGER PRIMARY KEY,
|
|
87
|
+
run_id INTEGER NOT NULL,
|
|
88
|
+
digest TEXT NOT NULL,
|
|
89
|
+
stamp TEXT NOT NULL,
|
|
90
|
+
at TEXT NOT NULL,
|
|
91
|
+
UNIQUE (run_id, digest)
|
|
92
|
+
);
|
|
93
|
+
`;
|