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,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One step to approve a new route (docs/plans/plain-toolroll.md P2.5). There are no route classes: an approval stays
|
|
3
|
+
* bound to its exact route, and dispatch runs on no other bytes. When the route a task would dispatch on differs from the
|
|
4
|
+
* sealed one, Toolroll files the same words again on today's route (nothing is sealed) and keeps one "what changed" line,
|
|
5
|
+
* bound to that route's digest, for the one Approve. A legacy row with no route gets the same.
|
|
6
|
+
*
|
|
7
|
+
* approve() reads routeChangeProblem: a line that doesn't describe the route being approved refuses "changed".
|
|
8
|
+
*/
|
|
9
|
+
import { providerName } from "./provider-auth.js";
|
|
10
|
+
import { legOf, routeDigestOf, routeFromJson } from "./phase-routing.js";
|
|
11
|
+
import { deployGateScope, releaseCheckScope } from "./scope.js";
|
|
12
|
+
import { BUILT_IN } from "./store.js";
|
|
13
|
+
const ROLE = { plan: "planner", build: "builder", repair: "repair agent", review: "reviewer" };
|
|
14
|
+
const named = (one) => `${providerName(one.provider)} ${one.model ?? "(its default model)"}`;
|
|
15
|
+
const billing = (mode) => mode === "api-key" ? "API key" : mode === "subscription" ? "subscription" : null;
|
|
16
|
+
/** The sentence for what changed between the seal and the proposal. */
|
|
17
|
+
export function routeChangeWords(sealed, next) {
|
|
18
|
+
const changes = [];
|
|
19
|
+
for (const phase of ["build", "plan", "repair", "review"]) {
|
|
20
|
+
const now = legOf(next.route, phase);
|
|
21
|
+
const was = sealed.route !== null ? legOf(sealed.route, phase) : phase === "build" && sealed.profile != null ? { provider: sealed.profile.provider, model: "model" in sealed.profile ? (sealed.profile.model ?? null) : null } : null;
|
|
22
|
+
if (was === null) {
|
|
23
|
+
if (phase === "build")
|
|
24
|
+
changes.push(`The builder is ${named(now)}`);
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (was.provider === now.provider && was.model === now.model)
|
|
28
|
+
continue;
|
|
29
|
+
const line = `changes from ${named(was)} to ${named(now)}`;
|
|
30
|
+
// The repair agent is the builder's usual twin: one change is one sentence.
|
|
31
|
+
if (changes.some(one => one.endsWith(line)))
|
|
32
|
+
continue;
|
|
33
|
+
changes.push(`${changes.length === 0 ? "The" : "the"} ${ROLE[phase]} ${line}`);
|
|
34
|
+
}
|
|
35
|
+
const from = billing(sealed.authMode), to = billing(next.authMode);
|
|
36
|
+
const paid = from !== null && to !== null && from !== to ? ` (${from} → ${to})` : "";
|
|
37
|
+
if (changes.length === 0)
|
|
38
|
+
return "The agents' settings changed since you approved this. Approve to continue.";
|
|
39
|
+
return `${changes.join("; ")}${paid}. Approve to continue.`;
|
|
40
|
+
}
|
|
41
|
+
/** File the same words on today's route, and keep the line. Null: nothing to propose (a release, nothing changed, or no
|
|
42
|
+
* route Toolroll can name), and the task stays as it is. It seals nothing. */
|
|
43
|
+
export function proposeNewRoute(store, taskRef, taskId, now) {
|
|
44
|
+
return store.transact(() => {
|
|
45
|
+
const before = store.getScope(taskId);
|
|
46
|
+
if (before === null || deployGateScope(before) || releaseCheckScope(before))
|
|
47
|
+
return null;
|
|
48
|
+
const sealedRoute = routeFromJson(before.approvedRouteJson ?? null);
|
|
49
|
+
const refiled = store.refileScope(taskId, now);
|
|
50
|
+
if (refiled === null || refiled.profileState === "unresolved")
|
|
51
|
+
return null;
|
|
52
|
+
const route = routeFromJson(refiled.proposedRouteJson ?? null);
|
|
53
|
+
if (route === null)
|
|
54
|
+
return null;
|
|
55
|
+
if (refiled.approvedDigest === refiled.digest)
|
|
56
|
+
return null;
|
|
57
|
+
// How the last attempt billed, against how the new builder would: said when they differ.
|
|
58
|
+
const lastBilled = store.runsFor(taskRef).find(one => one.role === "builder")?.authMode ?? null;
|
|
59
|
+
const billed = store.agentsFor([legOf(route, "build").provider])[0]?.billing ?? null;
|
|
60
|
+
const words = routeChangeWords({ route: sealedRoute, profile: before.approvedProfile ?? null, authMode: lastBilled }, { route, authMode: billed });
|
|
61
|
+
store.handle.prepare(`INSERT INTO route_change (task_ref, scope_digest, to_route_digest, words, created_at) VALUES (?, ?, ?, ?, ?)
|
|
62
|
+
ON CONFLICT (task_ref) DO UPDATE SET scope_digest = excluded.scope_digest, to_route_digest = excluded.to_route_digest, words = excluded.words, created_at = excluded.created_at`)
|
|
63
|
+
.run(taskRef, refiled.digest, routeDigestOf(route), words, now.toISOString());
|
|
64
|
+
return { words };
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/** The line for this task's waiting plan, or null. Only while it describes exactly the plan up for approval. */
|
|
68
|
+
export function routeChangeOf(store, taskId) {
|
|
69
|
+
const ref = store.lookupRef(taskId);
|
|
70
|
+
if (ref === null)
|
|
71
|
+
return null;
|
|
72
|
+
const row = store.handle.prepare("SELECT scope_digest, to_route_digest, words FROM route_change WHERE task_ref = ?").get(ref.id);
|
|
73
|
+
const scope = store.getScope(taskId);
|
|
74
|
+
if (row === undefined || scope === null || scope.digest !== row["scope_digest"] || scope.approvedDigest === scope.digest)
|
|
75
|
+
return null;
|
|
76
|
+
const route = routeFromJson(scope.proposedRouteJson ?? null);
|
|
77
|
+
return route !== null && routeDigestOf(route) === row["to_route_digest"] ? String(row["words"]) : null;
|
|
78
|
+
}
|
|
79
|
+
/** Each sweep: an unapproved row from before routing gets its route, and the same one Approve. A standing old approval
|
|
80
|
+
* is left alone (the stale check reaches it if its route ever differs). */
|
|
81
|
+
export function routeLegacyPlans(store, now) {
|
|
82
|
+
const rows = store.handle.prepare(`SELECT r.id AS ref, r.external_id AS task FROM task_scope s JOIN task_ref r ON r.external_id = s.task_id AND r.backend = ? JOIN task t ON t.id = r.external_id
|
|
83
|
+
WHERE s.route_era IS NULL AND t.state = 'queued' AND (s.approved_digest IS NULL OR s.approved_digest <> s.digest) LIMIT 20`).all(BUILT_IN);
|
|
84
|
+
let routed = 0;
|
|
85
|
+
for (const row of rows)
|
|
86
|
+
if (proposeNewRoute(store, Number(row["ref"]), String(row["task"]), now) !== null)
|
|
87
|
+
routed++;
|
|
88
|
+
return routed;
|
|
89
|
+
}
|
package/dist/route-hooks.js
CHANGED
|
@@ -80,8 +80,9 @@ const VERB = String.raw `(?:INSERT(?:\s+OR\s+\w+)?\s+INTO|REPLACE\s+INTO|UPDATE(
|
|
|
80
80
|
const writes = (tables) => new RegExp(String.raw `\b${VERB}\s+(?:main\s*\.\s*)?["\`\[]?(?:${tables.join("|")})["\`\]]?(?![\w])`, "i");
|
|
81
81
|
const ROUTING_WRITE = writes([...ROUTING_TABLES, "route_rule_one_winner"]);
|
|
82
82
|
const MODELS_WRITE = writes(MODELS_TABLES);
|
|
83
|
-
/** The two columns only route-store.ts writes: any write statement that names one.
|
|
84
|
-
|
|
83
|
+
/** The two columns only route-store.ts writes: any write statement that names one. A lesson note keeps its own copy of
|
|
84
|
+
* how a revision was asked for: that one INSERT into its own table is not this column, and only it is left out. */
|
|
85
|
+
const COLUMN_WRITE = /\b(?:INSERT(?!\s+INTO\s+lesson_note\b)|REPLACE|UPDATE)\b[^]*\b(?:exec_class|revision_origin)\b/i;
|
|
85
86
|
/** Each string or template text in the file that writes a protected row, with the function it's in ("" at the top
|
|
86
87
|
* level): `Class#method`, or the top-level function. */
|
|
87
88
|
export function protectedWrites(file, source) {
|
package/dist/route-results.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { INELIGIBLE_REASONS, type RecordReason, type RecordState, type ResultRecordView, type Selection } from "./contracts/route-results.js";
|
|
2
|
-
import { type Store } from "./store.js";
|
|
2
|
+
import { type Run, type Store } from "./store.js";
|
|
3
3
|
export { cohortOf, EXEC_CLASS_OF_REASON, execClassOf, type CohortInput, type CohortRow } from "./route-store.js";
|
|
4
4
|
/** How long an accepted result waits before it counts, and how long a result may wait for an answer. */
|
|
5
5
|
export declare const MATURE_AFTER_MS: number;
|
|
@@ -39,6 +39,11 @@ export type RecordValue = {
|
|
|
39
39
|
/** The family is quiet, or past ACCUMULATE_FOR_MS: with a final verdict, the record closes. */
|
|
40
40
|
accumulationEnded: boolean;
|
|
41
41
|
};
|
|
42
|
+
/** Whether the first built run among `runs` passed its own check; null when none is recorded. The code map's report reads
|
|
43
|
+
* this too, so the two never disagree on what a first check is. */
|
|
44
|
+
export declare function firstCheckPassedOf(store: Store, runs: readonly Pick<Run, "id" | "outcome">[]): boolean | null;
|
|
45
|
+
/** How many of these task versions are a person's revision. */
|
|
46
|
+
export declare function personRevisionsOf(store: Store, refs: readonly number[]): number;
|
|
42
47
|
/**
|
|
43
48
|
* A task family's record, evaluated now from its rows (null: no code-writing run, or a family whose lineage can't be
|
|
44
49
|
* proved). The order is the plan's: what can't be attributed to one model and one cohort first, then a live attempt
|
package/dist/route-results.js
CHANGED
|
@@ -54,6 +54,17 @@ function classOfRun(run, kept) {
|
|
|
54
54
|
return "model";
|
|
55
55
|
}
|
|
56
56
|
const FAILED_OUTCOMES = new Set(["failed", "interrupted"]);
|
|
57
|
+
/** Whether the first built run among `runs` passed its own check; null when none is recorded. The code map's report reads
|
|
58
|
+
* this too, so the two never disagree on what a first check is. */
|
|
59
|
+
export function firstCheckPassedOf(store, runs) {
|
|
60
|
+
const first = runs.find(run => run.outcome === "built");
|
|
61
|
+
const status = first === undefined ? null : store.runCheckFor(first.id)?.status ?? null;
|
|
62
|
+
return status === "passed" ? true : status === "failed" ? false : null;
|
|
63
|
+
}
|
|
64
|
+
/** How many of these task versions are a person's revision. */
|
|
65
|
+
export function personRevisionsOf(store, refs) {
|
|
66
|
+
return Number(store.handle.prepare("SELECT COUNT(*) AS n FROM task_ref WHERE id IN (SELECT value FROM json_each(?)) AND revision_origin = 'person'").get(JSON.stringify(refs))?.["n"] ?? 0);
|
|
67
|
+
}
|
|
57
68
|
/**
|
|
58
69
|
* A task family's record, evaluated now from its rows (null: no code-writing run, or a family whose lineage can't be
|
|
59
70
|
* proved). The order is the plan's: what can't be attributed to one model and one cohort first, then a live attempt
|
|
@@ -106,10 +117,8 @@ export function evaluateFamily(store, rootTaskId, evidenceRoot, now) {
|
|
|
106
117
|
else
|
|
107
118
|
agentTurns += run.agentTurns;
|
|
108
119
|
}
|
|
109
|
-
const
|
|
110
|
-
const
|
|
111
|
-
const personRevisions = store.handle.prepare("SELECT COUNT(*) AS n FROM task_ref WHERE id IN (SELECT value FROM json_each(?)) AND revision_origin = 'person'")
|
|
112
|
-
.get(JSON.stringify(family.versions.filter(one => one.id !== family.root.id).map(one => one.refId)))?.["n"];
|
|
120
|
+
const firstCheck = firstCheckPassedOf(store, runs);
|
|
121
|
+
const personRevisions = personRevisionsOf(store, family.versions.filter(one => one.id !== family.root.id).map(one => one.refId));
|
|
113
122
|
const quiet = family.versions.every(one => one.state === "done" || one.state === "cancelled" || one.state === "failed") && runs.every(run => run.outcome !== null);
|
|
114
123
|
let modelFailures = 0, infraFailures = 0;
|
|
115
124
|
for (const run of runs) {
|
|
@@ -129,7 +138,7 @@ export function evaluateFamily(store, rootTaskId, evidenceRoot, now) {
|
|
|
129
138
|
firstStartedAt: first?.startedAt ?? runs[0].startedAt, acceptedAt: null, decidedAt: null, modelFailures, infraFailures,
|
|
130
139
|
apiMicrousd, subscriptionRuns, subscriptionTokens, unknownRuns, remediationRuns,
|
|
131
140
|
selections: attributed.map(one => one.selection).filter((one) => one !== null), settled: false,
|
|
132
|
-
personRevisions
|
|
141
|
+
personRevisions, firstCheckPassed: firstCheck,
|
|
133
142
|
agentTurns, turnsUnknownRuns,
|
|
134
143
|
accumulationEnded: quiet || now.getTime() >= Date.parse(first?.startedAt ?? runs[0].startedAt) + ACCUMULATE_FOR_MS,
|
|
135
144
|
};
|
package/dist/run-closeout.d.ts
CHANGED
package/dist/run-closeout.js
CHANGED
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { execFileSync } from "node:child_process";
|
|
17
17
|
import { existsSync, readdirSync, readlinkSync, realpathSync } from "node:fs";
|
|
18
|
-
import { hostname } from "node:os";
|
|
19
18
|
import { basename, sep } from "node:path";
|
|
20
19
|
import { ownedProcessCount, runOwnerTag } from "./exec.js";
|
|
20
|
+
import { localWitnessSql } from "./store.js";
|
|
21
21
|
import { sleep as defaultSleep } from "./sleep.js";
|
|
22
22
|
/** How long a process has after SIGTERM before SIGKILL. */
|
|
23
23
|
export const STOP_GRACE_MS = 10_000;
|
|
@@ -138,10 +138,11 @@ export async function closeOutRuns(store, clock, options = {}) {
|
|
|
138
138
|
if (process.platform === "win32")
|
|
139
139
|
return [];
|
|
140
140
|
const db = store.handle;
|
|
141
|
+
const local = localWitnessSql(db, "p"), mine = localWitnessSql(db);
|
|
141
142
|
const runs = db.prepare(`SELECT DISTINCT p.run AS run FROM run_process p JOIN run r ON r.id = p.run JOIN task_ref t ON t.id = r.task_ref
|
|
142
|
-
WHERE p.exited_at IS NULL AND p.pid IS NOT NULL AND p.containment IS NULL AND p.container IS NULL AND
|
|
143
|
+
WHERE p.exited_at IS NULL AND p.pid IS NOT NULL AND p.containment IS NULL AND p.container IS NULL AND ${local.sql}
|
|
143
144
|
AND r.outcome IS NOT NULL${options.repo === undefined ? "" : " AND t.repo = ?"} ORDER BY p.run`)
|
|
144
|
-
.all(...[
|
|
145
|
+
.all(...[...local.params, ...(options.repo === undefined ? [] : [options.repo])]).map(row => Number(row["run"]));
|
|
145
146
|
const out = [];
|
|
146
147
|
if (runs.length === 0)
|
|
147
148
|
return out;
|
|
@@ -151,7 +152,7 @@ export async function closeOutRuns(store, clock, options = {}) {
|
|
|
151
152
|
if (ownedProcessCount(runOwnerTag(store, runId)) > 0)
|
|
152
153
|
continue;
|
|
153
154
|
const witnesses = db.prepare(`SELECT pid, process_group, observed_at FROM run_process
|
|
154
|
-
WHERE run = ? AND exited_at IS NULL AND pid IS NOT NULL AND containment IS NULL AND container IS NULL AND
|
|
155
|
+
WHERE run = ? AND exited_at IS NULL AND pid IS NOT NULL AND containment IS NULL AND container IS NULL AND ${mine.sql}`).all(runId, ...mine.params)
|
|
155
156
|
.map(row => ({ pid: Number(row["pid"]), group: Number(row["process_group"]) === 1, observedAt: Date.parse(String(row["observed_at"])) }))
|
|
156
157
|
.filter(one => Number.isFinite(one.observedAt));
|
|
157
158
|
if (witnesses.length === 0)
|
|
@@ -252,8 +253,9 @@ export function checkoutTargets(census, inside, roots, guard) {
|
|
|
252
253
|
function finishedRunProcesses(store, census) {
|
|
253
254
|
const byPid = new Map(census.map(row => [row.pid, row]));
|
|
254
255
|
const roots = new Set();
|
|
256
|
+
const local = localWitnessSql(store.handle, "p");
|
|
255
257
|
const rows = store.handle.prepare(`SELECT p.pid AS pid, p.observed_at AS observed_at FROM run_process p JOIN run r ON r.id = p.run
|
|
256
|
-
WHERE p.exited_at IS NULL AND p.pid IS NOT NULL AND
|
|
258
|
+
WHERE p.exited_at IS NULL AND p.pid IS NOT NULL AND ${local.sql} AND r.outcome IS NOT NULL`).all(...local.params);
|
|
257
259
|
for (const row of rows) {
|
|
258
260
|
const now = byPid.get(Number(row["pid"]));
|
|
259
261
|
const observedAt = Date.parse(String(row["observed_at"]));
|
package/dist/runner-process.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* The record is the plane's own state (the folder that holds the database). It grants nothing by itself: it can
|
|
14
14
|
* only let a name whose every recorded holder has exited be taken over early.
|
|
15
15
|
*/
|
|
16
|
+
import { sameMachineHost } from "./machine-identity.js";
|
|
16
17
|
import { createHash, createHmac, randomUUID, timingSafeEqual } from "node:crypto";
|
|
17
18
|
import { execFileSync } from "node:child_process";
|
|
18
19
|
import { closeSync, fstatSync, lstatSync, mkdirSync, openSync, readSync, realpathSync, renameSync, rmSync, writeFileSync, fsyncSync, constants } from "node:fs";
|
|
@@ -195,7 +196,7 @@ export function holderProvedGone(record, holder, host, probe = realProcessProbe)
|
|
|
195
196
|
if (typeof record.signature !== "string" || !/^[a-f0-9]{64}$/.test(record.signature)
|
|
196
197
|
|| !timingSafeEqual(Buffer.from(record.signature, "hex"), Buffer.from(expected, "hex")))
|
|
197
198
|
return { gone: false, why: "its process record is not authenticated" };
|
|
198
|
-
if (record.runner !== holder.runner.name || record.host !== holder.runner.host || record.host
|
|
199
|
+
if (record.runner !== holder.runner.name || record.host !== holder.runner.host || !sameMachineHost(record.host, host))
|
|
199
200
|
return { gone: false, why: "its record is for another worker or machine" };
|
|
200
201
|
if (record.registeredAt !== holder.runner.registeredAt || record.credential !== credentialDigest(holder.credentialHash))
|
|
201
202
|
return { gone: false, why: "its record is from an earlier start" };
|
|
@@ -229,7 +230,7 @@ export function recordStandsFor(record, holder, host) {
|
|
|
229
230
|
if (typeof record.signature !== "string" || !/^[a-f0-9]{64}$/.test(record.signature)
|
|
230
231
|
|| !timingSafeEqual(Buffer.from(record.signature, "hex"), Buffer.from(expected, "hex")))
|
|
231
232
|
return "its process record is not authenticated";
|
|
232
|
-
if (record.runner !== holder.runner.name || record.host !== holder.runner.host || record.host
|
|
233
|
+
if (record.runner !== holder.runner.name || record.host !== holder.runner.host || !sameMachineHost(record.host, host))
|
|
233
234
|
return "its record is for another worker or machine";
|
|
234
235
|
if (record.registeredAt !== holder.runner.registeredAt || record.credential !== credentialDigest(holder.credentialHash))
|
|
235
236
|
return "its record is from an earlier start";
|
package/dist/runner.js
CHANGED
|
@@ -380,7 +380,8 @@ export function recoverDead(store, now, livenessMs = DEFAULT_LIVENESS_MS) {
|
|
|
380
380
|
const claims = store.releaseClaimsOf(candidate.name, now, requeued);
|
|
381
381
|
const worktrees = store.releaseWorktreesOf(candidate.name, now);
|
|
382
382
|
const settled = store.recoverRunnerWork(candidate.name, now);
|
|
383
|
-
|
|
383
|
+
// Only what recovery left in the queue: a vanished build it stopped for a person isn't requeued (vanished-retry.ts).
|
|
384
|
+
return { runner: candidate.name, claims, worktrees, runs: settled.runs, requeued: [...new Set([...requeued, ...settled.requeued])].filter(id => store.getTask(id)?.state === "queued") };
|
|
384
385
|
});
|
|
385
386
|
if (recovery !== null)
|
|
386
387
|
recovered.push(recovery);
|
package/dist/scope.d.ts
CHANGED
|
@@ -205,6 +205,7 @@ export declare function canonicalProfileJson(profile: ExecutionProfile): string;
|
|
|
205
205
|
* same 128 bits every other safety digest here uses (finding 21). */
|
|
206
206
|
export declare function profileDigestOf(profile: ExecutionProfile): string;
|
|
207
207
|
export declare const CHAIN_DIGEST_VERSION = 1;
|
|
208
|
+
export declare const LIVE_CHAIN_VERSION = 2;
|
|
208
209
|
/** One entry of an approved fallback chain: the WHOLE execution profile
|
|
209
210
|
* (so a claude->codex switch cannot inherit one provider's repair config)
|
|
210
211
|
* and the auth mode it runs under. Order is authority. */
|
|
@@ -214,15 +215,15 @@ export type ChainEntry = {
|
|
|
214
215
|
};
|
|
215
216
|
/** The stored snapshot bytes for a chain: the version rides IN the
|
|
216
217
|
* snapshot, exactly like a single profile's. */
|
|
217
|
-
export declare function canonicalChainJson(entries: readonly ChainEntry[]): string;
|
|
218
|
+
export declare function canonicalChainJson(entries: readonly ChainEntry[], version?: number): string;
|
|
218
219
|
/** sha256 over a domain-separated canonical chain encoding — a DIFFERENT
|
|
219
220
|
* domain from a single profile ("standing-orders:chain:" vs
|
|
220
221
|
* ":profile:"), so a chain digest can never collide with a profile
|
|
221
222
|
* digest, truncated to the same 128 bits. */
|
|
222
|
-
export declare function chainDigestOf(entries: readonly ChainEntry[]): string;
|
|
223
|
+
export declare function chainDigestOf(entries: readonly ChainEntry[], version?: number): string;
|
|
223
224
|
/** Strict re-hydration of a stored chain — every entry proved through the
|
|
224
225
|
* single-profile rehydrator; anything unexpected is null. */
|
|
225
|
-
export declare function chainFromJson(json: string | null): ChainEntry[] | null;
|
|
226
|
+
export declare function chainFromJson(json: string | null, version?: number): ChainEntry[] | null;
|
|
226
227
|
/**
|
|
227
228
|
* THE STRICT RAW TERMS READ (raw authority repair): the stored columns of
|
|
228
229
|
* a scope or a standing order, proved EXACTLY — a JSON list is a list of
|
|
@@ -499,6 +500,7 @@ export declare function digestOf(scope: Pick<Scope, "goal" | "outOfScope" | "tou
|
|
|
499
500
|
releaseCheck?: ReleaseCheckPurpose | null;
|
|
500
501
|
}, target?: ExecutionProfile | null | {
|
|
501
502
|
chain: readonly ChainEntry[];
|
|
503
|
+
version?: number;
|
|
502
504
|
}, route?: PhaseRoute | null): string;
|
|
503
505
|
export declare function propose(store: Store, input: ScopeInput): Scope;
|
|
504
506
|
export type GuardedProposeResult = {
|
|
@@ -793,7 +795,23 @@ export declare function fileAndSealUnderMode(store: Store, input: ScopeInput & {
|
|
|
793
795
|
reason: "no-mode" | "not-signer" | "not-covered" | "coordinator-filed" | "acceptance-required" | "profile-unresolved";
|
|
794
796
|
detail?: string;
|
|
795
797
|
};
|
|
796
|
-
export declare function approve(store: Store, taskId: string, by: string, now: Date, sawDigest: string, token: string, mutation?: Mutation
|
|
798
|
+
export declare function approve(store: Store, taskId: string, by: string, now: Date, sawDigest: string, token: string, mutation?: Mutation,
|
|
799
|
+
/** A fast start (fast-start.ts): Build is this same yes, with its marker in the proof. Only that road passes it. */
|
|
800
|
+
fastStart?: {
|
|
801
|
+
rule: string;
|
|
802
|
+
at: string;
|
|
803
|
+
},
|
|
804
|
+
/** A person's own yes in chat (owner's decision, docs/plans/toolroll-cards.md): their tap or signed-in click stands in
|
|
805
|
+
* for a password, so `token` is unused. Every rule below still applies. The caller has proved the person is the one
|
|
806
|
+
* tapping. `shown` is what a card that showed its terms whole showed: those words, and the digest kept when it was
|
|
807
|
+
* made. Null when the card showed less: the seal then opens none of the walls a person's reading opens. */
|
|
808
|
+
chat?: {
|
|
809
|
+
modeDigest: string;
|
|
810
|
+
shown: {
|
|
811
|
+
expect: string;
|
|
812
|
+
terms: string;
|
|
813
|
+
} | null;
|
|
814
|
+
}): ApproveResult;
|
|
797
815
|
/** The ledger act that records how an approval's person was proved (password-now, sso-now, fresh-password, fresh-sso). */
|
|
798
816
|
export declare const APPROVAL_PROOF = "approval proved";
|
|
799
817
|
/** The proof row's detail: when the person was proved, the generation, and the exact digest they said yes to. */
|
|
@@ -835,6 +853,7 @@ export type ScopeAuthority = {
|
|
|
835
853
|
route: PhaseRoute;
|
|
836
854
|
digest: string;
|
|
837
855
|
authMode: AuthMode | null;
|
|
856
|
+
chain?: ChainEntry[];
|
|
838
857
|
} | {
|
|
839
858
|
ok: false;
|
|
840
859
|
reason: "terms" | "unrouted" | "unresolved" | "profile" | "chain" | "route" | "parity" | "digest" | "auth-mode";
|
|
@@ -1031,3 +1050,8 @@ export declare function localApprovalEligible(store: Store, by: string, taskId:
|
|
|
1031
1050
|
reason: LocalApprovalRefusal;
|
|
1032
1051
|
message: string;
|
|
1033
1052
|
};
|
|
1053
|
+
/** A chain that may dispatch: version 2, a base and at least one fallback. An older chain is history, never this. */
|
|
1054
|
+
export declare function liveChainFromJson(json: string | null): ChainEntry[] | null;
|
|
1055
|
+
/** The chain in full, the words a yes agrees to: order, agent, model, how it bills, what it may do, and whether Toolroll
|
|
1056
|
+
* can see it run out. Every approval surface shows these lines; empty for a scope with no chain. */
|
|
1057
|
+
export declare function chainTermsLines(scope: Pick<Scope, "proposedChainJson">): string[];
|
package/dist/scope.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
// The rubric's vocabulary loads before anything that reaches the plan contract (see contracts/acceptance-terms.ts).
|
|
2
2
|
export { ACCEPTANCE_LIMITS, EVIDENCE_KINDS } from "./contracts/acceptance-terms.js";
|
|
3
|
+
import { cardShowsScope, cardTermsDigest } from "./chat-terms.js";
|
|
3
4
|
import { mintSecret } from "./secret-mint.js";
|
|
4
5
|
import { passwordlessOn } from "./passwordless.js";
|
|
6
|
+
import { routeChangeProblem } from "./route-change-check.js";
|
|
7
|
+
import { issueImported } from "./task-source.js";
|
|
5
8
|
import { currentPasswordSource, passwordGuardOf, provenPasswordAccount } from "./sign-in-guard.js";
|
|
6
9
|
import { validateScopeText } from "./task-text.js";
|
|
7
10
|
import { contractChangesOf } from "./contract-changes.js";
|
|
@@ -152,29 +155,30 @@ export function profileDigestOf(profile) {
|
|
|
152
155
|
.digest("hex")
|
|
153
156
|
.slice(0, 32);
|
|
154
157
|
}
|
|
155
|
-
// ---- fallback chains (
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
//
|
|
158
|
+
// ---- fallback chains (fallback.ts, docs/plans/fallback-chains.md) ----------
|
|
159
|
+
// Version 1 is history (v30-v114): an old approval still decodes and its digest
|
|
160
|
+
// still re-derives, so old tasks verify and render, and it holds no authority.
|
|
161
|
+
// Version 2 is what filing writes now: only it dispatches.
|
|
159
162
|
export const CHAIN_DIGEST_VERSION = 1;
|
|
163
|
+
export const LIVE_CHAIN_VERSION = 2;
|
|
160
164
|
/** The stored snapshot bytes for a chain: the version rides IN the
|
|
161
165
|
* snapshot, exactly like a single profile's. */
|
|
162
|
-
export function canonicalChainJson(entries) {
|
|
163
|
-
return canonicalJson({ digestVersion:
|
|
166
|
+
export function canonicalChainJson(entries, version = CHAIN_DIGEST_VERSION) {
|
|
167
|
+
return canonicalJson({ digestVersion: version, chain: entries });
|
|
164
168
|
}
|
|
165
169
|
/** sha256 over a domain-separated canonical chain encoding — a DIFFERENT
|
|
166
170
|
* domain from a single profile ("standing-orders:chain:" vs
|
|
167
171
|
* ":profile:"), so a chain digest can never collide with a profile
|
|
168
172
|
* digest, truncated to the same 128 bits. */
|
|
169
|
-
export function chainDigestOf(entries) {
|
|
173
|
+
export function chainDigestOf(entries, version = CHAIN_DIGEST_VERSION) {
|
|
170
174
|
return createHash("sha256")
|
|
171
|
-
.update(`standing-orders:chain:v${
|
|
175
|
+
.update(`standing-orders:chain:v${version}:${canonicalChainJson(entries, version)}`, "utf8")
|
|
172
176
|
.digest("hex")
|
|
173
177
|
.slice(0, 32);
|
|
174
178
|
}
|
|
175
179
|
/** Strict re-hydration of a stored chain — every entry proved through the
|
|
176
180
|
* single-profile rehydrator; anything unexpected is null. */
|
|
177
|
-
export function chainFromJson(json) {
|
|
181
|
+
export function chainFromJson(json, version = CHAIN_DIGEST_VERSION) {
|
|
178
182
|
if (json === null)
|
|
179
183
|
return null;
|
|
180
184
|
let parsed;
|
|
@@ -187,7 +191,7 @@ export function chainFromJson(json) {
|
|
|
187
191
|
if (parsed === null || typeof parsed !== "object")
|
|
188
192
|
return null;
|
|
189
193
|
const wrapper = parsed;
|
|
190
|
-
if (wrapper.digestVersion !==
|
|
194
|
+
if (wrapper.digestVersion !== version)
|
|
191
195
|
return null;
|
|
192
196
|
// Exact keys on the wrapper and on every entry (v48 integrity): a
|
|
193
197
|
// snapshot carrying anything this code never writes is not one it wrote.
|
|
@@ -600,7 +604,7 @@ route) {
|
|
|
600
604
|
const innerDigest = target == null
|
|
601
605
|
? null
|
|
602
606
|
: "chain" in target
|
|
603
|
-
? chainDigestOf(target.chain)
|
|
607
|
+
? chainDigestOf(target.chain, target.version)
|
|
604
608
|
: profileDigestOf(target);
|
|
605
609
|
return createHash("sha256")
|
|
606
610
|
.update(JSON.stringify({
|
|
@@ -1070,7 +1074,14 @@ export function fileAndSealUnderMode(store, input) {
|
|
|
1070
1074
|
return { ok: true, scope: store.getScope(input.taskId), basis: "mode" };
|
|
1071
1075
|
});
|
|
1072
1076
|
}
|
|
1073
|
-
export function approve(store, taskId, by, now, sawDigest, token, mutation = {}
|
|
1077
|
+
export function approve(store, taskId, by, now, sawDigest, token, mutation = {},
|
|
1078
|
+
/** A fast start (fast-start.ts): Build is this same yes, with its marker in the proof. Only that road passes it. */
|
|
1079
|
+
fastStart,
|
|
1080
|
+
/** A person's own yes in chat (owner's decision, docs/plans/toolroll-cards.md): their tap or signed-in click stands in
|
|
1081
|
+
* for a password, so `token` is unused. Every rule below still applies. The caller has proved the person is the one
|
|
1082
|
+
* tapping. `shown` is what a card that showed its terms whole showed: those words, and the digest kept when it was
|
|
1083
|
+
* made. Null when the card showed less: the seal then opens none of the walls a person's reading opens. */
|
|
1084
|
+
chat) {
|
|
1074
1085
|
// Read and write in one transaction, re-reading inside it — the credential
|
|
1075
1086
|
// check included, so a token rotated between authentication and the write
|
|
1076
1087
|
// cannot leave an approval signed by an authority that no longer exists.
|
|
@@ -1078,7 +1089,14 @@ export function approve(store, taskId, by, now, sawDigest, token, mutation = {})
|
|
|
1078
1089
|
// this approval — which would resurrect the old wording *and* mark it
|
|
1079
1090
|
// agreed, the precise opposite of what the digest is for.
|
|
1080
1091
|
return store.transact(() => {
|
|
1081
|
-
const
|
|
1092
|
+
const repoNow = store.lookupRef(taskId)?.repo ?? null;
|
|
1093
|
+
const account = chat === undefined ? null : store.accountOf(by);
|
|
1094
|
+
// A machine never reaches this road, and the person's role and reach are read here, in the seal's transaction.
|
|
1095
|
+
if (chat !== undefined && machineActing())
|
|
1096
|
+
return { ok: false, reason: "machine-proof" };
|
|
1097
|
+
if (chat !== undefined && (account === null || account.revokedAt !== null || account.role !== "approver" || repoNow === null || !store.accountCanAccess(by, repoNow)))
|
|
1098
|
+
return { ok: false, reason: "not-an-approver" };
|
|
1099
|
+
const authenticated = chat !== undefined ? { ok: true, method: "chat", at: now.getTime() } : authenticateApprover(store, by, token, repoNow);
|
|
1082
1100
|
if (!authenticated.ok)
|
|
1083
1101
|
return authenticated;
|
|
1084
1102
|
// ONLY A PERSON APPROVES (docs/plans/approval-policy.md, phase 1): a lead token or a remote API token proves the
|
|
@@ -1089,7 +1107,7 @@ export function approve(store, taskId, by, now, sawDigest, token, mutation = {})
|
|
|
1089
1107
|
// localApprovalEligible, read again here inside the seal's transaction).
|
|
1090
1108
|
const local = authenticated.method === "local-owner" ? currentLocalOwner() : null;
|
|
1091
1109
|
if (authenticated.method === "local-owner") {
|
|
1092
|
-
if (local === null || local.act !== "approve-plan")
|
|
1110
|
+
if (local === null || (local.act !== "approve-plan" && !(local.act === "build-now" && fastStart !== undefined)))
|
|
1093
1111
|
return { ok: false, reason: "policy", message: localOwnerRefusal(store.accountOf(by)?.passwordSet === true, "/").message };
|
|
1094
1112
|
const eligible = localApprovalEligible(store, by, taskId);
|
|
1095
1113
|
if (!eligible.ok)
|
|
@@ -1106,6 +1124,16 @@ export function approve(store, taskId, by, now, sawDigest, token, mutation = {})
|
|
|
1106
1124
|
// the new one in silence — which is the whole failure this guards.
|
|
1107
1125
|
if (sawDigest !== scope.digest)
|
|
1108
1126
|
return { ok: false, reason: "changed" };
|
|
1127
|
+
// What the card showed when it was made is what this yes is to: other words, or words leaving out a widening, aren't.
|
|
1128
|
+
const shown = chat?.shown ?? null;
|
|
1129
|
+
if (shown !== null && (cardTermsDigest(shown.terms) !== shown.expect || !cardShowsScope(scope, shown.terms)))
|
|
1130
|
+
return { ok: false, reason: "changed" };
|
|
1131
|
+
// The "what changed" line a person read describes this exact route, or this yes isn't to what they read (route-change.ts).
|
|
1132
|
+
if (routeChangeProblem(store, taskId, scope))
|
|
1133
|
+
return { ok: false, reason: "changed" };
|
|
1134
|
+
// A fast start is never a task from outside (github-import.ts): that takes the ordinary ceremony.
|
|
1135
|
+
if (fastStart !== undefined && issueImported(store, taskId))
|
|
1136
|
+
return { ok: false, reason: "policy", message: "A task from outside takes the ordinary approval." };
|
|
1109
1137
|
// An unresolved scope cannot say exactly what would run, so nobody can
|
|
1110
1138
|
// agree to it (foundations finding 16) — restatement is the road.
|
|
1111
1139
|
if (scope.profileState === "unresolved") {
|
|
@@ -1144,7 +1172,7 @@ export function approve(store, taskId, by, now, sawDigest, token, mutation = {})
|
|
|
1144
1172
|
// How the person was proved, beside every yes they give (a vote kept for protected work, or the seal): a password
|
|
1145
1173
|
// typed now, a re-check now, or a remembered one — with when, the credential generation and the exact digest.
|
|
1146
1174
|
const proved = () => store.recordAction({ at: now.toISOString(), actor: by, repo: store.lookupRef(taskId)?.repo ?? null, taskId, runId: null, action: APPROVAL_PROOF,
|
|
1147
|
-
outcome: authenticated.method, source: "work", detail: approvalProofDetail(authenticated.at, store.accountOf(by)?.generation ?? 0, scope.digest, local === null ? undefined : { generation: local.localGeneration, transport: local.transport }) });
|
|
1175
|
+
outcome: authenticated.method, source: "work", detail: (fastStart === undefined ? approvalProofDetail : fastProofDetail(fastStart))(authenticated.at, store.accountOf(by)?.generation ?? 0, scope.digest, local === null ? undefined : { generation: local.localGeneration, transport: local.transport }) });
|
|
1148
1176
|
if (gate.verdict === "vote") {
|
|
1149
1177
|
// The same person again adds nothing: no new vote, no proof row (and the approval roads refund a re-check).
|
|
1150
1178
|
if (gate.already)
|
|
@@ -1164,7 +1192,8 @@ export function approve(store, taskId, by, now, sawDigest, token, mutation = {})
|
|
|
1164
1192
|
// profile — the exact bytes the digest the approver signed was bound
|
|
1165
1193
|
// to. Routing saveScope here would re-run resolution and could sign a
|
|
1166
1194
|
// profile nobody saw.
|
|
1167
|
-
const sealed = store.sealScopeApproval(taskId, by, now, mutation)
|
|
1195
|
+
const sealed = chat === undefined ? store.sealScopeApproval(taskId, by, now, mutation)
|
|
1196
|
+
: store.sealScopeApproval(taskId, by, now, mutation, { kind: "chat", modeDigest: chat.modeDigest, ...(shown === null ? {} : { card: "shown", cardDigest: shown.expect, shownTerms: shown.terms }) }, "person");
|
|
1168
1197
|
const approved = sealed ? store.getScope(taskId) : null;
|
|
1169
1198
|
if (!sealed || approved === null) {
|
|
1170
1199
|
refused.result = { ok: false, reason: sealed ? "no-scope" : "changed" };
|
|
@@ -1185,6 +1214,9 @@ export function approve(store, taskId, by, now, sawDigest, token, mutation = {})
|
|
|
1185
1214
|
export const APPROVAL_PROOF = "approval proved";
|
|
1186
1215
|
/** The proof row's detail: when the person was proved, the generation, and the exact digest they said yes to. */
|
|
1187
1216
|
export const approvalProofDetail = (at, generation, digest, local) => `${new Date(at).toISOString()} · generation ${generation}${local === undefined ? "" : ` · local ${local.generation} ${local.transport}`} · digest ${digest}`;
|
|
1217
|
+
/** A fast start's proof: the v1 record proofDetailOf reads, with the marker. `local` keeps the legacy words
|
|
1218
|
+
* localSealStandsSql reads, so a local owner's seal stands on the same rule. */
|
|
1219
|
+
const fastProofDetail = (fastStart) => (at, generation, digest, local) => JSON.stringify({ v: 1, at: new Date(at).toISOString(), generation, digest, fastStart, ...(local === undefined ? {} : { local: `proof · local ${local.generation} ${local.transport} · seal` }) });
|
|
1188
1220
|
/** The proof of THIS approval: the approver's own row for these exact bytes (never another approval's words). */
|
|
1189
1221
|
export function approvalProofOf(store, taskId, by, digest) {
|
|
1190
1222
|
const rows = store.handle.prepare("SELECT outcome, detail FROM action_ledger WHERE task_id = ? AND action = ? AND actor = ? ORDER BY id DESC LIMIT 50").all(taskId, APPROVAL_PROOF, by);
|
|
@@ -1281,10 +1313,11 @@ export function scopeAuthorityOf(scope, env = {}) {
|
|
|
1281
1313
|
if (profile === null) {
|
|
1282
1314
|
return { ok: false, reason: "profile", problem: profileJson === null ? "the scope carries no agent profile" : "the scope's agent profile cannot be read exactly (unknown keys, an unsafe number, or a clock no timer can hold)" };
|
|
1283
1315
|
}
|
|
1284
|
-
//
|
|
1285
|
-
//
|
|
1286
|
-
|
|
1287
|
-
|
|
1316
|
+
// Fallback agents: a current chain whose base IS this profile. An older
|
|
1317
|
+
// chain (v30-v114) holds no authority: saving the scope again re-files it.
|
|
1318
|
+
const chain = scope.proposedChainJson == null ? null : liveChainFromJson(scope.proposedChainJson);
|
|
1319
|
+
if (scope.proposedChainJson != null && (chain === null || profileDigestOf(chain[0].profile) !== profileDigestOf(profile))) {
|
|
1320
|
+
return { ok: false, reason: "chain", problem: "this scope named fallback agents from an older version, or they can't be read — save the scope again to choose its agents" };
|
|
1288
1321
|
}
|
|
1289
1322
|
const routeJson = scope.proposedRouteJson ?? null;
|
|
1290
1323
|
const route = routeFromJson(routeJson);
|
|
@@ -1294,7 +1327,7 @@ export function scopeAuthorityOf(scope, env = {}) {
|
|
|
1294
1327
|
const parity = routeParityProblem(route, profile, { riskLevel: scope.riskLevel ?? "routine", qualityMode: scope.qualityMode ?? "default" });
|
|
1295
1328
|
if (parity !== null)
|
|
1296
1329
|
return { ok: false, reason: "parity", problem: parity };
|
|
1297
|
-
const digest = digestOf({ goal: scope.goal, outOfScope: scope.outOfScope, touches: scope.touches, budgetMicrousd: scope.budgetMicrousd, acceptance: scope.acceptance, candidate: scope.candidate ?? null, qualityMode: scope.qualityMode ?? "default", releaseTerms: scope.releaseTerms ?? null, releaseCheck: scope.releaseCheck ?? null }, profile, route);
|
|
1330
|
+
const digest = digestOf({ goal: scope.goal, outOfScope: scope.outOfScope, touches: scope.touches, budgetMicrousd: scope.budgetMicrousd, acceptance: scope.acceptance, candidate: scope.candidate ?? null, qualityMode: scope.qualityMode ?? "default", releaseTerms: scope.releaseTerms ?? null, releaseCheck: scope.releaseCheck ?? null }, chain === null ? profile : { chain, version: LIVE_CHAIN_VERSION }, route);
|
|
1298
1331
|
if (digest !== scope.digest) {
|
|
1299
1332
|
return { ok: false, reason: "digest", problem: "the scope's reference does not re-derive from its own terms, agents, and route — file it again" };
|
|
1300
1333
|
}
|
|
@@ -1310,8 +1343,11 @@ export function scopeAuthorityOf(scope, env = {}) {
|
|
|
1310
1343
|
if (!read.ok)
|
|
1311
1344
|
return { ok: false, reason: "auth-mode", problem: read.problem };
|
|
1312
1345
|
authMode = read.mode;
|
|
1346
|
+
// The base was filed under one credential; a yes on another isn't a yes to these terms.
|
|
1347
|
+
if (chain !== null && chain[0].authMode !== authMode)
|
|
1348
|
+
return { ok: false, reason: "auth-mode", problem: "the agent's sign-in changed since this was filed — save the scope again" };
|
|
1313
1349
|
}
|
|
1314
|
-
return { ok: true, profile, route, digest, authMode };
|
|
1350
|
+
return { ok: true, profile, route, digest, authMode, ...(chain === null ? {} : { chain }) };
|
|
1315
1351
|
}
|
|
1316
1352
|
/** The rubric's approval-card lines (v39): one per criterion, the id in
|
|
1317
1353
|
* front (the exact string a proof must answer by) and its required
|
|
@@ -1321,8 +1357,8 @@ export function acceptanceWords(criteria) {
|
|
|
1321
1357
|
if (criteria.length === 0)
|
|
1322
1358
|
return [];
|
|
1323
1359
|
return [
|
|
1324
|
-
`
|
|
1325
|
-
...criteria.slice(1).map(c => ` ${c.id}: ${c.statement} [
|
|
1360
|
+
` done when ${criteria[0].id}: ${criteria[0].statement} [shown by: ${criteria[0].evidence.join(", ")}]`,
|
|
1361
|
+
...criteria.slice(1).map(c => ` ${c.id}: ${c.statement} [shown by: ${c.evidence.join(", ")}]`),
|
|
1326
1362
|
];
|
|
1327
1363
|
}
|
|
1328
1364
|
/** The route's approval-card lines (v47): the WORKING route the digest
|
|
@@ -1340,8 +1376,8 @@ evidence = null) {
|
|
|
1340
1376
|
return [
|
|
1341
1377
|
` goal ${scope.goal}`,
|
|
1342
1378
|
...(scope.outOfScope === null ? [] : [` not this ${scope.outOfScope}`]),
|
|
1343
|
-
...(scope.candidate ? [`
|
|
1344
|
-
...(scope.touches.length === 0 ? [] : [`
|
|
1379
|
+
...(scope.candidate ? [` commit ${scope.candidate.slice(0, 7)} — checked out by Toolroll, no agent runs`] : []),
|
|
1380
|
+
...(scope.touches.length === 0 ? [] : [` files ${scope.touches.join(", ")}`]),
|
|
1345
1381
|
...acceptanceWords(scope.acceptance),
|
|
1346
1382
|
...(scope.budgetMicrousd === null
|
|
1347
1383
|
? []
|
|
@@ -1350,6 +1386,7 @@ evidence = null) {
|
|
|
1350
1386
|
// leg with its reason and readiness — before anyone signs.
|
|
1351
1387
|
...scopeRouteWords(scope, readiness),
|
|
1352
1388
|
...(evidence === null ? [] : [` why model ${evidence.replace(/^Why this model: /, "")}`]),
|
|
1389
|
+
...chainTermsLines(scope).map((line, at) => at === 0 ? ` fallback ${line}` : ` ${line}`),
|
|
1353
1390
|
// What the build may reach (egress.ts), part of what the yes agrees to.
|
|
1354
1391
|
...(scope.profile === null || scope.profile === undefined ? [] : [` network ${networkWords(scope.profile.network ?? null).replace(/^Network: /, "")}`]),
|
|
1355
1392
|
...(scope.profile === null || scope.profile === undefined ? [] : [` writes ${writesWords(scope.profile.writes)}`]),
|
|
@@ -1358,7 +1395,7 @@ evidence = null) {
|
|
|
1358
1395
|
...(scope.profile === null || scope.profile === undefined ? [] : ((terms) => [` reads ${terms.reads}`, ...(terms.mach === null ? [] : [` services ${terms.mach}`])])(sandboxTermsWords(scope.profile))),
|
|
1359
1396
|
...releaseTermsWords(scope.releaseTerms ?? null),
|
|
1360
1397
|
...(scope.releaseCheck == null ? [] : [` release ${releaseCheckSentence(scope.releaseCheck)}`]),
|
|
1361
|
-
`
|
|
1398
|
+
` version ${scope.digest.slice(0, 8)} (the full plan version is in --json)`,
|
|
1362
1399
|
` approved ${describeApproval(approval)}`,
|
|
1363
1400
|
];
|
|
1364
1401
|
}
|
|
@@ -1394,13 +1431,16 @@ function describeApproval(approval) {
|
|
|
1394
1431
|
if (approval.approved)
|
|
1395
1432
|
return `yes, by ${approval.by} at ${approval.at}`;
|
|
1396
1433
|
if (approval.reason === "changed") {
|
|
1397
|
-
return "no —
|
|
1434
|
+
return "no — edited after approval; approve it again";
|
|
1398
1435
|
}
|
|
1399
1436
|
return "no — nothing will build this until somebody agrees to it";
|
|
1400
1437
|
}
|
|
1401
1438
|
// ---- Only a person approves (docs/plans/approval-policy.md, phase 1). Kept last: the freshness tripwire pins places.
|
|
1402
1439
|
export { personProof } from "./person-proof.js";
|
|
1403
1440
|
import { personProof } from "./person-proof.js";
|
|
1441
|
+
import { permissionPlainWords } from "./chat-terms.js";
|
|
1442
|
+
import { limitRecognised } from "./exhaustion.js";
|
|
1443
|
+
import { SUBSCRIPTION_CAPABLE } from "./keys.js";
|
|
1404
1444
|
import { releaseCheckPurposeDigest, releaseTermsDigest } from "./contracts/release-terms.js";
|
|
1405
1445
|
import { CHECK_ACCESS_WIDTH, autoEvidenceOf, carriesUnsandboxed, newestReleaseGeneration, releaseAuthorityOfTask, releaseAutoApproves, releaseSealProblem } from "./release-authority.js";
|
|
1406
1446
|
/** Whether the command running now is a machine's: the lead (its token) or a remote API run. Trusted context only. */
|
|
@@ -1690,3 +1730,27 @@ export function localApprovalEligible(store, by, taskId) {
|
|
|
1690
1730
|
return no("unsandboxed");
|
|
1691
1731
|
return { ok: true };
|
|
1692
1732
|
}
|
|
1733
|
+
// ---- Fallback chains (fallback.ts). Kept last too.
|
|
1734
|
+
/** A chain that may dispatch: version 2, a base and at least one fallback. An older chain is history, never this. */
|
|
1735
|
+
export function liveChainFromJson(json) {
|
|
1736
|
+
const chain = chainFromJson(json, LIVE_CHAIN_VERSION);
|
|
1737
|
+
return chain === null || chain.length < 2 ? null : chain;
|
|
1738
|
+
}
|
|
1739
|
+
// The chain's four providers by name, here rather than provider-auth.ts: this file sits under the contracts, and that one
|
|
1740
|
+
// loads every adapter.
|
|
1741
|
+
const CHAIN_AGENT_NAMES = { claude: "Claude", codex: "Codex", gemini: "Gemini", openrouter: "OpenRouter" };
|
|
1742
|
+
/** The chain in full, the words a yes agrees to: order, agent, model, how it bills, what it may do, and whether Toolroll
|
|
1743
|
+
* can see it run out. Every approval surface shows these lines; empty for a scope with no chain. */
|
|
1744
|
+
export function chainTermsLines(scope) {
|
|
1745
|
+
const chain = liveChainFromJson(scope.proposedChainJson ?? null);
|
|
1746
|
+
if (chain === null)
|
|
1747
|
+
return [];
|
|
1748
|
+
return [
|
|
1749
|
+
"If an agent runs out of its plan, the work moves to the next one here:",
|
|
1750
|
+
...chain.map((entry, at) => {
|
|
1751
|
+
const paid = entry.authMode === "api-key" || SUBSCRIPTION_CAPABLE[entry.profile.provider] !== true;
|
|
1752
|
+
const last = at === chain.length - 1;
|
|
1753
|
+
return `${at + 1}. ${CHAIN_AGENT_NAMES[entry.profile.provider] ?? entry.profile.provider} ${entry.profile.model}, ${paid ? "on an API key, which spends money, and only while the project allows paid fallback" : "its plan"}; ${permissionPlainWords(entry.profile) ?? "agent permissions not known"}. ${last ? "Last in the chain." : limitRecognised(entry.profile.provider, entry.authMode) ? "Moves on when its plan runs out." : "Won't move on: Toolroll can't yet recognise this agent running out."}`;
|
|
1754
|
+
}),
|
|
1755
|
+
];
|
|
1756
|
+
}
|
package/dist/scout.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { ProviderId } from "./contracts/provider.js";
|
|
|
2
2
|
import type { Store } from "./store.js";
|
|
3
3
|
import { type ParsedDecision, type Problem } from "./decision.js";
|
|
4
4
|
import { type ParsedReport, type ReportProblem } from "./scout-report.js";
|
|
5
|
-
import type
|
|
5
|
+
import { type Runner } from "./builder.js";
|
|
6
6
|
import * as browserCheck from "./scout-browser.js";
|
|
7
7
|
/** The scout's own headless browser: the pinned Playwright tool, saving its
|
|
8
8
|
* screenshots straight into the run's image folder. */
|