toolroll 0.5.0
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/LICENSE +21 -0
- package/README.md +1113 -0
- package/THIRD_PARTY_NOTICES.md +449 -0
- package/dist/accent-colors.d.ts +48 -0
- package/dist/accent-colors.js +122 -0
- package/dist/action-ledger.d.ts +26 -0
- package/dist/action-ledger.js +75 -0
- package/dist/agent-fence.d.ts +42 -0
- package/dist/agent-fence.js +183 -0
- package/dist/agentconfig.d.ts +216 -0
- package/dist/agentconfig.js +454 -0
- package/dist/api-tokens.d.ts +16 -0
- package/dist/api-tokens.js +27 -0
- package/dist/approval-policy.d.ts +69 -0
- package/dist/approval-policy.js +137 -0
- package/dist/approval-rules-ui.d.ts +25 -0
- package/dist/approval-rules-ui.js +39 -0
- package/dist/assignment-adapters.d.ts +180 -0
- package/dist/assignment-adapters.js +239 -0
- package/dist/assignment-brief.d.ts +71 -0
- package/dist/assignment-brief.js +129 -0
- package/dist/assignment-delivery.d.ts +56 -0
- package/dist/assignment-delivery.js +160 -0
- package/dist/assignment-presentation.d.ts +26 -0
- package/dist/assignment-presentation.js +80 -0
- package/dist/assignment-status.d.ts +62 -0
- package/dist/assignment-status.js +152 -0
- package/dist/assignment-ui.d.ts +71 -0
- package/dist/assignment-ui.js +103 -0
- package/dist/assignment.d.ts +222 -0
- package/dist/assignment.js +399 -0
- package/dist/attest.d.ts +56 -0
- package/dist/attest.js +153 -0
- package/dist/backend.d.ts +97 -0
- package/dist/backend.js +166 -0
- package/dist/backup-ui.d.ts +22 -0
- package/dist/backup-ui.js +59 -0
- package/dist/backup.d.ts +84 -0
- package/dist/backup.js +421 -0
- package/dist/beads.d.ts +34 -0
- package/dist/beads.js +135 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +22 -0
- package/dist/board.d.ts +150 -0
- package/dist/board.js +210 -0
- package/dist/boot-identity.d.ts +63 -0
- package/dist/boot-identity.js +99 -0
- package/dist/browser/THIRD_PARTY_NOTICES.txt +2295 -0
- package/dist/browser/workspace.css +4 -0
- package/dist/browser/workspace.js +225 -0
- package/dist/browser-crew.d.ts +11 -0
- package/dist/browser-crew.js +50 -0
- package/dist/browser-shell.d.ts +8 -0
- package/dist/browser-shell.js +60 -0
- package/dist/browser-workspace.d.ts +780 -0
- package/dist/browser-workspace.js +47 -0
- package/dist/budget-alerts.d.ts +19 -0
- package/dist/budget-alerts.js +46 -0
- package/dist/builder.d.ts +370 -0
- package/dist/builder.js +3262 -0
- package/dist/capscan.d.ts +35 -0
- package/dist/capscan.js +113 -0
- package/dist/chat-acceptance.d.ts +71 -0
- package/dist/chat-acceptance.js +120 -0
- package/dist/chat-actions.d.ts +265 -0
- package/dist/chat-actions.js +1273 -0
- package/dist/chat-channel.d.ts +109 -0
- package/dist/chat-channel.js +652 -0
- package/dist/chat-continuity.d.ts +32 -0
- package/dist/chat-continuity.js +317 -0
- package/dist/chat-controls.d.ts +113 -0
- package/dist/chat-controls.js +53 -0
- package/dist/chat-delivery-state.d.ts +159 -0
- package/dist/chat-delivery-state.js +309 -0
- package/dist/chat-delivery.d.ts +38 -0
- package/dist/chat-delivery.js +693 -0
- package/dist/chat-display.d.ts +1 -0
- package/dist/chat-display.js +18 -0
- package/dist/chat-evidence.d.ts +123 -0
- package/dist/chat-evidence.js +207 -0
- package/dist/chat-flow.d.ts +44 -0
- package/dist/chat-flow.js +215 -0
- package/dist/chat-inbox.d.ts +57 -0
- package/dist/chat-inbox.js +123 -0
- package/dist/chat-polish.d.ts +16 -0
- package/dist/chat-polish.js +135 -0
- package/dist/chat-review.d.ts +46 -0
- package/dist/chat-review.js +98 -0
- package/dist/chat-rooms.d.ts +123 -0
- package/dist/chat-rooms.js +216 -0
- package/dist/chat-task-actions.d.ts +44 -0
- package/dist/chat-task-actions.js +79 -0
- package/dist/check-progress.d.ts +51 -0
- package/dist/check-progress.js +290 -0
- package/dist/child-database.d.ts +12 -0
- package/dist/child-database.js +30 -0
- package/dist/claim.d.ts +688 -0
- package/dist/claim.js +1740 -0
- package/dist/cli.d.ts +137 -0
- package/dist/cli.js +1461 -0
- package/dist/codex-limits.d.ts +15 -0
- package/dist/codex-limits.js +145 -0
- package/dist/coding-context.d.ts +55 -0
- package/dist/coding-context.js +92 -0
- package/dist/coding-handoff.d.ts +57 -0
- package/dist/coding-handoff.js +277 -0
- package/dist/coding-provider.d.ts +72 -0
- package/dist/coding-provider.js +424 -0
- package/dist/coding-shipping-ui.d.ts +4 -0
- package/dist/coding-shipping-ui.js +12 -0
- package/dist/coding-types.d.ts +62 -0
- package/dist/coding-types.js +1 -0
- package/dist/coding-ui.d.ts +17 -0
- package/dist/coding-ui.js +364 -0
- package/dist/coding-update.d.ts +15 -0
- package/dist/coding-update.js +149 -0
- package/dist/coding-workspace.d.ts +124 -0
- package/dist/coding-workspace.js +768 -0
- package/dist/container-state.d.ts +5 -0
- package/dist/container-state.js +62 -0
- package/dist/containment.d.ts +197 -0
- package/dist/containment.js +559 -0
- package/dist/contest.d.ts +318 -0
- package/dist/contest.js +753 -0
- package/dist/control-setup.d.ts +35 -0
- package/dist/control-setup.js +40 -0
- package/dist/control-ui.d.ts +29 -0
- package/dist/control-ui.js +53 -0
- package/dist/controller-service.d.ts +1 -0
- package/dist/controller-service.js +19 -0
- package/dist/controller-supervisor.d.ts +30 -0
- package/dist/controller-supervisor.js +104 -0
- package/dist/converse.d.ts +304 -0
- package/dist/converse.js +849 -0
- package/dist/coordinator-proposals.d.ts +29 -0
- package/dist/coordinator-proposals.js +137 -0
- package/dist/coordinator.d.ts +186 -0
- package/dist/coordinator.js +447 -0
- package/dist/credentials-ui.d.ts +25 -0
- package/dist/credentials-ui.js +48 -0
- package/dist/daemon.d.ts +209 -0
- package/dist/daemon.js +604 -0
- package/dist/decision.d.ts +120 -0
- package/dist/decision.js +388 -0
- package/dist/demo.d.ts +55 -0
- package/dist/demo.js +992 -0
- package/dist/desktop-access.d.ts +28 -0
- package/dist/desktop-access.js +88 -0
- package/dist/desktop-bundle.d.ts +16 -0
- package/dist/desktop-bundle.js +74 -0
- package/dist/desktop-host.d.ts +54 -0
- package/dist/desktop-host.js +508 -0
- package/dist/desktop-update-gate.d.ts +12 -0
- package/dist/desktop-update-gate.js +91 -0
- package/dist/desktop-update-recovery.d.ts +14 -0
- package/dist/desktop-update-recovery.js +243 -0
- package/dist/desktop-update.d.ts +110 -0
- package/dist/desktop-update.js +740 -0
- package/dist/discord-api.d.ts +20 -0
- package/dist/discord-api.js +138 -0
- package/dist/discord-chat.d.ts +16 -0
- package/dist/discord-chat.js +381 -0
- package/dist/discord-settings.d.ts +7 -0
- package/dist/discord-settings.js +75 -0
- package/dist/discord.d.ts +10 -0
- package/dist/discord.js +169 -0
- package/dist/discover.d.ts +75 -0
- package/dist/discover.js +150 -0
- package/dist/dispatch.d.ts +100 -0
- package/dist/dispatch.js +311 -0
- package/dist/dispose.d.ts +161 -0
- package/dist/dispose.js +803 -0
- package/dist/email-settings.d.ts +34 -0
- package/dist/email-settings.js +50 -0
- package/dist/envelope.d.ts +47 -0
- package/dist/envelope.js +105 -0
- package/dist/evidence-pack.d.ts +185 -0
- package/dist/evidence-pack.js +258 -0
- package/dist/evidence.d.ts +345 -0
- package/dist/evidence.js +782 -0
- package/dist/exec.d.ts +285 -0
- package/dist/exec.js +1841 -0
- package/dist/exhaustion.d.ts +85 -0
- package/dist/exhaustion.js +141 -0
- package/dist/export-ui.d.ts +4 -0
- package/dist/export-ui.js +17 -0
- package/dist/export.d.ts +48 -0
- package/dist/export.js +305 -0
- package/dist/flow-actions.d.ts +77 -0
- package/dist/flow-actions.js +203 -0
- package/dist/flow-code.d.ts +50 -0
- package/dist/flow-code.js +159 -0
- package/dist/flow-draft.d.ts +38 -0
- package/dist/flow-draft.js +80 -0
- package/dist/flow-engine.d.ts +66 -0
- package/dist/flow-engine.js +416 -0
- package/dist/flow-insights.d.ts +95 -0
- package/dist/flow-insights.js +149 -0
- package/dist/flow-live.d.ts +24 -0
- package/dist/flow-live.js +150 -0
- package/dist/flow-people.d.ts +24 -0
- package/dist/flow-people.js +91 -0
- package/dist/flow-replies.d.ts +48 -0
- package/dist/flow-replies.js +113 -0
- package/dist/flow-scripts.d.ts +38 -0
- package/dist/flow-scripts.js +73 -0
- package/dist/flow-secrets.d.ts +13 -0
- package/dist/flow-secrets.js +45 -0
- package/dist/flow-sort.d.ts +82 -0
- package/dist/flow-sort.js +153 -0
- package/dist/flow-steps.d.ts +40 -0
- package/dist/flow-steps.js +408 -0
- package/dist/flow-triggers.d.ts +244 -0
- package/dist/flow-triggers.js +959 -0
- package/dist/flows-ui.d.ts +20 -0
- package/dist/flows-ui.js +175 -0
- package/dist/flows.d.ts +229 -0
- package/dist/flows.js +968 -0
- package/dist/fonts.d.ts +19 -0
- package/dist/fonts.js +19 -0
- package/dist/gaps.d.ts +35 -0
- package/dist/gaps.js +101 -0
- package/dist/gate-failure.d.ts +40 -0
- package/dist/gate-failure.js +66 -0
- package/dist/git.d.ts +54 -0
- package/dist/git.js +94 -0
- package/dist/google-mail.d.ts +58 -0
- package/dist/google-mail.js +151 -0
- package/dist/grant.d.ts +136 -0
- package/dist/grant.js +238 -0
- package/dist/graph.d.ts +164 -0
- package/dist/graph.js +383 -0
- package/dist/guides.d.ts +22 -0
- package/dist/guides.js +363 -0
- package/dist/held.d.ts +150 -0
- package/dist/held.js +799 -0
- package/dist/invoke.d.ts +112 -0
- package/dist/invoke.js +780 -0
- package/dist/issues.d.ts +41 -0
- package/dist/issues.js +131 -0
- package/dist/job-object-helper.ps1 +252 -0
- package/dist/jsonl-discriminants.d.ts +11 -0
- package/dist/jsonl-discriminants.js +93 -0
- package/dist/keys.d.ts +104 -0
- package/dist/keys.js +229 -0
- package/dist/kits-ui.d.ts +17 -0
- package/dist/kits-ui.js +46 -0
- package/dist/kits.d.ts +86 -0
- package/dist/kits.js +215 -0
- package/dist/knowledge-cli.d.ts +101 -0
- package/dist/knowledge-cli.js +92 -0
- package/dist/knowledge-ui.d.ts +24 -0
- package/dist/knowledge-ui.js +40 -0
- package/dist/lead-context.d.ts +8 -0
- package/dist/lead-context.js +56 -0
- package/dist/lead-follow.d.ts +22 -0
- package/dist/lead-follow.js +167 -0
- package/dist/lead-status.d.ts +75 -0
- package/dist/lead-status.js +285 -0
- package/dist/ledger-chain.d.ts +46 -0
- package/dist/ledger-chain.js +187 -0
- package/dist/ledger-csv.d.ts +4 -0
- package/dist/ledger-csv.js +11 -0
- package/dist/ledger-view.d.ts +24 -0
- package/dist/ledger-view.js +58 -0
- package/dist/limits-ui.d.ts +26 -0
- package/dist/limits-ui.js +67 -0
- package/dist/link.d.ts +72 -0
- package/dist/link.js +217 -0
- package/dist/live.d.ts +96 -0
- package/dist/live.js +366 -0
- package/dist/liveness.d.ts +30 -0
- package/dist/liveness.js +42 -0
- package/dist/log.d.ts +7 -0
- package/dist/log.js +25 -0
- package/dist/mailbox.d.ts +57 -0
- package/dist/mailbox.js +150 -0
- package/dist/maintenance.d.ts +11 -0
- package/dist/maintenance.js +35 -0
- package/dist/mate-cli.d.ts +52 -0
- package/dist/mate-cli.js +345 -0
- package/dist/mate-contract.d.ts +10 -0
- package/dist/mate-contract.js +30 -0
- package/dist/mate-doors.d.ts +69 -0
- package/dist/mate-doors.js +548 -0
- package/dist/mate-progress.d.ts +29 -0
- package/dist/mate-progress.js +121 -0
- package/dist/mate-tools.d.ts +94 -0
- package/dist/mate-tools.js +1878 -0
- package/dist/mate.d.ts +92 -0
- package/dist/mate.js +435 -0
- package/dist/mcp-connect.d.ts +103 -0
- package/dist/mcp-connect.js +252 -0
- package/dist/mcp.d.ts +27 -0
- package/dist/mcp.js +651 -0
- package/dist/memory-cli.d.ts +466 -0
- package/dist/memory-cli.js +111 -0
- package/dist/memory-pass.d.ts +160 -0
- package/dist/memory-pass.js +409 -0
- package/dist/metrics.d.ts +3 -0
- package/dist/metrics.js +56 -0
- package/dist/mobile-viewport.d.ts +3 -0
- package/dist/mobile-viewport.js +37 -0
- package/dist/model-catalog.d.ts +118 -0
- package/dist/model-catalog.js +376 -0
- package/dist/models-cli.d.ts +102 -0
- package/dist/models-cli.js +66 -0
- package/dist/models-ui.d.ts +34 -0
- package/dist/models-ui.js +68 -0
- package/dist/modes.d.ts +80 -0
- package/dist/modes.js +182 -0
- package/dist/monitoring-settings.d.ts +44 -0
- package/dist/monitoring-settings.js +173 -0
- package/dist/monitoring-ui.d.ts +20 -0
- package/dist/monitoring-ui.js +51 -0
- package/dist/monitoring.d.ts +34 -0
- package/dist/monitoring.js +221 -0
- package/dist/names.d.ts +67 -0
- package/dist/names.js +102 -0
- package/dist/observations.d.ts +40 -0
- package/dist/observations.js +211 -0
- package/dist/oidc.d.ts +71 -0
- package/dist/oidc.js +182 -0
- package/dist/onboard.d.ts +108 -0
- package/dist/onboard.js +325 -0
- package/dist/openrouter-models.d.ts +30 -0
- package/dist/openrouter-models.js +120 -0
- package/dist/operate.d.ts +118 -0
- package/dist/operate.js +11321 -0
- package/dist/peek-cli.d.ts +97 -0
- package/dist/peek-cli.js +215 -0
- package/dist/peek.d.ts +134 -0
- package/dist/peek.js +578 -0
- package/dist/phase-routing.d.ts +307 -0
- package/dist/phase-routing.js +658 -0
- package/dist/plan-auto.d.ts +14 -0
- package/dist/plan-auto.js +92 -0
- package/dist/plan.d.ts +186 -0
- package/dist/plan.js +401 -0
- package/dist/planner-source.d.ts +251 -0
- package/dist/planner-source.js +460 -0
- package/dist/planner.d.ts +75 -0
- package/dist/planner.js +992 -0
- package/dist/policy-ui.d.ts +20 -0
- package/dist/policy-ui.js +41 -0
- package/dist/policy.d.ts +111 -0
- package/dist/policy.js +231 -0
- package/dist/prepared-evidence.d.ts +16 -0
- package/dist/prepared-evidence.js +96 -0
- package/dist/pricing.d.ts +45 -0
- package/dist/pricing.js +77 -0
- package/dist/principal.d.ts +42 -0
- package/dist/principal.js +82 -0
- package/dist/probe.d.ts +46 -0
- package/dist/probe.js +79 -0
- package/dist/process-custody.d.ts +17 -0
- package/dist/process-custody.js +91 -0
- package/dist/process-liveness.d.ts +10 -0
- package/dist/process-liveness.js +81 -0
- package/dist/process-recovery-anchor.d.ts +75 -0
- package/dist/process-recovery-anchor.js +188 -0
- package/dist/process-recovery-coalition.d.ts +25 -0
- package/dist/process-recovery-coalition.js +105 -0
- package/dist/process-recovery-eligibility.d.ts +178 -0
- package/dist/process-recovery-eligibility.js +170 -0
- package/dist/process-recovery-native.d.ts +172 -0
- package/dist/process-recovery-native.js +592 -0
- package/dist/process-recovery-provenance.d.ts +163 -0
- package/dist/process-recovery-provenance.js +292 -0
- package/dist/process-recovery-services.d.ts +56 -0
- package/dist/process-recovery-services.js +191 -0
- package/dist/process-recovery-settlement.d.ts +32 -0
- package/dist/process-recovery-settlement.js +132 -0
- package/dist/process-recovery.d.ts +41 -0
- package/dist/process-recovery.js +91 -0
- package/dist/process-tree.d.ts +41 -0
- package/dist/process-tree.js +264 -0
- package/dist/project-access.d.ts +14 -0
- package/dist/project-access.js +34 -0
- package/dist/project-cli.d.ts +18 -0
- package/dist/project-cli.js +104 -0
- package/dist/project-delete-ui.d.ts +22 -0
- package/dist/project-delete-ui.js +48 -0
- package/dist/project-delete.d.ts +55 -0
- package/dist/project-delete.js +413 -0
- package/dist/project-knowledge.d.ts +111 -0
- package/dist/project-knowledge.js +241 -0
- package/dist/project-learning.d.ts +100 -0
- package/dist/project-learning.js +438 -0
- package/dist/project-memory.d.ts +81 -0
- package/dist/project-memory.js +264 -0
- package/dist/project-skills.d.ts +152 -0
- package/dist/project-skills.js +660 -0
- package/dist/project-tools.d.ts +219 -0
- package/dist/project-tools.js +796 -0
- package/dist/project.d.ts +61 -0
- package/dist/project.js +125 -0
- package/dist/prompt.d.ts +22 -0
- package/dist/prompt.js +72 -0
- package/dist/proof.d.ts +513 -0
- package/dist/proof.js +1140 -0
- package/dist/proposal.d.ts +82 -0
- package/dist/proposal.js +210 -0
- package/dist/provider-connection.d.ts +22 -0
- package/dist/provider-connection.js +98 -0
- package/dist/provider-limits.d.ts +35 -0
- package/dist/provider-limits.js +95 -0
- package/dist/provider.d.ts +279 -0
- package/dist/provider.js +935 -0
- package/dist/publish.d.ts +107 -0
- package/dist/publish.js +650 -0
- package/dist/pulls.d.ts +118 -0
- package/dist/pulls.js +240 -0
- package/dist/push.d.ts +95 -0
- package/dist/push.js +353 -0
- package/dist/quality.d.ts +8 -0
- package/dist/quality.js +6 -0
- package/dist/recipe-ui.d.ts +18 -0
- package/dist/recipe-ui.js +152 -0
- package/dist/recipes.d.ts +81 -0
- package/dist/recipes.js +332 -0
- package/dist/remote.d.ts +67 -0
- package/dist/remote.js +122 -0
- package/dist/render.d.ts +64 -0
- package/dist/render.js +587 -0
- package/dist/report-summary.d.ts +13 -0
- package/dist/report-summary.js +15 -0
- package/dist/repos.d.ts +65 -0
- package/dist/repos.js +277 -0
- package/dist/repository-context-ui.d.ts +2 -0
- package/dist/repository-context-ui.js +10 -0
- package/dist/repository-context.d.ts +67 -0
- package/dist/repository-context.js +376 -0
- package/dist/restart-certification.d.ts +134 -0
- package/dist/restart-certification.js +226 -0
- package/dist/result-actions.d.ts +28 -0
- package/dist/result-actions.js +270 -0
- package/dist/result-completion.d.ts +18 -0
- package/dist/result-completion.js +66 -0
- package/dist/result-review.d.ts +213 -0
- package/dist/result-review.js +430 -0
- package/dist/retention-ui.d.ts +12 -0
- package/dist/retention-ui.js +39 -0
- package/dist/retention.d.ts +72 -0
- package/dist/retention.js +288 -0
- package/dist/review-context.d.ts +243 -0
- package/dist/review-context.js +1043 -0
- package/dist/review-evidence.d.ts +33 -0
- package/dist/review-evidence.js +56 -0
- package/dist/reviewer.d.ts +97 -0
- package/dist/reviewer.js +202 -0
- package/dist/routine.d.ts +228 -0
- package/dist/routine.js +764 -0
- package/dist/runner.d.ts +266 -0
- package/dist/runner.js +399 -0
- package/dist/scan.d.ts +32 -0
- package/dist/scan.js +92 -0
- package/dist/scope.d.ts +695 -0
- package/dist/scope.js +1176 -0
- package/dist/scout-report.d.ts +42 -0
- package/dist/scout-report.js +108 -0
- package/dist/scout.d.ts +69 -0
- package/dist/scout.js +351 -0
- package/dist/serve.d.ts +313 -0
- package/dist/serve.js +21776 -0
- package/dist/session-brief.d.ts +8 -0
- package/dist/session-brief.js +24 -0
- package/dist/session-cli.d.ts +13 -0
- package/dist/session-cli.js +284 -0
- package/dist/session-contract.d.ts +183 -0
- package/dist/session-contract.js +122 -0
- package/dist/session-http.d.ts +19 -0
- package/dist/session-http.js +154 -0
- package/dist/session-server.d.ts +12 -0
- package/dist/session-server.js +37 -0
- package/dist/session-service.d.ts +22 -0
- package/dist/session-service.js +157 -0
- package/dist/setup-guide.d.ts +39 -0
- package/dist/setup-guide.js +93 -0
- package/dist/sign-in-guard.d.ts +45 -0
- package/dist/sign-in-guard.js +74 -0
- package/dist/skills-ui.d.ts +11 -0
- package/dist/skills-ui.js +58 -0
- package/dist/skills.d.ts +97 -0
- package/dist/skills.js +276 -0
- package/dist/slack-api.d.ts +55 -0
- package/dist/slack-api.js +229 -0
- package/dist/slack-chat.d.ts +28 -0
- package/dist/slack-chat.js +413 -0
- package/dist/slack-settings.d.ts +7 -0
- package/dist/slack-settings.js +75 -0
- package/dist/slack-state.d.ts +13 -0
- package/dist/slack-state.js +9 -0
- package/dist/slack.d.ts +13 -0
- package/dist/slack.js +167 -0
- package/dist/spend-ui.d.ts +28 -0
- package/dist/spend-ui.js +95 -0
- package/dist/spend.d.ts +142 -0
- package/dist/spend.js +313 -0
- package/dist/sqlite-runtime.d.ts +3 -0
- package/dist/sqlite-runtime.js +6 -0
- package/dist/sso-settings.d.ts +33 -0
- package/dist/sso-settings.js +103 -0
- package/dist/sso-ui.d.ts +22 -0
- package/dist/sso-ui.js +39 -0
- package/dist/storage.d.ts +15 -0
- package/dist/storage.js +84 -0
- package/dist/store.d.ts +7754 -0
- package/dist/store.js +23279 -0
- package/dist/structured-output.d.ts +58 -0
- package/dist/structured-output.js +103 -0
- package/dist/style-asset.d.ts +7 -0
- package/dist/style-asset.js +41 -0
- package/dist/subscription-chat.d.ts +46 -0
- package/dist/subscription-chat.js +275 -0
- package/dist/summary.d.ts +33 -0
- package/dist/summary.js +71 -0
- package/dist/supervisor.mjs +297 -0
- package/dist/surface.d.ts +63 -0
- package/dist/surface.js +352 -0
- package/dist/sync.d.ts +94 -0
- package/dist/sync.js +279 -0
- package/dist/task-composer.d.ts +13 -0
- package/dist/task-composer.js +52 -0
- package/dist/task-control.d.ts +165 -0
- package/dist/task-control.js +214 -0
- package/dist/task-outcome-cli.d.ts +10 -0
- package/dist/task-outcome-cli.js +92 -0
- package/dist/task-text.d.ts +30 -0
- package/dist/task-text.js +41 -0
- package/dist/team-cli.d.ts +15 -0
- package/dist/team-cli.js +618 -0
- package/dist/team-contract.d.ts +92 -0
- package/dist/team-contract.js +1 -0
- package/dist/team-http.d.ts +17 -0
- package/dist/team-http.js +144 -0
- package/dist/team-leads.d.ts +81 -0
- package/dist/team-leads.js +565 -0
- package/dist/team-runtime.d.ts +27 -0
- package/dist/team-runtime.js +264 -0
- package/dist/team-ui.d.ts +3 -0
- package/dist/team-ui.js +9 -0
- package/dist/team-updates.d.ts +8 -0
- package/dist/team-updates.js +57 -0
- package/dist/teammate-admin.d.ts +53 -0
- package/dist/teammate-admin.js +111 -0
- package/dist/teammate-desk.d.ts +60 -0
- package/dist/teammate-desk.js +154 -0
- package/dist/teammate-memory.d.ts +64 -0
- package/dist/teammate-memory.js +196 -0
- package/dist/teammate-question.d.ts +59 -0
- package/dist/teammate-question.js +175 -0
- package/dist/teammate-tools.d.ts +108 -0
- package/dist/teammate-tools.js +271 -0
- package/dist/teammate-week.d.ts +68 -0
- package/dist/teammate-week.js +132 -0
- package/dist/teammate-work.d.ts +76 -0
- package/dist/teammate-work.js +280 -0
- package/dist/teammates-ui.d.ts +20 -0
- package/dist/teammates-ui.js +166 -0
- package/dist/teammates.d.ts +180 -0
- package/dist/teammates.js +253 -0
- package/dist/teams-api.d.ts +33 -0
- package/dist/teams-api.js +204 -0
- package/dist/teams-chat.d.ts +26 -0
- package/dist/teams-chat.js +215 -0
- package/dist/teams-settings.d.ts +12 -0
- package/dist/teams-settings.js +57 -0
- package/dist/teams.d.ts +21 -0
- package/dist/teams.js +124 -0
- package/dist/telegram-flow.d.ts +49 -0
- package/dist/telegram-flow.js +110 -0
- package/dist/telegram-mate.d.ts +137 -0
- package/dist/telegram-mate.js +707 -0
- package/dist/telegram-progress.d.ts +14 -0
- package/dist/telegram-progress.js +129 -0
- package/dist/telegram-settings.d.ts +9 -0
- package/dist/telegram-settings.js +36 -0
- package/dist/telegram-status.d.ts +75 -0
- package/dist/telegram-status.js +265 -0
- package/dist/telegram-team.d.ts +85 -0
- package/dist/telegram-team.js +359 -0
- package/dist/telegram.d.ts +230 -0
- package/dist/telegram.js +1931 -0
- package/dist/templates.d.ts +65 -0
- package/dist/templates.js +118 -0
- package/dist/tool-launcher.d.ts +1 -0
- package/dist/tool-launcher.js +101 -0
- package/dist/tools-ui.d.ts +30 -0
- package/dist/tools-ui.js +40 -0
- package/dist/transitions-recipes.d.ts +1 -0
- package/dist/transitions-recipes.js +203 -0
- package/dist/tree-proof.d.ts +34 -0
- package/dist/tree-proof.js +58 -0
- package/dist/verification-evidence.d.ts +44 -0
- package/dist/verification-evidence.js +238 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +10 -0
- package/dist/webhooks.d.ts +91 -0
- package/dist/webhooks.js +283 -0
- package/dist/work-index.d.ts +68 -0
- package/dist/work-index.js +384 -0
- package/dist/work-summary.d.ts +57 -0
- package/dist/work-summary.js +116 -0
- package/dist/workspace-motion.d.ts +4 -0
- package/dist/workspace-motion.js +133 -0
- package/dist/workspace-revision.d.ts +19 -0
- package/dist/workspace-revision.js +92 -0
- package/dist/workspace-ui.d.ts +204 -0
- package/dist/workspace-ui.js +411 -0
- package/dist/worktree-notices.d.ts +13 -0
- package/dist/worktree-notices.js +17 -0
- package/dist/worktree.d.ts +249 -0
- package/dist/worktree.js +720 -0
- package/package.json +122 -0
- package/scripts/canary-assertions.mjs +42 -0
- package/scripts/crash-canary.mjs +253 -0
- package/scripts/fixtures/crash-process.mjs +74 -0
- package/scripts/fixtures/pilot-scenarios.mjs +85 -0
- package/scripts/fixtures/restart-service.mjs +27 -0
- package/scripts/launchd-certification.mjs +143 -0
- package/scripts/pilot.mjs +121 -0
- package/scripts/proof-preflight.mjs +140 -0
- package/scripts/provider-canary.mjs +379 -0
- package/scripts/recovery-canary.mjs +135 -0
- package/scripts/restart-certification.mjs +98 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/** Cheap lead-facing status. Every fact comes from indexed SQLite records;
|
|
2
|
+
* this module never opens a repository, retained evidence, or a live log. */
|
|
3
|
+
import { COMPLETION_ACTION } from "./result-completion.js";
|
|
4
|
+
import { windowLabel } from "./provider-limits.js";
|
|
5
|
+
import { BUILT_IN, checkSuitesOf } from "./store.js";
|
|
6
|
+
const maybeNumber = (value) => value === null || value === undefined || !Number.isInteger(Number(value)) ? null : Number(value);
|
|
7
|
+
const CHECK_COLUMNS = "rc.status AS check_status, rc.exit_code AS check_exit_code, rc.suites_json AS check_suites_json";
|
|
8
|
+
function checkOf(row) {
|
|
9
|
+
const status = row?.["check_status"];
|
|
10
|
+
return {
|
|
11
|
+
status: status === "passed" || status === "failed" || status === "not-run" ? status : "unknown",
|
|
12
|
+
exitCode: maybeNumber(row?.["check_exit_code"]),
|
|
13
|
+
suites: checkSuitesOf(row?.["check_suites_json"]),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function phaseOf(row) {
|
|
17
|
+
const phase = row["phase"] == null ? null : String(row["phase"]);
|
|
18
|
+
if (phase !== null)
|
|
19
|
+
return phase;
|
|
20
|
+
const role = String(row["role"] ?? "builder");
|
|
21
|
+
return role === "planner" ? "planning" : role === "scout" ? "researching" : role === "reviewer" ? "reviewing" : "preparing";
|
|
22
|
+
}
|
|
23
|
+
const phaseWords = (phase) => ({
|
|
24
|
+
"agent-running": "agent working",
|
|
25
|
+
"validating-handoff": "checking handoff",
|
|
26
|
+
"capturing-evidence": "saving result",
|
|
27
|
+
committing: "committing",
|
|
28
|
+
"verifying-proof": "running checks",
|
|
29
|
+
"correcting-proof": "correcting result",
|
|
30
|
+
planning: "planning",
|
|
31
|
+
researching: "researching",
|
|
32
|
+
reviewing: "reviewing",
|
|
33
|
+
preparing: "preparing",
|
|
34
|
+
}[phase] ?? phase.replace(/-/g, " "));
|
|
35
|
+
function latestResult(store, taskRef) {
|
|
36
|
+
return store.handle.prepare(`SELECT run.id, run.outcome, run.reason, run.phase, run.finished_at, ${CHECK_COLUMNS}
|
|
37
|
+
FROM run INDEXED BY work_result LEFT JOIN run_check AS rc ON rc.run = run.id
|
|
38
|
+
WHERE run.task_ref = ? AND run.finished_at IS NOT NULL AND run.role IN ('builder','scout')
|
|
39
|
+
ORDER BY run.id DESC LIMIT 1`).get(taskRef);
|
|
40
|
+
}
|
|
41
|
+
function unfinishedAttempt(store, taskRef) {
|
|
42
|
+
return store.handle.prepare(`SELECT run.id, run.role, run.outcome, run.reason, run.phase, run.finished_at, ${CHECK_COLUMNS}
|
|
43
|
+
FROM run INDEXED BY work_unfinished LEFT JOIN run_check AS rc ON rc.run = run.id
|
|
44
|
+
WHERE run.task_ref = ? AND run.outcome IS NULL ORDER BY run.id DESC LIMIT 1`).get(taskRef);
|
|
45
|
+
}
|
|
46
|
+
function latestAttempt(store, taskRef) {
|
|
47
|
+
return store.handle.prepare(`SELECT run.id, run.role, run.outcome, run.reason, run.phase, run.finished_at, ${CHECK_COLUMNS}
|
|
48
|
+
FROM run INDEXED BY lead_status_task_run LEFT JOIN run_check AS rc ON rc.run = run.id
|
|
49
|
+
WHERE run.task_ref = ? AND run.finished_at IS NOT NULL ORDER BY run.id DESC LIMIT 1`).get(taskRef);
|
|
50
|
+
}
|
|
51
|
+
function newestAttemptSince(store, taskRef, run) {
|
|
52
|
+
return store.handle.prepare(`SELECT run.id, run.role, run.outcome, run.reason, run.phase, run.finished_at, ${CHECK_COLUMNS}
|
|
53
|
+
FROM run INDEXED BY lead_status_task_run LEFT JOIN run_check AS rc ON rc.run = run.id
|
|
54
|
+
WHERE run.task_ref = ? AND run.id >= ? ORDER BY run.id DESC LIMIT 1`).get(taskRef, run);
|
|
55
|
+
}
|
|
56
|
+
/** One indexed observation for `task wait`. `watchedRun` is the attempt the
|
|
57
|
+
* caller first saw. Each poll follows the task's newest attempt from there, so
|
|
58
|
+
* a retry that lands between polls is reported with its own run and outcome,
|
|
59
|
+
* and `replacedRun` says which attempt it replaced. Undefined means no attempt
|
|
60
|
+
* has been observed yet. */
|
|
61
|
+
export function taskWaitSnapshot(store, taskId, now, watchedRun) {
|
|
62
|
+
const task = store.handle.prepare(`SELECT task.id, task.state, ref.id AS task_ref, ref.plan, ref.strikes
|
|
63
|
+
FROM task INDEXED BY sqlite_autoindex_task_1
|
|
64
|
+
JOIN task_ref AS ref INDEXED BY sqlite_autoindex_task_ref_1 ON ref.backend = ? AND ref.external_id = task.id
|
|
65
|
+
WHERE task.id = ?`).get(BUILT_IN, taskId);
|
|
66
|
+
if (task === undefined)
|
|
67
|
+
return null;
|
|
68
|
+
const taskRef = Number(task["task_ref"]);
|
|
69
|
+
const state = String(task["state"]);
|
|
70
|
+
const attempt = watchedRun === undefined ? unfinishedAttempt(store, taskRef) : newestAttemptSince(store, taskRef, watchedRun);
|
|
71
|
+
const followed = maybeNumber(attempt?.["id"]);
|
|
72
|
+
const replacedRun = watchedRun !== undefined && followed !== null && followed !== watchedRun ? watchedRun : null;
|
|
73
|
+
const result = state === "done" ? latestResult(store, taskRef) : undefined;
|
|
74
|
+
const terminalAttempt = state === "failed" || state === "cancelled" ? latestAttempt(store, taskRef) : undefined;
|
|
75
|
+
const shown = attempt ?? result ?? terminalAttempt;
|
|
76
|
+
const run = maybeNumber(shown?.["id"]);
|
|
77
|
+
const check = checkOf(shown);
|
|
78
|
+
const phase = shown === undefined ? null : phaseWords(phaseOf(shown));
|
|
79
|
+
const answer = (outcome, next, terminal, exitCode, reason) => ({ task: taskId, outcome, run, replacedRun, phase, check, next, terminal, exitCode, reason });
|
|
80
|
+
// The followed attempt's own failure is the answer, even if the task's state
|
|
81
|
+
// has already moved on; a task-level success must not stand in for it.
|
|
82
|
+
if (watchedRun !== undefined && attempt !== undefined && attempt["outcome"] !== null && state !== "failed" && state !== "cancelled") {
|
|
83
|
+
const outcome = String(attempt["outcome"]);
|
|
84
|
+
if (outcome === "parked")
|
|
85
|
+
return answer("Needs a person", "Answer the task's question", true, 1, "needs-person");
|
|
86
|
+
if (outcome !== "built" && outcome !== "no-change")
|
|
87
|
+
return answer("Failed", "Inspect attempt", true, 1, "failed");
|
|
88
|
+
if (state !== "done")
|
|
89
|
+
return answer("Ready", "Review result", true, 0, "ready");
|
|
90
|
+
}
|
|
91
|
+
const completion = result === undefined ? undefined : store.handle.prepare(`SELECT 1 AS hit FROM action_ledger INDEXED BY work_completion
|
|
92
|
+
WHERE task_id = ? AND action = ? AND source = 'work' AND run_id = ? LIMIT 1`).get(taskId, COMPLETION_ACTION, Number(result["id"]));
|
|
93
|
+
if (state === "done" && completion !== undefined)
|
|
94
|
+
return answer("Complete", "No action needed", true, 0, "complete");
|
|
95
|
+
if (state === "done")
|
|
96
|
+
return answer("Ready", "Review result", true, 0, "ready");
|
|
97
|
+
if (state === "failed" || state === "cancelled")
|
|
98
|
+
return answer("Failed", state === "cancelled" ? "Review cancellation" : "Inspect failure", true, 1, "failed");
|
|
99
|
+
const decision = store.handle.prepare(`SELECT decision.id FROM run INDEXED BY lead_status_task_run
|
|
100
|
+
JOIN decision INDEXED BY work_open_decision ON decision.run = run.id
|
|
101
|
+
WHERE run.task_ref = ? AND decision.state <> 'answered' AND decision.answered_at IS NULL LIMIT 1`).get(taskRef);
|
|
102
|
+
if (decision !== undefined)
|
|
103
|
+
return answer("Needs a person", "Answer question", true, 1, "needs-person");
|
|
104
|
+
const hold = store.handle.prepare(`SELECT owner_kind FROM hold INDEXED BY hold_by_task
|
|
105
|
+
WHERE task_ref = ? AND (until IS NULL OR until > ?)
|
|
106
|
+
ORDER BY CASE owner_kind WHEN 'decision' THEN 0 WHEN 'incident' THEN 1 WHEN 'operator' THEN 2 WHEN 'revision' THEN 3 WHEN 'stop' THEN 4 WHEN 'contest' THEN 5 ELSE 6 END, id LIMIT 1`)
|
|
107
|
+
.get(taskRef, now.toISOString());
|
|
108
|
+
const holdKind = hold === undefined ? null : String(hold["owner_kind"]);
|
|
109
|
+
if (holdKind !== null && holdKind !== "backoff") {
|
|
110
|
+
const next = holdKind === "operator" ? "Remove hold" : holdKind === "revision" ? "Review plan" : holdKind === "stop" ? "Resume or close attempt" : holdKind === "contest" ? "Choose a result" : "Review task";
|
|
111
|
+
return answer("Needs a person", next, true, 1, "needs-person");
|
|
112
|
+
}
|
|
113
|
+
if (task["plan"] === "drafted")
|
|
114
|
+
return answer("Needs a person", "Review plan", true, 1, "needs-person");
|
|
115
|
+
if (Number(task["strikes"] ?? 0) >= 3)
|
|
116
|
+
return answer("Needs a person", "Review repeated failures", true, 1, "needs-person");
|
|
117
|
+
// An admitted attempt is already the work being watched. Its immutable
|
|
118
|
+
// approval snapshot lives on the run, so a damaged or later-edited current
|
|
119
|
+
// scope must not make `wait` abandon that attempt before it settles.
|
|
120
|
+
if (attempt !== undefined)
|
|
121
|
+
return answer("Running", phase === null ? "Wait for attempt" : `Wait — ${phase}`, false, null, null);
|
|
122
|
+
const scope = store.handle.prepare("SELECT digest, approved_digest, profile_state FROM task_scope WHERE task_id = ?").get(taskId);
|
|
123
|
+
if (scope === undefined)
|
|
124
|
+
return answer("Needs a person", "Add scope", true, 1, "needs-person");
|
|
125
|
+
if (scope["approved_digest"] !== scope["digest"] || scope["profile_state"] !== "resolved")
|
|
126
|
+
return answer("Needs a person", "Approve task", true, 1, "needs-person");
|
|
127
|
+
if (state === "running" && attempt === undefined) {
|
|
128
|
+
const liveClaim = store.handle.prepare(`SELECT 1 AS hit FROM claim INDEXED BY claim_by_task
|
|
129
|
+
WHERE task_ref = ? AND released_at IS NULL AND expires_at > ?
|
|
130
|
+
AND lease_generation = (SELECT MAX(newest.lease_generation) FROM claim AS newest INDEXED BY claim_by_task WHERE newest.task_ref = ?)
|
|
131
|
+
LIMIT 1`).get(taskRef, now.toISOString(), taskRef);
|
|
132
|
+
if (liveClaim !== undefined)
|
|
133
|
+
return answer("Queued", "Wait for attempt to start", false, null, null);
|
|
134
|
+
return answer("Needs a person", "Reconcile unfinished work", true, 1, "needs-person");
|
|
135
|
+
}
|
|
136
|
+
if (holdKind === "backoff")
|
|
137
|
+
return answer("Queued", "Wait for retry", false, null, null);
|
|
138
|
+
return answer("Queued", "Wait for a worker", false, null, null);
|
|
139
|
+
}
|
|
140
|
+
function queueReason(row) {
|
|
141
|
+
const hold = row["hold_kind"] == null ? null : String(row["hold_kind"]);
|
|
142
|
+
if (hold === "decision")
|
|
143
|
+
return "needs a decision";
|
|
144
|
+
if (hold === "incident")
|
|
145
|
+
return "needs attention";
|
|
146
|
+
if (hold === "operator")
|
|
147
|
+
return "on hold";
|
|
148
|
+
if (hold === "revision")
|
|
149
|
+
return "needs plan review";
|
|
150
|
+
if (hold === "stop")
|
|
151
|
+
return "stopped";
|
|
152
|
+
if (hold === "contest")
|
|
153
|
+
return "needs a result choice";
|
|
154
|
+
if (hold === "backoff")
|
|
155
|
+
return "retrying later";
|
|
156
|
+
if (row["plan"] === "drafted")
|
|
157
|
+
return "needs plan review";
|
|
158
|
+
if (Number(row["strikes"] ?? 0) >= 3)
|
|
159
|
+
return "stalled after failures";
|
|
160
|
+
if (Number(row["blocked"] ?? 0) === 1)
|
|
161
|
+
return "waiting for another task";
|
|
162
|
+
if (row["scope_digest"] == null)
|
|
163
|
+
return "needs a scope";
|
|
164
|
+
if (row["approved_digest"] !== row["scope_digest"] || row["profile_state"] !== "resolved")
|
|
165
|
+
return "needs approval";
|
|
166
|
+
return "ready for a worker";
|
|
167
|
+
}
|
|
168
|
+
/** Whole-installation status, deliberately aggregated and bounded. */
|
|
169
|
+
export function installationStatus(store, now) {
|
|
170
|
+
const runningRows = store.handle.prepare(`SELECT run.id, run.role, run.phase, ref.external_id AS task
|
|
171
|
+
FROM run INDEXED BY work_unfinished
|
|
172
|
+
JOIN task_ref AS ref ON ref.id = run.task_ref
|
|
173
|
+
JOIN claim ON claim.lease_id = run.lease_id
|
|
174
|
+
WHERE run.outcome IS NULL AND claim.released_at IS NULL AND claim.expires_at > ?
|
|
175
|
+
AND claim.lease_generation = (SELECT MAX(newest.lease_generation) FROM claim AS newest INDEXED BY claim_by_task WHERE newest.task_ref = run.task_ref)
|
|
176
|
+
ORDER BY run.id`).all(now.toISOString());
|
|
177
|
+
const phases = new Map();
|
|
178
|
+
const runningTasks = runningRows.map(row => {
|
|
179
|
+
const phase = phaseWords(phaseOf(row));
|
|
180
|
+
phases.set(phase, (phases.get(phase) ?? 0) + 1);
|
|
181
|
+
return { task: String(row["task"]), run: Number(row["id"]), phase };
|
|
182
|
+
});
|
|
183
|
+
const queuedRows = store.handle.prepare(`SELECT task.id, ref.plan, ref.strikes, scope.digest AS scope_digest,
|
|
184
|
+
scope.approved_digest, scope.profile_state,
|
|
185
|
+
EXISTS (SELECT 1 FROM task_edge WHERE task_edge.blocked = task.id
|
|
186
|
+
AND EXISTS (SELECT 1 FROM task AS blocker WHERE blocker.id = task_edge.blocker AND blocker.state <> 'done')) AS blocked,
|
|
187
|
+
(SELECT owner_kind FROM hold INDEXED BY hold_by_task WHERE hold.task_ref = ref.id AND (hold.until IS NULL OR hold.until > ?)
|
|
188
|
+
ORDER BY CASE owner_kind WHEN 'decision' THEN 0 WHEN 'incident' THEN 1 WHEN 'operator' THEN 2 ELSE 3 END, id LIMIT 1) AS hold_kind
|
|
189
|
+
FROM task INDEXED BY task_by_state
|
|
190
|
+
JOIN task_ref AS ref INDEXED BY sqlite_autoindex_task_ref_1 ON ref.backend = ? AND ref.external_id = task.id
|
|
191
|
+
LEFT JOIN task_scope AS scope ON scope.task_id = task.id
|
|
192
|
+
WHERE task.state = 'queued'`).all(now.toISOString(), BUILT_IN);
|
|
193
|
+
const reasons = new Map();
|
|
194
|
+
const queuedTasks = [];
|
|
195
|
+
for (const row of queuedRows) {
|
|
196
|
+
const reason = queueReason(row);
|
|
197
|
+
reasons.set(reason, (reasons.get(reason) ?? 0) + 1);
|
|
198
|
+
if (queuedTasks.length < 8)
|
|
199
|
+
queuedTasks.push({ task: String(row["id"]), reason });
|
|
200
|
+
}
|
|
201
|
+
const waitingCount = Number(store.handle.prepare(`SELECT COUNT(*) AS n FROM task INDEXED BY task_by_state
|
|
202
|
+
JOIN task_ref AS ref INDEXED BY sqlite_autoindex_task_ref_1 ON ref.backend = ? AND ref.external_id = task.id
|
|
203
|
+
WHERE task.state = 'done' AND EXISTS (SELECT 1 FROM run INDEXED BY work_result
|
|
204
|
+
WHERE run.task_ref = ref.id AND run.finished_at IS NOT NULL AND run.role IN ('builder','scout'))
|
|
205
|
+
AND NOT EXISTS (SELECT 1 FROM action_ledger INDEXED BY work_completion
|
|
206
|
+
WHERE action_ledger.task_id = task.id AND action_ledger.action = ? AND action_ledger.source = 'work'
|
|
207
|
+
AND action_ledger.run_id = (SELECT id FROM run INDEXED BY work_result WHERE task_ref = ref.id AND finished_at IS NOT NULL AND role IN ('builder','scout') ORDER BY id DESC LIMIT 1))`)
|
|
208
|
+
.get(BUILT_IN, COMPLETION_ACTION)?.["n"] ?? 0);
|
|
209
|
+
const waitingRows = store.handle.prepare(`SELECT task.id AS task, result.id, ${CHECK_COLUMNS}
|
|
210
|
+
FROM task INDEXED BY task_done_recent
|
|
211
|
+
JOIN task_ref AS ref INDEXED BY sqlite_autoindex_task_ref_1 ON ref.backend = ? AND ref.external_id = task.id
|
|
212
|
+
JOIN run AS result ON result.id = (SELECT id FROM run INDEXED BY work_result WHERE task_ref = ref.id AND finished_at IS NOT NULL AND role IN ('builder','scout') ORDER BY id DESC LIMIT 1)
|
|
213
|
+
LEFT JOIN run_check AS rc ON rc.run = result.id
|
|
214
|
+
WHERE task.state = 'done' AND NOT EXISTS (SELECT 1 FROM action_ledger INDEXED BY work_completion
|
|
215
|
+
WHERE action_ledger.task_id = task.id AND action_ledger.action = ? AND action_ledger.source = 'work' AND action_ledger.run_id = result.id)
|
|
216
|
+
ORDER BY task.updated_at DESC, task.id DESC LIMIT 5`).all(BUILT_IN, COMPLETION_ACTION);
|
|
217
|
+
// Run ids grow with time, so the newest finished release check is the
|
|
218
|
+
// first finished row down the partial index.
|
|
219
|
+
const release = store.handle.prepare(`SELECT run.id, run.finished_at, ${CHECK_COLUMNS}, ref.external_id AS task
|
|
220
|
+
FROM run_check AS rc INDEXED BY run_check_release
|
|
221
|
+
JOIN run ON run.id = rc.run
|
|
222
|
+
JOIN task_ref AS ref ON ref.id = run.task_ref
|
|
223
|
+
WHERE rc.release = 1 AND run.finished_at IS NOT NULL
|
|
224
|
+
ORDER BY rc.run DESC LIMIT 1`).get();
|
|
225
|
+
return {
|
|
226
|
+
generatedAt: now.toISOString(),
|
|
227
|
+
running: { count: runningTasks.length, phases: [...phases].map(([phase, count]) => ({ phase, count })), tasks: runningTasks.slice(0, 8) },
|
|
228
|
+
queued: {
|
|
229
|
+
count: queuedRows.length,
|
|
230
|
+
reasons: [...reasons].map(([reason, count]) => ({ reason, count })).sort((a, b) => b.count - a.count || a.reason.localeCompare(b.reason)),
|
|
231
|
+
tasks: queuedTasks,
|
|
232
|
+
},
|
|
233
|
+
waitingForReview: { count: waitingCount, results: waitingRows.map(row => ({ task: String(row["task"]), run: Number(row["id"]), check: checkOf(row) })) },
|
|
234
|
+
releaseCheck: release === undefined ? null : { task: String(release["task"]), run: Number(release["id"]), finishedAt: release["finished_at"] == null ? null : String(release["finished_at"]), check: checkOf(release) },
|
|
235
|
+
planWindows: store.handle.prepare(`SELECT provider, window, used_percent, window_minutes, resets_at, reached, plan, observed_at
|
|
236
|
+
FROM provider_limit INDEXED BY sqlite_autoindex_provider_limit_1 ORDER BY provider, window`).all().map(row => ({
|
|
237
|
+
provider: String(row["provider"]),
|
|
238
|
+
plan: row["plan"] == null ? null : String(row["plan"]),
|
|
239
|
+
window: String(row["window"]),
|
|
240
|
+
usedPercent: Number(row["used_percent"]),
|
|
241
|
+
windowMinutes: row["window_minutes"] == null ? null : Number(row["window_minutes"]),
|
|
242
|
+
resetsAt: row["resets_at"] == null ? null : String(row["resets_at"]),
|
|
243
|
+
reached: Number(row["reached"]) === 1,
|
|
244
|
+
observedAt: String(row["observed_at"]),
|
|
245
|
+
})),
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
export function renderInstallationStatus(status) {
|
|
249
|
+
const lines = [];
|
|
250
|
+
lines.push(status.running.count === 0 ? "Running: none" : `Running: ${status.running.count} — ${status.running.tasks.map(one => `${one.task} (#${one.run}, ${one.phase})`).join(", ")}${status.running.count > status.running.tasks.length ? ", …" : ""}`);
|
|
251
|
+
lines.push(status.queued.count === 0 ? "Queued: none" : `Queued: ${status.queued.count} — ${status.queued.tasks.map(one => `${one.task} (${one.reason})`).join(", ")}${status.queued.count > status.queued.tasks.length ? ", …" : ""}`);
|
|
252
|
+
lines.push(status.waitingForReview.count === 0 ? "Ready for review: none" : `Ready for review: ${status.waitingForReview.count} — ${status.waitingForReview.results.map(one => `${one.task} (#${one.run})`).join(", ")}${status.waitingForReview.count > status.waitingForReview.results.length ? ", …" : ""}`);
|
|
253
|
+
if (status.releaseCheck === null) {
|
|
254
|
+
lines.push("Release check: none recorded");
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
const check = status.releaseCheck.check;
|
|
258
|
+
lines.push(`Release check: ${status.releaseCheck.task} #${status.releaseCheck.run} — ${check.status}${check.exitCode === null ? "" : ` (exit ${check.exitCode})`}`);
|
|
259
|
+
if (check.suites.length > 0) {
|
|
260
|
+
lines.push(` Suites: ${check.suites.map(suite => `${suite.name} ${suite.status}${suite.exitCode === null ? "" : ` (exit ${suite.exitCode})`}`).join("; ")}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (status.planWindows.length === 0) {
|
|
264
|
+
lines.push("Plan windows: none recorded");
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
const providers = new Map();
|
|
268
|
+
for (const window of status.planWindows) {
|
|
269
|
+
const key = `${window.provider}${window.plan === null ? "" : ` ${window.plan}`}`;
|
|
270
|
+
const list = providers.get(key) ?? [];
|
|
271
|
+
list.push(window);
|
|
272
|
+
providers.set(key, list);
|
|
273
|
+
}
|
|
274
|
+
for (const [provider, windows] of providers) {
|
|
275
|
+
const detail = windows.map(window => `${windowLabel(window.window, window.windowMinutes)} ${window.usedPercent}%${window.reached ? " (limit reached)" : ""}`).join(", ");
|
|
276
|
+
lines.push(`Plan windows: ${provider} — ${detail}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return lines.slice(0, 12);
|
|
280
|
+
}
|
|
281
|
+
export function renderTaskWait(snapshot, outcome = snapshot.outcome) {
|
|
282
|
+
const check = snapshot.check.status === "unknown" ? "checks unknown" : snapshot.check.status === "not-run" ? "checks not run" : `checks ${snapshot.check.status}${snapshot.check.exitCode === null ? "" : ` (exit ${snapshot.check.exitCode})`}`;
|
|
283
|
+
const run = snapshot.run === null ? "no run" : snapshot.replacedRun === null ? `run #${snapshot.run}` : `run #${snapshot.run}, a retry that replaced run #${snapshot.replacedRun}`;
|
|
284
|
+
return `${outcome} | ${run} | ${check} | next: ${snapshot.next}`;
|
|
285
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Database } from "./store.js";
|
|
2
|
+
export declare const LEDGER_CHAIN_SCHEMA = "\nCREATE TABLE IF NOT EXISTS ledger_seal (\n id INTEGER PRIMARY KEY,\n prev TEXT NOT NULL,\n hash TEXT NOT NULL\n);\nCREATE TRIGGER IF NOT EXISTS ledger_seal_no_update BEFORE UPDATE ON ledger_seal\nBEGIN SELECT RAISE(ABORT, 'the ledger chain is append-only'); END;\nCREATE TRIGGER IF NOT EXISTS ledger_seal_no_delete BEFORE DELETE ON ledger_seal\nBEGIN SELECT RAISE(ABORT, 'the ledger chain is append-only'); END;\nCREATE TABLE IF NOT EXISTS ledger_checkpoint (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n through INTEGER NOT NULL,\n hash TEXT NOT NULL,\n at TEXT NOT NULL,\n by TEXT NOT NULL\n);\nCREATE TRIGGER IF NOT EXISTS ledger_checkpoint_no_update BEFORE UPDATE ON ledger_checkpoint\nBEGIN SELECT RAISE(ABORT, 'ledger checkpoints are append-only'); END;\nCREATE TRIGGER IF NOT EXISTS ledger_checkpoint_no_delete BEFORE DELETE ON ledger_checkpoint\nBEGIN SELECT RAISE(ABORT, 'ledger checkpoints are append-only'); END;\n";
|
|
3
|
+
/** Where the chain starts: the previous fingerprint of the first entry. */
|
|
4
|
+
export declare const LEDGER_GENESIS: string;
|
|
5
|
+
/** A whole number column as JavaScript can hold it: one past 2^53 (only a hand edit puts one there) reads as its text,
|
|
6
|
+
* which `rowProblem` names, instead of failing the read. */
|
|
7
|
+
export declare const safeWhole: (column: string, as?: string) => string;
|
|
8
|
+
/** An entry's fingerprint: the previous one and the entry's own fields, in a fixed order. */
|
|
9
|
+
export declare function entryHash(prev: string, row: Record<string, unknown>): string;
|
|
10
|
+
export declare const IN_RANGE: string;
|
|
11
|
+
/** Why a row can't be an entry the ledger wrote: each field is exactly the kind of value its writers store. */
|
|
12
|
+
export declare function rowProblem(row: Record<string, unknown>): string | null;
|
|
13
|
+
/** Seal every entry written since the last seal, in id order. Returns how many were sealed. Call inside a write transaction. */
|
|
14
|
+
export declare function sealLedger(db: Database): number;
|
|
15
|
+
export type LedgerChainReport = {
|
|
16
|
+
ok: boolean;
|
|
17
|
+
/** Entries checked, and the last sealed entry's id and fingerprint (the head). */
|
|
18
|
+
entries: number;
|
|
19
|
+
through: number | null;
|
|
20
|
+
head: string;
|
|
21
|
+
/** Entries written after the last seal (sealed on the next pass; not yet part of the proof). */
|
|
22
|
+
unsealed: number;
|
|
23
|
+
checkpoints: number;
|
|
24
|
+
/** The first place the chain breaks, in words. */
|
|
25
|
+
problem: {
|
|
26
|
+
id: number | null;
|
|
27
|
+
what: string;
|
|
28
|
+
} | null;
|
|
29
|
+
/** When the whole chain was last walked from its first entry (later entries were checked as they were added). */
|
|
30
|
+
checkedAt: string | null;
|
|
31
|
+
};
|
|
32
|
+
/** Where a walk may start instead of the first entry: a head this process walked to before. */
|
|
33
|
+
export type VerifiedHead = {
|
|
34
|
+
through: number;
|
|
35
|
+
head: string;
|
|
36
|
+
entries: number;
|
|
37
|
+
};
|
|
38
|
+
/** Walk the chain: every entry sealed, every seal an entry, each fingerprint right and linked to the one before,
|
|
39
|
+
* and every checkpoint still matching. With `from`, the walk starts at a head verified earlier, after checking that
|
|
40
|
+
* the head is still there (a chain rebuilt since doesn't match it). Reads only; call inside one read transaction. */
|
|
41
|
+
export declare function verifyLedgerChain(db: Database, from?: VerifiedHead, checkedAt?: string | null): LedgerChainReport;
|
|
42
|
+
/** Whether the chain still ends at a head someone copied off the machine (`<through>:<hash>`). */
|
|
43
|
+
export declare function matchesOutsideCheckpoint(db: Database, checkpoint: string): {
|
|
44
|
+
ok: boolean;
|
|
45
|
+
what: string;
|
|
46
|
+
};
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v103: the action ledger as a hash chain. Every entry is sealed, in id
|
|
3
|
+
* order, with a fingerprint of its own fields and the previous entry's
|
|
4
|
+
* fingerprint; changing, removing or slipping in an entry breaks the chain
|
|
5
|
+
* from that point, and `verifyLedgerChain` names the first place it breaks.
|
|
6
|
+
*
|
|
7
|
+
* The ledger itself is append-only (its triggers refuse UPDATE and DELETE);
|
|
8
|
+
* the chain is what still shows it when someone with the database file drops
|
|
9
|
+
* those triggers. It has no secret key: someone who can rewrite the file can
|
|
10
|
+
* also drop seals and let the next pass reseal, so what proves history is a
|
|
11
|
+
* checkpoint's head copied off the machine (and, from sprint 6, streamed to
|
|
12
|
+
* the customer's log system), plus a running console noticing that a head it
|
|
13
|
+
* already walked to has changed.
|
|
14
|
+
*
|
|
15
|
+
* Entries are sealed shortly after they're written (by the worker, and
|
|
16
|
+
* before anything reads, verifies or exports the chain), so the triggers
|
|
17
|
+
* that write the ledger stay plain SQL any build can run.
|
|
18
|
+
*/
|
|
19
|
+
import { createHash } from "node:crypto";
|
|
20
|
+
export const LEDGER_CHAIN_SCHEMA = `
|
|
21
|
+
CREATE TABLE IF NOT EXISTS ledger_seal (
|
|
22
|
+
id INTEGER PRIMARY KEY,
|
|
23
|
+
prev TEXT NOT NULL,
|
|
24
|
+
hash TEXT NOT NULL
|
|
25
|
+
);
|
|
26
|
+
CREATE TRIGGER IF NOT EXISTS ledger_seal_no_update BEFORE UPDATE ON ledger_seal
|
|
27
|
+
BEGIN SELECT RAISE(ABORT, 'the ledger chain is append-only'); END;
|
|
28
|
+
CREATE TRIGGER IF NOT EXISTS ledger_seal_no_delete BEFORE DELETE ON ledger_seal
|
|
29
|
+
BEGIN SELECT RAISE(ABORT, 'the ledger chain is append-only'); END;
|
|
30
|
+
CREATE TABLE IF NOT EXISTS ledger_checkpoint (
|
|
31
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
32
|
+
through INTEGER NOT NULL,
|
|
33
|
+
hash TEXT NOT NULL,
|
|
34
|
+
at TEXT NOT NULL,
|
|
35
|
+
by TEXT NOT NULL
|
|
36
|
+
);
|
|
37
|
+
CREATE TRIGGER IF NOT EXISTS ledger_checkpoint_no_update BEFORE UPDATE ON ledger_checkpoint
|
|
38
|
+
BEGIN SELECT RAISE(ABORT, 'ledger checkpoints are append-only'); END;
|
|
39
|
+
CREATE TRIGGER IF NOT EXISTS ledger_checkpoint_no_delete BEFORE DELETE ON ledger_checkpoint
|
|
40
|
+
BEGIN SELECT RAISE(ABORT, 'ledger checkpoints are append-only'); END;
|
|
41
|
+
`;
|
|
42
|
+
/** Where the chain starts: the previous fingerprint of the first entry. */
|
|
43
|
+
export const LEDGER_GENESIS = createHash("sha256").update("standing-orders/action-ledger/v103", "utf8").digest("hex");
|
|
44
|
+
const FIELDS = ["id", "at", "actor", "repo", "task_id", "run_id", "action", "outcome", "source", "detail"];
|
|
45
|
+
/** A whole number column as JavaScript can hold it: one past 2^53 (only a hand edit puts one there) reads as its text,
|
|
46
|
+
* which `rowProblem` names, instead of failing the read. */
|
|
47
|
+
export const safeWhole = (column, as = column) => `CASE WHEN typeof(${column}) = 'integer' AND ${column} NOT BETWEEN -9007199254740991 AND 9007199254740991 THEN CAST(${column} AS TEXT) ELSE ${column} END AS ${as}`;
|
|
48
|
+
/** The fields a seal covers, read safely. */
|
|
49
|
+
const SEALED = FIELDS.map(name => name === "id" || name === "run_id" ? safeWhole(name) : name).join(", ");
|
|
50
|
+
/** An entry's fingerprint: the previous one and the entry's own fields, in a fixed order. */
|
|
51
|
+
export function entryHash(prev, row) {
|
|
52
|
+
const fields = FIELDS.map(name => { const value = row[name]; return value === undefined ? null : typeof value === "bigint" ? Number(value) : value; });
|
|
53
|
+
return createHash("sha256").update(`${prev}\n${JSON.stringify(fields)}`, "utf8").digest("hex");
|
|
54
|
+
}
|
|
55
|
+
/** The largest entry number a seal can name (JavaScript's safe integers); anything outside 1..LARGEST was put there by hand. */
|
|
56
|
+
const LARGEST = Number.MAX_SAFE_INTEGER;
|
|
57
|
+
export const IN_RANGE = `BETWEEN 1 AND ${LARGEST}`;
|
|
58
|
+
/** Outside it, written so SQLite finds such rows by key rather than reading every row. */
|
|
59
|
+
const OUTSIDE = (column) => `${column} < 1 OR ${column} > ${LARGEST}`;
|
|
60
|
+
/** Why a row can't be an entry the ledger wrote: each field is exactly the kind of value its writers store. */
|
|
61
|
+
export function rowProblem(row) {
|
|
62
|
+
const whole = (value) => typeof value === "number" && Number.isSafeInteger(value);
|
|
63
|
+
const text = (value) => typeof value === "string";
|
|
64
|
+
if (!whole(row["id"]))
|
|
65
|
+
return "a number the ledger never writes";
|
|
66
|
+
if (row["run_id"] !== null && !whole(row["run_id"]))
|
|
67
|
+
return "a run number the ledger never writes";
|
|
68
|
+
for (const name of ["at", "actor", "action", "outcome", "source"])
|
|
69
|
+
if (!text(row[name]))
|
|
70
|
+
return `a ${name} the ledger never writes`;
|
|
71
|
+
for (const name of ["repo", "task_id", "detail"])
|
|
72
|
+
if (row[name] !== null && !text(row[name]))
|
|
73
|
+
return `a ${name} the ledger never writes`;
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
/** Seal every entry written since the last seal, in id order. Returns how many were sealed. Call inside a write transaction. */
|
|
77
|
+
export function sealLedger(db) {
|
|
78
|
+
const last = db.prepare(`SELECT id, hash FROM ledger_seal WHERE id ${IN_RANGE} ORDER BY id DESC LIMIT 1`).get();
|
|
79
|
+
let prevId = last === undefined ? 0 : Number(last["id"]);
|
|
80
|
+
let prev = last === undefined ? LEDGER_GENESIS : String(last["hash"]);
|
|
81
|
+
let sealed = 0;
|
|
82
|
+
const insert = db.prepare("INSERT INTO ledger_seal (id, prev, hash) VALUES (?, ?, ?)");
|
|
83
|
+
const next = db.prepare(`SELECT ${SEALED} FROM action_ledger WHERE id > ? AND id <= ${LARGEST} ORDER BY id LIMIT 1000`);
|
|
84
|
+
for (;;) {
|
|
85
|
+
const rows = next.all(prevId);
|
|
86
|
+
if (rows.length === 0)
|
|
87
|
+
return sealed;
|
|
88
|
+
for (const row of rows) {
|
|
89
|
+
const hash = entryHash(prev, row);
|
|
90
|
+
insert.run(Number(row["id"]), prev, hash);
|
|
91
|
+
prev = hash;
|
|
92
|
+
prevId = Number(row["id"]);
|
|
93
|
+
sealed++;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const PAGE = 1000;
|
|
98
|
+
/** Rows of a keyset query (`WHERE id > ? ... ORDER BY id LIMIT n`), a page at a time. */
|
|
99
|
+
function* pages(db, sql, from) {
|
|
100
|
+
const statement = db.prepare(sql);
|
|
101
|
+
let after = from;
|
|
102
|
+
for (;;) {
|
|
103
|
+
const page = statement.all(after);
|
|
104
|
+
yield* page;
|
|
105
|
+
if (page.length < PAGE)
|
|
106
|
+
return;
|
|
107
|
+
after = Number(page[page.length - 1]["id"]);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/** Walk the chain: every entry sealed, every seal an entry, each fingerprint right and linked to the one before,
|
|
111
|
+
* and every checkpoint still matching. With `from`, the walk starts at a head verified earlier, after checking that
|
|
112
|
+
* the head is still there (a chain rebuilt since doesn't match it). Reads only; call inside one read transaction. */
|
|
113
|
+
export function verifyLedgerChain(db, from, checkedAt = null) {
|
|
114
|
+
let prev = from?.head ?? LEDGER_GENESIS, entries = from?.entries ?? 0, through = from?.through ?? null, kept = 0;
|
|
115
|
+
const report = (problem, unsealed = 0) => ({ ok: problem === null, entries, through, head: prev, unsealed, checkpoints: kept, problem, checkedAt });
|
|
116
|
+
// Entries, seals and checkpoints are numbered from 1 up to what a seal can name; anything outside was put there by hand.
|
|
117
|
+
const outside = db.prepare(`SELECT CAST(id AS TEXT) AS id FROM action_ledger WHERE ${OUTSIDE("id")} LIMIT 1`).get();
|
|
118
|
+
if (outside !== undefined)
|
|
119
|
+
return report({ id: null, what: `entry #${String(outside["id"])} was added outside the sealed history` });
|
|
120
|
+
const strayed = db.prepare(`SELECT CAST(id AS TEXT) AS id FROM ledger_seal WHERE ${OUTSIDE("id")} LIMIT 1`).get();
|
|
121
|
+
if (strayed !== undefined)
|
|
122
|
+
return report({ id: null, what: `a seal for entry #${String(strayed["id"])} was added outside the chain` });
|
|
123
|
+
const astray = db.prepare(`SELECT CAST(through AS TEXT) AS through FROM ledger_checkpoint WHERE typeof(through) <> 'integer' OR ${OUTSIDE("through")} LIMIT 1`).get();
|
|
124
|
+
if (astray !== undefined)
|
|
125
|
+
return report({ id: null, what: `a checkpoint names entry #${String(astray["through"])}, outside the chain` });
|
|
126
|
+
const checkpoints = db.prepare("SELECT through, hash FROM ledger_checkpoint ORDER BY id").all()
|
|
127
|
+
.map(one => ({ through: Number(one["through"]), hash: String(one["hash"]) }));
|
|
128
|
+
kept = checkpoints.length;
|
|
129
|
+
if (from !== undefined) {
|
|
130
|
+
const still = db.prepare("SELECT hash FROM ledger_seal WHERE id = ?").get(from.through);
|
|
131
|
+
if (still === undefined || String(still["hash"]) !== from.head)
|
|
132
|
+
return report({ id: from.through, what: `the chain was rewritten since it was last checked (entry #${from.through} changed)` });
|
|
133
|
+
}
|
|
134
|
+
const rows = pages(db, `SELECT ${SEALED} FROM action_ledger WHERE id > ? AND id <= ${LARGEST} ORDER BY id LIMIT ${PAGE}`, from?.through ?? 0);
|
|
135
|
+
const seals = pages(db, `SELECT id, prev, hash FROM ledger_seal WHERE id > ? AND id <= ${LARGEST} ORDER BY id LIMIT ${PAGE}`, from?.through ?? 0);
|
|
136
|
+
const wanted = new Set(checkpoints.map(one => one.through));
|
|
137
|
+
const reached = new Map();
|
|
138
|
+
let row = rows.next();
|
|
139
|
+
for (let next = seals.next(); !next.done; next = seals.next()) {
|
|
140
|
+
const seal = next.value;
|
|
141
|
+
const id = Number(seal["id"]);
|
|
142
|
+
const entry = row.done ? undefined : row.value;
|
|
143
|
+
if (entry === undefined || Number(entry["id"]) > id)
|
|
144
|
+
return report({ id, what: `entry #${id} was removed after it was sealed` });
|
|
145
|
+
if (Number(entry["id"]) < id)
|
|
146
|
+
return report({ id: Number(entry["id"]), what: `entry #${Number(entry["id"])} was added inside the sealed history` });
|
|
147
|
+
const odd = rowProblem(entry);
|
|
148
|
+
if (odd !== null)
|
|
149
|
+
return report({ id, what: `entry #${id} holds ${odd}` });
|
|
150
|
+
if (String(seal["prev"]) !== prev)
|
|
151
|
+
return report({ id, what: `entry #${id} doesn't follow the entry before it` });
|
|
152
|
+
const hash = entryHash(prev, entry);
|
|
153
|
+
if (hash !== String(seal["hash"]))
|
|
154
|
+
return report({ id, what: `entry #${id} was changed after it was sealed` });
|
|
155
|
+
prev = hash;
|
|
156
|
+
through = id;
|
|
157
|
+
if (wanted.has(id))
|
|
158
|
+
reached.set(id, hash);
|
|
159
|
+
entries++;
|
|
160
|
+
row = rows.next();
|
|
161
|
+
}
|
|
162
|
+
// Every checkpoint row, each on its own: a later row for the same entry can't cover for an earlier one.
|
|
163
|
+
const sealedAt = db.prepare("SELECT hash FROM ledger_seal WHERE id = ?");
|
|
164
|
+
for (const checkpoint of checkpoints) {
|
|
165
|
+
const walked = reached.get(checkpoint.through) ?? (from !== undefined && checkpoint.through <= from.through ? sealedAt.get(checkpoint.through)?.["hash"] : undefined);
|
|
166
|
+
if (walked !== checkpoint.hash)
|
|
167
|
+
return report({ id: checkpoint.through, what: `the chain no longer matches the checkpoint at entry #${checkpoint.through}` });
|
|
168
|
+
}
|
|
169
|
+
let unsealed = 0;
|
|
170
|
+
for (; !row.done; row = rows.next())
|
|
171
|
+
unsealed++;
|
|
172
|
+
return report(null, unsealed);
|
|
173
|
+
}
|
|
174
|
+
/** Whether the chain still ends at a head someone copied off the machine (`<through>:<hash>`). */
|
|
175
|
+
export function matchesOutsideCheckpoint(db, checkpoint) {
|
|
176
|
+
const match = /^(\d+):([a-f0-9]{64})$/.exec(checkpoint.trim());
|
|
177
|
+
if (match === null)
|
|
178
|
+
return { ok: false, what: "A checkpoint looks like 1234:<64 hex characters>." };
|
|
179
|
+
if (!Number.isSafeInteger(Number(match[1])))
|
|
180
|
+
return { ok: false, what: "A checkpoint looks like 1234:<64 hex characters>." };
|
|
181
|
+
const seal = db.prepare("SELECT hash FROM ledger_seal WHERE id = ?").get(Number(match[1]));
|
|
182
|
+
if (seal === undefined)
|
|
183
|
+
return { ok: false, what: `The chain has no sealed entry #${match[1]}.` };
|
|
184
|
+
return String(seal["hash"]) === match[2]
|
|
185
|
+
? { ok: true, what: `The chain still matches the checkpoint at entry #${match[1]}.` }
|
|
186
|
+
: { ok: false, what: `The chain does NOT match the checkpoint at entry #${match[1]}: history up to it was rewritten.` };
|
|
187
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Quotes and CR/LF survive a CSV round trip. Prefix spreadsheet formulas
|
|
2
|
+
// with an apostrophe, including formulas hidden behind whitespace.
|
|
3
|
+
export function csvCell(value) {
|
|
4
|
+
const raw = value === null ? "" : String(value);
|
|
5
|
+
const safe = /^[\s\uFEFF]*[=+@-]/u.test(raw) || /^[\t\r\n]/u.test(raw) ? "'" + raw : raw;
|
|
6
|
+
return `"${safe.replaceAll('"', '""')}"`;
|
|
7
|
+
}
|
|
8
|
+
export const LEDGER_CSV_HEADER = "\uFEFF" + ["ID", "Time (UTC)", "Actor", "Project", "Task", "Run", "Action", "Outcome", "Source", "Detail"].map(csvCell).join(",") + "\r\n";
|
|
9
|
+
export function ledgerCsvRows(rows) {
|
|
10
|
+
return rows.map(row => [row.id, row.at, row.actor, row.repo, row.taskId, row.runId, row.action, row.outcome, row.source, row.detail].map(csvCell).join(",") + "\r\n").join("");
|
|
11
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { LedgerEntry } from "./action-ledger.js";
|
|
2
|
+
/** v103: the chain's state in one line, and (for an instance operator) a checkpoint to copy off the machine. */
|
|
3
|
+
export type LedgerChainView = {
|
|
4
|
+
ok: boolean;
|
|
5
|
+
entries: number;
|
|
6
|
+
through: number | null;
|
|
7
|
+
head: string;
|
|
8
|
+
unsealed: number;
|
|
9
|
+
problem: {
|
|
10
|
+
id: number | null;
|
|
11
|
+
what: string;
|
|
12
|
+
} | null;
|
|
13
|
+
/** When the whole chain was last walked. */
|
|
14
|
+
checkedAt: string | null;
|
|
15
|
+
latest: {
|
|
16
|
+
through: number;
|
|
17
|
+
hash: string;
|
|
18
|
+
at: string;
|
|
19
|
+
} | null;
|
|
20
|
+
csrf: string | null;
|
|
21
|
+
/** Today (UTC, YYYY-MM-DD): the audit export's default range ends here. */
|
|
22
|
+
today: string;
|
|
23
|
+
};
|
|
24
|
+
export declare function ledgerBody(rows: LedgerEntry[], projects: readonly string[], params: URLSearchParams, chain?: LedgerChainView): string;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { projectName } from "./project.js";
|
|
2
|
+
const html = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
3
|
+
/** The ledger's kinds of event, as the filter names them. */
|
|
4
|
+
const EVENT_KINDS = [["work", "Work"], ["request", "Requests"], ["access", "Access"], ["sign-in", "Sign-ins"], ["policy", "Policy changes"]];
|
|
5
|
+
function chainHtml(chain) {
|
|
6
|
+
const html = (value) => String(value ?? "").replace(/[&<>"']/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
7
|
+
const status = chain.ok
|
|
8
|
+
? `<p class="ledger-chain ok" data-ledger-chain="ok"><strong>Chain verified</strong> · ${chain.entries} entries through #${chain.through ?? 0} · head <code>${html(chain.head.slice(0, 16))}…</code>${chain.checkedAt === null ? "" : ` · checked in full ${html(chain.checkedAt.slice(11, 16))} UTC`}</p>`
|
|
9
|
+
: `<p class="ledger-chain problem" role="alert" data-ledger-chain="broken"><strong>Chain broken</strong> · ${html(chain.problem?.what ?? "the chain doesn't verify")}</p>`;
|
|
10
|
+
const latest = chain.latest === null ? "" : `<p class="meta ledger-checkpoint">Latest checkpoint (${html(chain.latest.at.slice(0, 16).replace("T", " "))} UTC), copy it somewhere outside this machine: <code class="ledger-checkpoint-value">${chain.latest.through}:${html(chain.latest.hash)}</code></p>`;
|
|
11
|
+
const make = chain.csrf === null ? "" : `<form method="post" action="/ledger/checkpoint" class="ledger-checkpoint-form"><input type="hidden" name="csrf" value="${html(chain.csrf)}"><button type="submit">Make a checkpoint</button></form>`;
|
|
12
|
+
const start = new Date(Date.parse(`${chain.today}T00:00:00Z`) - 29 * 86_400_000).toISOString().slice(0, 10);
|
|
13
|
+
const exportForm = `<details class="ledger-filter-panel"><summary>Audit export</summary><form method="get" action="/ledger/export" class="ledger-filters ledger-export" aria-label="Audit export">` +
|
|
14
|
+
`<label>From<input type="date" name="from" value="${html(start)}" required></label><label>To<input type="date" name="to" value="${html(chain.today)}" required></label>` +
|
|
15
|
+
`<button type="submit">Download</button></form><p class="meta">Every entry in the range with its seal, and an evidence pack for each task it names. JSON.</p></details>`;
|
|
16
|
+
return `<section class="ledger-chain-panel" aria-label="Ledger integrity">${status}${latest}${make}${exportForm}</section>`;
|
|
17
|
+
}
|
|
18
|
+
export function ledgerBody(rows, projects, params, chain) {
|
|
19
|
+
const selected = (key, value) => params.get(key) === value ? " selected" : "";
|
|
20
|
+
const more = rows.length > 50;
|
|
21
|
+
const shown = rows.slice(0, 50);
|
|
22
|
+
const fresh = new URLSearchParams();
|
|
23
|
+
for (const key of ["project", "actor", "source", "outcome", "task"]) {
|
|
24
|
+
const value = params.get(key);
|
|
25
|
+
if (value)
|
|
26
|
+
fresh.set(key, value);
|
|
27
|
+
}
|
|
28
|
+
const next = new URLSearchParams(fresh);
|
|
29
|
+
if (shown.length)
|
|
30
|
+
next.set("before", String(shown.at(-1).id));
|
|
31
|
+
const csv = new URLSearchParams(fresh);
|
|
32
|
+
csv.set("format", "csv");
|
|
33
|
+
return `<style>
|
|
34
|
+
.ledger-heading{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}.ledger-heading h1{margin:0}.ledger-caption{margin:8px 0 12px}
|
|
35
|
+
.ledger-filter-panel{margin-bottom:12px}.ledger-filter-panel summary{font-size:13px;padding:6px 0;cursor:pointer}.ledger-filters{display:grid;grid-template-columns:repeat(5,minmax(0,1fr)) auto auto;gap:8px;align-items:end;margin:0 0 12px}.ledger-filters label{display:grid;gap:3px;min-width:0;font-size:12px}.ledger-filters input,.ledger-filters select{width:100%;min-width:0;box-sizing:border-box;margin:0;padding:6px 8px;font-size:13px}.ledger-filters button{padding:7px 12px}.ledger-filters>a{padding:8px 0;font-size:13px}
|
|
36
|
+
.ledger-detail{display:block;color:var(--muted-foreground);font-size:12px}.ledger-list{border:1px solid var(--border);border-radius:8px;overflow:hidden}.ledger-event{display:grid;grid-template-columns:140px minmax(90px,1fr) minmax(160px,2fr) minmax(100px,1fr) minmax(100px,1fr);gap:8px;align-items:baseline;padding:7px 10px;border-bottom:1px solid var(--border);font-size:13px;line-height:1.4;overflow-wrap:anywhere}.ledger-event:last-child{border-bottom:0}.ledger-event .actor{color:inherit;font-weight:600;text-decoration:none}.ledger-time,.ledger-place,.ledger-result small{font-size:11px;color:var(--muted-foreground)}.ledger-time time{font-variant-numeric:tabular-nums}.ledger-place a{display:block}.ledger-result small{display:block}.ledger-labels{font-size:11px;color:var(--muted-foreground);font-weight:600}.ledger-empty{padding:12px}
|
|
37
|
+
@media(max-width:1000px){.ledger-filters{grid-template-columns:repeat(3,minmax(0,1fr)) auto}.ledger-event{grid-template-columns:122px minmax(80px,1fr) minmax(130px,2fr) minmax(85px,1fr)}.ledger-place{grid-column:2/4}.ledger-result{grid-column:4;grid-row:1/3}.ledger-labels{display:none}}
|
|
38
|
+
@media(max-width:600px){.ledger-filters{grid-template-columns:repeat(2,minmax(0,1fr))}.ledger-event{grid-template-columns:minmax(0,1fr) auto;gap:2px 8px;padding:8px 10px}.ledger-actor{grid-column:1;grid-row:1}.ledger-action{grid-column:1;grid-row:2}.ledger-result{grid-column:2;grid-row:1/3;text-align:right}.ledger-time{grid-column:1;grid-row:3}.ledger-place{grid-column:2;grid-row:3;text-align:right}.ledger-place a{display:inline}.ledger-heading>a{font-size:13px}}
|
|
39
|
+
.ledger-chain-panel{margin:0 0 12px}.ledger-chain{margin:0 0 4px;font-size:13px}.ledger-chain.ok strong{color:var(--success)}.ledger-checkpoint code{overflow-wrap:anywhere;user-select:all}.ledger-checkpoint-form{margin:6px 0 0}.ledger-export{grid-template-columns:repeat(2,minmax(0,180px)) auto;margin-bottom:4px}
|
|
40
|
+
</style><div class="ledger-heading"><h1>Action ledger</h1><a href="/ledger?${html(csv.toString())}" download>Export CSV</a></div>${chain === undefined ? "" : chainHtml(chain)}
|
|
41
|
+
<p class="meta ledger-caption">${shown.length} actions on this page · CSV includes all matching actions · Times in UTC</p>
|
|
42
|
+
<details class="ledger-filter-panel"><summary>Filters · ${fresh.size === 0 ? "all actions" : `${fresh.size} active`}</summary>
|
|
43
|
+
<form method="get" action="/ledger" class="ledger-filters" aria-label="Filter action ledger">
|
|
44
|
+
<label>Project<select name="project"><option value="">All accessible projects</option>${projects.map(repo => `<option value="${html(repo)}"${selected("project", repo)}>${html(projectName(repo))}</option>`).join("")}</select></label>
|
|
45
|
+
<label>Person or worker<input type="text" name="actor" value="${html(params.get("actor") ?? "")}" placeholder="Any actor"></label>
|
|
46
|
+
<label>Events<select name="source"><option value="">All events</option>${EVENT_KINDS.map(([value, label]) => `<option value="${value}"${selected("source", value)}>${label}</option>`).join("")}</select></label>
|
|
47
|
+
<label>Outcome<input type="text" name="outcome" value="${html(params.get("outcome") ?? "")}" placeholder="Any outcome"></label>
|
|
48
|
+
<label>Task<input type="text" name="task" value="${html(params.get("task") ?? "")}" placeholder="Any task"></label>
|
|
49
|
+
<button type="submit">Filter</button><a href="/ledger">Clear</a></form></details>
|
|
50
|
+
<div class="ledger-list" aria-label="Action history"><div class="ledger-event ledger-labels" aria-hidden="true"><span>Time (UTC)</span><span>Actor</span><span>Action</span><span>Project / work</span><span>Outcome / event</span></div>${shown.length === 0 ? `<p class="ledger-empty">No actions match these filters yet.</p>` : shown.map(row => {
|
|
51
|
+
const subject = row.runId !== null ? `<a href="/r/${row.runId}">Run #${row.runId}</a>` : row.taskId !== null ? `<a href="/t/${encodeURIComponent(row.taskId)}">${html(row.taskId)}</a>` : "";
|
|
52
|
+
const byActor = new URLSearchParams(fresh);
|
|
53
|
+
byActor.set("actor", row.actor);
|
|
54
|
+
return `<article class="ledger-event" data-ledger-id="${row.id}"><div class="ledger-time"><time datetime="${html(row.at)}" title="${html(row.at)}">${html(row.at.slice(0, 19).replace("T", " "))}</time></div><div class="ledger-actor"><a class="actor" href="/ledger?${html(byActor.toString())}" title="Filter by this actor">${html(row.actor)}</a></div><div class="ledger-action">${html(row.action)}${row.detail === null ? "" : `<small class="ledger-detail">${html(row.detail)}</small>`}</div><div class="ledger-place">${row.repo === null ? "Instance" : html(projectName(row.repo))} ${subject}</div><div class="ledger-result">${html(row.outcome)}<small>${html(row.source)}</small></div></article>`;
|
|
55
|
+
}).join("")}</div>
|
|
56
|
+
<p class="row"><a href="/ledger?${html(fresh.toString())}">Newest actions</a>${more ? `<a rel="next" href="/ledger?${html(next.toString())}">Older actions</a>` : ""}</p>
|
|
57
|
+
<p class="meta">Accepted requests record acceptance; worker events record results. History starts from this upgrade. Review evidence is on each task or run.</p>`;
|
|
58
|
+
}
|