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,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a provider said when an attempt failed, saved for a person (docs/plans/fleet-follow-through.md, part 5).
|
|
3
|
+
*
|
|
4
|
+
* Fallback between agents was removed in v115, and exhaustion.ts recognises only a dead sign-in. Telling a used-up
|
|
5
|
+
* subscription from any other failure needs a REAL response, so this saves them as they happen and nothing else. Nothing
|
|
6
|
+
* reads this table to route work: `hint` is a sort key for a person, never a recogniser, and a person's label decides
|
|
7
|
+
* nothing either. The text is the provider's, so it is data, scrubbed of secrets and bounded.
|
|
8
|
+
*/
|
|
9
|
+
import { readingFleet } from "./fleet-schema.js";
|
|
10
|
+
import { cleanFindingText } from "./findings.js";
|
|
11
|
+
export const CAPTURE_TEXT_LIMIT = 4_000;
|
|
12
|
+
/** Kept per provider, newest first. A labelled capture is never pruned: it is the evidence a person kept. */
|
|
13
|
+
const CAPTURES_KEPT = 40;
|
|
14
|
+
/** Words worth a person's look. Only ever a sort key. */
|
|
15
|
+
const LIMIT_WORDS = /\b(usage limit|rate limit|quota|credit|billing|exhaust|limit reached|too many requests|429|payment)/i;
|
|
16
|
+
/** Save one failed attempt's last words. Bookkeeping: it never costs the run anything. */
|
|
17
|
+
export function captureProviderFailure(store, input, now) {
|
|
18
|
+
try {
|
|
19
|
+
const text = cleanFindingText(input.texts.filter((one) => typeof one === "string" && one.trim() !== "").join("\n"), CAPTURE_TEXT_LIMIT);
|
|
20
|
+
if (text === "" && input.code === null)
|
|
21
|
+
return;
|
|
22
|
+
const run = store.getRun(input.run);
|
|
23
|
+
const repo = run === null ? null : store.refById(run.taskRef)?.repo ?? null;
|
|
24
|
+
store.handle.prepare(`INSERT INTO provider_capture (run, repo, provider, model, auth_mode, exit_code, code, text, hint, at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT (run) DO NOTHING`)
|
|
25
|
+
.run(input.run, repo, input.provider, input.model, input.authMode, input.exitCode, input.code === null ? null : cleanFindingText(input.code, 120), text, LIMIT_WORDS.test(`${input.code ?? ""} ${text}`) ? 1 : 0, now.toISOString());
|
|
26
|
+
store.handle.prepare(`DELETE FROM provider_capture WHERE provider = ? AND label IS NULL AND id NOT IN (SELECT id FROM provider_capture WHERE provider = ? ORDER BY id DESC LIMIT ?)`).run(input.provider, input.provider, CAPTURES_KEPT);
|
|
27
|
+
}
|
|
28
|
+
catch { /* a capture is never worth a run */ }
|
|
29
|
+
}
|
|
30
|
+
const captureOf = (row) => ({
|
|
31
|
+
id: Number(row["id"]), run: Number(row["run"]), repo: row["repo"] === null ? null : String(row["repo"]), provider: String(row["provider"]), model: row["model"] === null ? null : String(row["model"]),
|
|
32
|
+
authMode: row["auth_mode"] === null ? null : String(row["auth_mode"]), exitCode: row["exit_code"] === null ? null : Number(row["exit_code"]), code: row["code"] === null ? null : String(row["code"]),
|
|
33
|
+
text: String(row["text"]), hint: Number(row["hint"]) === 1, label: row["label"] === null ? null : String(row["label"]), labelledBy: row["labelled_by"] === null ? null : String(row["labelled_by"]), at: String(row["at"]),
|
|
34
|
+
});
|
|
35
|
+
/** Newest first, the ones mentioning limits at the top. */
|
|
36
|
+
export function providerCaptures(store, repos, limit = 50) {
|
|
37
|
+
return readingFleet(() => store.handle.prepare("SELECT * FROM provider_capture ORDER BY hint DESC, id DESC LIMIT 500").all().map(captureOf)
|
|
38
|
+
.filter(one => repos === null || (one.repo !== null && repos.includes(one.repo))).slice(0, limit), []);
|
|
39
|
+
}
|
|
40
|
+
export function providerCapture(store, id) {
|
|
41
|
+
return readingFleet(() => {
|
|
42
|
+
const row = store.handle.prepare("SELECT * FROM provider_capture WHERE id = ?").get(id);
|
|
43
|
+
return row === undefined ? null : captureOf(row);
|
|
44
|
+
}, null);
|
|
45
|
+
}
|
|
46
|
+
/** A person's reading of one capture. It marks it for keeping, and nothing routes on it. */
|
|
47
|
+
export function labelProviderCapture(store, id, label, by) {
|
|
48
|
+
return Number(store.handle.prepare("UPDATE provider_capture SET label = ?, labelled_by = ? WHERE id = ?").run(label, by, id).changes) === 1;
|
|
49
|
+
}
|
|
50
|
+
/** What goes in a fixture file: the provider's response and which version of what produced it. */
|
|
51
|
+
export function captureFixture(capture) {
|
|
52
|
+
return { version: 1, provider: capture.provider, model: capture.model, authMode: capture.authMode, exitCode: capture.exitCode, code: capture.code, text: capture.text, label: capture.label, labelledBy: capture.labelledBy, capturedAt: capture.at };
|
|
53
|
+
}
|
package/dist/publish.js
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import { createHash } from "node:crypto";
|
|
23
23
|
import { protectedPublicationHold } from "./result-completion.js";
|
|
24
|
+
import { fastPublishProblem } from "./fast-start.js";
|
|
24
25
|
import { writeFileSync, mkdtempSync, rmSync } from "node:fs";
|
|
25
26
|
import { tmpdir } from "node:os";
|
|
26
27
|
import { join } from "node:path";
|
|
@@ -142,6 +143,13 @@ export async function publishPass(store, options) {
|
|
|
142
143
|
report.problems.push(`publication ${publication.id}: ${held}`);
|
|
143
144
|
continue;
|
|
144
145
|
}
|
|
146
|
+
// A fast start waits for a person's Accept, and for a change that stayed small (fast-start.ts).
|
|
147
|
+
const fastTask = store.externalIdFor(publication.taskRef);
|
|
148
|
+
const fast = fastTask === null ? null : fastPublishProblem(store, fastTask, publication.run);
|
|
149
|
+
if (fast !== null) {
|
|
150
|
+
report.problems.push(`publication ${publication.id}: held: ${fast}`);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
145
153
|
}
|
|
146
154
|
if (publication.state === "intended") {
|
|
147
155
|
// The exact SHA to the exact ref. A branch that moved since completion
|
|
@@ -525,7 +533,7 @@ export async function sweepMerges(store, options) {
|
|
|
525
533
|
store.enqueueNotification({ source: { run: publication.run }, dedupeKey: key, kind: "merge", subject, body }, clock());
|
|
526
534
|
};
|
|
527
535
|
const pagePerson = (publication, suffix, subject, body) => {
|
|
528
|
-
store.enqueueEpisode(`merge-attn:${publication.id}`, { source: { run: publication.run }, kind: "merge", subject, body, pushClass: "merge", link: "/
|
|
536
|
+
store.enqueueEpisode(`merge-attn:${publication.id}`, { source: { run: publication.run }, kind: "merge", subject, body, pushClass: "merge", link: "/tasks?view=ready" }, suffix, clock());
|
|
529
537
|
};
|
|
530
538
|
const grant = store.publicationGrantFor(options.repo);
|
|
531
539
|
if (grant === null || grant.merge !== true || grant.mergeMethod == null)
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* read must show the PR open, on the accepted commit, with checks passing; GitHub's head-commit match guards
|
|
15
15
|
* the race; the branch is deleted; the merge commit and who merged are recorded in the ledger.
|
|
16
16
|
*/
|
|
17
|
+
import { fastPublishProblem } from "./fast-start.js";
|
|
17
18
|
import { createHash, randomUUID } from "node:crypto";
|
|
18
19
|
import { run as execRun } from "./exec.js";
|
|
19
20
|
import { bodyHashOf, publicationBody, publicationGrantOf } from "./publish.js";
|
|
@@ -411,6 +412,10 @@ export async function mergePullRequest(store, input) {
|
|
|
411
412
|
// A release task the plane approved (or is checking) automatically (auto-approve-green.md) never merges here either.
|
|
412
413
|
if (mergedTask !== null && autoReleaseSealed(store, mergedTask))
|
|
413
414
|
return refuse("release", "This result was approved automatically for a release; it merges only through `toolroll release`.");
|
|
415
|
+
// A fast start merges only after a person accepted it, and only a change that stayed small (fast-start.ts).
|
|
416
|
+
const fastHold = mergedTask === null ? null : fastPublishProblem(store, mergedTask, input.runId);
|
|
417
|
+
if (fastHold !== null)
|
|
418
|
+
return refuse("fast-start", fastHold);
|
|
414
419
|
const publication = store.publicationForRun(input.runId);
|
|
415
420
|
const follow = publication === null ? null : followOf(store, publication.id);
|
|
416
421
|
if (publication === null || follow === null || publication.prNumber === null)
|
package/dist/push.d.ts
CHANGED
|
@@ -69,10 +69,26 @@ export declare const PUSH_WORDS: Record<PushClass, {
|
|
|
69
69
|
title: string;
|
|
70
70
|
body: string;
|
|
71
71
|
}>;
|
|
72
|
+
/** The relative links a push may carry (docs/plans/plain-toolroll.md P1.4): every shape an older worker took, kept for
|
|
73
|
+
* good (/next and /review now redirect to Home and Tasks' Ready view; the rest keep their pages), then Home, Tasks'
|
|
74
|
+
* Ready and Board views, and a task by its stored number (/n/<n>, resolved under the viewer's own access). */
|
|
75
|
+
export declare const LINK_SHAPES: readonly RegExp[];
|
|
72
76
|
/** Static words and machine-minted numbers only — re-proved at send time. */
|
|
73
77
|
export declare function safePushLink(pushClass: PushClass, link: string | null): string;
|
|
74
|
-
/**
|
|
75
|
-
*
|
|
78
|
+
/** One line per ping (docs/plans/plain-toolroll.md P1.5), built only from the task's stored number and fixed words: no
|
|
79
|
+
* title, no free text. A fact about a task opens that task (/n/<n>); anything else keeps its class's fixed words and
|
|
80
|
+
* its own allow-listed link. Rows are tried in order; the first that matches says the line. */
|
|
81
|
+
export declare const PUSH_LINES: readonly {
|
|
82
|
+
when: (notification: Pick<Notification, "kind" | "pushClass">) => boolean;
|
|
83
|
+
say: (number: number) => string;
|
|
84
|
+
}[];
|
|
85
|
+
/** The one line a ping says, and where its tap lands. */
|
|
86
|
+
export declare function pushLineOf(notification: Pick<Notification, "kind" | "pushClass" | "body" | "link" | "taskRef">): {
|
|
87
|
+
body: string;
|
|
88
|
+
url: string;
|
|
89
|
+
};
|
|
90
|
+
/** `release`: the class and link the plane derived for this row at send time (releasePushOf): fixed words and a release's
|
|
91
|
+
* own link, never the task line. Absent or null, the row's own line. */
|
|
76
92
|
export declare function buildPushPayload(notification: Notification, waiting?: number | null, release?: {
|
|
77
93
|
link: string;
|
|
78
94
|
} | null): string;
|
package/dist/push.js
CHANGED
|
@@ -199,20 +199,24 @@ export const PUSH_WORDS = {
|
|
|
199
199
|
decision: { title: "Toolroll", body: "a decision needs you" },
|
|
200
200
|
pick: { title: "Toolroll", body: "agents finished — pick a winner" },
|
|
201
201
|
merge: { title: "Toolroll", body: "a pull request needs a person" },
|
|
202
|
-
attention: { title: "Toolroll", body: "
|
|
202
|
+
attention: { title: "Toolroll", body: "Toolroll needs your attention" },
|
|
203
203
|
progress: { title: "Toolroll", body: "checks in progress" },
|
|
204
204
|
release: { title: "Toolroll", body: RELEASE_PUSH_WORDS },
|
|
205
205
|
};
|
|
206
206
|
// `/approve/<n>`: a release's opaque locator (a task ref number: no version, commit or branch), resolved by the console.
|
|
207
207
|
const RELEASE_LINK = /^\/approve\/[0-9]{1,15}$/;
|
|
208
|
-
|
|
208
|
+
/** The relative links a push may carry (docs/plans/plain-toolroll.md P1.4): every shape an older worker took, kept for
|
|
209
|
+
* good (/next and /review now redirect to Home and Tasks' Ready view; the rest keep their pages), then Home, Tasks'
|
|
210
|
+
* Ready and Board views, and a task by its stored number (/n/<n>, resolved under the viewer's own access). */
|
|
211
|
+
export const LINK_SHAPES = [/^\/next$/, /^\/review$/, /^\/system$/, /^\/routines$/, /^\/routines\/[0-9]{1,15}$/, /^\/d\/[0-9]{1,15}$/, /^\/r\/[0-9]{1,15}$/, RELEASE_LINK,
|
|
212
|
+
/^\/$/, /^\/tasks\?view=ready$/, /^\/tasks\?view=board$/, /^\/n\/[1-9][0-9]{0,14}$/];
|
|
209
213
|
const CLASS_FALLBACK = {
|
|
210
|
-
decision: "/
|
|
211
|
-
pick: "/
|
|
212
|
-
merge: "/
|
|
213
|
-
attention: "/
|
|
214
|
-
progress: "/
|
|
215
|
-
release: "/
|
|
214
|
+
decision: "/",
|
|
215
|
+
pick: "/",
|
|
216
|
+
merge: "/tasks?view=ready",
|
|
217
|
+
attention: "/",
|
|
218
|
+
progress: "/",
|
|
219
|
+
release: "/",
|
|
216
220
|
};
|
|
217
221
|
/** Static words and machine-minted numbers only — re-proved at send time. */
|
|
218
222
|
export function safePushLink(pushClass, link) {
|
|
@@ -223,17 +227,37 @@ export function safePushLink(pushClass, link) {
|
|
|
223
227
|
return link;
|
|
224
228
|
return CLASS_FALLBACK[pushClass];
|
|
225
229
|
}
|
|
226
|
-
/**
|
|
227
|
-
*
|
|
230
|
+
/** One line per ping (docs/plans/plain-toolroll.md P1.5), built only from the task's stored number and fixed words: no
|
|
231
|
+
* title, no free text. A fact about a task opens that task (/n/<n>); anything else keeps its class's fixed words and
|
|
232
|
+
* its own allow-listed link. Rows are tried in order; the first that matches says the line. */
|
|
233
|
+
export const PUSH_LINES = [
|
|
234
|
+
{ when: one => one.kind === "plan-ready", say: number => `#${number}: plan ready` },
|
|
235
|
+
{ when: one => one.pushClass === "decision" || one.kind === "task-ask" || one.kind === "decision", say: number => `#${number} has a question` },
|
|
236
|
+
{ when: one => one.pushClass === "merge" || one.pushClass === "pick" || one.kind === "pull-request-ready" || one.kind === "acceptance-ready", say: number => `#${number} is ready to review` },
|
|
237
|
+
{ when: () => true, say: number => `#${number} needs you` },
|
|
238
|
+
];
|
|
239
|
+
/** The one line a ping says, and where its tap lands. */
|
|
240
|
+
export function pushLineOf(notification) {
|
|
241
|
+
const pushClass = notification.pushClass;
|
|
242
|
+
if (pushClass === "progress" && isCheckProgressLine(notification.body))
|
|
243
|
+
return { body: notification.body, url: safePushLink(pushClass, notification.link) };
|
|
244
|
+
const number = notification.taskRef;
|
|
245
|
+
if (typeof number === "number" && Number.isSafeInteger(number) && number > 0) {
|
|
246
|
+
const row = PUSH_LINES.find(one => one.when(notification));
|
|
247
|
+
return { body: row.say(number), url: `/n/${number}` };
|
|
248
|
+
}
|
|
249
|
+
return { body: PUSH_WORDS[pushClass].body, url: safePushLink(pushClass, notification.link) };
|
|
250
|
+
}
|
|
251
|
+
/** `release`: the class and link the plane derived for this row at send time (releasePushOf): fixed words and a release's
|
|
252
|
+
* own link, never the task line. Absent or null, the row's own line. */
|
|
228
253
|
export function buildPushPayload(notification, waiting = null, release = null) {
|
|
229
|
-
const
|
|
230
|
-
const words = PUSH_WORDS[pushClass];
|
|
254
|
+
const line = release !== null ? { body: PUSH_WORDS.release.body, url: safePushLink("release", release.link) } : pushLineOf(notification);
|
|
231
255
|
return JSON.stringify({
|
|
232
|
-
title:
|
|
233
|
-
body:
|
|
234
|
-
url:
|
|
256
|
+
title: "Toolroll",
|
|
257
|
+
body: line.body,
|
|
258
|
+
url: line.url,
|
|
235
259
|
// The opaque tag collapses a crash-duplicate on the lock screen.
|
|
236
|
-
tag: pushClass === "progress" && notification.run !== null ? `so-run-${notification.run}` : `so-${notification.id}`,
|
|
260
|
+
tag: release === null && notification.pushClass === "progress" && notification.run !== null ? `so-run-${notification.run}` : `so-${notification.id}`,
|
|
237
261
|
// The app-icon badge (Phase 2E, v2 S4): the server-computed
|
|
238
262
|
// waiting-on-you COUNT at send time — a number, never content, so the
|
|
239
263
|
// closed-class discipline holds. The console page recomputes and
|
|
@@ -131,6 +131,8 @@ export type AutoObservers = {
|
|
|
131
131
|
/** Whether main's protection holds for the account that merges (readMainProtection); null when unreadable. */
|
|
132
132
|
upToDateRequired: () => Promise<boolean | null>;
|
|
133
133
|
evidenceRoot: string;
|
|
134
|
+
/** The cases that ran in `sha`'s macOS CI (fenced-check-coverage.ts); null when they can't be read. Absent: never read. */
|
|
135
|
+
macosCases?: (sha: string) => Promise<ReadonlyMap<string, boolean> | null>;
|
|
134
136
|
};
|
|
135
137
|
/**
|
|
136
138
|
* Every check run GitHub holds for one commit — every attempt, not only the latest (`filter=all`; GitHub's default
|
|
@@ -169,6 +171,13 @@ export declare function readMergeBase(run: Exec, gh: string, repo: string, base:
|
|
|
169
171
|
* never from a ref in a checkout a build can write.
|
|
170
172
|
*/
|
|
171
173
|
export declare function readMainHead(run: Exec, gh: string, repo: string, branch?: string): Promise<string>;
|
|
174
|
+
/** GitHub's own "Update branch", with its expected head: a push that got there first refuses. */
|
|
175
|
+
export declare function updatePullRequestBranch(run: Exec, gh: string, repo: string, pr: number, expectedHead: string): Promise<{
|
|
176
|
+
ok: true;
|
|
177
|
+
} | {
|
|
178
|
+
ok: false;
|
|
179
|
+
why: string;
|
|
180
|
+
}>;
|
|
172
181
|
/** Whether `sha` already holds `ancestor` (GitHub's compare: `sha` is not behind it), as GitHub answers now. */
|
|
173
182
|
export declare function readContains(run: Exec, gh: string, repo: string, sha: string, ancestor: string): Promise<boolean>;
|
|
174
183
|
/**
|
package/dist/release-adapters.js
CHANGED
|
@@ -24,6 +24,10 @@ import { databasePath, finishReleaseOperation, requireFreshReleaseApproval, open
|
|
|
24
24
|
import { acquireDeploymentLock } from "./deploy-lock.js";
|
|
25
25
|
import { autoReleaseApprovalOf, liveReleaseOf } from "./scope.js";
|
|
26
26
|
import { recheckReleaseTask, releaseCheckStanding } from "./check-failure.js";
|
|
27
|
+
import { CHECK_REPORT_CAPTURE, readSealedJson } from "./check-report.js";
|
|
28
|
+
import { readCheckReport } from "./contracts/release-check.js";
|
|
29
|
+
import { macosCases, skipCoverage, skippedCases, uncoveredWords } from "./fenced-check-coverage.js";
|
|
30
|
+
import { runCheckStamp } from "./store.js";
|
|
27
31
|
import { noteCiRunning, releaseCiHoldOf, releaseCiRunningOf } from "./release-ci-hold.js";
|
|
28
32
|
import { autoPublishHeld, holdAutoPublish, liftCiHold, authorizeAutoCheck, giveUpAutoApproval, noteAutoCheckFailed, noteAutoRelease, releaseAutoApproves, sealAutoGreen } from "./release-auto.js";
|
|
29
33
|
import { proveCarry } from "./release-carry.js";
|
|
@@ -436,6 +440,7 @@ export function releaseAdapters(options) {
|
|
|
436
440
|
};
|
|
437
441
|
const release = storeReleaseAdapter(() => openStore(database), store => store.close(), options.now ?? (() => new Date()), undefined, { observe: sha => observeCi(sha), mergeBase: (base, sha) => readMergeBase(reads, ciGh, releaseRepo().slug, base, sha),
|
|
438
442
|
upToDateRequired: () => readUpToDateRequired(reads, ciGh, releaseRepo().slug), evidenceRoot: join(dirname(database), "evidence"),
|
|
443
|
+
macosCases: sha => macosCasesOf(run, ciGh, releaseRepo().slug, sha),
|
|
439
444
|
// A successful Actions job authenticates the job, not the candidate-writable JSON it uploads. Until the
|
|
440
445
|
// executor protects that reporting channel, production releases must take the person's approval road.
|
|
441
446
|
reportsProtected: false },
|
|
@@ -660,6 +665,7 @@ export function releaseAdapters(options) {
|
|
|
660
665
|
async complete(repo, sha) { return readSuitesComplete(reads, ciGh, repo, sha); },
|
|
661
666
|
async mainHead(repo) { return readMainHead(reads, ciGh, repo); },
|
|
662
667
|
async contains(repo, sha, ancestor) { return readContains(reads, ciGh, repo, sha, ancestor); },
|
|
668
|
+
async updateBranch(repo, pr, expectedHead) { return updatePullRequestBranch(reads, ciGh, repo, pr, expectedHead); },
|
|
663
669
|
async upToDateRequired(repo) { return readMainProtection(reads, ciGh, repo); },
|
|
664
670
|
async merge(repo, pr, head, message) {
|
|
665
671
|
// The squash commit's message lists every commit that landed after the approval (release-once.md). Never asked
|
|
@@ -989,6 +995,33 @@ nightlyReads) {
|
|
|
989
995
|
return answer.action === "rerun" ? { action: "rerun", why: answer.why } : answer.action === "pending" ? { action: "pending" } : { action: "stop", class: answer.class, why: answer.why };
|
|
990
996
|
});
|
|
991
997
|
},
|
|
998
|
+
async fencedCoverage(_taskId, run, candidate) {
|
|
999
|
+
// Today's road, read first and answered at once: a check that ran unsandboxed skips nothing for this reason. A run
|
|
1000
|
+
// from before stamps (a release resumed across an update) is judged by the level its dispatch recorded.
|
|
1001
|
+
const asked = withStore(store => {
|
|
1002
|
+
if (store.runCheckAccess(run) === "unsandboxed")
|
|
1003
|
+
return null;
|
|
1004
|
+
const reading = runCheckStamp(store, run);
|
|
1005
|
+
if (reading.state !== "current")
|
|
1006
|
+
return { refused: "how its check ran wasn't recorded, so what it skipped can't be judged" };
|
|
1007
|
+
if (reading.stamp.level === "unsandboxed")
|
|
1008
|
+
return null;
|
|
1009
|
+
const sealed = readSealedJson(store, auto.evidenceRoot, run, CHECK_REPORT_CAPTURE);
|
|
1010
|
+
const report = sealed === null ? null : readCheckReport(sealed.value);
|
|
1011
|
+
return report?.ok === true ? { skipped: skippedCases(report.value) } : { refused: "its report can't be read, so what it skipped can't be judged" };
|
|
1012
|
+
});
|
|
1013
|
+
if (asked === null)
|
|
1014
|
+
return { ok: true, covered: 0 };
|
|
1015
|
+
if ("refused" in asked)
|
|
1016
|
+
return { ok: false, why: asked.refused };
|
|
1017
|
+
if (asked.skipped.length === 0)
|
|
1018
|
+
return { ok: true, covered: 0 };
|
|
1019
|
+
const macos = await auto.macosCases?.(candidate) ?? null;
|
|
1020
|
+
if (macos === null)
|
|
1021
|
+
return { ok: false, why: "this commit's macOS CI reports couldn't be read, so what the sandboxed check skipped isn't covered" };
|
|
1022
|
+
const coverage = skipCoverage(asked.skipped, macos);
|
|
1023
|
+
return coverage.uncovered.length > 0 ? { ok: false, why: uncoveredWords(coverage.uncovered) } : { ok: true, covered: coverage.covered.length };
|
|
1024
|
+
},
|
|
992
1025
|
async standing(taskId, run) {
|
|
993
1026
|
return withStore(store => {
|
|
994
1027
|
const standing = releaseCheckStanding(store, auto.evidenceRoot, { taskId, run });
|
|
@@ -1317,6 +1350,13 @@ export async function readMainHead(run, gh, repo, branch = "main") {
|
|
|
1317
1350
|
throw new Error(`GitHub named no commit for ${branch}`);
|
|
1318
1351
|
return match[1];
|
|
1319
1352
|
}
|
|
1353
|
+
/** GitHub's own "Update branch", with its expected head: a push that got there first refuses. */
|
|
1354
|
+
export async function updatePullRequestBranch(run, gh, repo, pr, expectedHead) {
|
|
1355
|
+
if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repo) || !Number.isSafeInteger(pr) || pr < 1 || !/^[0-9a-f]{40}$/.test(expectedHead))
|
|
1356
|
+
return { ok: false, why: "not a pull request GitHub can be asked to update" };
|
|
1357
|
+
const ran = await run(gh, ["api", "--hostname", "github.com", "--method", "PUT", `repos/${repo}/pulls/${pr}/update-branch`, "-f", `expected_head_sha=${expectedHead}`], { cwd: "/", env: ciEnv() });
|
|
1358
|
+
return ran.code === 0 ? { ok: true } : { ok: false, why: (ran.stderr.trim().split("\n")[0] ?? "").slice(0, 200) || `gh exited ${ran.code}` };
|
|
1359
|
+
}
|
|
1320
1360
|
/** Whether `sha` already holds `ancestor` (GitHub's compare: `sha` is not behind it), as GitHub answers now. */
|
|
1321
1361
|
export async function readContains(run, gh, repo, sha, ancestor) {
|
|
1322
1362
|
if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repo) || !/^[0-9a-f]{40}$/.test(sha) || !/^[0-9a-f]{40}$/.test(ancestor))
|
|
@@ -1530,3 +1570,39 @@ export function releaseProgressRecorder(open, done) {
|
|
|
1530
1570
|
}
|
|
1531
1571
|
};
|
|
1532
1572
|
}
|
|
1573
|
+
/** The macOS CI's cases on exactly `sha`: the newest pull-request run of ci.yml there, each shard's newest attempt. Null
|
|
1574
|
+
* when they can't all be read. The folder is this function's own, removed before it returns. */
|
|
1575
|
+
async function macosCasesOf(run, gh, slug, sha) {
|
|
1576
|
+
const folder = mkdtempSync(join(tmpdir(), "so-macos-ci-"));
|
|
1577
|
+
try {
|
|
1578
|
+
const listed = await run(gh, ["api", `repos/${slug}/actions/runs?head_sha=${sha}&event=pull_request&per_page=100`], { timeoutMs: 60_000 });
|
|
1579
|
+
if (listed.code !== 0)
|
|
1580
|
+
return null;
|
|
1581
|
+
const runs = JSON.parse(listed.stdout).workflow_runs ?? [];
|
|
1582
|
+
const newest = runs.filter(one => typeof one.id === "number" && one.head_sha === sha && (one.path === ".github/workflows/ci.yml" || String(one.path).startsWith(".github/workflows/ci.yml@"))).map(one => one.id).sort((a, b) => b - a)[0];
|
|
1583
|
+
if (newest === undefined)
|
|
1584
|
+
return null;
|
|
1585
|
+
const got = await run(gh, ["run", "download", String(newest), "--repo", slug, "--pattern", "macos-shard-*", "--dir", folder], { timeoutMs: 180_000 });
|
|
1586
|
+
if (got.code !== 0)
|
|
1587
|
+
return null;
|
|
1588
|
+
const shards = new Map();
|
|
1589
|
+
for (const name of readdirSync(folder)) {
|
|
1590
|
+
const match = /^macos-(shard-\d+-\d+)-a([1-9]\d*)$/.exec(name);
|
|
1591
|
+
if (match === null)
|
|
1592
|
+
return null;
|
|
1593
|
+
const was = shards.get(match[1]);
|
|
1594
|
+
if (was === undefined || Number(match[2]) > was.attempt)
|
|
1595
|
+
shards.set(match[1], { attempt: Number(match[2]), name });
|
|
1596
|
+
}
|
|
1597
|
+
if (shards.size === 0)
|
|
1598
|
+
return null;
|
|
1599
|
+
const reports = [...shards.values()].map(one => JSON.parse(readFileSync(join(folder, one.name, "shard-report.json"), "utf8")));
|
|
1600
|
+
return macosCases(reports, `/Users/runner/work/${slug.split("/")[1]}/${slug.split("/")[1]}`);
|
|
1601
|
+
}
|
|
1602
|
+
catch {
|
|
1603
|
+
return null;
|
|
1604
|
+
}
|
|
1605
|
+
finally {
|
|
1606
|
+
rmSync(folder, { recursive: true, force: true });
|
|
1607
|
+
}
|
|
1608
|
+
}
|
package/dist/release-auto.js
CHANGED
|
@@ -19,7 +19,7 @@ import { sealedRun } from "./release-recheck.js";
|
|
|
19
19
|
import { runCheckLevel } from "./check-levels.js";
|
|
20
20
|
import { readVerifiedCommit, verifiedBlobs, verifiedTreeDiff, verifiedTreeEntries } from "./commit-extract.js";
|
|
21
21
|
import { EVAL_HOOK_FILES, evalHookProblem, LEARNING_HOOK_FILES, learningHookProblem, storeRewriteProblem } from "./eval-hooks.js";
|
|
22
|
-
import { mightName, testInfrastructure } from "./test-infra.js";
|
|
22
|
+
import { isTestPath, mightName, testImportProblem, testInfrastructure } from "./test-infra.js";
|
|
23
23
|
import { chatControlHref } from "./chat-controls.js";
|
|
24
24
|
import { INFRA_RERUNS_PER_CANDIDATE, REQUIRED_PR_CHECKS, REQUIRED_PR_CHECKS_WORKFLOW, prChecksVerdict } from "./release.js";
|
|
25
25
|
import { releaseCheckStanding } from "./check-failure.js";
|
|
@@ -359,18 +359,18 @@ function changeProblem(repo, base, candidate) {
|
|
|
359
359
|
// Taking a test away is never automatic, wherever it lives.
|
|
360
360
|
if (change.kind === "deleted" && TEST_FILE.test(rulePath(change.path)))
|
|
361
361
|
return `${change.path} was deleted (removing a test needs a person)`;
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
362
|
+
// Counts cannot prove that an assertion survived, and an appended line can disarm what it follows (`vi.mock` hoists,
|
|
363
|
+
// a top-level hook reaches every case): a test file that was there stays exactly as it was (H4).
|
|
364
|
+
if (change.before !== null && TEST_FILE.test(rulePath(change.path)))
|
|
365
|
+
return `${change.path} changes existing test code (new tests go in a new file; rewriting tests needs a person)`;
|
|
366
|
+
// A new test may not reach into another: it would run that file's code, and its mocks, here. From the verified tree.
|
|
367
|
+
// Any test extension: `src/` passes autoPathProblem whatever the file is called, so `.test.ts` alone would miss `.test.tsx`.
|
|
368
|
+
if (change.kind === "added" && isTestPath(rulePath(change.path))) {
|
|
369
|
+
const read = verifiedBlobs(gitDir, after.value.tree, [change.path]);
|
|
370
|
+
const bytes = read.ok ? read.value.get(change.path)?.bytes : undefined;
|
|
371
|
+
const why = bytes === undefined ? `${change.path}'s test code couldn't be read` : testImportProblem(change.path, bytes.toString("utf8"));
|
|
372
|
+
if (why !== null)
|
|
373
|
+
return `${why} (a person decides)`;
|
|
374
374
|
}
|
|
375
375
|
const why = autoPathProblem(change.path);
|
|
376
376
|
if (why !== null)
|
package/dist/release-card.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ReleaseCheckPurpose, type ReleaseTerms } from "./contracts/release-terms.js";
|
|
2
2
|
import type { Scope } from "./scope.js";
|
|
3
|
-
import type
|
|
3
|
+
import { type Store } from "./store.js";
|
|
4
4
|
/** One signed term as the card says it. `key` names the scope field or the release term's path. */
|
|
5
5
|
export type TermLine = {
|
|
6
6
|
key: string;
|
|
@@ -151,7 +151,6 @@ export type ReleaseCard = {
|
|
|
151
151
|
};
|
|
152
152
|
/** The card digest: everything the person is shown that can change under them. */
|
|
153
153
|
export declare function cardDigestOf(scopeDigest: string, formDigest: string, changes: Pick<ChangeList, "baseline" | "digest">): string;
|
|
154
|
-
/** The release card for a deploy gate, against the live records now. */
|
|
155
154
|
export declare function releaseCardOf(store: Store, scope: Scope, formDigest: string, words: {
|
|
156
155
|
profile: (profile: Scope["profile"]) => string | null;
|
|
157
156
|
}, read?: PullRequestReader): ReleaseCard;
|
package/dist/release-card.js
CHANGED
|
@@ -20,6 +20,7 @@ import { createHash } from "node:crypto";
|
|
|
20
20
|
import { lstatSync, realpathSync } from "node:fs";
|
|
21
21
|
import { join } from "node:path";
|
|
22
22
|
import { checkAccessWords } from "./check-access.js";
|
|
23
|
+
import { ranWords } from "./check-stamp.js";
|
|
23
24
|
import { CHECK_LEVEL_WORDS, effectiveCheckLevel } from "./check-levels.js";
|
|
24
25
|
import { verifiedRange } from "./commit-extract.js";
|
|
25
26
|
import { releaseCheckPurposeSchema, releaseTermsSchema } from "./contracts/release-terms.js";
|
|
@@ -29,6 +30,7 @@ import { routeFromJson } from "./phase-routing.js";
|
|
|
29
30
|
import { projectName } from "./project.js";
|
|
30
31
|
import { RELEASE_CI_RUNNING_WORDS, releaseCiRunningOf } from "./release-ci-hold.js";
|
|
31
32
|
import { RELEASE_PATH, releaseConfigProblem, releaseGitEnv, releaseRemote, releaseTools } from "./release-git.js";
|
|
33
|
+
import { taskCheckStamp } from "./store.js";
|
|
32
34
|
import { nightlyReleaseLines } from "./nightly-github.js";
|
|
33
35
|
const NONE = "none";
|
|
34
36
|
/** A budget in dollars from integer micro-dollars, in integer arithmetic: cents when it is whole cents, else every digit. */
|
|
@@ -382,6 +384,13 @@ function changeWords(changes, terms, cap) {
|
|
|
382
384
|
return [head, ...changes.commits.slice(0, cap).map(one => `• ${one.sha.slice(0, 7)} ${one.subject.slice(0, 100)}`), ...(changes.commits.length > cap ? [`• and ${changes.commits.length - cap} more`] : [])];
|
|
383
385
|
}
|
|
384
386
|
/** The release card for a deploy gate, against the live records now. */
|
|
387
|
+
/** How this task's last check ran, when one did and it wasn't plainly sandboxed: the sealed level above is what will run;
|
|
388
|
+
* this is what did. Nothing for a release no check has run for. */
|
|
389
|
+
function lastCheckWords(store, repo, taskId) {
|
|
390
|
+
const ran = repo === null ? null : taskCheckStamp(store, repo, taskId);
|
|
391
|
+
const words = ran === null ? null : ranWords(ran.reading);
|
|
392
|
+
return words === null ? [] : [`Last check: ${words}.`];
|
|
393
|
+
}
|
|
385
394
|
export function releaseCardOf(store, scope, formDigest, words, read) {
|
|
386
395
|
const terms = scope.releaseTerms ?? null;
|
|
387
396
|
// Either sealed form (Codex round 2, finding 1): the identity both share, for the head lines and the change list.
|
|
@@ -400,6 +409,8 @@ export function releaseCardOf(store, scope, formDigest, words, read) {
|
|
|
400
409
|
: `Carry later commits: ${terms.policy.carry ? "yes" : "no"} · checks: ${value("checks")} · expires ${terms.expiresAt}`,
|
|
401
410
|
// What an unsandboxed check means, before consent (Codex round 2, finding 1).
|
|
402
411
|
...(identity === null || releaseAccessWords(identity.access) === null ? [] : [releaseAccessWords(identity.access)]),
|
|
412
|
+
// A check that already ran for this task, from its stamp (H2): never read as sandboxed when it can't be shown.
|
|
413
|
+
...lastCheckWords(store, identity?.repo ?? null, scope.taskId),
|
|
403
414
|
// Option A's disclosure, on the card as on the approval sheet: never approved from chat without it.
|
|
404
415
|
...(terms !== null && terms.policy.carry && terms.access !== "sandboxed" ? ["This project's checks don't run in the sandbox: later test-only commits will run outside the sandbox without being shown to you first."] : []),
|
|
405
416
|
// Filed while CI runs (Alex, 2026-10-29): a person may approve now, and is told plainly what still waits.
|
package/dist/release-carry.d.ts
CHANGED
|
@@ -44,8 +44,13 @@ export declare function stricterOf(a: ReleasePolicy | null, b: ReleasePolicy | n
|
|
|
44
44
|
* the rules don't rely on that).
|
|
45
45
|
*/
|
|
46
46
|
export declare function rulePath(path: string): string;
|
|
47
|
-
/**
|
|
48
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Why one changed path doesn't carry, in words, or null when it does. Every rule reads the normalized path. `fresh`: the
|
|
49
|
+
* paths that didn't exist at the approved commit. A test that was there stays exactly as it was (H4): new tests go in new
|
|
50
|
+
* files. Only "unchanged" holds up, since `vi.mock` hoists and a top-level hook reaches every case above it, so an
|
|
51
|
+
* appended line can disarm what it follows.
|
|
52
|
+
*/
|
|
53
|
+
export declare function changeRefusal(change: TreeChange, policy: ReleasePolicy, fresh?: ReadonlySet<string>): string | null;
|
|
49
54
|
/**
|
|
50
55
|
* A release scope's terms, from the plane's own records: the project's repository, the approved commit and its
|
|
51
56
|
* verified tree, the effective policy, the live release check grant and the check access (only the fence), and the
|
package/dist/release-carry.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* read only Toolroll's records and verified objects. (Coverage, in release-recheck.ts, trusts test code by Alex's
|
|
20
20
|
* explicit decision; nothing here does.)
|
|
21
21
|
*/
|
|
22
|
+
import { prepareProtectedStorage, storageRefusalOf } from "./protected-storage.js";
|
|
22
23
|
import { createHash, randomUUID } from "node:crypto";
|
|
23
24
|
import { closeSync, constants, fstatSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, readlinkSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
24
25
|
import { homedir } from "node:os";
|
|
@@ -26,14 +27,17 @@ import { join, sep } from "node:path";
|
|
|
26
27
|
import { execFileSync } from "node:child_process";
|
|
27
28
|
import { checkLaunchRoot, fenceFor, linuxFenceAvailable, macosFenceAvailable, runFolders } from "./agent-fence.js";
|
|
28
29
|
import { touchesProtected } from "./approval-policy.js";
|
|
29
|
-
import { extractCommit, readVerifiedCommit, verifiedFirstParentWalk, verifiedTreeDiff } from "./commit-extract.js";
|
|
30
|
+
import { extractCommit, readVerifiedCommit, verifiedBlobs, verifiedFirstParentWalk, verifiedTreeDiff } from "./commit-extract.js";
|
|
31
|
+
import { testImportProblem } from "./test-infra.js";
|
|
30
32
|
import { projectGitDir } from "./check-report.js";
|
|
33
|
+
import { checkAccessOf } from "./check-access.js";
|
|
34
|
+
import { stampRefusal } from "./check-stamp.js";
|
|
31
35
|
import { CARRY_RULES_VERSION, readReleasePolicy, releasePolicyDigest } from "./contracts/release-terms.js";
|
|
32
36
|
import { run } from "./exec.js";
|
|
33
37
|
import { proxyEnv, startEgressProxy } from "./egress.js";
|
|
34
38
|
import { liveReleaseOf } from "./scope.js";
|
|
35
39
|
import { UNSANDBOXED_CARRY_ACTION, carriesUnsandboxed } from "./release-authority.js";
|
|
36
|
-
import { carryReleaseApproval, releaseAuthority, releaseCarryTo, sealAnchorArtifact, } from "./store.js";
|
|
40
|
+
import { carryReleaseApproval, releaseAuthority, releaseCarryTo, sealAnchorArtifact, taskCheckStamp, } from "./store.js";
|
|
37
41
|
// ------------------------------------------------------------------ the rules
|
|
38
42
|
/** The journey scripts: tests, by Alex's decision (they carry). */
|
|
39
43
|
export const JOURNEY_SCRIPTS = Object.freeze(["scripts/app-e2e.mjs", "scripts/flows-e2e.mjs"]);
|
|
@@ -111,8 +115,15 @@ export function stricterOf(a, b) {
|
|
|
111
115
|
export function rulePath(path) {
|
|
112
116
|
return path.toLowerCase().split("/").map(part => part.replace(/[. ]+$/, "")).filter(part => part !== "" && part !== ".").join("/");
|
|
113
117
|
}
|
|
114
|
-
/**
|
|
115
|
-
|
|
118
|
+
/** Tests by the rules' classes: what the existing-test rule holds still. */
|
|
119
|
+
const carriedTest = (normal) => CARRY_RULES.classes.some(one => (one.name === "unit tests" || one.name === "journey scripts") && one.matches(normal));
|
|
120
|
+
/**
|
|
121
|
+
* Why one changed path doesn't carry, in words, or null when it does. Every rule reads the normalized path. `fresh`: the
|
|
122
|
+
* paths that didn't exist at the approved commit. A test that was there stays exactly as it was (H4): new tests go in new
|
|
123
|
+
* files. Only "unchanged" holds up, since `vi.mock` hoists and a top-level hook reaches every case above it, so an
|
|
124
|
+
* appended line can disarm what it follows.
|
|
125
|
+
*/
|
|
126
|
+
export function changeRefusal(change, policy, fresh = new Set()) {
|
|
116
127
|
const path = change.path;
|
|
117
128
|
const normal = rulePath(path);
|
|
118
129
|
if (change.kind === "deleted")
|
|
@@ -133,6 +144,8 @@ export function changeRefusal(change, policy) {
|
|
|
133
144
|
return `${path} is protected in this project`;
|
|
134
145
|
if (!CARRY_RULES.classes.some(one => one.matches(normal)))
|
|
135
146
|
return `${path} isn't a test, a doc or the changelog`;
|
|
147
|
+
if (change.kind === "modified" && carriedTest(normal) && !fresh.has(path))
|
|
148
|
+
return `${path} changes an existing test, and new tests go in a new file`;
|
|
136
149
|
return null;
|
|
137
150
|
}
|
|
138
151
|
// ------------------------------------------------------------------ release terms
|
|
@@ -211,6 +224,11 @@ export function preflightCarry(store, input) {
|
|
|
211
224
|
if ((a.accessDigest !== "sandboxed" || store.projectCheckAccess(a.repo) !== "sandboxed") && policy.unsandboxedCarry !== true) {
|
|
212
225
|
return refuse("rules", "This project's checks run outside the sandbox, and carrying release approvals then wasn't on when this release was approved, or is off now.");
|
|
213
226
|
}
|
|
227
|
+
// A carry leans on the check the release already had: it must be shown to have stayed within what was approved (H2).
|
|
228
|
+
const ran = taskCheckStamp(store, a.repo, a.tipTask);
|
|
229
|
+
const unshown = ran === null ? null : stampRefusal(ran.reading, checkAccessOf(a.accessDigest));
|
|
230
|
+
if (unshown !== null)
|
|
231
|
+
return refuse("rules", `This release's approval can't carry: ${unshown}.`);
|
|
214
232
|
if (!/^[0-9a-f]{40}([0-9a-f]{24})?$/.test(input.to))
|
|
215
233
|
return refuse("ancestry", `${input.to} isn't a full commit id.`);
|
|
216
234
|
if (input.to === a.tipSha)
|
|
@@ -234,6 +252,10 @@ export function preflightCarry(store, input) {
|
|
|
234
252
|
treeOf.set(commit.id, commit.tree);
|
|
235
253
|
const commits = [];
|
|
236
254
|
let churn = 0;
|
|
255
|
+
const whole = verifiedTreeDiff(gitDir, a.approvedTree, chain[0].tree);
|
|
256
|
+
if (!whole.ok)
|
|
257
|
+
return refuse("shape", `The change since the approval couldn't be read: ${whole.message}`);
|
|
258
|
+
const fresh = new Set(whole.value.filter(one => one.kind === "added").map(one => one.path));
|
|
237
259
|
for (const commit of ordered) {
|
|
238
260
|
const parentTree = treeOf.get(commit.parents[0]);
|
|
239
261
|
if (parentTree === undefined)
|
|
@@ -242,21 +264,30 @@ export function preflightCarry(store, input) {
|
|
|
242
264
|
if (!diff.ok)
|
|
243
265
|
return refuse("shape", `${commit.id.slice(0, 7)} couldn't be read: ${diff.message}`);
|
|
244
266
|
for (const change of diff.value) {
|
|
245
|
-
const why = changeRefusal(change, policy);
|
|
267
|
+
const why = changeRefusal(change, policy, fresh);
|
|
246
268
|
if (why !== null)
|
|
247
269
|
return refuse("rules", `${commit.id.slice(0, 7)}: ${why}.`, change.path);
|
|
248
270
|
}
|
|
249
271
|
churn += diff.value.reduce((n, one) => n + one.added + one.deleted, 0);
|
|
250
272
|
commits.push({ sha: commit.id, files: diff.value.map(one => ({ path: one.path, added: one.added, deleted: one.deleted })) });
|
|
251
273
|
}
|
|
252
|
-
const whole = verifiedTreeDiff(gitDir, a.approvedTree, chain[0].tree);
|
|
253
|
-
if (!whole.ok)
|
|
254
|
-
return refuse("shape", `The change since the approval couldn't be read: ${whole.message}`);
|
|
255
274
|
for (const change of whole.value) {
|
|
256
|
-
const why = changeRefusal(change, policy);
|
|
275
|
+
const why = changeRefusal(change, policy, fresh);
|
|
257
276
|
if (why !== null)
|
|
258
277
|
return refuse("rules", `Since the approval: ${why}.`, change.path);
|
|
259
278
|
}
|
|
279
|
+
// No new test may reach into another (H4). Read from the verified tree, never from a checkout.
|
|
280
|
+
// By the rules' own classifier, so everything that carries as a test is read (a new journey script too).
|
|
281
|
+
const newTests = whole.value.filter(one => one.kind === "added" && carriedTest(rulePath(one.path))).map(one => one.path);
|
|
282
|
+
const added = newTests.length === 0 ? null : verifiedBlobs(gitDir, chain[0].tree, newTests);
|
|
283
|
+
if (added !== null && !added.ok)
|
|
284
|
+
return refuse("shape", `The new tests couldn't be read: ${added.message}`);
|
|
285
|
+
for (const path of newTests) {
|
|
286
|
+
const bytes = added !== null && added.ok ? added.value.get(path)?.bytes : undefined;
|
|
287
|
+
const why = bytes === undefined ? `${path} couldn't be read` : testImportProblem(path, bytes.toString("utf8"));
|
|
288
|
+
if (why !== null)
|
|
289
|
+
return refuse("rules", `Since the approval: ${why}.`, path);
|
|
290
|
+
}
|
|
260
291
|
const changedLines = whole.value.reduce((n, one) => n + one.added + one.deleted, 0);
|
|
261
292
|
if (changedLines > policy.limits.lines)
|
|
262
293
|
return refuse("limits", `${changedLines} lines changed since the approval; at most ${policy.limits.lines} carry.`);
|
|
@@ -274,11 +305,19 @@ export function fencedBuildRunner(store) {
|
|
|
274
305
|
if (!macosFenceAvailable() && !linuxFenceAvailable()) {
|
|
275
306
|
return { code: 1, stdout: "", stderr: "This machine can't confine the build (no macOS sandbox, no working bubblewrap), so it didn't run.\n", timedOut: false, notFound: false };
|
|
276
307
|
}
|
|
308
|
+
// The storage check, awaited off this thread (protected-storage.ts); its words are the build's when it can't finish.
|
|
309
|
+
let storage;
|
|
310
|
+
try {
|
|
311
|
+
storage = await prepareProtectedStorage();
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
return { code: 1, stdout: "", stderr: `${storageRefusalOf(error)?.message ?? "The build's fence could not be prepared, so it didn't run."}\n`, timedOut: false, notFound: false };
|
|
315
|
+
}
|
|
277
316
|
const egress = await startEgressProxy({ hosts: [] });
|
|
278
317
|
try {
|
|
279
318
|
const network = { mode: "allowlist", proxyPort: egress.port, brokerPorts: [], loopback: "proxy-only", socketRoots: [...options.writable], token: egress.secret };
|
|
280
319
|
const launch = fenceFor("check", {
|
|
281
|
-
databaseFile: store.databaseFile(), worktree: options.cwd, boundary: null, binding: null, runFolders: runFolders(null), poolRoots: [],
|
|
320
|
+
databaseFile: store.databaseFile(), worktree: options.cwd, boundary: null, binding: null, runFolders: runFolders(null), poolRoots: [], storage,
|
|
282
321
|
writes: { writes: { subpaths: [...options.writable], prefixes: [] }, readOnly: [] }, network,
|
|
283
322
|
});
|
|
284
323
|
return await run(file, args, { cwd: options.cwd, env: { ...options.env, ...proxyEnv(egress, "proxy-only") }, timeoutMs: options.timeoutMs, processGroup: true,
|