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,565 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { scanForSecrets } from './evidence.js';
|
|
3
|
+
import { ceilingDigestOf } from './principal.js';
|
|
4
|
+
import { normalizeProjectAccess, projectAccessAllows, readProjectAccess } from './project-access.js';
|
|
5
|
+
class TeamRefusal extends Error {
|
|
6
|
+
code;
|
|
7
|
+
constructor(code, message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.code = code;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function refuse(code, message) { throw new TeamRefusal(code, message); }
|
|
13
|
+
function str(value, label, max = 256) {
|
|
14
|
+
if (typeof value !== 'string' || value.trim().length === 0 || value.length > max || /[\x00]/.test(value))
|
|
15
|
+
refuse('invalid', `${label} is required (up to ${max} characters).`);
|
|
16
|
+
return value.trim();
|
|
17
|
+
}
|
|
18
|
+
function messageText(value) {
|
|
19
|
+
const text = str(value, 'Message', 2000);
|
|
20
|
+
if (scanForSecrets(text).length > 0)
|
|
21
|
+
refuse('secret-in-message', 'Remove credentials from the message before sending it.');
|
|
22
|
+
return text;
|
|
23
|
+
}
|
|
24
|
+
function integer(value, label = 'Revision') {
|
|
25
|
+
if (!Number.isSafeInteger(value) || Number(value) < 0)
|
|
26
|
+
refuse('invalid', `${label} must be a nonnegative integer.`);
|
|
27
|
+
return Number(value);
|
|
28
|
+
}
|
|
29
|
+
function rank(role) { return role === 'manager' ? 2 : role === 'contributor' ? 1 : 0; }
|
|
30
|
+
function role(value) {
|
|
31
|
+
if (value !== 'viewer' && value !== 'contributor' && value !== 'manager')
|
|
32
|
+
refuse('invalid', 'Choose Viewer, Contributor or Manager.');
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
function hash(value) { return createHash('sha256').update(JSON.stringify(value)).digest('hex'); }
|
|
36
|
+
function stable(value) {
|
|
37
|
+
if (Array.isArray(value))
|
|
38
|
+
return value.map(stable);
|
|
39
|
+
if (value !== null && typeof value === 'object')
|
|
40
|
+
return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => [k, stable(v)]));
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
/** One central database owns audiences and queued turns. No model or provider calls here. */
|
|
44
|
+
export class TeamLeads {
|
|
45
|
+
store;
|
|
46
|
+
enrolled;
|
|
47
|
+
constructor(store, enrolled) {
|
|
48
|
+
this.store = store;
|
|
49
|
+
this.enrolled = enrolled;
|
|
50
|
+
}
|
|
51
|
+
get db() { return this.store.handle; }
|
|
52
|
+
account(actor) {
|
|
53
|
+
const account = this.store.accountOf(actor.name);
|
|
54
|
+
if (account === null || account.revokedAt !== null || account.generation !== actor.generation)
|
|
55
|
+
refuse('signed-out', 'Sign in again to use this conversation.');
|
|
56
|
+
return account;
|
|
57
|
+
}
|
|
58
|
+
leadRow(id) {
|
|
59
|
+
const row = this.db.prepare('SELECT * FROM team_lead WHERE id=?').get(id);
|
|
60
|
+
if (!row)
|
|
61
|
+
refuse('not-found', 'Lead not available.');
|
|
62
|
+
return row;
|
|
63
|
+
}
|
|
64
|
+
leadProjects(id) { return this.db.prepare('SELECT project FROM team_lead_project WHERE lead=? ORDER BY project').all(id).map(r => String(r['project'])); }
|
|
65
|
+
leadOf(row) {
|
|
66
|
+
return { id: String(row['id']), name: String(row['name']), instructions: String(row['instructions']), projects: this.leadProjects(String(row['id'])), revision: Number(row['revision']), status: row['status'] === 'paused' ? 'paused' : 'active', createdBy: String(row['created_by']) };
|
|
67
|
+
}
|
|
68
|
+
leadAccess(actor, id, required = 'viewer') {
|
|
69
|
+
const account = this.account(actor), lead = this.leadOf(this.leadRow(id));
|
|
70
|
+
const member = this.db.prepare('SELECT * FROM team_lead_member WHERE lead=? AND account=? AND active=1').get(id, actor.name);
|
|
71
|
+
if (!member || rank(member['role']) < rank(required))
|
|
72
|
+
refuse('forbidden', 'You do not have access to this lead.');
|
|
73
|
+
if (lead.projects.some(p => !projectAccessAllows(account.projects, p) || !this.enrolled().includes(p)))
|
|
74
|
+
refuse('forbidden', 'Managing this lead requires access to all of its projects.');
|
|
75
|
+
if (required !== 'viewer' && account.role !== 'approver')
|
|
76
|
+
refuse('read-only', 'Your account can read shared conversations. An operator account is needed to send requests.');
|
|
77
|
+
return { lead, role: String(member['role']), member };
|
|
78
|
+
}
|
|
79
|
+
projects(actor, value, allowed) {
|
|
80
|
+
let projects;
|
|
81
|
+
try {
|
|
82
|
+
projects = normalizeProjectAccess(value);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
refuse('invalid', 'Choose valid project paths.');
|
|
86
|
+
}
|
|
87
|
+
if (projects === null || projects.length === 0)
|
|
88
|
+
refuse('invalid', 'Choose at least one project.');
|
|
89
|
+
const current = this.account(actor), enrolled = this.enrolled();
|
|
90
|
+
if (projects.some(p => !enrolled.includes(p) || !projectAccessAllows(current.projects, p) || (allowed !== undefined && !allowed.includes(p))))
|
|
91
|
+
refuse('forbidden', 'Choose projects you can access within this lead.');
|
|
92
|
+
return projects;
|
|
93
|
+
}
|
|
94
|
+
conversationOf(row, actor) {
|
|
95
|
+
const followRow = this.db.prepare('SELECT * FROM team_follow WHERE conversation=? AND account=? AND generation=? AND enabled=1').get(String(row['id']), actor.name, actor.generation);
|
|
96
|
+
const revisions = this.memberRevisions(String(row['id']), actor.name);
|
|
97
|
+
const follow = followRow !== undefined && revisions !== null && revisions.lead === Number(followRow['lead_member_revision']) && revisions.participant === Number(followRow['participant_revision']);
|
|
98
|
+
return { id: String(row['id']), leadId: String(row['lead']), title: String(row['title']), visibility: row['visibility'] === 'team' ? 'team' : 'private', projects: [...(readProjectAccess(row['projects_json']) ?? [])], revision: Number(row['revision']), threadId: Number(row['thread']), createdBy: String(row['created_by']), follow };
|
|
99
|
+
}
|
|
100
|
+
access(actor, conversationId, required = 'viewer') {
|
|
101
|
+
const account = this.account(actor);
|
|
102
|
+
const row = this.db.prepare('SELECT * FROM team_conversation WHERE id=?').get(conversationId);
|
|
103
|
+
if (!row || (row['visibility'] === 'private' && row['created_by'] !== actor.name))
|
|
104
|
+
refuse('not-found', 'Conversation not available.');
|
|
105
|
+
const { lead, role: leadRole } = this.leadAccess(actor, String(row['lead']), required);
|
|
106
|
+
const member = this.db.prepare('SELECT * FROM team_participant WHERE conversation=? AND account=? AND active=1').get(conversationId, actor.name);
|
|
107
|
+
const conversation = this.conversationOf(row, actor);
|
|
108
|
+
if (!member || rank(member['role']) < rank(required) || conversation.projects.length === 0 || conversation.projects.some(p => !lead.projects.includes(p) || !this.enrolled().includes(p) || !projectAccessAllows(account.projects, p)))
|
|
109
|
+
refuse('forbidden', 'You no longer have access to this conversation.');
|
|
110
|
+
return { conversation, lead, role: rank(leadRole) < rank(member['role']) ? leadRole : String(member['role']) };
|
|
111
|
+
}
|
|
112
|
+
event(lead, conversation, kind, actor, now) {
|
|
113
|
+
this.db.prepare('INSERT INTO team_event(lead,conversation,kind,actor,created_at) VALUES(?,?,?,?,?)').run(lead, conversation, kind, actor, now.toISOString());
|
|
114
|
+
}
|
|
115
|
+
/** Permission recheck and a small indexed cursor query; it never rebuilds chat history. */
|
|
116
|
+
cursor(actor, conversationId) {
|
|
117
|
+
try {
|
|
118
|
+
this.account(actor);
|
|
119
|
+
if (conversationId) {
|
|
120
|
+
const { lead } = this.access(actor, conversationId);
|
|
121
|
+
return Number(this.db.prepare('SELECT COALESCE(MAX(id),0) AS n FROM team_event WHERE (conversation=? OR (conversation IS NULL AND lead=?))').get(conversationId, lead.id)?.['n'] ?? 0);
|
|
122
|
+
}
|
|
123
|
+
return Number(this.db.prepare(`SELECT COALESCE(MAX(e.id),0) AS n FROM team_event e JOIN team_lead_member m ON m.lead=e.lead AND m.account=? AND m.active=1
|
|
124
|
+
WHERE e.conversation IS NULL OR EXISTS(SELECT 1 FROM team_participant p JOIN team_conversation c ON c.id=p.conversation WHERE p.conversation=e.conversation AND p.account=? AND p.active=1 AND (c.visibility='team' OR c.created_by=?))`).get(actor.name, actor.name, actor.name)?.['n'] ?? 0);
|
|
125
|
+
}
|
|
126
|
+
catch (e) {
|
|
127
|
+
if (e instanceof TeamRefusal)
|
|
128
|
+
return null;
|
|
129
|
+
throw e;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
snapshot(actor, conversationId, leadId) {
|
|
133
|
+
const account = this.account(actor);
|
|
134
|
+
const leads = this.db.prepare('SELECT l.* FROM team_lead l JOIN team_lead_member m ON m.lead=l.id WHERE m.account=? AND m.active=1 ORDER BY l.created_at,l.id').all(actor.name).map(r => this.leadOf(r)).filter(l => l.projects.every(p => projectAccessAllows(account.projects, p) && this.enrolled().includes(p)));
|
|
135
|
+
const conversations = [];
|
|
136
|
+
for (const r of this.db.prepare(`SELECT c.* FROM team_conversation c JOIN team_participant p ON p.conversation=c.id WHERE p.account=? AND p.active=1 ORDER BY c.created_at DESC,c.id`).all(actor.name)) {
|
|
137
|
+
try {
|
|
138
|
+
const a = this.access(actor, String(r['id']));
|
|
139
|
+
if (!leadId || a.lead.id === leadId)
|
|
140
|
+
conversations.push(a.conversation);
|
|
141
|
+
}
|
|
142
|
+
catch (e) {
|
|
143
|
+
if (!(e instanceof TeamRefusal))
|
|
144
|
+
throw e;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const selected = conversationId ? this.access(actor, conversationId).conversation : conversations[0] ?? null;
|
|
148
|
+
const access = selected ? this.access(actor, selected.id) : null;
|
|
149
|
+
const messages = selected ? this.db.prepare(`SELECT * FROM (SELECT m.*,q.author,q.request_id,q.status,q.revision AS message_revision,q.error FROM mate_message m LEFT JOIN team_message q ON q.message=m.id WHERE m.thread=? ORDER BY m.id DESC LIMIT 201) ORDER BY id`).all(selected.threadId).map(r => ({ id: Number(r['id']), author: r['role'] === 'assistant' ? access.lead.name : String(r['author'] ?? selected.createdBy), role: String(r['role']), text: String(r['text']), status: (r['status'] ?? 'answered'), revision: Number(r['message_revision'] ?? 1), createdAt: String(r['created_at']), requestId: r['request_id'] == null ? null : String(r['request_id']), turnId: r['turn'] == null ? null : Number(r['turn']), error: r['error'] == null ? null : String(r['error']) })) : [];
|
|
150
|
+
const participants = selected ? this.db.prepare('SELECT account,role,active FROM team_participant WHERE conversation=? ORDER BY account').all(selected.id).map(r => ({ account: String(r['account']), role: String(r['role']), active: Number(r['active']) === 1 })) : [];
|
|
151
|
+
const canManage = account.role === 'approver' && (access ? access.role === 'manager' : leadId ? this.leadAccess(actor, leadId).role === 'manager' : false);
|
|
152
|
+
return { leads, conversations, selected, participants, messages: messages.slice(-200), canManage, canCreateLead: account.role === 'approver' && this.enrolled().some(p => projectAccessAllows(account.projects, p)), canSend: account.role === 'approver' && access !== null && rank(access.role) >= 1 && access.lead.status === 'active', cursor: this.cursor(actor, selected?.id) ?? 0, truncated: messages.length > 200, projects: this.enrolled().filter(p => projectAccessAllows(account.projects, p)), accounts: canManage ? this.store.accountFacts().filter(a => a.revokedAt === null && (selected?.projects ?? []).every(p => projectAccessAllows(a.projects, p))).map(a => a.name) : [] };
|
|
153
|
+
}
|
|
154
|
+
execute(actor, request, now = new Date()) {
|
|
155
|
+
try {
|
|
156
|
+
const result = this.store.transact(() => {
|
|
157
|
+
this.account(actor);
|
|
158
|
+
const requestId = request.args['requestId'];
|
|
159
|
+
const key = requestId === undefined ? null : str(requestId, 'Request ID', 128);
|
|
160
|
+
const digest = hash(stable(request));
|
|
161
|
+
if (key) {
|
|
162
|
+
const seen = this.db.prepare('SELECT payload_hash,response_json FROM team_request WHERE account=? AND generation=? AND request_id=?').get(actor.name, actor.generation, key);
|
|
163
|
+
if (seen) {
|
|
164
|
+
if (seen['payload_hash'] !== digest)
|
|
165
|
+
refuse('request-conflict', 'This request ID was already used for different content.');
|
|
166
|
+
return JSON.parse(String(seen['response_json']));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const response = this.perform(actor, request, now);
|
|
170
|
+
if (key)
|
|
171
|
+
this.db.prepare('INSERT INTO team_request(account,generation,request_id,payload_hash,response_json) VALUES(?,?,?,?,?)').run(actor.name, actor.generation, key, digest, JSON.stringify(response));
|
|
172
|
+
return response;
|
|
173
|
+
});
|
|
174
|
+
if (request.operation === 'read')
|
|
175
|
+
return result; // Personal read receipts never rebuild the shared workspace.
|
|
176
|
+
const conversationId = typeof request.args['conversationId'] === 'string' ? request.args['conversationId'] : undefined;
|
|
177
|
+
const leadId = typeof request.args['leadId'] === 'string' ? request.args['leadId'] : undefined;
|
|
178
|
+
// A membership operation may deliberately remove the caller; its receipt still stands.
|
|
179
|
+
try {
|
|
180
|
+
return { ...result, snapshot: this.snapshot(actor, conversationId, leadId) };
|
|
181
|
+
}
|
|
182
|
+
catch (e) {
|
|
183
|
+
if (e instanceof TeamRefusal)
|
|
184
|
+
return result;
|
|
185
|
+
throw e;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
catch (e) {
|
|
189
|
+
if (e instanceof TeamRefusal)
|
|
190
|
+
return { version: 1, ok: false, code: e.code, message: e.message };
|
|
191
|
+
throw e;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
perform(actor, request, now) {
|
|
195
|
+
const a = request.args;
|
|
196
|
+
const ok = (message, result) => ({ version: 1, ok: true, code: 'ok', message, ...(result === undefined ? {} : { result }) });
|
|
197
|
+
if (request.operation === 'list' || request.operation === 'show') {
|
|
198
|
+
if (typeof a['conversationId'] === 'string')
|
|
199
|
+
this.access(actor, a['conversationId']);
|
|
200
|
+
if (typeof a['leadId'] === 'string')
|
|
201
|
+
this.leadAccess(actor, a['leadId']);
|
|
202
|
+
return ok('Conversation loaded.');
|
|
203
|
+
}
|
|
204
|
+
if (request.operation === 'create-lead') {
|
|
205
|
+
if (this.account(actor).role !== 'approver')
|
|
206
|
+
refuse('read-only', 'An operator account is needed to create a lead.');
|
|
207
|
+
const projects = this.projects(actor, a['projects']), id = randomUUID(), name = str(a['name'], 'Lead name', 120), instructions = typeof a['instructions'] === 'string' ? a['instructions'] : '';
|
|
208
|
+
if (instructions.length > 12000)
|
|
209
|
+
refuse('invalid', 'Lead instructions must be at most 12,000 characters.');
|
|
210
|
+
if (scanForSecrets(instructions).length > 0)
|
|
211
|
+
refuse('secret-in-context', 'Remove credentials from the lead instructions.');
|
|
212
|
+
this.db.prepare('INSERT INTO team_lead(id,name,instructions,status,created_by,created_at) VALUES(?,?,?,?,?,?)').run(id, name, instructions, 'active', actor.name, now.toISOString());
|
|
213
|
+
for (const p of projects)
|
|
214
|
+
this.db.prepare('INSERT INTO team_lead_project(lead,project) VALUES(?,?)').run(id, p);
|
|
215
|
+
this.db.prepare("INSERT INTO team_lead_member(lead,account,role) VALUES(?,?,'manager')").run(id, actor.name);
|
|
216
|
+
this.event(id, null, 'lead-created', actor.name, now);
|
|
217
|
+
return ok('Lead created.', { leadId: id });
|
|
218
|
+
}
|
|
219
|
+
if (request.operation === 'update-lead') {
|
|
220
|
+
const id = str(a['leadId'], 'Lead'), { lead } = this.leadAccess(actor, id, 'manager');
|
|
221
|
+
if (lead.revision !== integer(a['expectedRevision']))
|
|
222
|
+
refuse('conflict', 'This lead changed. Reload before editing.');
|
|
223
|
+
const name = a['name'] === undefined ? lead.name : str(a['name'], 'Lead name', 120), instructions = a['instructions'] === undefined ? lead.instructions : String(a['instructions']);
|
|
224
|
+
const status = a['status'] ?? lead.status;
|
|
225
|
+
if (status !== 'active' && status !== 'paused')
|
|
226
|
+
refuse('invalid', 'Choose Active or Paused.');
|
|
227
|
+
if (instructions.length > 12000)
|
|
228
|
+
refuse('invalid', 'Lead instructions must be at most 12,000 characters.');
|
|
229
|
+
if (scanForSecrets(instructions).length > 0)
|
|
230
|
+
refuse('secret-in-context', 'Remove credentials from the lead instructions.');
|
|
231
|
+
if (a['projects'] !== undefined)
|
|
232
|
+
refuse('fixed-scope', 'Create a new lead for a different project scope. Existing conversation history keeps its original scope.');
|
|
233
|
+
this.db.prepare('UPDATE team_lead SET name=?,instructions=?,status=?,revision=revision+1 WHERE id=?').run(name, instructions, status, id);
|
|
234
|
+
this.event(id, null, 'lead-updated', actor.name, now);
|
|
235
|
+
return ok('Lead updated.', { leadId: id });
|
|
236
|
+
}
|
|
237
|
+
if (request.operation === 'create-conversation') {
|
|
238
|
+
const leadId = str(a['leadId'], 'Lead'), { lead } = this.leadAccess(actor, leadId, 'contributor');
|
|
239
|
+
const projects = this.projects(actor, a['projects'], lead.projects), id = randomUUID(), title = str(a['title'], 'Conversation title', 160), visibility = a['visibility'];
|
|
240
|
+
if (visibility !== 'private' && visibility !== 'team')
|
|
241
|
+
refuse('invalid', 'Choose Private or Team.');
|
|
242
|
+
const thread = this.store.openTeamMateThread(actor.name, ceilingDigestOf(projects), now);
|
|
243
|
+
this.db.prepare('INSERT INTO team_conversation(id,lead,title,visibility,projects_json,thread,created_by,created_at) VALUES(?,?,?,?,?,?,?,?)').run(id, leadId, title, visibility, JSON.stringify(projects), thread.id, actor.name, now.toISOString());
|
|
244
|
+
this.db.prepare("INSERT INTO team_participant(conversation,account,role) VALUES(?,?,'manager')").run(id, actor.name);
|
|
245
|
+
this.event(leadId, id, 'conversation-created', actor.name, now);
|
|
246
|
+
return ok('Conversation created.', { conversationId: id, threadId: thread.id });
|
|
247
|
+
}
|
|
248
|
+
if (request.operation === 'member') {
|
|
249
|
+
const conversationId = typeof a['conversationId'] === 'string' ? a['conversationId'] : null;
|
|
250
|
+
const access = conversationId ? this.access(actor, conversationId, 'manager') : null;
|
|
251
|
+
const leadId = access?.lead.id ?? str(a['leadId'], 'Lead'), { lead } = this.leadAccess(actor, leadId, 'manager');
|
|
252
|
+
const expected = integer(a['expectedRevision']);
|
|
253
|
+
if (expected !== (access?.conversation.revision ?? lead.revision))
|
|
254
|
+
refuse('conflict', 'Membership changed. Reload before editing.');
|
|
255
|
+
if (access?.conversation.visibility === 'private')
|
|
256
|
+
refuse('private', 'A private conversation cannot add people. Create a fresh team conversation.');
|
|
257
|
+
if (a['active'] !== undefined && typeof a['active'] !== 'boolean')
|
|
258
|
+
refuse('invalid', 'Choose whether this membership is active.');
|
|
259
|
+
const account = str(a['account'], 'Account'), memberRole = role(a['role']), active = a['active'] !== false, target = this.store.accountOf(account), projects = access?.conversation.projects ?? lead.projects;
|
|
260
|
+
if (!target || (active && (target.revokedAt !== null || projects.some(p => !projectAccessAllows(target.projects, p)))))
|
|
261
|
+
refuse('forbidden', 'This person needs access to every project in the conversation.');
|
|
262
|
+
if (active && memberRole !== 'viewer' && target.role !== 'approver')
|
|
263
|
+
refuse('read-only', 'Viewer accounts can join with the Viewer role.');
|
|
264
|
+
if (active && conversationId) {
|
|
265
|
+
const lm = this.db.prepare('SELECT role FROM team_lead_member WHERE lead=? AND account=? AND active=1').get(leadId, account);
|
|
266
|
+
if (a['joinLead'] === true) {
|
|
267
|
+
if (integer(a['expectedLeadRevision']) !== lead.revision)
|
|
268
|
+
refuse('conflict', 'Lead membership changed. Reload before adding this person.');
|
|
269
|
+
if (lead.projects.some(p => !projectAccessAllows(target.projects, p)))
|
|
270
|
+
refuse('forbidden', 'This person needs access to every project in the lead.');
|
|
271
|
+
if (!lm || rank(lm['role']) < rank(memberRole)) {
|
|
272
|
+
this.db.prepare('INSERT INTO team_lead_member(lead,account,role,active) VALUES(?,?,?,1) ON CONFLICT(lead,account) DO UPDATE SET role=excluded.role,active=1,revision=team_lead_member.revision+1').run(leadId, account, memberRole);
|
|
273
|
+
this.db.prepare('UPDATE team_lead SET revision=revision+1 WHERE id=?').run(leadId);
|
|
274
|
+
this.revokeMemberSessions(leadId, null, account, now);
|
|
275
|
+
this.event(leadId, null, 'membership-changed', actor.name, now);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
else if (!lm || rank(lm['role']) < rank(memberRole))
|
|
279
|
+
refuse('lead-membership', 'Add this person to the lead with the same role first.');
|
|
280
|
+
}
|
|
281
|
+
const table = conversationId ? 'team_participant' : 'team_lead_member', column = conversationId ? 'conversation' : 'lead', id = conversationId ?? leadId;
|
|
282
|
+
const old = this.db.prepare(`SELECT role,active FROM ${table} WHERE ${column}=? AND account=?`).get(id, account);
|
|
283
|
+
if (old?.['role'] === 'manager' && Number(old['active']) === 1 && (!active || memberRole !== 'manager') && !this.db.prepare(`SELECT 1 FROM ${table} WHERE ${column}=? AND account<>? AND role='manager' AND active=1`).get(id, account))
|
|
284
|
+
refuse('last-manager', 'Keep at least one manager.');
|
|
285
|
+
this.db.prepare(`INSERT INTO ${table}(${column},account,role,active) VALUES(?,?,?,?) ON CONFLICT(${column},account) DO UPDATE SET role=excluded.role,active=excluded.active,revision=${table}.revision+1`).run(id, account, memberRole, active ? 1 : 0);
|
|
286
|
+
this.db.prepare(`UPDATE ${conversationId ? 'team_conversation' : 'team_lead'} SET revision=revision+1 WHERE id=?`).run(id);
|
|
287
|
+
this.revokeMemberSessions(leadId, conversationId, account, now);
|
|
288
|
+
this.event(leadId, conversationId, 'membership-changed', actor.name, now);
|
|
289
|
+
return ok(active ? 'Membership updated.' : 'Access removed.');
|
|
290
|
+
}
|
|
291
|
+
if (request.operation === 'transfer')
|
|
292
|
+
return this.transfer(actor, a, now);
|
|
293
|
+
const conversationId = str(a['conversationId'], 'Conversation');
|
|
294
|
+
const { conversation, lead, role: memberRole } = this.access(actor, conversationId, request.operation === 'read' ? 'viewer' : 'contributor');
|
|
295
|
+
if (request.operation === 'send') {
|
|
296
|
+
if (lead.status !== 'active')
|
|
297
|
+
refuse('paused', 'This lead is paused. Ask a manager to resume it.');
|
|
298
|
+
const text = messageText(a['text']), requestId = str(a['requestId'], 'Request ID', 128), digest = hash({ conversationId, text });
|
|
299
|
+
const seen = this.db.prepare('SELECT * FROM team_message WHERE conversation=? AND author=? AND request_id=?').get(conversationId, actor.name, requestId);
|
|
300
|
+
if (seen) {
|
|
301
|
+
if (seen['payload_hash'] !== digest)
|
|
302
|
+
refuse('request-conflict', 'This request ID already saved a different message.');
|
|
303
|
+
return ok('Message already saved.', { messageId: Number(seen['message']), status: String(seen['status']) });
|
|
304
|
+
}
|
|
305
|
+
const revisions = this.memberRevisions(conversationId, actor.name);
|
|
306
|
+
const messageId = this.store.appendMateMessage({ thread: conversation.threadId, turn: null, role: 'operator', text }, now);
|
|
307
|
+
this.db.prepare("INSERT INTO team_message(message,conversation,author,author_generation,lead_member_revision,participant_revision,request_id,payload_hash,status) VALUES(?,?,?,?,?,?,?,?,'queued')").run(messageId, conversationId, actor.name, actor.generation, revisions.lead, revisions.participant, requestId, digest);
|
|
308
|
+
this.event(lead.id, conversationId, 'message-queued', actor.name, now);
|
|
309
|
+
return ok('Message saved.', { messageId, status: 'queued' });
|
|
310
|
+
}
|
|
311
|
+
if (request.operation === 'read') {
|
|
312
|
+
const id = integer(a['messageId'], 'Message');
|
|
313
|
+
if (id > 0 && !this.db.prepare('SELECT 1 FROM mate_message WHERE id=? AND thread=?').get(id, conversation.threadId))
|
|
314
|
+
refuse('not-found', 'Message not available.');
|
|
315
|
+
this.db.prepare('INSERT INTO team_read(conversation,account,message) VALUES(?,?,?) ON CONFLICT(conversation,account) DO UPDATE SET message=MAX(team_read.message,excluded.message)').run(conversationId, actor.name, id);
|
|
316
|
+
return ok('Read position saved.');
|
|
317
|
+
}
|
|
318
|
+
if (request.operation === 'follow') {
|
|
319
|
+
if (typeof a['enabled'] !== 'boolean')
|
|
320
|
+
refuse('invalid', 'Choose whether to follow this conversation.');
|
|
321
|
+
const revisions = this.memberRevisions(conversationId, actor.name);
|
|
322
|
+
this.db.prepare('INSERT INTO team_follow(conversation,account,generation,lead_member_revision,participant_revision,enabled) VALUES(?,?,?,?,?,?) ON CONFLICT(conversation,account) DO UPDATE SET generation=excluded.generation,lead_member_revision=excluded.lead_member_revision,participant_revision=excluded.participant_revision,enabled=excluded.enabled').run(conversationId, actor.name, actor.generation, revisions.lead, revisions.participant, a['enabled'] ? 1 : 0);
|
|
323
|
+
this.event(lead.id, conversationId, 'follow-changed', actor.name, now);
|
|
324
|
+
return ok(a['enabled'] ? 'Updates enabled.' : 'Updates paused.');
|
|
325
|
+
}
|
|
326
|
+
const id = integer(a['messageId'], 'Message'), message = this.db.prepare('SELECT * FROM team_message WHERE message=? AND conversation=?').get(id, conversationId);
|
|
327
|
+
if (!message)
|
|
328
|
+
refuse('not-found', 'Message not available.');
|
|
329
|
+
if (request.operation === 'stop') {
|
|
330
|
+
if (message['author'] !== actor.name && memberRole !== 'manager')
|
|
331
|
+
refuse('forbidden', `Only ${String(message['author'])} or a conversation manager can stop this turn.`);
|
|
332
|
+
if (message['status'] === 'running') {
|
|
333
|
+
this.db.prepare('UPDATE team_message SET stop_requested=1 WHERE message=?').run(id);
|
|
334
|
+
if (message['turn_id'] !== null) {
|
|
335
|
+
const turn = this.store.getMateTurn(Number(message['turn_id']));
|
|
336
|
+
if (turn)
|
|
337
|
+
this.store.finalizeMateTurn(turn.id, turn.generation, { state: 'failed', settledMicrousd: turn.reservedMicrousd, unknownSpend: true, tokensIn: 0, tokensOut: 0, failureReason: 'stopped' }, now);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
else if (message['status'] === 'queued' || message['status'] === 'uncertain')
|
|
341
|
+
this.db.prepare("UPDATE team_message SET status='cancelled',revision=revision+1,generation=generation+1,stop_requested=1 WHERE message=?").run(id);
|
|
342
|
+
else
|
|
343
|
+
refuse('finished', 'This message has already finished.');
|
|
344
|
+
this.event(lead.id, conversationId, 'stop-requested', actor.name, now);
|
|
345
|
+
return ok('Stop requested.', { messageId: id });
|
|
346
|
+
}
|
|
347
|
+
if (request.operation === 'edit' || request.operation === 'withdraw') {
|
|
348
|
+
if (message['author'] !== actor.name)
|
|
349
|
+
refuse('forbidden', 'Only the author can change a queued message.');
|
|
350
|
+
if (Number(message['revision']) !== integer(a['expectedRevision']) || message['status'] !== 'queued')
|
|
351
|
+
refuse('conflict', 'This message changed or has started. Send a follow-up instead.');
|
|
352
|
+
if (request.operation === 'edit') {
|
|
353
|
+
const text = messageText(a['text']);
|
|
354
|
+
this.db.prepare('UPDATE mate_message SET text=? WHERE id=?').run(text, id);
|
|
355
|
+
this.db.prepare('UPDATE team_message SET revision=revision+1 WHERE message=?').run(id);
|
|
356
|
+
}
|
|
357
|
+
else
|
|
358
|
+
this.db.prepare("UPDATE team_message SET status='cancelled',revision=revision+1 WHERE message=?").run(id);
|
|
359
|
+
this.event(lead.id, conversationId, 'message-updated', actor.name, now);
|
|
360
|
+
return ok(request.operation === 'edit' ? 'Message updated.' : 'Message withdrawn.', { messageId: id });
|
|
361
|
+
}
|
|
362
|
+
refuse('unsupported', 'This action is not available here.');
|
|
363
|
+
}
|
|
364
|
+
revokeMemberSessions(leadId, conversationId, account, now) {
|
|
365
|
+
this.db.prepare(`UPDATE mate_session SET ended_at=?,ended_by='membership-changed' WHERE approver=? AND ended_at IS NULL
|
|
366
|
+
AND EXISTS(SELECT 1 FROM team_mate_session ms JOIN team_conversation c ON c.thread=ms.thread
|
|
367
|
+
WHERE ms.session=mate_session.id AND c.lead=? AND (? IS NULL OR c.id=?))`).run(now.toISOString(), account, leadId, conversationId, conversationId);
|
|
368
|
+
}
|
|
369
|
+
memberRevisions(conversationId, account) {
|
|
370
|
+
const row = this.db.prepare(`SELECT lm.revision AS lr,p.revision AS pr FROM team_conversation c JOIN team_lead_member lm ON lm.lead=c.lead AND lm.account=? AND lm.active=1 JOIN team_participant p ON p.conversation=c.id AND p.account=lm.account AND p.active=1 WHERE c.id=?`).get(account, conversationId);
|
|
371
|
+
return row ? { lead: Number(row['lr']), participant: Number(row['pr']) } : null;
|
|
372
|
+
}
|
|
373
|
+
authorStillAllowed(row) {
|
|
374
|
+
try {
|
|
375
|
+
const actor = { name: String(row['author']), generation: Number(row['author_generation']) };
|
|
376
|
+
const a = this.access(actor, String(row['conversation']), 'contributor'), r = this.memberRevisions(a.conversation.id, actor.name);
|
|
377
|
+
return a.lead.status === 'active' && r?.lead === Number(row['lead_member_revision']) && r.participant === Number(row['participant_revision']);
|
|
378
|
+
}
|
|
379
|
+
catch (e) {
|
|
380
|
+
if (e instanceof TeamRefusal)
|
|
381
|
+
return false;
|
|
382
|
+
throw e;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
claimNext(runner, now = new Date(), eligible) {
|
|
386
|
+
str(runner, 'Runner', 180);
|
|
387
|
+
return this.store.transact(() => {
|
|
388
|
+
const candidates = this.db.prepare(`SELECT q.*,m.text,c.lead,c.thread FROM team_message q JOIN mate_message m ON m.id=q.message JOIN team_conversation c ON c.id=q.conversation JOIN team_lead l ON l.id=c.lead AND l.status='active'
|
|
389
|
+
WHERE q.status='queued' AND NOT EXISTS(SELECT 1 FROM team_message live WHERE live.conversation=q.conversation AND live.status IN ('running','uncertain'))
|
|
390
|
+
AND NOT EXISTS(SELECT 1 FROM team_message earlier WHERE earlier.conversation=q.conversation AND earlier.status='queued' AND earlier.message<q.message)
|
|
391
|
+
AND (SELECT COUNT(*) FROM team_message running JOIN team_conversation other ON other.id=running.conversation WHERE running.status='running' AND other.lead=c.lead)<2
|
|
392
|
+
ORDER BY CASE WHEN q.request_id LIKE 'team-update:%' AND m.created_at>? THEN 1 ELSE 0 END,COALESCE(c.last_claimed_at,''),q.message LIMIT 100`).all(new Date(now.getTime() - 60_000).toISOString());
|
|
393
|
+
for (const row of candidates) {
|
|
394
|
+
if (!this.authorStillAllowed(row)) {
|
|
395
|
+
this.db.prepare("UPDATE team_message SET status='cancelled',revision=revision+1,error='Access changed before this message started.' WHERE message=?").run(Number(row['message']));
|
|
396
|
+
this.event(String(row['lead']), String(row['conversation']), 'message-cancelled', String(row['author']), now);
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
if (eligible && !eligible({ actor: { name: String(row['author']), generation: Number(row['author_generation']) }, conversationId: String(row['conversation']), threadId: Number(row['thread']), messageId: Number(row['message']) }))
|
|
400
|
+
continue;
|
|
401
|
+
this.db.prepare("UPDATE team_message SET status='running',generation=generation+1,runner=?,claimed_at=? WHERE message=? AND status='queued'").run(runner, now.toISOString(), Number(row['message']));
|
|
402
|
+
this.db.prepare('UPDATE team_conversation SET last_claimed_at=? WHERE id=?').run(now.toISOString(), String(row['conversation']));
|
|
403
|
+
const lead = this.leadOf(this.leadRow(String(row['lead']))), conversation = this.access({ name: String(row['author']), generation: Number(row['author_generation']) }, String(row['conversation'])).conversation;
|
|
404
|
+
this.event(lead.id, conversation.id, 'message-started', String(row['author']), now);
|
|
405
|
+
return { conversationId: conversation.id, leadId: lead.id, threadId: conversation.threadId, messageId: Number(row['message']), messageRevision: Number(row['revision']), generation: Number(row['generation']) + 1, runner, actor: { name: String(row['author']), generation: Number(row['author_generation']) }, text: String(row['text']), projects: conversation.projects, instructions: lead.instructions, requestId: String(row['request_id']) };
|
|
406
|
+
}
|
|
407
|
+
return null;
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
claimed(claim) { return this.db.prepare(`SELECT q.* FROM team_message q JOIN team_conversation c ON c.id=q.conversation WHERE q.message=? AND q.conversation=? AND q.generation=? AND q.revision=? AND q.runner=? AND q.status='running' AND c.lead=? AND c.thread=? AND q.author=? AND q.author_generation=? AND q.request_id=?`).get(claim.messageId, claim.conversationId, claim.generation, claim.messageRevision, claim.runner, claim.leadId, claim.threadId, claim.actor.name, claim.actor.generation, claim.requestId); }
|
|
411
|
+
current(claim) { const row = this.claimed(claim); return row !== undefined && Number(row['stop_requested']) === 0 && this.authorStillAllowed(row); }
|
|
412
|
+
bindTurn(claim, turnId) {
|
|
413
|
+
return this.store.transact(() => { if (!this.current(claim))
|
|
414
|
+
return false; const turn = this.store.getMateTurn(turnId); if (!turn || turn.thread !== claim.threadId || turn.approver !== claim.actor.name)
|
|
415
|
+
return false; const changed = this.db.prepare('UPDATE team_message SET turn_id=? WHERE message=? AND turn_id IS NULL').run(turnId, claim.messageId); return Number(changed.changes) === 1; });
|
|
416
|
+
}
|
|
417
|
+
finish(claim, result, now = new Date()) {
|
|
418
|
+
return this.store.transact(() => {
|
|
419
|
+
const row = this.claimed(claim);
|
|
420
|
+
if (!row)
|
|
421
|
+
return false;
|
|
422
|
+
const allowed = this.authorStillAllowed(row), stopped = Number(row['stop_requested']) !== 0;
|
|
423
|
+
const status = result.status === 'uncertain' ? 'uncertain' : allowed && !stopped ? result.status : 'cancelled';
|
|
424
|
+
if (result.turnId !== undefined && row['turn_id'] !== null && Number(row['turn_id']) !== result.turnId)
|
|
425
|
+
return false;
|
|
426
|
+
this.db.prepare('UPDATE team_message SET status=?,error=?,turn_id=COALESCE(turn_id,?),revision=revision+1 WHERE message=?').run(status, result.error ?? (!allowed ? 'Access changed while this message was running.' : stopped ? 'Stopped.' : null), result.turnId ?? null, claim.messageId);
|
|
427
|
+
if (allowed && !stopped && result.text)
|
|
428
|
+
this.store.appendMateMessage({ thread: claim.threadId, turn: result.turnId ?? (row['turn_id'] === null ? null : Number(row['turn_id'])), role: 'assistant', text: result.text }, now);
|
|
429
|
+
this.event(claim.leadId, claim.conversationId, 'message-finished', claim.actor.name, now);
|
|
430
|
+
return true;
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
/** A pre-admission refusal saves a waiting reason. Admitted work is never requeued. */
|
|
434
|
+
defer(claim, error, now = new Date()) {
|
|
435
|
+
return this.store.transact(() => {
|
|
436
|
+
const row = this.claimed(claim);
|
|
437
|
+
if (!row || row['turn_id'] !== null || !this.current(claim))
|
|
438
|
+
return false;
|
|
439
|
+
if (this.db.prepare('SELECT turn FROM mate_message WHERE id=?').get(claim.messageId)?.['turn'] !== null)
|
|
440
|
+
return false;
|
|
441
|
+
const changed = this.db.prepare("UPDATE team_message SET status='queued',generation=generation+1,revision=revision+1,runner=NULL,claimed_at=NULL,error=? WHERE message=? AND status='running' AND turn_id IS NULL AND generation=?").run(error, claim.messageId, claim.generation);
|
|
442
|
+
if (Number(changed.changes) !== 1)
|
|
443
|
+
return false;
|
|
444
|
+
if (row['error'] !== error)
|
|
445
|
+
this.event(claim.leadId, claim.conversationId, 'message-waiting', claim.actor.name, now);
|
|
446
|
+
return true;
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
/** Reconcile only recorded terminal receipts; never infer success from time or rerun a turn. */
|
|
450
|
+
reconcileFinished(now = new Date()) {
|
|
451
|
+
return this.store.transact(() => {
|
|
452
|
+
const rows = this.db.prepare(`SELECT q.*,c.lead,c.thread,t.state AS turn_state,t.failure_reason FROM team_message q
|
|
453
|
+
JOIN team_conversation c ON c.id=q.conversation JOIN mate_turn t ON t.id=q.turn_id
|
|
454
|
+
WHERE q.status='running' AND t.state IN ('answered','failed')
|
|
455
|
+
AND NOT EXISTS(SELECT 1 FROM chat_turn step WHERE step.mate_turn=t.id AND step.state IN ('queued','running'))
|
|
456
|
+
ORDER BY q.message LIMIT 100`).all();
|
|
457
|
+
let changed = 0;
|
|
458
|
+
for (const row of rows) {
|
|
459
|
+
// An unknown provider outcome remains a stop point even when the account
|
|
460
|
+
// lost access: permission revocation is not proof an effect did not occur.
|
|
461
|
+
const uncertain = this.deliveryUncertain(Number(row['turn_id']));
|
|
462
|
+
const cancelled = Number(row['stop_requested']) === 1 || !this.authorStillAllowed(row);
|
|
463
|
+
const status = uncertain ? 'uncertain' : cancelled ? 'cancelled' : row['turn_state'] === 'answered' ? 'answered' : 'failed';
|
|
464
|
+
const error = uncertain ? 'Delivery could not be confirmed. Inspect saved activity before continuing.' : cancelled ? 'Stopped or conversation access changed.' : status === 'failed' ? String(row['failure_reason'] ?? 'The saved turn failed.') : null;
|
|
465
|
+
const result = this.db.prepare("UPDATE team_message SET status=?,error=?,generation=generation+1,revision=revision+1 WHERE message=? AND status='running' AND generation=?").run(status, error, Number(row['message']), Number(row['generation']));
|
|
466
|
+
if (Number(result.changes) === 1) {
|
|
467
|
+
changed++;
|
|
468
|
+
this.event(String(row['lead']), String(row['conversation']), 'message-reconciled', String(row['author']), now);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return changed;
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
deliveryUncertain(turnId) { return this.db.prepare("SELECT 1 FROM chat_turn WHERE mate_turn=? AND (unknown_spend=1 OR state IN ('queued','running') OR settled_microusd IS NULL) LIMIT 1").get(turnId) !== undefined; }
|
|
475
|
+
activeRunners() { return this.db.prepare("SELECT DISTINCT runner FROM team_message WHERE status='running' AND runner IS NOT NULL").all().map(r => String(r['runner'])); }
|
|
476
|
+
/** Called only after the runtime proves this runner has stopped. Never repeats work. */
|
|
477
|
+
recover(runner, now = new Date()) {
|
|
478
|
+
return this.store.transact(() => {
|
|
479
|
+
const rows = this.db.prepare("SELECT q.*,c.lead FROM team_message q JOIN team_conversation c ON c.id=q.conversation WHERE runner=? AND status='running'").all(runner);
|
|
480
|
+
for (const row of rows) {
|
|
481
|
+
const turn = row['turn_id'] === null ? null : this.store.getMateTurn(Number(row['turn_id']));
|
|
482
|
+
const status = turn?.state === 'answered' ? 'answered' : turn?.state === 'failed' && !this.deliveryUncertain(turn.id) ? 'failed' : 'uncertain';
|
|
483
|
+
const error = status === 'uncertain' ? 'Delivery unconfirmed after the previous worker stopped. Inspect activity before continuing.' : status === 'failed' ? (turn?.failureReason ?? 'The saved turn failed.') : null;
|
|
484
|
+
this.db.prepare('UPDATE team_message SET status=?,generation=generation+1,revision=revision+1,error=? WHERE message=?').run(status, error, Number(row['message']));
|
|
485
|
+
this.event(String(row['lead']), String(row['conversation']), status === 'uncertain' ? 'delivery-unconfirmed' : 'message-reconciled', String(row['author']), now);
|
|
486
|
+
}
|
|
487
|
+
return rows.length;
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
followGrants() {
|
|
491
|
+
const grants = [];
|
|
492
|
+
for (const row of this.db.prepare('SELECT * FROM team_follow WHERE enabled=1').all()) {
|
|
493
|
+
const actor = { name: String(row['account']), generation: Number(row['generation']) };
|
|
494
|
+
try {
|
|
495
|
+
const a = this.access(actor, String(row['conversation']), 'contributor');
|
|
496
|
+
if (a.lead.status === 'active' && a.conversation.follow)
|
|
497
|
+
grants.push({ actor, conversation: a.conversation, lead: a.lead });
|
|
498
|
+
}
|
|
499
|
+
catch (e) {
|
|
500
|
+
if (!(e instanceof TeamRefusal))
|
|
501
|
+
throw e;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return grants;
|
|
505
|
+
}
|
|
506
|
+
taskOwner(taskId) {
|
|
507
|
+
const status = this.store.revisionAncestryStatus(taskId);
|
|
508
|
+
if (status.problem !== null)
|
|
509
|
+
return null;
|
|
510
|
+
const ancestry = status.chain;
|
|
511
|
+
const root = ancestry[ancestry.length - 1];
|
|
512
|
+
const ref = this.store.lookupRef(root ?? taskId);
|
|
513
|
+
if (!ref)
|
|
514
|
+
return null;
|
|
515
|
+
const row = this.db.prepare('SELECT lead,revision,conversation FROM team_task_owner WHERE task_ref=?').get(ref.id);
|
|
516
|
+
return row ? { leadId: String(row['lead']), revision: Number(row['revision']), conversationId: row['conversation'] === null ? null : String(row['conversation']) } : null;
|
|
517
|
+
}
|
|
518
|
+
/** Attach newly filed work in its original admission transaction. Never steals an existing owner. */
|
|
519
|
+
recordTaskOwner(taskId, leadId, conversationId, actor, now = new Date()) {
|
|
520
|
+
return this.store.transact(() => {
|
|
521
|
+
const lead = this.leadAccess(actor, leadId, 'contributor').lead;
|
|
522
|
+
const ancestry = this.store.revisionAncestryStatus(taskId);
|
|
523
|
+
if (ancestry.problem !== null)
|
|
524
|
+
return false;
|
|
525
|
+
const ref = this.store.lookupRef(ancestry.chain[ancestry.chain.length - 1] ?? taskId);
|
|
526
|
+
if (!ref || ref.repo === null || !lead.projects.includes(ref.repo) || !this.store.accountCanAccess(actor.name, ref.repo))
|
|
527
|
+
return false;
|
|
528
|
+
if (conversationId) {
|
|
529
|
+
const access = this.access(actor, conversationId, 'contributor');
|
|
530
|
+
if (access.lead.id !== leadId || !access.conversation.projects.includes(ref.repo))
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
533
|
+
const changed = this.db.prepare('INSERT INTO team_task_owner(task_ref,lead,conversation,changed_by,changed_at) VALUES(?,?,?,?,?) ON CONFLICT(task_ref) DO NOTHING').run(ref.id, leadId, conversationId, actor.name, now.toISOString());
|
|
534
|
+
if (Number(changed.changes) === 1)
|
|
535
|
+
this.event(leadId, conversationId, 'task-owner-changed', actor.name, now);
|
|
536
|
+
return Number(changed.changes) === 1;
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
transfer(actor, a, now) {
|
|
540
|
+
const taskId = str(a['taskId'], 'Task'), leadId = str(a['leadId'], 'Lead');
|
|
541
|
+
const target = this.leadAccess(actor, leadId, 'manager').lead, ancestry = this.store.revisionAncestryStatus(taskId);
|
|
542
|
+
if (ancestry.problem !== null)
|
|
543
|
+
refuse('invalid-task', 'Task history needs inspection before ownership can change.');
|
|
544
|
+
const ref = this.store.lookupRef(ancestry.chain[ancestry.chain.length - 1] ?? taskId);
|
|
545
|
+
if (!ref || ref.repo === null || !target.projects.includes(ref.repo) || !this.store.accountCanAccess(actor.name, ref.repo))
|
|
546
|
+
refuse('forbidden', 'The receiving lead must have access to this task’s project.');
|
|
547
|
+
const old = this.taskOwner(taskId), expected = integer(a['expectedRevision']);
|
|
548
|
+
if ((old?.revision ?? 0) !== expected)
|
|
549
|
+
refuse('conflict', 'Task ownership changed. Reload before transferring.');
|
|
550
|
+
if (old)
|
|
551
|
+
this.leadAccess(actor, old.leadId, 'manager');
|
|
552
|
+
const conversationId = a['conversationId'] === undefined ? null : str(a['conversationId'], 'Conversation');
|
|
553
|
+
if (conversationId) {
|
|
554
|
+
const destination = this.access(actor, conversationId, 'contributor');
|
|
555
|
+
if (destination.lead.id !== leadId || !destination.conversation.projects.includes(ref.repo))
|
|
556
|
+
refuse('forbidden', 'Choose a conversation belonging to the receiving lead and task project.');
|
|
557
|
+
}
|
|
558
|
+
this.db.prepare('INSERT INTO team_task_owner(task_ref,lead,conversation,changed_by,changed_at) VALUES(?,?,?,?,?) ON CONFLICT(task_ref) DO UPDATE SET lead=excluded.lead,conversation=excluded.conversation,revision=team_task_owner.revision+1,changed_by=excluded.changed_by,changed_at=excluded.changed_at').run(ref.id, leadId, conversationId, actor.name, now.toISOString());
|
|
559
|
+
this.store.recordAction({ at: now.toISOString(), actor: actor.name, repo: ref.repo, taskId, runId: null, action: 'task lead transferred', outcome: JSON.stringify({ from: old?.leadId ?? null, to: leadId, revision: expected + 1 }), source: 'work' });
|
|
560
|
+
this.event(leadId, null, 'task-owner-changed', actor.name, now);
|
|
561
|
+
if (old && old.leadId !== leadId)
|
|
562
|
+
this.event(old.leadId, null, 'task-owner-changed', actor.name, now);
|
|
563
|
+
return { version: 1, ok: true, code: 'ok', message: 'Task responsibility transferred.', result: { taskId, leadId, revision: expected + 1 } };
|
|
564
|
+
}
|
|
565
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Store, ChatConfig } from './store.js';
|
|
2
|
+
import type { WorkspaceRevision } from './workspace-revision.js';
|
|
3
|
+
import { TeamLeads } from './team-leads.js';
|
|
4
|
+
import type { TeamActor, TeamExecute } from './team-contract.js';
|
|
5
|
+
import { type MateTurnInput } from './mate.js';
|
|
6
|
+
export type TeamRuntimeOptions = {
|
|
7
|
+
store: Store;
|
|
8
|
+
repos: () => readonly string[];
|
|
9
|
+
evidenceRoot: string;
|
|
10
|
+
provider: () => {
|
|
11
|
+
config: ChatConfig;
|
|
12
|
+
key: string | null;
|
|
13
|
+
} | null;
|
|
14
|
+
subscriptionRunner?: MateTurnInput['subscriptionRunner'];
|
|
15
|
+
fetcher?: typeof fetch;
|
|
16
|
+
clock?: () => Date;
|
|
17
|
+
capacity?: number;
|
|
18
|
+
workspaceRevision?: WorkspaceRevision;
|
|
19
|
+
};
|
|
20
|
+
export declare function createTeamRuntime(options: TeamRuntimeOptions): {
|
|
21
|
+
execute: TeamExecute;
|
|
22
|
+
start: () => void;
|
|
23
|
+
close: () => Promise<void>;
|
|
24
|
+
pass: () => Promise<void>;
|
|
25
|
+
cursor: (actor: TeamActor, conversationId?: string) => number | null;
|
|
26
|
+
domain: TeamLeads;
|
|
27
|
+
};
|