tinybeeai 0.0.0-stage → 0.16.1
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/.railway/railway.ts +83 -0
- package/.railway/tsconfig.json +13 -0
- package/AGENTS.md +102 -0
- package/Dockerfile +108 -0
- package/LICENSE +21 -0
- package/README.md +275 -2
- package/bin/localdost.js +29 -0
- package/bin/tinybeeai.js +28 -0
- package/compose.yml +63 -0
- package/demo/payloads/ci_failure.json +46 -0
- package/demo/payloads/outage.json +18 -0
- package/demo/payloads/pr_comment_edited.json +50 -0
- package/demo/payloads/pr_merged.json +80 -0
- package/demo/payloads/pr_opened.json +69 -0
- package/demo/payloads/pr_synchronize.json +69 -0
- package/demo/payloads/ticket_created.json +138 -0
- package/demo/sample-app/playwright.config.ts +20 -0
- package/demo/sample-app/server.ts +163 -0
- package/demo/sample-app/tests/checkout.spec.ts +13 -0
- package/demo/sample-app/tests/smoke.spec.ts +8 -0
- package/dist/agent/approval-retries.js +59 -0
- package/dist/agent/approval-retries.js.map +1 -0
- package/dist/agent/approvals.js +390 -0
- package/dist/agent/approvals.js.map +1 -0
- package/dist/agent/chat-bridge.js +154 -0
- package/dist/agent/chat-bridge.js.map +1 -0
- package/dist/agent/chat-lead.js +475 -0
- package/dist/agent/chat-lead.js.map +1 -0
- package/dist/agent/chat-loop.js +106 -0
- package/dist/agent/chat-loop.js.map +1 -0
- package/dist/agent/chat-mcp.js +49 -0
- package/dist/agent/chat-mcp.js.map +1 -0
- package/dist/agent/chat-tool-blocks.js +453 -0
- package/dist/agent/chat-tool-blocks.js.map +1 -0
- package/dist/agent/chat-view.js +94 -0
- package/dist/agent/chat-view.js.map +1 -0
- package/dist/agent/chat-views.js +781 -0
- package/dist/agent/chat-views.js.map +1 -0
- package/dist/agent/chat.js +1703 -0
- package/dist/agent/chat.js.map +1 -0
- package/dist/agent/destinations.js +150 -0
- package/dist/agent/destinations.js.map +1 -0
- package/dist/agent/llm-cli.js +451 -0
- package/dist/agent/llm-cli.js.map +1 -0
- package/dist/agent/llm-codex.js +493 -0
- package/dist/agent/llm-codex.js.map +1 -0
- package/dist/agent/llm-openai.js +211 -0
- package/dist/agent/llm-openai.js.map +1 -0
- package/dist/agent/llm.js +1464 -0
- package/dist/agent/llm.js.map +1 -0
- package/dist/agent/memory.js +33 -0
- package/dist/agent/memory.js.map +1 -0
- package/dist/agent/mock-views.js +93 -0
- package/dist/agent/mock-views.js.map +1 -0
- package/dist/agent/model-availability.js +207 -0
- package/dist/agent/model-availability.js.map +1 -0
- package/dist/agent/model-errors.js +188 -0
- package/dist/agent/model-errors.js.map +1 -0
- package/dist/agent/model-guard.js +311 -0
- package/dist/agent/model-guard.js.map +1 -0
- package/dist/agent/prompts.js +374 -0
- package/dist/agent/prompts.js.map +1 -0
- package/dist/agent/qa-lenses/qa-auth.md +32 -0
- package/dist/agent/qa-lenses/qa-check-then-act.md +33 -0
- package/dist/agent/qa-lenses/qa-control-has-no-effect.md +32 -0
- package/dist/agent/qa-lenses/qa-dates.md +31 -0
- package/dist/agent/qa-lenses/qa-double-submit.md +34 -0
- package/dist/agent/qa-lenses/qa-feature-flag.md +29 -0
- package/dist/agent/qa-lenses/qa-migration.md +35 -0
- package/dist/agent/qa-lenses/qa-money.md +38 -0
- package/dist/agent/qa-lenses/qa-response-shape-break.md +33 -0
- package/dist/agent/qa-lenses/qa-validation.md +29 -0
- package/dist/agent/qa-prompts.js +660 -0
- package/dist/agent/qa-prompts.js.map +1 -0
- package/dist/agent/reasoning.js +55 -0
- package/dist/agent/reasoning.js.map +1 -0
- package/dist/agent/redact.js +48 -0
- package/dist/agent/redact.js.map +1 -0
- package/dist/agent/rules.js +927 -0
- package/dist/agent/rules.js.map +1 -0
- package/dist/agent/runs.js +440 -0
- package/dist/agent/runs.js.map +1 -0
- package/dist/agent/send-retry.js +62 -0
- package/dist/agent/send-retry.js.map +1 -0
- package/dist/agent/team-notes.js +288 -0
- package/dist/agent/team-notes.js.map +1 -0
- package/dist/agent/token-meter.js +31 -0
- package/dist/agent/token-meter.js.map +1 -0
- package/dist/agent/triage.js +506 -0
- package/dist/agent/triage.js.map +1 -0
- package/dist/api.js +1047 -0
- package/dist/api.js.map +1 -0
- package/dist/approval-targets.js +107 -0
- package/dist/approval-targets.js.map +1 -0
- package/dist/bee/cloudflared.js +108 -0
- package/dist/bee/cloudflared.js.map +1 -0
- package/dist/bee/health.js +99 -0
- package/dist/bee/health.js.map +1 -0
- package/dist/bee/hive-client.js +75 -0
- package/dist/bee/hive-client.js.map +1 -0
- package/dist/bee/identity.js +39 -0
- package/dist/bee/identity.js.map +1 -0
- package/dist/bee/models.js +510 -0
- package/dist/bee/models.js.map +1 -0
- package/dist/bee/pairing.js +351 -0
- package/dist/bee/pairing.js.map +1 -0
- package/dist/bee/relay-client.js +50 -0
- package/dist/bee/relay-client.js.map +1 -0
- package/dist/bee/routes.js +214 -0
- package/dist/bee/routes.js.map +1 -0
- package/dist/bee/setup-page.js +223 -0
- package/dist/bee/setup-page.js.map +1 -0
- package/dist/bee/state.js +41 -0
- package/dist/bee/state.js.map +1 -0
- package/dist/bee/status.js +117 -0
- package/dist/bee/status.js.map +1 -0
- package/dist/bee/tunnel-manager.js +245 -0
- package/dist/bee/tunnel-manager.js.map +1 -0
- package/dist/bee/updater.js +268 -0
- package/dist/bee/updater.js.map +1 -0
- package/dist/boot.js +41 -0
- package/dist/boot.js.map +1 -0
- package/dist/call/index.js +145 -0
- package/dist/call/index.js.map +1 -0
- package/dist/case-folders.js +294 -0
- package/dist/case-folders.js.map +1 -0
- package/dist/cases.js +554 -0
- package/dist/cases.js.map +1 -0
- package/dist/chat-blocks.js +709 -0
- package/dist/chat-blocks.js.map +1 -0
- package/dist/chat-parse-util.js +38 -0
- package/dist/chat-parse-util.js.map +1 -0
- package/dist/chat-results.js +96 -0
- package/dist/chat-results.js.map +1 -0
- package/dist/chat-stats.js +193 -0
- package/dist/chat-stats.js.map +1 -0
- package/dist/chat-view-specs.js +186 -0
- package/dist/chat-view-specs.js.map +1 -0
- package/dist/cli/doctor-ops.js +361 -0
- package/dist/cli/doctor-ops.js.map +1 -0
- package/dist/cli/doctor.js +289 -0
- package/dist/cli/doctor.js.map +1 -0
- package/dist/cli/dogfood.js +70 -0
- package/dist/cli/dogfood.js.map +1 -0
- package/dist/cli/env-file.js +34 -0
- package/dist/cli/env-file.js.map +1 -0
- package/dist/cli/eval-chat-cases.js +359 -0
- package/dist/cli/eval-chat-cases.js.map +1 -0
- package/dist/cli/eval-chat-results.js +85 -0
- package/dist/cli/eval-chat-results.js.map +1 -0
- package/dist/cli/eval-chat.js +508 -0
- package/dist/cli/eval-chat.js.map +1 -0
- package/dist/cli/eval-designer.js +67 -0
- package/dist/cli/eval-designer.js.map +1 -0
- package/dist/cli/eval-qa-review.js +284 -0
- package/dist/cli/eval-qa-review.js.map +1 -0
- package/dist/cli/golden-export.js +35 -0
- package/dist/cli/golden-export.js.map +1 -0
- package/dist/cli/index.js +90 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init.js +746 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/scan-repo.js +199 -0
- package/dist/cli/scan-repo.js.map +1 -0
- package/dist/cli/seed.js +108 -0
- package/dist/cli/seed.js.map +1 -0
- package/dist/cli/tinybee.js +247 -0
- package/dist/cli/tinybee.js.map +1 -0
- package/dist/config.js +138 -0
- package/dist/config.js.map +1 -0
- package/dist/connect/aivision-local.js +833 -0
- package/dist/connect/aivision-local.js.map +1 -0
- package/dist/connect/aivision-token.js +84 -0
- package/dist/connect/aivision-token.js.map +1 -0
- package/dist/connect/crypto.js +65 -0
- package/dist/connect/crypto.js.map +1 -0
- package/dist/connect/deps.js +110 -0
- package/dist/connect/deps.js.map +1 -0
- package/dist/connect/engines.js +144 -0
- package/dist/connect/engines.js.map +1 -0
- package/dist/connect/github-access.js +74 -0
- package/dist/connect/github-access.js.map +1 -0
- package/dist/connect/github-app.js +129 -0
- package/dist/connect/github-app.js.map +1 -0
- package/dist/connect/index.js +9 -0
- package/dist/connect/index.js.map +1 -0
- package/dist/connect/model-choices.js +201 -0
- package/dist/connect/model-choices.js.map +1 -0
- package/dist/connect/providers.js +900 -0
- package/dist/connect/providers.js.map +1 -0
- package/dist/connect/readiness.js +232 -0
- package/dist/connect/readiness.js.map +1 -0
- package/dist/connect/redact.js +16 -0
- package/dist/connect/redact.js.map +1 -0
- package/dist/connect/registry.js +406 -0
- package/dist/connect/registry.js.map +1 -0
- package/dist/connect/store.js +283 -0
- package/dist/connect/store.js.map +1 -0
- package/dist/connect/tunnel.js +134 -0
- package/dist/connect/tunnel.js.map +1 -0
- package/dist/connect/types.js +8 -0
- package/dist/connect/types.js.map +1 -0
- package/dist/connections/index.js +5 -0
- package/dist/connections/index.js.map +1 -0
- package/dist/connections/manager.js +223 -0
- package/dist/connections/manager.js.map +1 -0
- package/dist/connections/presets.js +264 -0
- package/dist/connections/presets.js.map +1 -0
- package/dist/connections/store.js +132 -0
- package/dist/connections/store.js.map +1 -0
- package/dist/connections/tools.js +206 -0
- package/dist/connections/tools.js.map +1 -0
- package/dist/db-schema.js +355 -0
- package/dist/db-schema.js.map +1 -0
- package/dist/db.js +379 -0
- package/dist/db.js.map +1 -0
- package/dist/delivery.js +49 -0
- package/dist/delivery.js.map +1 -0
- package/dist/deploy-wait.js +701 -0
- package/dist/deploy-wait.js.map +1 -0
- package/dist/disk.js +243 -0
- package/dist/disk.js.map +1 -0
- package/dist/dot-health.js +966 -0
- package/dist/dot-health.js.map +1 -0
- package/dist/dot-remove.js +246 -0
- package/dist/dot-remove.js.map +1 -0
- package/dist/events.js +38 -0
- package/dist/events.js.map +1 -0
- package/dist/evidence-pack.js +423 -0
- package/dist/evidence-pack.js.map +1 -0
- package/dist/flows/answers.js +289 -0
- package/dist/flows/answers.js.map +1 -0
- package/dist/flows/change.js +811 -0
- package/dist/flows/change.js.map +1 -0
- package/dist/flows/checkStatus.js +524 -0
- package/dist/flows/checkStatus.js.map +1 -0
- package/dist/flows/ci.js +236 -0
- package/dist/flows/ci.js.map +1 -0
- package/dist/flows/ciFailure.js +76 -0
- package/dist/flows/ciFailure.js.map +1 -0
- package/dist/flows/common.js +522 -0
- package/dist/flows/common.js.map +1 -0
- package/dist/flows/default-branch.js +29 -0
- package/dist/flows/default-branch.js.map +1 -0
- package/dist/flows/defectDraft.js +447 -0
- package/dist/flows/defectDraft.js.map +1 -0
- package/dist/flows/defects.js +623 -0
- package/dist/flows/defects.js.map +1 -0
- package/dist/flows/digest.js +295 -0
- package/dist/flows/digest.js.map +1 -0
- package/dist/flows/impact.js +82 -0
- package/dist/flows/impact.js.map +1 -0
- package/dist/flows/index.js +23 -0
- package/dist/flows/index.js.map +1 -0
- package/dist/flows/intent-sources.js +72 -0
- package/dist/flows/intent-sources.js.map +1 -0
- package/dist/flows/mergeCases.js +169 -0
- package/dist/flows/mergeCases.js.map +1 -0
- package/dist/flows/nightlyRun.js +82 -0
- package/dist/flows/nightlyRun.js.map +1 -0
- package/dist/flows/outage.js +64 -0
- package/dist/flows/outage.js.map +1 -0
- package/dist/flows/postMerge.js +5 -0
- package/dist/flows/postMerge.js.map +1 -0
- package/dist/flows/prCheck.js +5 -0
- package/dist/flows/prCheck.js.map +1 -0
- package/dist/flows/prTarget.js +66 -0
- package/dist/flows/prTarget.js.map +1 -0
- package/dist/flows/qa.js +363 -0
- package/dist/flows/qa.js.map +1 -0
- package/dist/flows/questions.js +160 -0
- package/dist/flows/questions.js.map +1 -0
- package/dist/flows/recovery.js +64 -0
- package/dist/flows/recovery.js.map +1 -0
- package/dist/flows/rerun.js +50 -0
- package/dist/flows/rerun.js.map +1 -0
- package/dist/flows/testDesign.js +294 -0
- package/dist/flows/testDesign.js.map +1 -0
- package/dist/flows/testDesignJob.js +46 -0
- package/dist/flows/testDesignJob.js.map +1 -0
- package/dist/flows/testRepo.js +228 -0
- package/dist/flows/testRepo.js.map +1 -0
- package/dist/flows/testSavings.js +381 -0
- package/dist/flows/testSavings.js.map +1 -0
- package/dist/flows/testStatus.js +343 -0
- package/dist/flows/testStatus.js.map +1 -0
- package/dist/flows/ticketContext.js +425 -0
- package/dist/flows/ticketContext.js.map +1 -0
- package/dist/flows/ticketReview.js +249 -0
- package/dist/flows/ticketReview.js.map +1 -0
- package/dist/flows/ticks.js +73 -0
- package/dist/flows/ticks.js.map +1 -0
- package/dist/hive/admin.js +122 -0
- package/dist/hive/admin.js.map +1 -0
- package/dist/hive/auth.js +130 -0
- package/dist/hive/auth.js.map +1 -0
- package/dist/hive/billing.js +278 -0
- package/dist/hive/billing.js.map +1 -0
- package/dist/hive/config.js +63 -0
- package/dist/hive/config.js.map +1 -0
- package/dist/hive/db.js +186 -0
- package/dist/hive/db.js.map +1 -0
- package/dist/hive/embed.js +76 -0
- package/dist/hive/embed.js.map +1 -0
- package/dist/hive/errors.js +17 -0
- package/dist/hive/errors.js.map +1 -0
- package/dist/hive/google.js +75 -0
- package/dist/hive/google.js.map +1 -0
- package/dist/hive/index.js +25 -0
- package/dist/hive/index.js.map +1 -0
- package/dist/hive/jwt.js +57 -0
- package/dist/hive/jwt.js.map +1 -0
- package/dist/hive/keys.js +61 -0
- package/dist/hive/keys.js.map +1 -0
- package/dist/hive/mail.js +27 -0
- package/dist/hive/mail.js.map +1 -0
- package/dist/hive/native-push.js +255 -0
- package/dist/hive/native-push.js.map +1 -0
- package/dist/hive/pairing.js +342 -0
- package/dist/hive/pairing.js.map +1 -0
- package/dist/hive/presence.js +121 -0
- package/dist/hive/presence.js.map +1 -0
- package/dist/hive/push.js +185 -0
- package/dist/hive/push.js.map +1 -0
- package/dist/hive/relay.js +205 -0
- package/dist/hive/relay.js.map +1 -0
- package/dist/hive/routes.js +228 -0
- package/dist/hive/routes.js.map +1 -0
- package/dist/hive/server.js +12 -0
- package/dist/hive/server.js.map +1 -0
- package/dist/hive/session.js +28 -0
- package/dist/hive/session.js.map +1 -0
- package/dist/hive/tenants.js +287 -0
- package/dist/hive/tenants.js.map +1 -0
- package/dist/hive/tunnel/cloudflare.js +160 -0
- package/dist/hive/tunnel/cloudflare.js.map +1 -0
- package/dist/hive/tunnel/mock.js +52 -0
- package/dist/hive/tunnel/mock.js.map +1 -0
- package/dist/hive/tunnel/provider.js +19 -0
- package/dist/hive/tunnel/provider.js.map +1 -0
- package/dist/hive/updates.js +61 -0
- package/dist/hive/updates.js.map +1 -0
- package/dist/http-error.js +11 -0
- package/dist/http-error.js.map +1 -0
- package/dist/impact/classify.js +226 -0
- package/dist/impact/classify.js.map +1 -0
- package/dist/impact/find.js +259 -0
- package/dist/impact/find.js.map +1 -0
- package/dist/impact/pr.js +54 -0
- package/dist/impact/pr.js.map +1 -0
- package/dist/impact/store.js +365 -0
- package/dist/impact/store.js.map +1 -0
- package/dist/impact/text.js +158 -0
- package/dist/impact/text.js.map +1 -0
- package/dist/index.js +288 -0
- package/dist/index.js.map +1 -0
- package/dist/integrations/adf.js +397 -0
- package/dist/integrations/adf.js.map +1 -0
- package/dist/integrations/bot-marker.js +103 -0
- package/dist/integrations/bot-marker.js.map +1 -0
- package/dist/integrations/github-issues.js +705 -0
- package/dist/integrations/github-issues.js.map +1 -0
- package/dist/integrations/github.js +1284 -0
- package/dist/integrations/github.js.map +1 -0
- package/dist/integrations/issue-create.js +60 -0
- package/dist/integrations/issue-create.js.map +1 -0
- package/dist/integrations/jira.js +279 -0
- package/dist/integrations/jira.js.map +1 -0
- package/dist/integrations/linear.js +273 -0
- package/dist/integrations/linear.js.map +1 -0
- package/dist/integrations/own-identity-warm.js +47 -0
- package/dist/integrations/own-identity-warm.js.map +1 -0
- package/dist/integrations/own-identity.js +39 -0
- package/dist/integrations/own-identity.js.map +1 -0
- package/dist/integrations/status-change.js +42 -0
- package/dist/integrations/status-change.js.map +1 -0
- package/dist/lead-defects.js +202 -0
- package/dist/lead-defects.js.map +1 -0
- package/dist/lead-digest.js +333 -0
- package/dist/lead-digest.js.map +1 -0
- package/dist/lead-release.js +286 -0
- package/dist/lead-release.js.map +1 -0
- package/dist/lead.js +328 -0
- package/dist/lead.js.map +1 -0
- package/dist/legacy-names.js +51 -0
- package/dist/legacy-names.js.map +1 -0
- package/dist/limits.js +44 -0
- package/dist/limits.js.map +1 -0
- package/dist/link-trust.js +224 -0
- package/dist/link-trust.js.map +1 -0
- package/dist/links.js +219 -0
- package/dist/links.js.map +1 -0
- package/dist/loopback.js +35 -0
- package/dist/loopback.js.map +1 -0
- package/dist/model-cap.js +60 -0
- package/dist/model-cap.js.map +1 -0
- package/dist/model-usage.js +173 -0
- package/dist/model-usage.js.map +1 -0
- package/dist/monitor/backfill.js +39 -0
- package/dist/monitor/backfill.js.map +1 -0
- package/dist/monitor/browser-probe.js +198 -0
- package/dist/monitor/browser-probe.js.map +1 -0
- package/dist/monitor/catch-up.js +31 -0
- package/dist/monitor/catch-up.js.map +1 -0
- package/dist/monitor/pollers.js +1606 -0
- package/dist/monitor/pollers.js.map +1 -0
- package/dist/monitor/probes.js +178 -0
- package/dist/monitor/probes.js.map +1 -0
- package/dist/monitor/self-health.js +228 -0
- package/dist/monitor/self-health.js.map +1 -0
- package/dist/monitor/statuspage.js +105 -0
- package/dist/monitor/statuspage.js.map +1 -0
- package/dist/nightly-scheduler.js +127 -0
- package/dist/nightly-scheduler.js.map +1 -0
- package/dist/nightly.js +141 -0
- package/dist/nightly.js.map +1 -0
- package/dist/notify/headline.js +114 -0
- package/dist/notify/headline.js.map +1 -0
- package/dist/notify/push.js +174 -0
- package/dist/notify/push.js.map +1 -0
- package/dist/object-cards.js +585 -0
- package/dist/object-cards.js.map +1 -0
- package/dist/object-links.js +47 -0
- package/dist/object-links.js.map +1 -0
- package/dist/onboarding-checks.js +141 -0
- package/dist/onboarding-checks.js.map +1 -0
- package/dist/onboarding.js +401 -0
- package/dist/onboarding.js.map +1 -0
- package/dist/path-filters.js +62 -0
- package/dist/path-filters.js.map +1 -0
- package/dist/phone/alerts.js +67 -0
- package/dist/phone/alerts.js.map +1 -0
- package/dist/phone/auth.js +240 -0
- package/dist/phone/auth.js.map +1 -0
- package/dist/phone/pairing.js +442 -0
- package/dist/phone/pairing.js.map +1 -0
- package/dist/phone/prefs.js +27 -0
- package/dist/phone/prefs.js.map +1 -0
- package/dist/phone/tailscale.js +161 -0
- package/dist/phone/tailscale.js.map +1 -0
- package/dist/pr-scope.js +441 -0
- package/dist/pr-scope.js.map +1 -0
- package/dist/proc.js +156 -0
- package/dist/proc.js.map +1 -0
- package/dist/profile/cache.js +108 -0
- package/dist/profile/cache.js.map +1 -0
- package/dist/profile/ci.js +216 -0
- package/dist/profile/ci.js.map +1 -0
- package/dist/profile/index.js +8 -0
- package/dist/profile/index.js.map +1 -0
- package/dist/profile/job.js +135 -0
- package/dist/profile/job.js.map +1 -0
- package/dist/profile/queries/go-tags.scm +47 -0
- package/dist/profile/queries/java-tags.scm +25 -0
- package/dist/profile/queries/javascript-tags.scm +104 -0
- package/dist/profile/queries/python-tags.scm +19 -0
- package/dist/profile/queries/typescript-tags.scm +28 -0
- package/dist/profile/scan-thread.js +42 -0
- package/dist/profile/scan-thread.js.map +1 -0
- package/dist/profile/scan-worker.js +16 -0
- package/dist/profile/scan-worker.js.map +1 -0
- package/dist/profile/scan.js +351 -0
- package/dist/profile/scan.js.map +1 -0
- package/dist/profile/stack.js +528 -0
- package/dist/profile/stack.js.map +1 -0
- package/dist/profile/store.js +35 -0
- package/dist/profile/store.js.map +1 -0
- package/dist/profile/tests.js +399 -0
- package/dist/profile/tests.js.map +1 -0
- package/dist/queue.js +350 -0
- package/dist/queue.js.map +1 -0
- package/dist/refs.js +756 -0
- package/dist/refs.js.map +1 -0
- package/dist/release-notes.js +272 -0
- package/dist/release-notes.js.map +1 -0
- package/dist/reports/build.js +243 -0
- package/dist/reports/build.js.map +1 -0
- package/dist/reports/index.js +211 -0
- package/dist/reports/index.js.map +1 -0
- package/dist/reports/store.js +150 -0
- package/dist/reports/store.js.map +1 -0
- package/dist/reports/ticket-titles.js +124 -0
- package/dist/reports/ticket-titles.js.map +1 -0
- package/dist/reports/trace-map.js +1278 -0
- package/dist/reports/trace-map.js.map +1 -0
- package/dist/reports/traceability.js +422 -0
- package/dist/reports/traceability.js.map +1 -0
- package/dist/repos/mirror.js +356 -0
- package/dist/repos/mirror.js.map +1 -0
- package/dist/repos-github.js +108 -0
- package/dist/repos-github.js.map +1 -0
- package/dist/repos.js +891 -0
- package/dist/repos.js.map +1 -0
- package/dist/review/backend-anthropic.js +90 -0
- package/dist/review/backend-anthropic.js.map +1 -0
- package/dist/review/backend-cli.js +248 -0
- package/dist/review/backend-cli.js.map +1 -0
- package/dist/review/backend-codex.js +81 -0
- package/dist/review/backend-codex.js.map +1 -0
- package/dist/review/backend-mock.js +686 -0
- package/dist/review/backend-mock.js.map +1 -0
- package/dist/review/backend-only.js +168 -0
- package/dist/review/backend-only.js.map +1 -0
- package/dist/review/backend.js +105 -0
- package/dist/review/backend.js.map +1 -0
- package/dist/review/baseline/render-v1.js +483 -0
- package/dist/review/baseline/render-v1.js.map +1 -0
- package/dist/review/change-summary.js +197 -0
- package/dist/review/change-summary.js.map +1 -0
- package/dist/review/changelog.js +40 -0
- package/dist/review/changelog.js.map +1 -0
- package/dist/review/check.js +494 -0
- package/dist/review/check.js.map +1 -0
- package/dist/review/checkout.js +185 -0
- package/dist/review/checkout.js.map +1 -0
- package/dist/review/classify.js +178 -0
- package/dist/review/classify.js.map +1 -0
- package/dist/review/coverage.js +347 -0
- package/dist/review/coverage.js.map +1 -0
- package/dist/review/decision-eval.js +121 -0
- package/dist/review/decision-eval.js.map +1 -0
- package/dist/review/deliver.js +104 -0
- package/dist/review/deliver.js.map +1 -0
- package/dist/review/designer-eval.js +280 -0
- package/dist/review/designer-eval.js.map +1 -0
- package/dist/review/dogfood-digest.js +136 -0
- package/dist/review/dogfood-digest.js.map +1 -0
- package/dist/review/dogfood-eval.js +486 -0
- package/dist/review/dogfood-eval.js.map +1 -0
- package/dist/review/environment.js +109 -0
- package/dist/review/environment.js.map +1 -0
- package/dist/review/eval.js +405 -0
- package/dist/review/eval.js.map +1 -0
- package/dist/review/glob.js +110 -0
- package/dist/review/glob.js.map +1 -0
- package/dist/review/golden-export.js +219 -0
- package/dist/review/golden-export.js.map +1 -0
- package/dist/review/impact.js +406 -0
- package/dist/review/impact.js.map +1 -0
- package/dist/review/imports.js +542 -0
- package/dist/review/imports.js.map +1 -0
- package/dist/review/index.js +512 -0
- package/dist/review/index.js.map +1 -0
- package/dist/review/intent.js +128 -0
- package/dist/review/intent.js.map +1 -0
- package/dist/review/lenses.js +167 -0
- package/dist/review/lenses.js.map +1 -0
- package/dist/review/linked-tickets.js +293 -0
- package/dist/review/linked-tickets.js.map +1 -0
- package/dist/review/merge.js +370 -0
- package/dist/review/merge.js.map +1 -0
- package/dist/review/on-demand.js +218 -0
- package/dist/review/on-demand.js.map +1 -0
- package/dist/review/prompt-eval.js +299 -0
- package/dist/review/prompt-eval.js.map +1 -0
- package/dist/review/prompt-variants.js +37 -0
- package/dist/review/prompt-variants.js.map +1 -0
- package/dist/review/readability-eval.js +217 -0
- package/dist/review/readability-eval.js.map +1 -0
- package/dist/review/readability.js +295 -0
- package/dist/review/readability.js.map +1 -0
- package/dist/review/render.js +2409 -0
- package/dist/review/render.js.map +1 -0
- package/dist/review/repo.js +194 -0
- package/dist/review/repo.js.map +1 -0
- package/dist/review/report-plan.js +90 -0
- package/dist/review/report-plan.js.map +1 -0
- package/dist/review/routes.js +195 -0
- package/dist/review/routes.js.map +1 -0
- package/dist/review/run-summary.js +34 -0
- package/dist/review/run-summary.js.map +1 -0
- package/dist/review/run.js +245 -0
- package/dist/review/run.js.map +1 -0
- package/dist/review/scope.js +454 -0
- package/dist/review/scope.js.map +1 -0
- package/dist/review/security.js +432 -0
- package/dist/review/security.js.map +1 -0
- package/dist/review/signals.js +550 -0
- package/dist/review/signals.js.map +1 -0
- package/dist/review/specs.js +150 -0
- package/dist/review/specs.js.map +1 -0
- package/dist/review/ste.js +133 -0
- package/dist/review/ste.js.map +1 -0
- package/dist/review/test-repo-file.js +18 -0
- package/dist/review/test-repo-file.js.map +1 -0
- package/dist/review/test-repo-lines.js +53 -0
- package/dist/review/test-repo-lines.js.map +1 -0
- package/dist/review/ticket-on-demand.js +232 -0
- package/dist/review/ticket-on-demand.js.map +1 -0
- package/dist/review/yaml.js +298 -0
- package/dist/review/yaml.js.map +1 -0
- package/dist/room.js +361 -0
- package/dist/room.js.map +1 -0
- package/dist/routes/activity.js +219 -0
- package/dist/routes/activity.js.map +1 -0
- package/dist/routes/call.js +29 -0
- package/dist/routes/call.js.map +1 -0
- package/dist/routes/cards.js +40 -0
- package/dist/routes/cards.js.map +1 -0
- package/dist/routes/cases.js +133 -0
- package/dist/routes/cases.js.map +1 -0
- package/dist/routes/connect.js +268 -0
- package/dist/routes/connect.js.map +1 -0
- package/dist/routes/connection-presets.js +32 -0
- package/dist/routes/connection-presets.js.map +1 -0
- package/dist/routes/connections.js +177 -0
- package/dist/routes/connections.js.map +1 -0
- package/dist/routes/disk.js +19 -0
- package/dist/routes/disk.js.map +1 -0
- package/dist/routes/docs.js +71 -0
- package/dist/routes/docs.js.map +1 -0
- package/dist/routes/dot-suggest.js +1002 -0
- package/dist/routes/dot-suggest.js.map +1 -0
- package/dist/routes/evidence-pack.js +16 -0
- package/dist/routes/evidence-pack.js.map +1 -0
- package/dist/routes/health.js +121 -0
- package/dist/routes/health.js.map +1 -0
- package/dist/routes/impact.js +48 -0
- package/dist/routes/impact.js.map +1 -0
- package/dist/routes/lead.js +50 -0
- package/dist/routes/lead.js.map +1 -0
- package/dist/routes/links.js +13 -0
- package/dist/routes/links.js.map +1 -0
- package/dist/routes/markdown.js +62 -0
- package/dist/routes/markdown.js.map +1 -0
- package/dist/routes/onboarding.js +47 -0
- package/dist/routes/onboarding.js.map +1 -0
- package/dist/routes/phone.js +231 -0
- package/dist/routes/phone.js.map +1 -0
- package/dist/routes/release-notes.js +30 -0
- package/dist/routes/release-notes.js.map +1 -0
- package/dist/routes/reports.js +102 -0
- package/dist/routes/reports.js.map +1 -0
- package/dist/routes/repos.js +91 -0
- package/dist/routes/repos.js.map +1 -0
- package/dist/routes/review.js +283 -0
- package/dist/routes/review.js.map +1 -0
- package/dist/routes/roles.js +114 -0
- package/dist/routes/roles.js.map +1 -0
- package/dist/routes/room.js +21 -0
- package/dist/routes/room.js.map +1 -0
- package/dist/routes/runner.js +156 -0
- package/dist/routes/runner.js.map +1 -0
- package/dist/routes/runs.js +84 -0
- package/dist/routes/runs.js.map +1 -0
- package/dist/routes/setup.js +18 -0
- package/dist/routes/setup.js.map +1 -0
- package/dist/routes/test-runs.js +220 -0
- package/dist/routes/test-runs.js.map +1 -0
- package/dist/routes/ticket-status.js +17 -0
- package/dist/routes/ticket-status.js.map +1 -0
- package/dist/routes/traceability.js +143 -0
- package/dist/routes/traceability.js.map +1 -0
- package/dist/routes/true.js +142 -0
- package/dist/routes/true.js.map +1 -0
- package/dist/routes/updates.js +70 -0
- package/dist/routes/updates.js.map +1 -0
- package/dist/runner/adapters/index.js +12 -0
- package/dist/runner/adapters/index.js.map +1 -0
- package/dist/runner/adapters/playwright.js +68 -0
- package/dist/runner/adapters/playwright.js.map +1 -0
- package/dist/runner/aivision/cases.js +172 -0
- package/dist/runner/aivision/cases.js.map +1 -0
- package/dist/runner/aivision/cli.js +201 -0
- package/dist/runner/aivision/cli.js.map +1 -0
- package/dist/runner/aivision/index.js +281 -0
- package/dist/runner/aivision/index.js.map +1 -0
- package/dist/runner/aivision/server.js +394 -0
- package/dist/runner/aivision/server.js.map +1 -0
- package/dist/runner/aivision/shared.js +140 -0
- package/dist/runner/aivision/shared.js.map +1 -0
- package/dist/runner/artifact-cap.js +189 -0
- package/dist/runner/artifact-cap.js.map +1 -0
- package/dist/runner/artifacts.js +433 -0
- package/dist/runner/artifacts.js.map +1 -0
- package/dist/runner/command.js +564 -0
- package/dist/runner/command.js.map +1 -0
- package/dist/runner/copyfiles.js +221 -0
- package/dist/runner/copyfiles.js.map +1 -0
- package/dist/runner/engines.js +32 -0
- package/dist/runner/engines.js.map +1 -0
- package/dist/runner/evidence.js +114 -0
- package/dist/runner/evidence.js.map +1 -0
- package/dist/runner/host-env.js +160 -0
- package/dist/runner/host-env.js.map +1 -0
- package/dist/runner/index.js +1221 -0
- package/dist/runner/index.js.map +1 -0
- package/dist/runner/observe.js +603 -0
- package/dist/runner/observe.js.map +1 -0
- package/dist/runner/playwright-json.js +108 -0
- package/dist/runner/playwright-json.js.map +1 -0
- package/dist/runner/pwgen/exec.js +192 -0
- package/dist/runner/pwgen/exec.js.map +1 -0
- package/dist/runner/pwgen/explore.js +111 -0
- package/dist/runner/pwgen/explore.js.map +1 -0
- package/dist/runner/pwgen/index.js +423 -0
- package/dist/runner/pwgen/index.js.map +1 -0
- package/dist/runner/pwgen/propose.js +69 -0
- package/dist/runner/pwgen/propose.js.map +1 -0
- package/dist/runner/pwgen/source.js +632 -0
- package/dist/runner/pwgen/source.js.map +1 -0
- package/dist/runner/setup.js +222 -0
- package/dist/runner/setup.js.map +1 -0
- package/dist/runner/step-reporter.js +107 -0
- package/dist/runner/step-reporter.js.map +1 -0
- package/dist/runner/test-repo.js +326 -0
- package/dist/runner/test-repo.js.map +1 -0
- package/dist/runner/typed-value.js +46 -0
- package/dist/runner/typed-value.js.map +1 -0
- package/dist/runner/verify.js +133 -0
- package/dist/runner/verify.js.map +1 -0
- package/dist/runner/worktree.js +349 -0
- package/dist/runner/worktree.js.map +1 -0
- package/dist/sandbox.js +61 -0
- package/dist/sandbox.js.map +1 -0
- package/dist/server.js +262 -0
- package/dist/server.js.map +1 -0
- package/dist/shadow-week.js +126 -0
- package/dist/shadow-week.js.map +1 -0
- package/dist/shutdown.js +92 -0
- package/dist/shutdown.js.map +1 -0
- package/dist/slack/index.js +645 -0
- package/dist/slack/index.js.map +1 -0
- package/dist/stages/config.js +147 -0
- package/dist/stages/config.js.map +1 -0
- package/dist/stages/index.js +41 -0
- package/dist/stages/index.js.map +1 -0
- package/dist/suites.js +293 -0
- package/dist/suites.js.map +1 -0
- package/dist/target-url.js +116 -0
- package/dist/target-url.js.map +1 -0
- package/dist/test-runs.js +662 -0
- package/dist/test-runs.js.map +1 -0
- package/dist/ticket-keys.js +97 -0
- package/dist/ticket-keys.js.map +1 -0
- package/dist/ticket-supersede.js +105 -0
- package/dist/ticket-supersede.js.map +1 -0
- package/dist/tinybee/ed25519.js +99 -0
- package/dist/tinybee/ed25519.js.map +1 -0
- package/dist/tinybee/models-routes.js +19 -0
- package/dist/tinybee/models-routes.js.map +1 -0
- package/dist/tinybee/pair-payload.js +41 -0
- package/dist/tinybee/pair-payload.js.map +1 -0
- package/dist/tinybee/update-manifest.js +77 -0
- package/dist/tinybee/update-manifest.js.map +1 -0
- package/dist/tinybee/well-known.js +64 -0
- package/dist/tinybee/well-known.js.map +1 -0
- package/dist/tinybee/workflows/activity.js +23 -0
- package/dist/tinybee/workflows/activity.js.map +1 -0
- package/dist/tinybee/workflows/cron.js +210 -0
- package/dist/tinybee/workflows/cron.js.map +1 -0
- package/dist/tinybee/workflows/engine.js +371 -0
- package/dist/tinybee/workflows/engine.js.map +1 -0
- package/dist/tinybee/workflows/from-text.js +203 -0
- package/dist/tinybee/workflows/from-text.js.map +1 -0
- package/dist/tinybee/workflows/routes.js +189 -0
- package/dist/tinybee/workflows/routes.js.map +1 -0
- package/dist/tinybee/workflows/scheduler.js +100 -0
- package/dist/tinybee/workflows/scheduler.js.map +1 -0
- package/dist/tinybee/workflows/skills.js +227 -0
- package/dist/tinybee/workflows/skills.js.map +1 -0
- package/dist/tinybee/workflows/store.js +205 -0
- package/dist/tinybee/workflows/store.js.map +1 -0
- package/dist/tinybee/workflows/types.js +180 -0
- package/dist/tinybee/workflows/types.js.map +1 -0
- package/dist/tinybee/workflows/yaml.js +323 -0
- package/dist/tinybee/workflows/yaml.js.map +1 -0
- package/dist/true/api.js +495 -0
- package/dist/true/api.js.map +1 -0
- package/dist/true/client.js +292 -0
- package/dist/true/client.js.map +1 -0
- package/dist/true/clock.js +36 -0
- package/dist/true/clock.js.map +1 -0
- package/dist/true/events.js +125 -0
- package/dist/true/events.js.map +1 -0
- package/dist/true/hosts.js +98 -0
- package/dist/true/hosts.js.map +1 -0
- package/dist/true/index.js +29 -0
- package/dist/true/index.js.map +1 -0
- package/dist/true/key.js +54 -0
- package/dist/true/key.js.map +1 -0
- package/dist/true/line.js +69 -0
- package/dist/true/line.js.map +1 -0
- package/dist/true/mask.js +31 -0
- package/dist/true/mask.js.map +1 -0
- package/dist/true/names.js +114 -0
- package/dist/true/names.js.map +1 -0
- package/dist/true/outbox.js +233 -0
- package/dist/true/outbox.js.map +1 -0
- package/dist/true/replay.js +164 -0
- package/dist/true/replay.js.map +1 -0
- package/dist/true/settings.js +123 -0
- package/dist/true/settings.js.map +1 -0
- package/dist/true/status.js +189 -0
- package/dist/true/status.js.map +1 -0
- package/dist/true/sync-cases.js +222 -0
- package/dist/true/sync-cases.js.map +1 -0
- package/dist/true/sync-manual.js +295 -0
- package/dist/true/sync-manual.js.map +1 -0
- package/dist/true/sync-runs.js +109 -0
- package/dist/true/sync-runs.js.map +1 -0
- package/dist/true/worker.js +414 -0
- package/dist/true/worker.js.map +1 -0
- package/dist/types.js +32 -0
- package/dist/types.js.map +1 -0
- package/dist/updates/asks.js +38 -0
- package/dist/updates/asks.js.map +1 -0
- package/dist/updates/index.js +835 -0
- package/dist/updates/index.js.map +1 -0
- package/dist/updates/owner.js +24 -0
- package/dist/updates/owner.js.map +1 -0
- package/dist/updates/prefs.js +118 -0
- package/dist/updates/prefs.js.map +1 -0
- package/dist/updates/route.js +78 -0
- package/dist/updates/route.js.map +1 -0
- package/dist/updates/sources.js +345 -0
- package/dist/updates/sources.js.map +1 -0
- package/dist/updates/words.js +452 -0
- package/dist/updates/words.js.map +1 -0
- package/dist/version.js +53 -0
- package/dist/version.js.map +1 -0
- package/dist/view-compute.js +382 -0
- package/dist/view-compute.js.map +1 -0
- package/dist/view-spec.js +894 -0
- package/dist/view-spec.js.map +1 -0
- package/dist/web/apple-touch-icon.png +0 -0
- package/dist/web/assets/ApprovalCard-Br_z5z7G.css +1 -0
- package/dist/web/assets/ApprovalCard-CfGhGFo_.js +3 -0
- package/dist/web/assets/CardFromRef-BaTGmZ9c.js +2 -0
- package/dist/web/assets/CardFromRef-DO7i2uDd.css +1 -0
- package/dist/web/assets/Cases-8l_6UcLh.js +2 -0
- package/dist/web/assets/Cases-CpfED0uB.css +1 -0
- package/dist/web/assets/Chat-BKDiR0qt.js +3 -0
- package/dist/web/assets/Chat-DxjdyYUu.css +1 -0
- package/dist/web/assets/ConfirmDialog-CIMveD-b.js +2 -0
- package/dist/web/assets/ConnectSheet-Cm5vbZdR.js +4 -0
- package/dist/web/assets/Docs-HKGuY0WE.js +2 -0
- package/dist/web/assets/Docs-fodKx8S7.css +1 -0
- package/dist/web/assets/DotDetail-DskS3NWe.js +2 -0
- package/dist/web/assets/DotDetail-OWRSD_1x.css +1 -0
- package/dist/web/assets/DotStage-BZDclfzV.css +1 -0
- package/dist/web/assets/DotStage-CtMNzCN3.js +2 -0
- package/dist/web/assets/Dots-BqCsS34m.js +2 -0
- package/dist/web/assets/EvidencePack-DQ3kCJX4.js +2 -0
- package/dist/web/assets/EvidencePack-DV_9ETsA.css +1 -0
- package/dist/web/assets/FailedDeliveries-1F2kFcs9.js +2 -0
- package/dist/web/assets/FailedDeliveries-DXa2_VvI.css +1 -0
- package/dist/web/assets/Feed-5wiHRTFK.js +2 -0
- package/dist/web/assets/GfmView-C-NYaduC.js +4 -0
- package/dist/web/assets/ImpactList-C-RNuDB0.js +2 -0
- package/dist/web/assets/ImpactList-C612dFdw.css +1 -0
- package/dist/web/assets/Inbox-kc1bKVL_.js +2 -0
- package/dist/web/assets/JoinScreen-C3L4wmP3.js +2 -0
- package/dist/web/assets/JoinScreen-COeAUv5v.css +1 -0
- package/dist/web/assets/LeadDialog-N3Zul9Rw.js +8 -0
- package/dist/web/assets/LeadDialog-xyWjAuAS.css +1 -0
- package/dist/web/assets/ListeningLine-BRFNaczY.js +2 -0
- package/dist/web/assets/ListeningLine-pN8CgyiK.css +1 -0
- package/dist/web/assets/Mascots-pIyGpxtD.js +2 -0
- package/dist/web/assets/ModelChoices-BxO1zyyr.js +2 -0
- package/dist/web/assets/ModelChoices-DQJnVxfs.css +1 -0
- package/dist/web/assets/NewDotDialog-CDPUWmgN.js +2 -0
- package/dist/web/assets/NewDotDialog-DrPPqkSZ.css +1 -0
- package/dist/web/assets/NewTestRunDialog-BQBthVHE.js +2 -0
- package/dist/web/assets/NoBreakNames-Csxdqdt6.js +2 -0
- package/dist/web/assets/Pair-omex_K7H.js +2 -0
- package/dist/web/assets/PushSection-CmqHlE_2.js +2 -0
- package/dist/web/assets/ReleaseNotes-B6VqFewP.js +2 -0
- package/dist/web/assets/ReleaseNotes-DBJUL7Vm.css +1 -0
- package/dist/web/assets/ReportPage-BHS1xWEt.js +162 -0
- package/dist/web/assets/Reports-BEHnDIb1.js +2 -0
- package/dist/web/assets/ReviewChangeDialog-BVSt-cw-.js +2 -0
- package/dist/web/assets/ReviewChangeDialog-BbIlHRx0.css +1 -0
- package/dist/web/assets/Room-BoG_uge4.js +2 -0
- package/dist/web/assets/Room-evAfiW_e.css +1 -0
- package/dist/web/assets/RunActions-ZY7yf1AF.js +2 -0
- package/dist/web/assets/RunDetail-C8St20GQ.css +1 -0
- package/dist/web/assets/RunDetail-qADXjBvd.js +9 -0
- package/dist/web/assets/RunRow-BKEJv8ib.js +2 -0
- package/dist/web/assets/RunnerTry-Dgpp67-s.js +6 -0
- package/dist/web/assets/RunnerTry-DsweWOfO.css +1 -0
- package/dist/web/assets/Settings-fs-QJwNM.js +2 -0
- package/dist/web/assets/Setup-DI6hBEP-.js +2 -0
- package/dist/web/assets/TestRows-wh03T5Li.js +4 -0
- package/dist/web/assets/TestRun-DTOyF-Pv.js +2 -0
- package/dist/web/assets/TestRuns-ggDjm1Q8.js +2 -0
- package/dist/web/assets/TinyBeeAddLaptop-bc3pmh3T.js +2 -0
- package/dist/web/assets/TinyBeeAi-CkyOzhm5.js +2 -0
- package/dist/web/assets/TinyBeeConnections-B2K_P38u.js +2 -0
- package/dist/web/assets/TinyBeeHome-DSpxX5KS.js +3 -0
- package/dist/web/assets/TinyBeePlan-BamiLziu.js +2 -0
- package/dist/web/assets/TinyBeeSignup-vHcz0Wen.js +2 -0
- package/dist/web/assets/TinyBeeSkills-By9RLUse.js +2 -0
- package/dist/web/assets/TinyBeeWorkflow-DeNO7Jsh.js +2 -0
- package/dist/web/assets/TinyBeeWorkflowEditor-B6OGC2OT.js +2 -0
- package/dist/web/assets/TinyBeeWorkflows-BN4-h6lt.js +2 -0
- package/dist/web/assets/Trace-BBCI1aIB.css +1 -0
- package/dist/web/assets/Trace-aF92UWaH.js +2 -0
- package/dist/web/assets/UpdatesSection-CCWEbkue.js +2 -0
- package/dist/web/assets/Welcome-CCcy558Q.js +2 -0
- package/dist/web/assets/activity-C3qilrDm.css +1 -0
- package/dist/web/assets/activity-CBfTSxqw.js +2 -0
- package/dist/web/assets/authedMedia-BTYt_tyu.js +2 -0
- package/dist/web/assets/beeApi-BHW8Tx1s.js +2 -0
- package/dist/web/assets/data-CmXvx3Mj.js +2 -0
- package/dist/web/assets/engines-CfdJODFj.css +1 -0
- package/dist/web/assets/format-BBZ8vFyT.js +2 -0
- package/dist/web/assets/forms-Coc7p2UF.css +1 -0
- package/dist/web/assets/hivePush-DZJDokj7.js +2 -0
- package/dist/web/assets/impactModel-MY1OFuiP.js +2 -0
- package/dist/web/assets/index-B6qKm1NW.js +13 -0
- package/dist/web/assets/index-BvKyT1X7.css +1 -0
- package/dist/web/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/dist/web/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/dist/web/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/dist/web/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/dist/web/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/dist/web/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/dist/web/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/dist/web/assets/lead-BL_P_kri.css +1 -0
- package/dist/web/assets/model-BIQy5IVG.js +2 -0
- package/dist/web/assets/modelsApi-PUSySRQw.js +2 -0
- package/dist/web/assets/newDotModel-BaLLyw57.js +2 -0
- package/dist/web/assets/onboarding-bfKqnM4Z.css +1 -0
- package/dist/web/assets/phone-Rs7FoyG0.css +1 -0
- package/dist/web/assets/phoneApi-BG_1-M-D.js +3 -0
- package/dist/web/assets/releaseModel-EynYMVOC.js +2 -0
- package/dist/web/assets/releaseNotesModel-A1Rtv3cV.js +2 -0
- package/dist/web/assets/reports-BWaA1HhM.js +37 -0
- package/dist/web/assets/reports-DXwdWQsB.css +1 -0
- package/dist/web/assets/router-W6nZsQ33.js +2 -0
- package/dist/web/assets/runs-BY0Owv93.css +1 -0
- package/dist/web/assets/shadowWeekModel-BUUiww09.js +2 -0
- package/dist/web/assets/storage-yvL36AWt.js +2 -0
- package/dist/web/assets/store-IqrqHuFR.js +2 -0
- package/dist/web/assets/testruns-BzIZ0cR0.css +1 -0
- package/dist/web/assets/testruns-DyaqX4ji.js +2 -0
- package/dist/web/assets/true-9Q68cs5b.js +2 -0
- package/dist/web/assets/true-DPGBpoir.css +1 -0
- package/dist/web/assets/ui-DcmhYyYp.js +13 -0
- package/dist/web/assets/ui-DxEDgbIx.css +1 -0
- package/dist/web/assets/usePauseDot-BV60ZVy1.js +2 -0
- package/dist/web/assets/usePauseDot-NrSzMI1C.css +1 -0
- package/dist/web/assets/usePush-Dsm-TjlG.js +2 -0
- package/dist/web/assets/useRefChips-CbafNx59.js +68 -0
- package/dist/web/assets/useRestoreDot-D0B6W82H.js +2 -0
- package/dist/web/assets/view-worker-pyeVAhUT.js +2 -0
- package/dist/web/assets/workflowUi-QYsd3n9J.js +2 -0
- package/dist/web/favicon.png +0 -0
- package/dist/web/icon-192.png +0 -0
- package/dist/web/icon-512.png +0 -0
- package/dist/web/icon-maskable-512.png +0 -0
- package/dist/web/index.html +45 -0
- package/dist/web/manifest.webmanifest +23 -0
- package/dist/web/mascots/argus-attention.svg +7 -0
- package/dist/web/mascots/argus-ok.svg +7 -0
- package/dist/web/mascots/argus-outage.svg +7 -0
- package/dist/web/mascots/argus-running.svg +7 -0
- package/dist/web/mascots/bolt-attention.svg +7 -0
- package/dist/web/mascots/bolt-ok.svg +7 -0
- package/dist/web/mascots/bolt-outage.svg +7 -0
- package/dist/web/mascots/bolt-running.svg +7 -0
- package/dist/web/mascots/echo-attention.svg +7 -0
- package/dist/web/mascots/echo-ok.svg +7 -0
- package/dist/web/mascots/echo-outage.svg +7 -0
- package/dist/web/mascots/echo-running.svg +7 -0
- package/dist/web/mascots/lens-attention.svg +7 -0
- package/dist/web/mascots/lens-ok.svg +7 -0
- package/dist/web/mascots/lens-outage.svg +7 -0
- package/dist/web/mascots/lens-running.svg +7 -0
- package/dist/web/mascots/moss-attention.svg +7 -0
- package/dist/web/mascots/moss-ok.svg +7 -0
- package/dist/web/mascots/moss-outage.svg +7 -0
- package/dist/web/mascots/moss-running.svg +7 -0
- package/dist/web/mascots/nit-attention.svg +7 -0
- package/dist/web/mascots/nit-ok.svg +7 -0
- package/dist/web/mascots/nit-outage.svg +7 -0
- package/dist/web/mascots/nit-running.svg +7 -0
- package/dist/web/mascots/nova-attention.svg +7 -0
- package/dist/web/mascots/nova-ok.svg +7 -0
- package/dist/web/mascots/nova-outage.svg +7 -0
- package/dist/web/mascots/nova-running.svg +7 -0
- package/dist/web/mascots/penn-attention.svg +7 -0
- package/dist/web/mascots/penn-ok.svg +7 -0
- package/dist/web/mascots/penn-outage.svg +7 -0
- package/dist/web/mascots/penn-running.svg +7 -0
- package/dist/web/mascots/pip-attention.svg +7 -0
- package/dist/web/mascots/pip-ok.svg +7 -0
- package/dist/web/mascots/pip-outage.svg +7 -0
- package/dist/web/mascots/pip-running.svg +7 -0
- package/dist/web/mascots/ward-attention.svg +7 -0
- package/dist/web/mascots/ward-ok.svg +7 -0
- package/dist/web/mascots/ward-outage.svg +7 -0
- package/dist/web/mascots/ward-running.svg +7 -0
- package/dist/web/sw.js +98 -0
- package/dist/webhook-seen.js +53 -0
- package/dist/webhook-seen.js.map +1 -0
- package/dist/webhooks.js +625 -0
- package/dist/webhooks.js.map +1 -0
- package/dist/zip.js +63 -0
- package/dist/zip.js.map +1 -0
- package/docs/API-V2.md +532 -0
- package/docs/CHAT-EVAL.md +130 -0
- package/docs/CHAT-UI.md +416 -0
- package/docs/CONNECTIONS.md +89 -0
- package/docs/DATA-MODEL.md +518 -0
- package/docs/DEPLOY.md +577 -0
- package/docs/DOGFOOD-LEDGER.md +81 -0
- package/docs/GITHUB.md +148 -0
- package/docs/INTEGRATION-MODEL.md +200 -0
- package/docs/LEAD-DOT.md +553 -0
- package/docs/LIST-PATTERNS.md +52 -0
- package/docs/MULTI-REPO.md +766 -0
- package/docs/PARITY.md +276 -0
- package/docs/PR-REVIEW-QA.md +1266 -0
- package/docs/PR-REVIEW.md +357 -0
- package/docs/SANDBOX.md +320 -0
- package/docs/SETUP.md +776 -0
- package/docs/SLACK.md +80 -0
- package/docs/STAGES.md +619 -0
- package/docs/TRACE-MAP.md +89 -0
- package/docs/TRACKERS.md +225 -0
- package/docs/VOICE.md +36 -0
- package/docs/tinybee/00-README.md +45 -0
- package/docs/tinybee/01-PRD.md +216 -0
- package/docs/tinybee/02-architecture-pairing.md +199 -0
- package/docs/tinybee/03-local-agent.md +222 -0
- package/docs/tinybee/04-local-setup-ui.md +128 -0
- package/docs/tinybee/05-mobile-app-ux.md +199 -0
- package/docs/tinybee/06-environments-and-delivery.md +148 -0
- package/docs/tinybee/07-coding-agent-handoff.md +116 -0
- package/docs/tinybee/OPEN-QUESTIONS.md +26 -0
- package/docs/tinybee/PROGRESS.md +134 -0
- package/package.json +111 -4
|
@@ -0,0 +1,1703 @@
|
|
|
1
|
+
// Chat tools, their executor, and the entry point the HTTP API calls.
|
|
2
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
3
|
+
import { get, insert, list, newId } from '../db.js';
|
|
4
|
+
import { subscribe } from '../events.js';
|
|
5
|
+
import { enqueue } from '../queue.js';
|
|
6
|
+
import { bindingsFor, callConnectionTool, isMcpToolName, toolsForWatch } from '../connections/index.js';
|
|
7
|
+
import { getReport, proposeReportEditAction, reportContextFor, REPORT_EDIT_TOOL, scrubReportEdit } from '../reports/index.js';
|
|
8
|
+
import { recordAsk, recordAskForJob } from '../updates/asks.js';
|
|
9
|
+
import { liveJobId } from '../ticket-supersede.js';
|
|
10
|
+
import { parseChatBlock } from '../chat-blocks.js';
|
|
11
|
+
import { BLOCK_LOOKUP_TOOLS, blocksFromToolResult } from './chat-tool-blocks.js';
|
|
12
|
+
import { flakyBySpecBlocks, passRateBlocks } from '../chat-stats.js';
|
|
13
|
+
import { TEST_RESULT_KINDS, TEST_RESULT_STATUSES, testResultRows } from '../chat-results.js';
|
|
14
|
+
import { COMPOSE_VIEW_DEFINITION, COMPOSE_VIEW_TOOL, ResultRegistry, foreignDotOf, foreignSourceLine, refuseResult, VIEW_SOURCE_TOOLS, ViewStream, composeView, holdRows, holdViewBlocks, rowsSummary, tagResult, } from './chat-views.js';
|
|
15
|
+
import { casesForTicket, designedCasesHref, designedLine } from '../flows/testDesign.js';
|
|
16
|
+
import { isLead, membersOf, teamIds } from '../lead.js';
|
|
17
|
+
import { dispatchLeadTool, leadChatTools, releaseBlock, releaseText } from './chat-lead.js';
|
|
18
|
+
import { releaseCallFor } from '../lead-release.js';
|
|
19
|
+
import { pickChatView } from './chat-view.js';
|
|
20
|
+
import { buildTriageBlock, triageLine, triageWindowOf } from './triage.js';
|
|
21
|
+
import { getLlm } from './llm.js';
|
|
22
|
+
import { ModelUnavailableError } from './model-errors.js';
|
|
23
|
+
import { capLine, capState, collectModelCalls, tokensOfCalls, withModelContext } from '../model-usage.js';
|
|
24
|
+
import { withReasoning } from './reasoning.js';
|
|
25
|
+
import { scrubEcho, teamNotes } from './team-notes.js';
|
|
26
|
+
import { addMemory, listMemory } from './memory.js';
|
|
27
|
+
import { gate } from './rules.js';
|
|
28
|
+
import { addStep, finishRun, startRun } from './runs.js';
|
|
29
|
+
import { parseReviewTarget, requestReview } from '../review/on-demand.js';
|
|
30
|
+
import { fetchTicketRef, findTicketRefs, parseTicketInput, requestTestDesign, requestTicketReview } from '../review/ticket-on-demand.js';
|
|
31
|
+
import { config, liveEnv } from '../config.js';
|
|
32
|
+
import { PHONE_BLOCKED_MESSAGE } from '../phone/auth.js';
|
|
33
|
+
import { githubFor } from '../integrations/github.js';
|
|
34
|
+
import { hasAppRepo, watchRepos } from '../repos.js';
|
|
35
|
+
import { renderRefs } from '../refs.js';
|
|
36
|
+
import { buildObjectCard, objectOfCard, refKey, ticketToCard } from '../object-cards.js';
|
|
37
|
+
import { objectLinksIn } from '../object-links.js';
|
|
38
|
+
import { CaseInputError, casesBlock, listCases } from '../cases.js';
|
|
39
|
+
/** Added to the description of every lookup tool whose result the chat shows as a list (docs/CHAT-UI.md). */
|
|
40
|
+
const LIST_TOOL_NOTE = ' The chat shows the rows as a list the person can act on, so write one or two sentences about them and do not repeat every row. Set context to true when you read this only to decide what to do next; the chat then shows no list.';
|
|
41
|
+
/**
|
|
42
|
+
* get_ticket reads a ticket and shows no list. It shows the review the dot wrote for the ticket only when the
|
|
43
|
+
* person asks about that review, and a call marked `context` never does (docs/CHAT-UI.md).
|
|
44
|
+
*/
|
|
45
|
+
const TICKET_TOOL_NOTE = ' The chat shows the review the dot wrote for the ticket only when the person asks about that review. Set context to true when you read the ticket only to decide what to do next, such as before you review it or design test cases for it; the chat then shows no review.';
|
|
46
|
+
const TICKET_CONTEXT_PROPERTY = {
|
|
47
|
+
type: 'boolean',
|
|
48
|
+
description: 'True when you read the ticket only to decide what to do next, not to answer the question. The chat then shows no review of it.',
|
|
49
|
+
};
|
|
50
|
+
const CONTEXT_PROPERTY = {
|
|
51
|
+
type: 'boolean',
|
|
52
|
+
description: 'True when you read this only to decide what to do next, not to answer the question. The chat then shows no list.',
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* The lookup tools that answer with a block take an optional `context` flag, so a lookup the model makes only to
|
|
56
|
+
* choose its next step stays on the collapsed tool line and shows no list (KAT-136, KAT-133).
|
|
57
|
+
*/
|
|
58
|
+
function withContextFlag(tools) {
|
|
59
|
+
return tools.map((t) => {
|
|
60
|
+
const lookup = BLOCK_LOOKUP_TOOLS.includes(t.name);
|
|
61
|
+
if (!lookup && t.name !== 'get_ticket')
|
|
62
|
+
return t;
|
|
63
|
+
return {
|
|
64
|
+
...t,
|
|
65
|
+
description: `${t.description ?? ''}${lookup ? LIST_TOOL_NOTE : TICKET_TOOL_NOTE}`,
|
|
66
|
+
input_schema: {
|
|
67
|
+
...t.input_schema,
|
|
68
|
+
properties: {
|
|
69
|
+
...(t.input_schema.properties ?? {}),
|
|
70
|
+
context: lookup ? CONTEXT_PROPERTY : TICKET_CONTEXT_PROPERTY,
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
export const CHAT_TOOLS = withContextFlag([
|
|
77
|
+
{
|
|
78
|
+
name: 'list_runs',
|
|
79
|
+
description: 'List recent runs (test runs, ticket reviews, outages, CI triage, digests), newest first, with outcome and headline.',
|
|
80
|
+
input_schema: {
|
|
81
|
+
type: 'object',
|
|
82
|
+
properties: {
|
|
83
|
+
dotId: { type: 'string', description: 'Watch id. Omit for the selected dot, or all dots when none is selected.' },
|
|
84
|
+
kind: { type: 'string', enum: ['pr_check', 'post_merge', 'ci_failure', 'ticket_review', 'outage', 'recovery', 'digest', 'rerun', 'nightly_run'] },
|
|
85
|
+
onlyProblems: { type: 'boolean', description: 'Only runs whose outcome is not pass or info.' },
|
|
86
|
+
sinceHours: { type: 'number', description: 'Look back this many hours. Default 24.' },
|
|
87
|
+
limit: { type: 'integer', description: 'Max rows. Default 10.' },
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: 'get_run',
|
|
93
|
+
description: 'Full detail of one run: steps, failing tests with errors, verdict, and outward actions with their gate result.',
|
|
94
|
+
input_schema: { type: 'object', properties: { runId: { type: 'string' } }, required: ['runId'] },
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: 'list_tickets_reviewed',
|
|
98
|
+
description: 'Tickets this dot reviewed recently, with risks and open questions.',
|
|
99
|
+
input_schema: { type: 'object', properties: { dotId: { type: 'string' }, limit: { type: 'integer' } } },
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: 'rerun_tests',
|
|
103
|
+
description: 'Queue a new test run for a dot. Optionally limit to a grep pattern or spec files.',
|
|
104
|
+
input_schema: {
|
|
105
|
+
type: 'object',
|
|
106
|
+
properties: {
|
|
107
|
+
dotId: { type: 'string' },
|
|
108
|
+
grep: { type: 'string', description: 'Playwright --grep pattern, e.g. @smoke or checkout.' },
|
|
109
|
+
specs: { type: 'array', items: { type: 'string' } },
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: 'review_pr',
|
|
115
|
+
description: 'Ask the dot to review a pull request, a commit, or a commit range now. An open PR gets the PR check (sticky comment and check); a merged PR, a commit, or a range gets the post-merge review. Comments and checks follow the dot rules, and the review starts now, so do not call it when the person asks for a condition the tool cannot honor (no comment, a later time, a different rule); say what the review will do instead. Pass exactly one of pr, sha, or base with head.',
|
|
116
|
+
input_schema: {
|
|
117
|
+
type: 'object',
|
|
118
|
+
properties: {
|
|
119
|
+
dotId: { type: 'string' },
|
|
120
|
+
pr: { type: 'integer', description: 'PR number.' },
|
|
121
|
+
repo: { type: 'string', description: 'owner/name when the PR came as a link. It must be the dot repo.' },
|
|
122
|
+
sha: { type: 'string', description: 'Commit sha, 7 to 40 hex characters.' },
|
|
123
|
+
base: { type: 'string', description: 'Range start: a sha or branch.' },
|
|
124
|
+
head: { type: 'string', description: 'Range end: a sha or branch.' },
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: 'get_memory',
|
|
130
|
+
description: 'What the dot remembers: flaky specs, environment quirks, conventions, decisions, notes.',
|
|
131
|
+
input_schema: { type: 'object', properties: { dotId: { type: 'string' } } },
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
name: 'add_memory',
|
|
135
|
+
description: 'Save something the user wants the dot to remember.',
|
|
136
|
+
input_schema: {
|
|
137
|
+
type: 'object',
|
|
138
|
+
properties: {
|
|
139
|
+
dotId: { type: 'string' },
|
|
140
|
+
text: { type: 'string' },
|
|
141
|
+
kind: { type: 'string', enum: ['flaky_spec', 'env_quirk', 'convention', 'decision', 'note'] },
|
|
142
|
+
},
|
|
143
|
+
required: ['text'],
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: 'probe_status',
|
|
148
|
+
description: 'Current production probe state per URL: up or down, last status code, consecutive failures.',
|
|
149
|
+
input_schema: { type: 'object', properties: { dotId: { type: 'string' } } },
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
name: 'list_cases',
|
|
153
|
+
description: "The dot's stored test cases with their latest result: key (TC-12), title, kind, priority, status, requirements, and spec file. Filter by a requirement key such as SHOP-12, a status, or a kind. The chat shows the cases as a list the person can tick and run, so do not repeat every row in the answer.",
|
|
154
|
+
input_schema: {
|
|
155
|
+
type: 'object',
|
|
156
|
+
properties: {
|
|
157
|
+
dotId: { type: 'string' },
|
|
158
|
+
requirement: { type: 'string', description: 'Requirement key or ref, e.g. SHOP-12 or owner/name#12.' },
|
|
159
|
+
status: { type: 'string', enum: ['draft', 'ready', 'retired', 'all'], description: 'Default: draft and ready.' },
|
|
160
|
+
kind: { type: 'string', enum: ['manual', 'playwright', 'aivision'] },
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
name: 'get_ticket',
|
|
166
|
+
description: 'Read one ticket through the connected tracker: title, type, priority, description, acceptance criteria, and the last review this dot wrote for it. Pass a Jira, Linear, or GitHub issue URL, or a key such as SHOP-12. Call it first when the person asks about a ticket.',
|
|
167
|
+
input_schema: {
|
|
168
|
+
type: 'object',
|
|
169
|
+
properties: { dotId: { type: 'string' }, ticket: { type: 'string', description: 'Ticket URL or key.' } },
|
|
170
|
+
required: ['ticket'],
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
name: 'review_ticket',
|
|
175
|
+
description: 'Review a ticket now, the way the dot reviews a new ticket: risks, open questions, acceptance criteria gaps, test ideas, and a comment draft that follows the dot rules (ask first by default). Pass a Jira, Linear, or GitHub issue URL, or a key. Use it when the person asks to check or review a ticket.',
|
|
176
|
+
input_schema: {
|
|
177
|
+
type: 'object',
|
|
178
|
+
properties: { dotId: { type: 'string' }, ticket: { type: 'string', description: 'Ticket URL or key.' } },
|
|
179
|
+
required: ['ticket'],
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
name: 'design_tests',
|
|
184
|
+
description: 'Design manual test cases for a ticket now: the dot reads the ticket, uses its last review (or analyzes the ticket), and stores the cases as drafts linked to the ticket. Nothing is posted anywhere. Pass a Jira, Linear, or GitHub issue URL, or a key. The chat shows the designed cases as a card, so do not repeat every row. Use it when the person asks to design, write, or draft test cases for a ticket. Never call it for a request that says not to, or to wait.',
|
|
185
|
+
input_schema: {
|
|
186
|
+
type: 'object',
|
|
187
|
+
properties: { dotId: { type: 'string' }, ticket: { type: 'string', description: 'Ticket URL or key.' } },
|
|
188
|
+
required: ['ticket'],
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
name: 'pending_approvals',
|
|
193
|
+
description: 'What waits for a person to approve: comment drafts, checks, report edits, with what each is about and how long it has waited. Oldest wait first. Use it for "what is waiting for my approval".',
|
|
194
|
+
input_schema: { type: 'object', properties: { dotId: { type: 'string' }, limit: { type: 'integer' } } },
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
name: 'open_prs',
|
|
198
|
+
description: 'Open pull requests in the repo each dot watches, read from GitHub: number, title, author, link, last update. Use it for "what is the ongoing work" or "which PRs are open".',
|
|
199
|
+
input_schema: { type: 'object', properties: { dotId: { type: 'string' }, limit: { type: 'integer', description: 'Max PRs per repo. Default 10.' } } },
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
name: 'runs_in_progress',
|
|
203
|
+
description: 'Runs working right now and jobs queued behind them, with how long each has run. Use it for "what is running" or "what is the ongoing work".',
|
|
204
|
+
input_schema: { type: 'object', properties: { dotId: { type: 'string' } } },
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
name: 'recent_defects',
|
|
208
|
+
description: 'Runs that found a real problem (failing tests, product failures, high risks), newest first, with the failing tests and the suspected cause. Use it for "what broke" or "what broke since the last merge".',
|
|
209
|
+
input_schema: {
|
|
210
|
+
type: 'object',
|
|
211
|
+
properties: {
|
|
212
|
+
dotId: { type: 'string' },
|
|
213
|
+
sinceLastMerge: { type: 'boolean', description: 'Start at the latest post-merge run instead of a time window.' },
|
|
214
|
+
sinceHours: { type: 'number', description: 'Look back this many hours. Default 168 (one week).' },
|
|
215
|
+
limit: { type: 'integer', description: 'Max rows. Default 8.' },
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
name: 'pass_rate',
|
|
221
|
+
description: 'The test pass rate for the last 7 days against the week before, with tests run, failed, and flaky, as a row of number tiles and a line chart by day. The answer shows the tiles and chart itself, so write one sentence about them and do not list the numbers. Use it for "pass rate this week" or "how are the tests doing".',
|
|
222
|
+
input_schema: {
|
|
223
|
+
type: 'object',
|
|
224
|
+
properties: { dotId: { type: 'string', description: 'Watch id. Omit for the selected dot, or all dots when none is selected.' } },
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
name: 'test_results',
|
|
229
|
+
description: 'Test results of finished runs, one row per test per run, for a composed view (compose_view) that groups, counts, and compares them: by hour of day, by branch, by spec, before and after a merge. Rows have at (the run time, ISO), runId, dot, branch, kind, suite (the spec file), file, title, status (passed, failed, flaky, timedOut), durationMs, and retries; skipped tests are left out. You get the columns, a count, and a sample; the view reads all the rows by this result\'s resultId. Filter here, not in the view: days, branch, status, spec, run kind. Use splitAt last_merge to add a period column (before or after the newest merge into the default branch) for "since the last release" questions. Use it only to feed compose_view; pass_rate and flaky_by_spec already answer their own questions.',
|
|
230
|
+
input_schema: {
|
|
231
|
+
type: 'object',
|
|
232
|
+
properties: {
|
|
233
|
+
dotId: { type: 'string', description: 'Watch id. Omit for the selected dot, or all dots when none is selected.' },
|
|
234
|
+
sinceDays: { type: 'integer', description: 'Look back this many days. Default 7 (14 with splitAt), at most 90.' },
|
|
235
|
+
branch: { type: 'string', description: 'Only runs about this branch: the head branch of a PR check, or the branch a merge went into.' },
|
|
236
|
+
status: { type: 'string', enum: [...TEST_RESULT_STATUSES], description: 'Only results with this status.' },
|
|
237
|
+
file: { type: 'string', description: 'Only specs whose path holds this text.' },
|
|
238
|
+
kind: { type: 'string', enum: [...TEST_RESULT_KINDS], description: 'Only runs of this kind.' },
|
|
239
|
+
splitAt: { type: 'string', enum: ['last_merge'], description: 'Add a period column: before or after the newest merge into the default branch.' },
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
COMPOSE_VIEW_DEFINITION,
|
|
244
|
+
{
|
|
245
|
+
name: 'flaky_by_spec',
|
|
246
|
+
description: 'Which spec files had tests that passed only on a retry, counted per spec, as a bar chart with the latest runs. The answer shows the chart itself, so write one sentence about it and do not list the numbers. Use it for "flaky tests by spec" or "which specs are flaky".',
|
|
247
|
+
input_schema: {
|
|
248
|
+
type: 'object',
|
|
249
|
+
properties: {
|
|
250
|
+
dotId: { type: 'string', description: 'Watch id. Omit for the selected dot, or all dots when none is selected.' },
|
|
251
|
+
sinceDays: { type: 'integer', description: 'Look back this many days. Default 7, at most 90.' },
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
]);
|
|
256
|
+
/** Tools that only read. */
|
|
257
|
+
export const READ_ONLY_CHAT_TOOLS = [
|
|
258
|
+
'list_runs',
|
|
259
|
+
'get_run',
|
|
260
|
+
'list_tickets_reviewed',
|
|
261
|
+
'get_memory',
|
|
262
|
+
'probe_status',
|
|
263
|
+
'get_ticket',
|
|
264
|
+
'pending_approvals',
|
|
265
|
+
'open_prs',
|
|
266
|
+
'runs_in_progress',
|
|
267
|
+
'recent_defects',
|
|
268
|
+
'pass_rate',
|
|
269
|
+
'flaky_by_spec',
|
|
270
|
+
'test_results',
|
|
271
|
+
'team_status',
|
|
272
|
+
'release_call',
|
|
273
|
+
COMPOSE_VIEW_TOOL,
|
|
274
|
+
];
|
|
275
|
+
const PR_URL = /\bhttps?:\/\/github\.com\/([\w.-]+)\/([\w.-]+)\/pull\/(\d{1,9})\b[^\s)>\]]*/gi;
|
|
276
|
+
/** GitHub PR links in a chat line. */
|
|
277
|
+
export function findPrLinks(text) {
|
|
278
|
+
return [...text.matchAll(PR_URL)].map((m) => ({ owner: m[1], repo: m[2], pr: Number(m[3]), url: m[0] }));
|
|
279
|
+
}
|
|
280
|
+
/** A question about a target ("did you review TAD-3?", "what is TAD-3 about?") reads; it does not start a review. */
|
|
281
|
+
const QUESTION = /^(?:\s*(?:hey|hi|ok|okay)[,!\s]+)?\s*(?:did|do|does|have|has|was|were|is|are|when|why|who|what|which|how|where|show|get|read|summari[sz]e|describe|tell\s+me|explain|open)\b/i;
|
|
282
|
+
const REVIEW_VERB = /\b(?:check|review|re-?review|look\s+(?:at|into|over)|qa|test|analy[sz]e|refine|go\s+through|thoughts|risks?)\b/i;
|
|
283
|
+
/** Text left when the link or key is removed: "check this", "", "?". */
|
|
284
|
+
function residue(text, part) {
|
|
285
|
+
return text
|
|
286
|
+
.replace(part, ' ')
|
|
287
|
+
.replace(/[\s?!.,;:]+/g, ' ')
|
|
288
|
+
.trim();
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Words that turn a review request into a refusal or a condition: "do not review TAD-3", "review
|
|
292
|
+
* #12 without commenting", "check it later", "after CI passes". A review always runs now and posts
|
|
293
|
+
* as the dot rules say, so it cannot honor these; the answer handles them instead.
|
|
294
|
+
*/
|
|
295
|
+
const REVIEW_HOLD = /\b(?:not|dont|never|without|skip|avoid|stop|hold\s+off|wait|later|after|before|once|until|unless|except|tomorrow|tonight)\b|n['\u2019]t\b/i;
|
|
296
|
+
/** Words that ask for test cases to be written: "design tests for TAD-3", "draft test cases". */
|
|
297
|
+
const DESIGN_VERB = /\b(?:design|write|draft|create|generate|add|make)\b[^.?!]*\b(?:tests?|test\s+cases?|cases?)\b/i;
|
|
298
|
+
/** The link and key text out of a chat line, so a word in a Linear slug is not read as a condition. */
|
|
299
|
+
function withoutTargets(text, watch) {
|
|
300
|
+
let out = text.replace(PR_URL, ' ');
|
|
301
|
+
for (const t of findTicketRefs(text, watch))
|
|
302
|
+
out = out.replace(t.url ?? t.key, ' ');
|
|
303
|
+
return out;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* True when a line that asks to review or check something carries a refusal or a condition
|
|
307
|
+
* ("don't review TAD-3", "review the PR without commenting", "check it after CI"). The chat then
|
|
308
|
+
* reads the target or explains the rules, and does not start a review.
|
|
309
|
+
*/
|
|
310
|
+
export function reviewRequestHeld(text, watch) {
|
|
311
|
+
const rest = withoutTargets(text, watch);
|
|
312
|
+
return REVIEW_VERB.test(rest) && REVIEW_HOLD.test(rest);
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* True when a line that asks to design test cases carries a refusal or a condition ("do not design tests for
|
|
316
|
+
* TAD-3", "design tests for it later"). design_tests is not offered for that turn.
|
|
317
|
+
*/
|
|
318
|
+
export function designRequestHeld(text, watch) {
|
|
319
|
+
const rest = withoutTargets(text, watch);
|
|
320
|
+
return (DESIGN_VERB.test(rest) && REVIEW_HOLD.test(rest)) || reviewRequestHeld(text, watch) || reviewHasConditions(text);
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* The tool a chat line asks for by naming a ticket or a PR, called before the model so the answer
|
|
324
|
+
* starts from the right data. A ticket link with "check" or "review", or a bare link, reviews the
|
|
325
|
+
* ticket; a question about it reads it. A PR link asks for a review unless it is a question.
|
|
326
|
+
* Null when the line names neither.
|
|
327
|
+
*/
|
|
328
|
+
export function plannedFirstTool(text, watch) {
|
|
329
|
+
const tickets = findTicketRefs(text, watch);
|
|
330
|
+
const prs = findPrLinks(text);
|
|
331
|
+
if (tickets.length) {
|
|
332
|
+
const t = tickets[0];
|
|
333
|
+
const ticket = t.url ?? t.key;
|
|
334
|
+
const rest = residue(text, ticket);
|
|
335
|
+
// "design tests for TAD-3" asks for test cases, not a review, unless it carries a refusal or a condition.
|
|
336
|
+
if (!QUESTION.test(text) && DESIGN_VERB.test(rest) && !designRequestHeld(text, watch)) {
|
|
337
|
+
const designInput = { ticket };
|
|
338
|
+
if (watch)
|
|
339
|
+
designInput.dotId = watch.id;
|
|
340
|
+
return { name: 'design_tests', input: designInput };
|
|
341
|
+
}
|
|
342
|
+
const review = !QUESTION.test(text) && !reviewRequestHeld(text, watch) && (REVIEW_VERB.test(text) || /^(?:this|it|pls|please)?$/i.test(rest));
|
|
343
|
+
const input = { ticket };
|
|
344
|
+
if (watch)
|
|
345
|
+
input.dotId = watch.id;
|
|
346
|
+
return { name: review ? 'review_ticket' : 'get_ticket', input };
|
|
347
|
+
}
|
|
348
|
+
if (prs.length &&
|
|
349
|
+
!QUESTION.test(text) &&
|
|
350
|
+
!reviewRequestHeld(text, watch) &&
|
|
351
|
+
!reviewHasConditions(`review PR ${prs[0].pr}${text.slice(text.indexOf(prs[0].url) + prs[0].url.length)}`)) {
|
|
352
|
+
const p = prs[0];
|
|
353
|
+
const input = { pr: p.pr, repo: `${p.owner}/${p.repo}` };
|
|
354
|
+
if (watch)
|
|
355
|
+
input.dotId = watch.id;
|
|
356
|
+
return { name: 'review_pr', input };
|
|
357
|
+
}
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
/** Trailing words a review request may carry and still mean "review it now, as the rules say". */
|
|
361
|
+
const REVIEW_TAIL_OK = /^(?:[\s?!.,;:]|\b(?:please|pls|now|again|thanks|thank\s+you|for\s+me|asap|right\s+away)\b)*$/i;
|
|
362
|
+
/**
|
|
363
|
+
* A chat line that asks for a review: "review PR 12", "please review #12", "can you review commit
|
|
364
|
+
* abc1234", "review abc1234..def5678". The line must open with the request, so a question about a
|
|
365
|
+
* review ("did you review PR 12?") or a refusal ("don't review #12") is not one. Null otherwise.
|
|
366
|
+
* The rule-based chat and the tool-less CLI backends use it to route the request to review_pr.
|
|
367
|
+
*/
|
|
368
|
+
export function reviewIntent(line) {
|
|
369
|
+
const r = readReviewRequest(line);
|
|
370
|
+
return r && !r.conditional ? r.target : null;
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* True when the line asks for a review of a target but adds words the review cannot honor
|
|
374
|
+
* ("review PR 58 without posting a comment", "review #7 after CI passes"). The tool-using chat
|
|
375
|
+
* hides review_pr for that turn, so the model answers instead of queuing a review that comments.
|
|
376
|
+
*/
|
|
377
|
+
export function reviewHasConditions(line) {
|
|
378
|
+
return readReviewRequest(line)?.conditional === true;
|
|
379
|
+
}
|
|
380
|
+
function readReviewRequest(line) {
|
|
381
|
+
// Keywords match in any case; branch names keep theirs, since git refs are case-sensitive.
|
|
382
|
+
const t = line.trim();
|
|
383
|
+
const ask = /^(?:(?:hey|hi|ok|okay)[,!\s]+)?(?:please\s+|pls\s+)?(?:(?:can|could|would|will)\s+you\s+(?:please\s+)?|go\s+(?:and\s+)?)?(?:re-?)?review\s+(.+)$/i.exec(t);
|
|
384
|
+
if (!ask)
|
|
385
|
+
return null;
|
|
386
|
+
// The target is the first word after an optional noun: "PR 7", "#7", "commit abc1234", "abc1234..release".
|
|
387
|
+
const m = /^(?:the\s+)?(?:(pr|pull request|pull)\s*#?\s*|commits?\s+|sha\s+|range\s+)?(\S+)/i.exec(ask[1]);
|
|
388
|
+
if (!m)
|
|
389
|
+
return null;
|
|
390
|
+
// Words after the target may only be politeness. Anything else is a condition the review cannot
|
|
391
|
+
// honor ("after CI passes", "without posting a comment", "but skip the check"), so the answer
|
|
392
|
+
// handles it instead. Only the words after the target count, so a branch named
|
|
393
|
+
// release-after-fix still works.
|
|
394
|
+
const conditional = !REVIEW_TAIL_OK.test(ask[1].slice(m[0].length));
|
|
395
|
+
const word = m[2].replace(/[?!,;:]+$/, '').replace(/\.$/, '');
|
|
396
|
+
if (m[1] || /^#\d/.test(word)) {
|
|
397
|
+
const n = /^#?(\d{1,9})$/.exec(word);
|
|
398
|
+
return n ? { target: { pr: Number(n[1]) }, conditional } : null;
|
|
399
|
+
}
|
|
400
|
+
const range = /^(.+?)\.{2,3}(.+)$/.exec(word);
|
|
401
|
+
if (range) {
|
|
402
|
+
const ref = /^(?!-)(?!.*\.\.)[A-Za-z0-9._/-]{1,200}$/;
|
|
403
|
+
const sha = (x) => (/^[0-9a-f]{7,40}$/i.test(x) ? x.toLowerCase() : x);
|
|
404
|
+
return ref.test(range[1]) && ref.test(range[2]) ? { target: { base: sha(range[1]), head: sha(range[2]) }, conditional } : null;
|
|
405
|
+
}
|
|
406
|
+
// A bare word must look like a sha (hex with a letter and a digit) unless a noun named it.
|
|
407
|
+
const hex = word.toLowerCase();
|
|
408
|
+
if (/^[0-9a-f]{7,40}$/.test(hex) && (/^(commits?|sha)\s/i.test(ask[1].replace(/^the\s+/i, '')) || (/[a-f]/.test(hex) && /\d/.test(hex))))
|
|
409
|
+
return { target: { sha: hex }, conditional };
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
const chatScope = new AsyncLocalStorage();
|
|
413
|
+
/** Requests one chat turn of a lead sends to its members (KAT-405). */
|
|
414
|
+
export const MAX_ROUTED = 3;
|
|
415
|
+
/** Counts one routed request in this turn and returns the count so far. Outside a turn every request is the first. */
|
|
416
|
+
export function bumpRouted() {
|
|
417
|
+
const s = chatScope.getStore();
|
|
418
|
+
if (!s)
|
|
419
|
+
return 1;
|
|
420
|
+
s.routed = (s.routed ?? 0) + 1;
|
|
421
|
+
return s.routed;
|
|
422
|
+
}
|
|
423
|
+
/** A view id that is new in this turn. */
|
|
424
|
+
function nextViewId(scope) {
|
|
425
|
+
return `b-view-${(scope.views = (scope.views ?? 0) + 1)}`;
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* For a backend that can see a compose_view call while the model is still writing it: opens the stream that sends the nodes as they
|
|
429
|
+
* close. Call it where the turn runs, and keep the function it returns for the callbacks of the model's stream, which run apart from
|
|
430
|
+
* the turn. It gives nothing for a call (nothing is shown) and for a turn with nobody listening.
|
|
431
|
+
*/
|
|
432
|
+
export function viewStreamOpener() {
|
|
433
|
+
const scope = chatScope.getStore();
|
|
434
|
+
return () => {
|
|
435
|
+
if (!scope || !scope.stream || scope.channel === 'call')
|
|
436
|
+
return undefined;
|
|
437
|
+
const stream = new ViewStream({
|
|
438
|
+
results: (scope.results ??= new ResultRegistry()),
|
|
439
|
+
...(scope.watch ? { watch: scope.watch } : {}),
|
|
440
|
+
...(scope.channel ? { channel: scope.channel } : {}),
|
|
441
|
+
triage: (window) => buildTriageBlock(scope.watch, window ? { window } : {}),
|
|
442
|
+
nextId: () => nextViewId(scope),
|
|
443
|
+
emit: (e) => {
|
|
444
|
+
try {
|
|
445
|
+
scope.emit(e);
|
|
446
|
+
}
|
|
447
|
+
catch {
|
|
448
|
+
/* the stream closed */
|
|
449
|
+
}
|
|
450
|
+
},
|
|
451
|
+
});
|
|
452
|
+
(scope.viewStreams ??= []).push(stream);
|
|
453
|
+
return stream;
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
/** The stream that carried this input, when one did and no call has taken it yet. */
|
|
457
|
+
function claimViewStream(scope, input) {
|
|
458
|
+
return scope.viewStreams?.find((s) => s.claim(input));
|
|
459
|
+
}
|
|
460
|
+
/** Ends the streams a call did not claim: a view that streamed and has no finished block to replace it is taken away. */
|
|
461
|
+
export function closeViewStreams() {
|
|
462
|
+
const scope = chatScope.getStore();
|
|
463
|
+
for (const s of scope?.viewStreams ?? [])
|
|
464
|
+
s.close();
|
|
465
|
+
if (scope)
|
|
466
|
+
scope.viewStreams = [];
|
|
467
|
+
}
|
|
468
|
+
function emitChat(e) {
|
|
469
|
+
const s = chatScope.getStore();
|
|
470
|
+
if (!s)
|
|
471
|
+
return;
|
|
472
|
+
try {
|
|
473
|
+
s.emit(e);
|
|
474
|
+
}
|
|
475
|
+
catch {
|
|
476
|
+
/* the stream closed */
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
/** Shows the run card once per turn; the card follows the run's own updates, so it can appear while the run is still going. */
|
|
480
|
+
function emitRunCard(runId) {
|
|
481
|
+
const s = chatScope.getStore();
|
|
482
|
+
if (!s)
|
|
483
|
+
return;
|
|
484
|
+
const cards = (s.runCards ??= new Set());
|
|
485
|
+
if (cards.has(runId))
|
|
486
|
+
return;
|
|
487
|
+
cards.add(runId);
|
|
488
|
+
emitChat({ type: 'run', runId });
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Tool definitions for one chat turn: the built-in tools, the dot's MCP tools, and
|
|
492
|
+
* propose_report_edit when the chat is scoped to a report page.
|
|
493
|
+
*/
|
|
494
|
+
export function chatToolsFor(watch, reportId) {
|
|
495
|
+
// A lead's chat reads the whole team, routes actions to its members, and has two tools of its own (KAT-405).
|
|
496
|
+
const tools = watch && isLead(watch) ? leadChatTools(CHAT_TOOLS) : [...CHAT_TOOLS];
|
|
497
|
+
if (watch)
|
|
498
|
+
tools.push(...toolsForWatch(watch));
|
|
499
|
+
if (reportId)
|
|
500
|
+
tools.push(REPORT_EDIT_TOOL);
|
|
501
|
+
return tools;
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* gate() records every outward action on a run. Chat has no run of its own, so the first gated
|
|
505
|
+
* action of a turn opens one (trigger kind chat) and the turn finishes it.
|
|
506
|
+
*/
|
|
507
|
+
function archivedNote(watch) {
|
|
508
|
+
return get('watches', watch.id)?.archived ? `${watch.name} is archived. Restore it to run this.` : undefined;
|
|
509
|
+
}
|
|
510
|
+
function chatRun(watch, title) {
|
|
511
|
+
const note = archivedNote(watch);
|
|
512
|
+
if (note)
|
|
513
|
+
throw new Error(note);
|
|
514
|
+
const s = chatScope.getStore();
|
|
515
|
+
if (s?.runId)
|
|
516
|
+
return s.runId;
|
|
517
|
+
const run = startRun(watch.id, 'rerun', { source: 'user', kind: 'chat', dedupeKey: `chat:${Date.now().toString(36)}`, title });
|
|
518
|
+
addStep(run.id, 'Asked in chat', title);
|
|
519
|
+
if (s)
|
|
520
|
+
s.runId = run.id;
|
|
521
|
+
if (s?.origin)
|
|
522
|
+
recordAsk({ watchId: watch.id, runId: run.id, origin: s.origin });
|
|
523
|
+
return run.id;
|
|
524
|
+
}
|
|
525
|
+
function approvalFor(runId, match) {
|
|
526
|
+
return list('approvals', { runId }).find(match);
|
|
527
|
+
}
|
|
528
|
+
/** Job statuses after which the queue will not start the job again. */
|
|
529
|
+
const JOB_SETTLED = new Set(['done', 'failed']);
|
|
530
|
+
/**
|
|
531
|
+
* Resolves with the run id once the queue starts the run for `jobId`, or undefined when the job
|
|
532
|
+
* settled without one. It waits on events, not on a clock: a run event ends it at once, and the
|
|
533
|
+
* job row is re-read at the queue's own tick while nothing has happened. A job queued behind
|
|
534
|
+
* long runs therefore still gets its card, however long it waits. `maxMs` is a safety valve for a
|
|
535
|
+
* queue that was stopped, so a chat turn cannot hang forever.
|
|
536
|
+
*/
|
|
537
|
+
export function waitForRunOfJob(askedJobId, maxMs = 5 * 60_000) {
|
|
538
|
+
// A review that a newer edit of its ticket replaced runs as the newer job: follow it (src/ticket-supersede.ts).
|
|
539
|
+
const jobOf = () => liveJobId(askedJobId);
|
|
540
|
+
const find = () => list('runs', { jobId: jobOf() })[0]?.id;
|
|
541
|
+
const existing = find();
|
|
542
|
+
if (existing)
|
|
543
|
+
return Promise.resolve(existing);
|
|
544
|
+
return new Promise((resolve) => {
|
|
545
|
+
const done = (id) => {
|
|
546
|
+
clearTimeout(limit);
|
|
547
|
+
clearInterval(check);
|
|
548
|
+
unsub();
|
|
549
|
+
resolve(id);
|
|
550
|
+
};
|
|
551
|
+
const unsub = subscribe((e) => {
|
|
552
|
+
if (e.type === 'run' && e.run.jobId === jobOf())
|
|
553
|
+
done(e.run.id);
|
|
554
|
+
});
|
|
555
|
+
const check = setInterval(() => {
|
|
556
|
+
const id = find();
|
|
557
|
+
if (id)
|
|
558
|
+
return done(id);
|
|
559
|
+
const job = get('jobs', jobOf());
|
|
560
|
+
if (!job || JOB_SETTLED.has(job.status))
|
|
561
|
+
done(undefined);
|
|
562
|
+
}, 500);
|
|
563
|
+
const limit = setTimeout(() => done(undefined), maxMs);
|
|
564
|
+
check.unref?.();
|
|
565
|
+
limit.unref?.();
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
/** How long a tool call holds the model's turn for the run id; after that the card still arrives. */
|
|
569
|
+
function runGraceMs() {
|
|
570
|
+
const n = Number(process.env.LOCALDOST_CHAT_RUN_GRACE_MS);
|
|
571
|
+
return Number.isFinite(n) && n >= 0 && process.env.LOCALDOST_CHAT_RUN_GRACE_MS ? n : 4000;
|
|
572
|
+
}
|
|
573
|
+
/** Waits for a run to finish, up to timeoutMs. Returns the run as it stands then. */
|
|
574
|
+
function waitForRunDone(runId, timeoutMs) {
|
|
575
|
+
const now = get('runs', runId);
|
|
576
|
+
if (!now || now.status !== 'running' || timeoutMs <= 0)
|
|
577
|
+
return Promise.resolve(now ?? undefined);
|
|
578
|
+
return new Promise((resolve) => {
|
|
579
|
+
const done = () => {
|
|
580
|
+
clearTimeout(t);
|
|
581
|
+
unsub();
|
|
582
|
+
resolve(get('runs', runId) ?? undefined);
|
|
583
|
+
};
|
|
584
|
+
const unsub = subscribe((e) => {
|
|
585
|
+
if (e.type === 'run' && e.run.id === runId && e.run.status !== 'running')
|
|
586
|
+
done();
|
|
587
|
+
});
|
|
588
|
+
const t = setTimeout(done, timeoutMs);
|
|
589
|
+
t.unref?.();
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
/** How long review_ticket waits for the review before it answers. LOCALDOST_CHAT_REVIEW_WAIT_SEC, default 150. */
|
|
593
|
+
function reviewWaitMs() {
|
|
594
|
+
const raw = liveEnv.LOCALDOST_CHAT_REVIEW_WAIT_SEC;
|
|
595
|
+
const v = raw === undefined || raw === '' ? NaN : Number(raw);
|
|
596
|
+
return Number.isFinite(v) && v >= 0 ? v * 1000 : 150_000;
|
|
597
|
+
}
|
|
598
|
+
function clip(text, max = 160) {
|
|
599
|
+
const one = text.replace(/\s+/g, ' ').trim();
|
|
600
|
+
return one.length > max ? `${one.slice(0, max - 3)}...` : one;
|
|
601
|
+
}
|
|
602
|
+
/** True when a tool's value is a failure: an error text, or a refusal with a message. */
|
|
603
|
+
export function toolFailed(value) {
|
|
604
|
+
if (!value || typeof value !== 'object')
|
|
605
|
+
return false;
|
|
606
|
+
const v = value;
|
|
607
|
+
return typeof v.error === 'string' || (typeof v.message === 'string' && (v.ok === false || v.status === 'error'));
|
|
608
|
+
}
|
|
609
|
+
/** One line for the tool line in chat. */
|
|
610
|
+
export function summarizeToolResult(name, value) {
|
|
611
|
+
if (!value || typeof value !== 'object')
|
|
612
|
+
return clip(String(value ?? 'Done'));
|
|
613
|
+
const v = value;
|
|
614
|
+
if (typeof v.error === 'string')
|
|
615
|
+
return clip(v.error);
|
|
616
|
+
if (typeof v.message === 'string' && v.ok === false)
|
|
617
|
+
return clip(v.message);
|
|
618
|
+
// A request a lead sent to a member says who got it and what it was asked: "Asked PR Reviewer to check PR #12 again" (KAT-405).
|
|
619
|
+
if (typeof v.routedTo === 'string' && typeof v.ask === 'string')
|
|
620
|
+
return clip(`Asked ${v.routedTo} to ${v.ask}`);
|
|
621
|
+
switch (name) {
|
|
622
|
+
case 'team_status':
|
|
623
|
+
return clip(`${String(v.status ?? 'Team')}: ${String(v.reason ?? '')}`);
|
|
624
|
+
case 'release_call':
|
|
625
|
+
return clip(`${String(v.answer ?? 'Release')}: ${String(v.summary ?? '')}`);
|
|
626
|
+
case 'list_runs':
|
|
627
|
+
return `${Number(v.count ?? 0)} runs`;
|
|
628
|
+
case 'get_run':
|
|
629
|
+
return clip(String(v.headline ?? v.trigger ?? v.status ?? 'Run loaded'));
|
|
630
|
+
case 'rerun_tests':
|
|
631
|
+
return v.queued ? `Queued a run for ${String(v.dot ?? 'the dot')}` : clip(String(v.reason ?? 'Nothing queued'));
|
|
632
|
+
case 'review_pr':
|
|
633
|
+
return v.duplicate
|
|
634
|
+
? clip(`Already reviewing: ${String(v.title ?? '')}`)
|
|
635
|
+
: v.queued
|
|
636
|
+
? clip(`Queued a review: ${String(v.title ?? '')}`)
|
|
637
|
+
: 'Nothing queued';
|
|
638
|
+
case 'add_memory':
|
|
639
|
+
return v.saved ? 'Saved to memory' : 'Not saved';
|
|
640
|
+
case 'get_ticket':
|
|
641
|
+
return clip(`${String(v.key ?? '')}: ${String(v.title ?? '')}`);
|
|
642
|
+
case 'review_ticket':
|
|
643
|
+
return v.duplicate
|
|
644
|
+
? clip(`Already reviewing: ${String(v.title ?? '')}`)
|
|
645
|
+
: v.status === 'running' || v.status === 'queued'
|
|
646
|
+
? clip(`Reviewing: ${String(v.title ?? '')}`)
|
|
647
|
+
: clip(String(v.headline ?? v.title ?? 'Review queued'));
|
|
648
|
+
case 'design_tests':
|
|
649
|
+
return v.duplicate
|
|
650
|
+
? clip(`Already designing: ${String(v.title ?? '')}`)
|
|
651
|
+
: v.status === 'running' || v.status === 'queued'
|
|
652
|
+
? clip(`Designing: ${String(v.title ?? '')}`)
|
|
653
|
+
: `${Number(v.count ?? 0)} ${Number(v.count ?? 0) === 1 ? 'case' : 'cases'} for ${String(v.ticket ?? 'the ticket')}`;
|
|
654
|
+
case 'pending_approvals':
|
|
655
|
+
return `${Number(v.count ?? 0)} waiting`;
|
|
656
|
+
case 'open_prs':
|
|
657
|
+
return `${Number(v.count ?? 0)} open`;
|
|
658
|
+
case 'runs_in_progress':
|
|
659
|
+
return `${Number(v.count ?? 0)} running, ${Number(v.queuedCount ?? 0)} queued`;
|
|
660
|
+
case 'recent_defects':
|
|
661
|
+
return `${Number(v.count ?? 0)} found`;
|
|
662
|
+
case 'list_cases':
|
|
663
|
+
return `${Number(v.count ?? 0)} ${Number(v.count ?? 0) === 1 ? 'case' : 'cases'}`;
|
|
664
|
+
case 'pass_rate':
|
|
665
|
+
return typeof v.passRatePercent === 'number' ? `${v.passRatePercent}% passed` : clip(String(v.note ?? 'No test runs'));
|
|
666
|
+
case 'flaky_by_spec':
|
|
667
|
+
return Number(v.specs ?? 0) > 0 ? `${Number(v.specs)} flaky specs` : clip(String(v.note ?? 'No flaky tests'));
|
|
668
|
+
case 'test_results':
|
|
669
|
+
return Number(v.count ?? 0) > 0 ? `${Number(v.count)} test results` : clip(String(v.note ?? 'No test results'));
|
|
670
|
+
case COMPOSE_VIEW_TOOL:
|
|
671
|
+
return Array.isArray(v.shown) && v.shown.length ? `Showed ${v.shown.join(', ')}` : clip(String(v.note ?? 'No view'));
|
|
672
|
+
case 'probe_status': {
|
|
673
|
+
const probes = Array.isArray(v.probes) ? v.probes : [];
|
|
674
|
+
return probes.length ? `${probes.length} probe${probes.length === 1 ? '' : 's'}` : clip(String(v.note ?? 'No probes'));
|
|
675
|
+
}
|
|
676
|
+
case 'propose_report_edit':
|
|
677
|
+
return v.status === 'executed'
|
|
678
|
+
? 'Edit saved'
|
|
679
|
+
: v.status === 'pending_approval'
|
|
680
|
+
? 'Edit waits for your approval'
|
|
681
|
+
: clip(String(v.message ?? v.status ?? ''));
|
|
682
|
+
}
|
|
683
|
+
if (Array.isArray(value))
|
|
684
|
+
return `${value.length} item${value.length === 1 ? '' : 's'}`;
|
|
685
|
+
if (isMcpToolName(name)) {
|
|
686
|
+
const result = v.result;
|
|
687
|
+
if (typeof result?.text === 'string' && result.text)
|
|
688
|
+
return clip(result.text);
|
|
689
|
+
return clip(String(v.message ?? v.status ?? 'Done'));
|
|
690
|
+
}
|
|
691
|
+
return clip(JSON.stringify(value));
|
|
692
|
+
}
|
|
693
|
+
async function runMcpTool(name, input, ctx) {
|
|
694
|
+
const watch = ctx.watch;
|
|
695
|
+
if (!watch)
|
|
696
|
+
return { ok: false, status: 'error', tool: name, message: 'Pick a dot first; MCP tools belong to a dot.' };
|
|
697
|
+
const binding = bindingsFor(watch).get(name);
|
|
698
|
+
const archived = binding && !binding.tool.readOnly ? archivedNote(watch) : undefined;
|
|
699
|
+
if (archived)
|
|
700
|
+
return { ok: false, status: 'error', tool: name, message: archived };
|
|
701
|
+
// Read-only tools need no run; anything else is gated and recorded on the chat run.
|
|
702
|
+
const runId = binding && !binding.tool.readOnly ? chatRun(watch, `Run ${binding.tool.name} on ${binding.conn.name}`) : '';
|
|
703
|
+
const outcome = await callConnectionTool(watch, name, input, runId);
|
|
704
|
+
if (outcome.approvalId) {
|
|
705
|
+
const approval = get('approvals', outcome.approvalId);
|
|
706
|
+
if (approval)
|
|
707
|
+
emitChat({ type: 'approval', approval });
|
|
708
|
+
}
|
|
709
|
+
return outcome;
|
|
710
|
+
}
|
|
711
|
+
async function proposeReportEdit(input, ctx) {
|
|
712
|
+
const reportId = ctx.reportId;
|
|
713
|
+
if (!reportId)
|
|
714
|
+
return { error: 'No report page is open in this chat.' };
|
|
715
|
+
const page = getReport(reportId);
|
|
716
|
+
if (!page)
|
|
717
|
+
return { error: 'That report page no longer exists.' };
|
|
718
|
+
const markdown = typeof input.markdown === 'string' ? input.markdown : '';
|
|
719
|
+
if (!markdown.trim())
|
|
720
|
+
return { error: 'markdown is required: send the full new page.' };
|
|
721
|
+
const watch = (page.watchId ? get('watches', page.watchId) : undefined) ?? ctx.watch;
|
|
722
|
+
if (!watch)
|
|
723
|
+
return { error: 'This page belongs to no dot, so nobody can approve an edit.' };
|
|
724
|
+
const archived = archivedNote(watch);
|
|
725
|
+
if (archived)
|
|
726
|
+
return { error: archived };
|
|
727
|
+
// KAT-399: a copy of the Team notes in the model's page is cut before the card shows the diff and before the page is saved.
|
|
728
|
+
const edit = scrubReportEdit(watch, { markdown, note: str(input.note), title: str(input.title) });
|
|
729
|
+
if (!edit)
|
|
730
|
+
return { error: 'The page you wrote only repeats the Team notes. Write the page again in your own words.' };
|
|
731
|
+
const action = proposeReportEditAction({ reportId, markdown: edit.markdown, note: edit.note, title: edit.title });
|
|
732
|
+
action.meta = { ...action.meta, askedBy: 'user' };
|
|
733
|
+
const runId = chatRun(watch, `Edit report "${page.title}"`);
|
|
734
|
+
const status = await gate(action, watch, runId);
|
|
735
|
+
const approval = approvalFor(runId, (a) => a.action.kind === 'report_edit' && a.action.meta?.reportId === reportId && a.status === 'pending');
|
|
736
|
+
if (approval && status === 'pending_approval')
|
|
737
|
+
emitChat({ type: 'approval', approval });
|
|
738
|
+
return { status, reportId, ...(approval ? { approvalId: approval.id } : {}) };
|
|
739
|
+
}
|
|
740
|
+
function str(v) {
|
|
741
|
+
return typeof v === 'string' && v.trim() ? v.trim() : undefined;
|
|
742
|
+
}
|
|
743
|
+
function resolveWatch(input, ctx) {
|
|
744
|
+
const id = str(input.dotId);
|
|
745
|
+
if (id)
|
|
746
|
+
return get('watches', id) ?? undefined;
|
|
747
|
+
if (ctx.watch)
|
|
748
|
+
return ctx.watch;
|
|
749
|
+
const all = list('watches').filter((w) => !w.archived);
|
|
750
|
+
return all.length === 1 ? all[0] : undefined;
|
|
751
|
+
}
|
|
752
|
+
function minutesSince(iso) {
|
|
753
|
+
return Math.max(0, Math.round((Date.now() - Date.parse(iso)) / 60000));
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* A run that found something a person should look at: a failed outcome, or a high risk. A run that broke on
|
|
757
|
+
* infrastructure (a crashed job, an environment failure) is not a defect in the product or its tests (KAT-158).
|
|
758
|
+
*/
|
|
759
|
+
function isDefectRun(r) {
|
|
760
|
+
const o = r.verdict?.outcome;
|
|
761
|
+
if (o === 'product_failure' || o === 'test_failure' || o === 'flaky')
|
|
762
|
+
return true;
|
|
763
|
+
if (o === 'environment_failure')
|
|
764
|
+
return false;
|
|
765
|
+
return (r.verdict?.findings ?? []).some((f) => f.severity === 'high') && o !== 'pass' && o !== 'info';
|
|
766
|
+
}
|
|
767
|
+
function newestFirst(rows) {
|
|
768
|
+
return [...rows].sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
769
|
+
}
|
|
770
|
+
export function summarizeRun(r) {
|
|
771
|
+
return {
|
|
772
|
+
runId: r.id,
|
|
773
|
+
dotId: r.watchId,
|
|
774
|
+
kind: r.kind,
|
|
775
|
+
status: r.status,
|
|
776
|
+
at: r.createdAt,
|
|
777
|
+
trigger: r.trigger?.title ?? null,
|
|
778
|
+
outcome: r.verdict?.outcome ?? null,
|
|
779
|
+
headline: r.verdict?.headline ?? null,
|
|
780
|
+
tests: r.report ? { passed: r.report.passed, failed: r.report.failed, flaky: r.report.flaky } : null,
|
|
781
|
+
pendingApprovals: (r.actions ?? []).filter((a) => a.result === 'pending_approval').length,
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
/** Same call key for the same tool and input; dotId of the selected dot counts as no dotId. */
|
|
785
|
+
function callKey(name, input, ctx) {
|
|
786
|
+
const o = { ...(input && typeof input === 'object' ? input : {}) };
|
|
787
|
+
if (ctx.watch && o.dotId === ctx.watch.id)
|
|
788
|
+
delete o.dotId;
|
|
789
|
+
const sorted = Object.keys(o)
|
|
790
|
+
.sort()
|
|
791
|
+
.map((k) => [k, o[k]]);
|
|
792
|
+
return `${name}:${JSON.stringify(sorted)}`;
|
|
793
|
+
}
|
|
794
|
+
/** True when this turn already called the tool with the same input. */
|
|
795
|
+
export function isRepeatChatCall(name, input, ctx) {
|
|
796
|
+
return chatScope.getStore()?.calls?.has(callKey(name, input, ctx)) === true;
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* Runs a tool for a backend: a repeat of a call made this turn, finished or still running, returns the
|
|
800
|
+
* first result with a note and shows no second card; anything else shows the card and runs the tool.
|
|
801
|
+
* The call is recorded before it runs, so sibling calls that start together cannot both execute.
|
|
802
|
+
*/
|
|
803
|
+
export async function callChatTool(name, input, ctx, onTool) {
|
|
804
|
+
const scope = chatScope.getStore();
|
|
805
|
+
const key = callKey(name, input, ctx);
|
|
806
|
+
const prev = scope?.calls?.get(key);
|
|
807
|
+
if (prev !== undefined)
|
|
808
|
+
return { repeated: true, note: 'This exact call already ran this turn; use the result you have. Do not call it again.', result: await prev };
|
|
809
|
+
onTool(name, input);
|
|
810
|
+
const running = executeChatTool(name, input, ctx);
|
|
811
|
+
if (scope) {
|
|
812
|
+
(scope.calls ??= new Map()).set(key, running);
|
|
813
|
+
// A call that threw is not a result to repeat; a later identical call runs again.
|
|
814
|
+
running.catch(() => scope.calls?.get(key) === running && scope.calls.delete(key));
|
|
815
|
+
}
|
|
816
|
+
return running;
|
|
817
|
+
}
|
|
818
|
+
let toolStub;
|
|
819
|
+
export function setChatToolStub(fn) {
|
|
820
|
+
toolStub = fn;
|
|
821
|
+
}
|
|
822
|
+
/** Runs one chat tool and streams a one-line result to the chat when a turn is in progress. */
|
|
823
|
+
export async function executeChatTool(name, rawInput, ctx) {
|
|
824
|
+
let dispatched;
|
|
825
|
+
try {
|
|
826
|
+
dispatched = await dispatchChatTool(name, rawInput, ctx);
|
|
827
|
+
}
|
|
828
|
+
catch (err) {
|
|
829
|
+
// The chat shows the failed call as one line with the reason; the backend still gets the error.
|
|
830
|
+
emitChat({ type: 'tool_result', name, summary: clip(err instanceof Error ? err.message : String(err)), error: true });
|
|
831
|
+
throw err;
|
|
832
|
+
}
|
|
833
|
+
// A tool that answers with blocks hands them over in `blocks`; the model only sees the numbers.
|
|
834
|
+
const { value, blocks: attached } = splitBlocks(dispatched);
|
|
835
|
+
// A lookup result gets an id for the turn, so a composed view can read it (KAT-366). The model reads the id with the result.
|
|
836
|
+
const turn = chatScope.getStore();
|
|
837
|
+
if (turn && VIEW_SOURCE_TOOLS.includes(name) && value && typeof value === 'object' && !toolFailed(value)) {
|
|
838
|
+
// Only the chat's own dot feeds a view: a lookup for another dot gets no id, and the model reads why.
|
|
839
|
+
const own = turn.watch ?? ctx.watch;
|
|
840
|
+
const rawObj = (rawInput && typeof rawInput === 'object' ? rawInput : {});
|
|
841
|
+
const foreign = foreignDotOf(rawObj, own);
|
|
842
|
+
if (foreign && own)
|
|
843
|
+
refuseResult(value, foreignSourceLine(foreign, own));
|
|
844
|
+
else {
|
|
845
|
+
const entry = (turn.results ??= new ResultRegistry()).add(name, (rawInput && typeof rawInput === 'object' ? rawInput : {}), value, attached);
|
|
846
|
+
tagResult(value, entry.id);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
// The lookup tools answer with their rows, and the matching block is built from that result, so a stubbed tool
|
|
850
|
+
// (the chat eval) shows the same block as the real one. A call marked `context` shows none (docs/CHAT-UI.md).
|
|
851
|
+
const blocks = chatScope.getStore()?.channel === 'call' ? [] : attached.length ? attached : lookupBlocks(name, value, rawInput, ctx);
|
|
852
|
+
emitChat({ type: 'tool_result', name, summary: summarizeToolResult(name, value), ...(toolFailed(value) ? { error: true } : {}) });
|
|
853
|
+
// The result of queued work replies where it was asked (KAT-143).
|
|
854
|
+
const origin = chatScope.getStore()?.origin;
|
|
855
|
+
const jobId = value && typeof value === 'object' ? value.jobId : undefined;
|
|
856
|
+
if (origin && typeof jobId === 'string')
|
|
857
|
+
recordAskForJob(jobId, origin);
|
|
858
|
+
for (const b of blocks)
|
|
859
|
+
emitBlock({ ...b, tool: name });
|
|
860
|
+
if (name === 'design_tests' && value && typeof value === 'object' && typeof value.runId === 'string')
|
|
861
|
+
emitRunCard(value.runId);
|
|
862
|
+
if (name === 'review_ticket' && value && typeof value === 'object' && typeof value.runId === 'string') {
|
|
863
|
+
emitRunCard(value.runId);
|
|
864
|
+
const designed = value.testCases;
|
|
865
|
+
const scope = chatScope.getStore();
|
|
866
|
+
if (designed && scope)
|
|
867
|
+
(scope.lines ??= []).push(designedChatLine(designed));
|
|
868
|
+
const approvalId = value.approvalId;
|
|
869
|
+
const approval = typeof approvalId === 'string' ? get('approvals', approvalId) : undefined;
|
|
870
|
+
if (approval)
|
|
871
|
+
emitChat({ type: 'approval', approval });
|
|
872
|
+
}
|
|
873
|
+
// A design run that already has its card (runId) needs no late one; a queued one gets its card when it starts.
|
|
874
|
+
const queuedDesign = name === 'design_tests' && value && typeof value === 'object' && typeof value.runId !== 'string';
|
|
875
|
+
if ((name === 'rerun_tests' || name === 'review_pr' || queuedDesign) &&
|
|
876
|
+
value &&
|
|
877
|
+
typeof value === 'object' &&
|
|
878
|
+
typeof value.jobId === 'string') {
|
|
879
|
+
const scope = chatScope.getStore();
|
|
880
|
+
const wait = waitForRunOfJob(value.jobId);
|
|
881
|
+
const emitRun = (runId) => {
|
|
882
|
+
if (runId)
|
|
883
|
+
emitRunCard(runId);
|
|
884
|
+
};
|
|
885
|
+
// Fast path: the model learns the run id when the queue starts the job right away.
|
|
886
|
+
let graceTimer;
|
|
887
|
+
const grace = new Promise((resolve) => {
|
|
888
|
+
graceTimer = setTimeout(() => resolve('late'), runGraceMs());
|
|
889
|
+
graceTimer.unref?.();
|
|
890
|
+
});
|
|
891
|
+
const first = await Promise.race([wait, grace]).finally(() => clearTimeout(graceTimer));
|
|
892
|
+
if (first !== 'late') {
|
|
893
|
+
if (first)
|
|
894
|
+
value.runId = first;
|
|
895
|
+
emitRun(first);
|
|
896
|
+
}
|
|
897
|
+
else if (scope?.stream) {
|
|
898
|
+
// The job waits behind other runs. The model answers now; the turn's stream stays open until
|
|
899
|
+
// the card is sent (runChat awaits `pending`), so a busy queue never hides it.
|
|
900
|
+
scope.pending.push(wait.then(emitRun));
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
return value;
|
|
904
|
+
}
|
|
905
|
+
/** The test designer's part of a review_ticket result: the count, the line, and the filtered Test cases link (KAT-164). */
|
|
906
|
+
export function designedChat(dotId, d) {
|
|
907
|
+
return {
|
|
908
|
+
designed: d.added,
|
|
909
|
+
alreadyThere: d.existing,
|
|
910
|
+
line: designedLine(d),
|
|
911
|
+
link: designedCasesHref(dotId, d),
|
|
912
|
+
note: 'localdost adds this line with its link at the end of the answer. Do not repeat it.',
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
/** The chat line for designed cases, with its link: "3 test cases designed. [Open the test cases](#/cases?...)". */
|
|
916
|
+
export function designedChatLine(t) {
|
|
917
|
+
return `${t.line}. [Open the test cases](${t.link})`;
|
|
918
|
+
}
|
|
919
|
+
const BLOCKS_KEY = '__blocks';
|
|
920
|
+
/** A tool result paired with the blocks it carries. Used only between a tool and executeChatTool. */
|
|
921
|
+
export function withBlocks(result, extra) {
|
|
922
|
+
const out = { ...extra, ...result.facts };
|
|
923
|
+
Object.defineProperty(out, BLOCKS_KEY, { value: result.blocks, enumerable: false });
|
|
924
|
+
return out;
|
|
925
|
+
}
|
|
926
|
+
export function splitBlocks(value) {
|
|
927
|
+
const blocks = value && typeof value === 'object' ? value[BLOCKS_KEY] : undefined;
|
|
928
|
+
return { value, blocks: Array.isArray(blocks) ? blocks : [] };
|
|
929
|
+
}
|
|
930
|
+
/**
|
|
931
|
+
* Words that ask about the dot's own review of a ticket: that it ran, what it found, what it asked, the comment it
|
|
932
|
+
* drafted. "comment" alone is not one: "the latest comment on TAD-3 in Jira" asks about the tracker.
|
|
933
|
+
*/
|
|
934
|
+
const TICKET_REVIEW_WORDS = /\b(?:review(?:ed|s)?|risks?|open\s+questions?|test\s+ideas?|findings?|(?:your|drafted?|review)\s+comments?|comments?\s+(?:did\s+)?you|you\s+(?:comment(?:ed)?|post(?:ed)?|draft(?:ed)?))\b/i;
|
|
935
|
+
/** A request for a new review, not a question about the stored one: "review TAD-3 again", "please review TAD-3". */
|
|
936
|
+
const REVIEW_REQUEST = /^\s*(?:(?:hey|hi|ok|okay)[,!\s]+)?(?:(?:please|pls|just|can\s+you|could\s+you|would\s+you|will\s+you)\s+)*(?:re-?review|review|re-?check|check)\b/i;
|
|
937
|
+
/** A risk in general, not a risk the review found: "the risks of shipping TAD-3". */
|
|
938
|
+
const GENERAL_RISK = /\brisks?\s+(?:of|in|with|around)\s+(?:shipping|releasing|deploying|merging|launching|rolling)\b/gi;
|
|
939
|
+
/**
|
|
940
|
+
* True when the person asks about the review the dot wrote for a ticket. get_ticket then shows that review as a
|
|
941
|
+
* block, unless the model marked the call `context`; a plain "what is TAD-3 about?" reads the ticket and shows
|
|
942
|
+
* none. A refusal or a condition ("do not review TAD-3"), a request for a new review ("review TAD-3 again"),
|
|
943
|
+
* and a question about the tracker or about shipping the ticket are not questions about the stored review.
|
|
944
|
+
*/
|
|
945
|
+
export function asksAboutTicketReview(question, watch) {
|
|
946
|
+
if (!question)
|
|
947
|
+
return false;
|
|
948
|
+
const rest = withoutTargets(question, watch);
|
|
949
|
+
if (REVIEW_REQUEST.test(rest) || reviewRequestHeld(question, watch))
|
|
950
|
+
return false;
|
|
951
|
+
return TICKET_REVIEW_WORDS.test(rest.replace(GENERAL_RISK, ' '));
|
|
952
|
+
}
|
|
953
|
+
/** Blocks for the result of a lookup tool; none when the call is a context lookup, failed, or has no rows. */
|
|
954
|
+
function lookupBlocks(name, value, rawInput, ctx) {
|
|
955
|
+
const input = (rawInput && typeof rawInput === 'object' ? rawInput : {});
|
|
956
|
+
const dot = str(input.dotId) || ctx.watch ? resolveWatch(input, ctx) : undefined;
|
|
957
|
+
const question = chatScope.getStore()?.question;
|
|
958
|
+
return blocksFromToolResult(name, value, input, {
|
|
959
|
+
...(dot ? { dot } : {}),
|
|
960
|
+
...(name === 'get_ticket' ? { showTicketReview: asksAboutTicketReview(question, dot ?? ctx.watch) } : {}),
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
/** Checks a block, streams it, and keeps it for the stored reply. A block that fails the check is dropped; the text still answers. */
|
|
964
|
+
function emitBlock(raw, opts = {}) {
|
|
965
|
+
const parsed = parseChatBlock(raw);
|
|
966
|
+
if (!parsed.ok)
|
|
967
|
+
return undefined;
|
|
968
|
+
const scope = chatScope.getStore();
|
|
969
|
+
if (scope)
|
|
970
|
+
(scope.blocks ??= new Map()).set(parsed.block.id, parsed.block);
|
|
971
|
+
if (!opts.quiet)
|
|
972
|
+
emitChat({ type: 'block', block: parsed.block });
|
|
973
|
+
return parsed.block;
|
|
974
|
+
}
|
|
975
|
+
/**
|
|
976
|
+
* The objects whose card the client draws under the person's message: the links it unfurls, which are the first three distinct
|
|
977
|
+
* PR, issue, Jira, and Linear addresses (src/object-links.ts, the same function the client calls). A link past the third, or an
|
|
978
|
+
* address the client does not unfurl, is not in this set, so the answer still gets its card.
|
|
979
|
+
*/
|
|
980
|
+
function unfurledKeys(userText) {
|
|
981
|
+
return new Set(objectLinksIn(userText ?? '').map((l) => l.key));
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* Streams the object cards of a tool call as one block. A link the person pasted unfurls into a card under their own message,
|
|
985
|
+
* so a card for the same object is left out here; every other object the dot reads or acts on gets a card in the answer (KAT-136).
|
|
986
|
+
*/
|
|
987
|
+
function emitCards(cards, tool) {
|
|
988
|
+
const pasted = unfurledKeys(chatScope.getStore()?.userText);
|
|
989
|
+
const fresh = cards.filter((c) => !pasted.has(refKey(objectOfCard(c))));
|
|
990
|
+
if (fresh.length)
|
|
991
|
+
emitBlock({ id: `b-card-${fresh.map((c) => c.ref).join('-')}`, kind: 'cards', cards: fresh, tool });
|
|
992
|
+
}
|
|
993
|
+
/** Builds the PR card after the tool answered, so GitHub's reads never hold the model. It lands in the answer when it is ready. */
|
|
994
|
+
function cardLater(build, tool) {
|
|
995
|
+
const scope = chatScope.getStore();
|
|
996
|
+
if (!scope?.stream)
|
|
997
|
+
return;
|
|
998
|
+
scope.pending.push(build()
|
|
999
|
+
.then((card) => {
|
|
1000
|
+
if (!card)
|
|
1001
|
+
return;
|
|
1002
|
+
const pasted = unfurledKeys(scope.userText);
|
|
1003
|
+
if (pasted.has(refKey(objectOfCard(card))))
|
|
1004
|
+
return;
|
|
1005
|
+
emitBlock({ id: `b-card-${card.ref}`, kind: 'cards', cards: [card], tool });
|
|
1006
|
+
})
|
|
1007
|
+
.catch(() => undefined));
|
|
1008
|
+
}
|
|
1009
|
+
/**
|
|
1010
|
+
* The answer of test_results for a list of runs: the model reads the columns, the count, and a sample, and the full rows are held for
|
|
1011
|
+
* compose_view under the result id. The chat eval answers the tool from fixed runs through this same function (src/cli/eval-chat.ts).
|
|
1012
|
+
*/
|
|
1013
|
+
export function testResultsAnswer(input, runs, scope, names, now) {
|
|
1014
|
+
const status = str(input.status);
|
|
1015
|
+
const kind = str(input.kind);
|
|
1016
|
+
if (status && !TEST_RESULT_STATUSES.includes(status))
|
|
1017
|
+
return { error: `status must be one of ${TEST_RESULT_STATUSES.join(', ')}.` };
|
|
1018
|
+
if (kind && !TEST_RESULT_KINDS.includes(kind))
|
|
1019
|
+
return { error: `kind must be one of ${TEST_RESULT_KINDS.join(', ')}.` };
|
|
1020
|
+
if (input.splitAt !== undefined && input.splitAt !== 'last_merge')
|
|
1021
|
+
return { error: 'splitAt must be last_merge.' };
|
|
1022
|
+
const days = typeof input.sinceDays === 'number' && Number.isFinite(input.sinceDays) ? input.sinceDays : undefined;
|
|
1023
|
+
const res = testResultRows(runs, {
|
|
1024
|
+
...(now !== undefined ? { now } : {}),
|
|
1025
|
+
...(days !== undefined ? { sinceDays: days } : {}),
|
|
1026
|
+
...(str(input.branch) ? { branch: str(input.branch) } : {}),
|
|
1027
|
+
...(status ? { status } : {}),
|
|
1028
|
+
...(str(input.file) ? { file: str(input.file) } : {}),
|
|
1029
|
+
...(kind ? { kind } : {}),
|
|
1030
|
+
...(input.splitAt === 'last_merge' ? { splitAt: 'last_merge' } : {}),
|
|
1031
|
+
dotName: (id) => names.get(id),
|
|
1032
|
+
});
|
|
1033
|
+
return holdRows({
|
|
1034
|
+
scope,
|
|
1035
|
+
window: res.window,
|
|
1036
|
+
...rowsSummary(res.table, res.total),
|
|
1037
|
+
...(res.table.more ? { more: res.table.more } : {}),
|
|
1038
|
+
...(res.split ? { split: res.split } : {}),
|
|
1039
|
+
...(res.note ? { note: res.note } : { note: 'The rows are held for compose_view. Read them there with this resultId.' }),
|
|
1040
|
+
}, res.table);
|
|
1041
|
+
}
|
|
1042
|
+
/**
|
|
1043
|
+
* The open pull requests of the app repos of these dots, read from GitHub, one call per repo. A repo two dots share is read once. With
|
|
1044
|
+
* `maxRepos` (a lead's chat reads many dots, KAT-405) the rest are skipped and the result says how many.
|
|
1045
|
+
*/
|
|
1046
|
+
export async function readOpenPrs(dots, limit, o = {}) {
|
|
1047
|
+
if (!dots.length)
|
|
1048
|
+
return { count: 0, prs: [], note: 'No dot with a repo to read pull requests from.' };
|
|
1049
|
+
const seen = new Set();
|
|
1050
|
+
const prs = [];
|
|
1051
|
+
const errors = [];
|
|
1052
|
+
let skipped = 0;
|
|
1053
|
+
let read = 0;
|
|
1054
|
+
for (const w of dots) {
|
|
1055
|
+
// Every app repo of the dot, not only the first: `repo` on the dot mirrors just that one.
|
|
1056
|
+
for (const r of watchRepos(w).filter((x) => x.role === 'app')) {
|
|
1057
|
+
const full = `${r.owner}/${r.name}`;
|
|
1058
|
+
if (seen.has(full.toLowerCase()))
|
|
1059
|
+
continue;
|
|
1060
|
+
seen.add(full.toLowerCase());
|
|
1061
|
+
if (o.maxRepos !== undefined && read >= o.maxRepos) {
|
|
1062
|
+
skipped++;
|
|
1063
|
+
continue;
|
|
1064
|
+
}
|
|
1065
|
+
read++;
|
|
1066
|
+
try {
|
|
1067
|
+
const rows = (await githubFor(w, { repo: { owner: r.owner, name: r.name } }).listPulls(50)).filter((p) => p.state === 'open').slice(0, limit);
|
|
1068
|
+
for (const p of rows) {
|
|
1069
|
+
prs.push({
|
|
1070
|
+
dotId: w.id,
|
|
1071
|
+
repo: full,
|
|
1072
|
+
number: p.number,
|
|
1073
|
+
title: p.title,
|
|
1074
|
+
author: p.author ?? null,
|
|
1075
|
+
url: p.url ?? null,
|
|
1076
|
+
updatedAt: p.updatedAt ?? null,
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
catch (err) {
|
|
1081
|
+
errors.push(`${full}: ${err instanceof Error ? err.message : String(err)}`.slice(0, 200));
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
return {
|
|
1086
|
+
count: prs.length,
|
|
1087
|
+
prs,
|
|
1088
|
+
...(errors.length ? { errors } : {}),
|
|
1089
|
+
...(skipped ? { skipped, note: `Skipped ${skipped} ${skipped === 1 ? 'repo' : 'repos'}. Ask about one dot to see them.` } : {}),
|
|
1090
|
+
...(prs.length || errors.length || skipped ? {} : { note: 'No open pull requests.' }),
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
async function dispatchChatTool(name, rawInput, ctx) {
|
|
1094
|
+
const input = (rawInput && typeof rawInput === 'object' ? rawInput : {});
|
|
1095
|
+
// compose_view only arranges what the lookups returned, so a stubbed eval still runs the real one.
|
|
1096
|
+
if (toolStub && name !== COMPOSE_VIEW_TOOL)
|
|
1097
|
+
return toolStub(name, input, ctx);
|
|
1098
|
+
if (isMcpToolName(name))
|
|
1099
|
+
return runMcpTool(name, rawInput, ctx);
|
|
1100
|
+
// A Lead QA dot (KAT-405) answers from all its members and routes an action to the member whose job it is.
|
|
1101
|
+
if (ctx.watch && isLead(ctx.watch) && !ctx.member) {
|
|
1102
|
+
const led = await dispatchLeadTool(name, input, { ...ctx, team: ctx.team ?? membersOf(ctx.watch) }, { run: dispatchChatTool });
|
|
1103
|
+
if (led !== undefined)
|
|
1104
|
+
return led;
|
|
1105
|
+
}
|
|
1106
|
+
switch (name) {
|
|
1107
|
+
case 'get_ticket': {
|
|
1108
|
+
const watch = resolveWatch(input, ctx);
|
|
1109
|
+
if (!watch)
|
|
1110
|
+
return { error: 'Which dot? More than one exists; pass dotId.' };
|
|
1111
|
+
const parsed = parseTicketInput(input.ticket, watch);
|
|
1112
|
+
if (!parsed.ok)
|
|
1113
|
+
return { error: parsed.error };
|
|
1114
|
+
const res = await fetchTicketRef(watch, parsed.ref);
|
|
1115
|
+
if (!res.ok)
|
|
1116
|
+
return { error: res.error };
|
|
1117
|
+
const t = res.ticket;
|
|
1118
|
+
const last = newestFirst(list('runs', { watchId: watch.id, kind: 'ticket_review' })).find((r) => r.trigger?.ticket?.key === t.key && r.trigger.ticket.tracker === t.tracker);
|
|
1119
|
+
const cut = (x, n) => (x && x.length > n ? `${x.slice(0, n)} [truncated]` : (x ?? null));
|
|
1120
|
+
const card = ticketToCard(t, watch);
|
|
1121
|
+
emitCards([card], 'get_ticket');
|
|
1122
|
+
// A composed view may show the card too, even when the person pasted the link and it already unfurled (KAT-366).
|
|
1123
|
+
return holdViewBlocks({
|
|
1124
|
+
key: t.key,
|
|
1125
|
+
tracker: t.tracker,
|
|
1126
|
+
title: t.title,
|
|
1127
|
+
type: t.type ?? null,
|
|
1128
|
+
priority: t.priority ?? null,
|
|
1129
|
+
labels: t.labels ?? [],
|
|
1130
|
+
reporter: t.reporter ?? null,
|
|
1131
|
+
url: t.url ?? null,
|
|
1132
|
+
description: cut(t.description, 2500),
|
|
1133
|
+
acceptanceCriteria: cut(t.acceptanceCriteria, 1500),
|
|
1134
|
+
lastReview: last
|
|
1135
|
+
? {
|
|
1136
|
+
runId: last.id,
|
|
1137
|
+
at: last.createdAt,
|
|
1138
|
+
headline: last.verdict?.headline ?? null,
|
|
1139
|
+
commentStatus: last.actions.find((a) => a.action.kind === 'ticket_comment')?.result ?? 'none',
|
|
1140
|
+
}
|
|
1141
|
+
: null,
|
|
1142
|
+
status: t.status ?? null,
|
|
1143
|
+
assignee: t.assignee ?? null,
|
|
1144
|
+
}, [{ id: `b-card-${card.ref}`, kind: 'cards', cards: [card], tool: 'get_ticket' }]);
|
|
1145
|
+
}
|
|
1146
|
+
case 'review_ticket': {
|
|
1147
|
+
const watch = resolveWatch(input, ctx);
|
|
1148
|
+
if (!watch)
|
|
1149
|
+
return { error: 'Which dot? More than one exists; pass dotId.' };
|
|
1150
|
+
const parsed = parseTicketInput(input.ticket, watch);
|
|
1151
|
+
if (!parsed.ok)
|
|
1152
|
+
return { error: parsed.error };
|
|
1153
|
+
const res = await requestTicketReview(watch, parsed.ref, { requestedBy: 'chat' });
|
|
1154
|
+
if (!res.ok)
|
|
1155
|
+
return { error: res.error };
|
|
1156
|
+
const base = { queued: !res.duplicate, duplicate: res.duplicate, jobId: res.jobId, title: res.title, ticket: res.ticket.key, dot: watch.name };
|
|
1157
|
+
emitCards([ticketToCard(res.ticket, watch)], 'review_ticket');
|
|
1158
|
+
const runId = await waitForRunOfJob(res.jobId, 4000);
|
|
1159
|
+
if (!runId)
|
|
1160
|
+
return { ...base, status: 'queued', note: 'The review is queued behind other work; its result lands in the run list.' };
|
|
1161
|
+
// The card shows as soon as the run starts and follows the run to its end, however long the wait below is.
|
|
1162
|
+
emitRunCard(runId);
|
|
1163
|
+
const run = await waitForRunDone(runId, reviewWaitMs());
|
|
1164
|
+
if (!run || run.status === 'running')
|
|
1165
|
+
return { ...base, runId, status: 'running', note: 'The review is still running; the run card updates when it finishes.' };
|
|
1166
|
+
const comment = run.actions.find((a) => a.action.kind === 'ticket_comment');
|
|
1167
|
+
const approval = approvalFor(runId, (a) => a.action.kind === 'ticket_comment' && a.status === 'pending');
|
|
1168
|
+
return {
|
|
1169
|
+
...base,
|
|
1170
|
+
runId,
|
|
1171
|
+
status: run.status,
|
|
1172
|
+
outcome: run.verdict?.outcome ?? null,
|
|
1173
|
+
headline: run.verdict?.headline ?? null,
|
|
1174
|
+
risks: (run.verdict?.findings ?? []).slice(0, 5).map((f) => `${f.severity}: ${f.title}`),
|
|
1175
|
+
openQuestions: (run.verdict?.openQuestions ?? []).slice(0, 5),
|
|
1176
|
+
testIdeas: (run.verdict?.testIdeas ?? []).slice(0, 3),
|
|
1177
|
+
...(run.designed && !run.designed.error ? { testCases: designedChat(watch.id, run.designed) } : {}),
|
|
1178
|
+
comment: comment?.result ?? 'none',
|
|
1179
|
+
...(approval ? { approvalId: approval.id } : {}),
|
|
1180
|
+
...(run.status === 'failed' ? { error: run.steps.at(-1)?.detail ?? 'The review failed.' } : {}),
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
case 'design_tests': {
|
|
1184
|
+
const watch = resolveWatch(input, ctx);
|
|
1185
|
+
if (!watch)
|
|
1186
|
+
return { error: 'Which dot? More than one exists; pass dotId.' };
|
|
1187
|
+
const parsed = parseTicketInput(input.ticket, watch);
|
|
1188
|
+
if (!parsed.ok)
|
|
1189
|
+
return { error: parsed.error };
|
|
1190
|
+
const res = await requestTestDesign(watch, parsed.ref, { requestedBy: 'chat' });
|
|
1191
|
+
if (!res.ok)
|
|
1192
|
+
return { error: res.error };
|
|
1193
|
+
const base = { queued: !res.duplicate, duplicate: res.duplicate, jobId: res.jobId, title: res.title, ticket: res.ticket.key, dot: watch.name };
|
|
1194
|
+
const runId = await waitForRunOfJob(res.jobId, runGraceMs());
|
|
1195
|
+
if (!runId)
|
|
1196
|
+
return {
|
|
1197
|
+
...base,
|
|
1198
|
+
status: 'queued',
|
|
1199
|
+
note: 'The test design is queued behind other work; its run card shows in this chat when the run starts, and the run list has it too.',
|
|
1200
|
+
};
|
|
1201
|
+
emitRunCard(runId);
|
|
1202
|
+
const run = await waitForRunDone(runId, reviewWaitMs());
|
|
1203
|
+
if (!run || run.status === 'running')
|
|
1204
|
+
return { ...base, runId, status: 'running', note: 'The test design is still running; the run card updates when it finishes.' };
|
|
1205
|
+
if (run.status === 'failed' || run.designed?.error || !run.designed)
|
|
1206
|
+
return { ...base, runId, status: run.status, error: run.designed?.error ?? run.steps.at(-1)?.detail ?? 'The test design failed.' };
|
|
1207
|
+
const designed = run.designed;
|
|
1208
|
+
const cases = casesForTicket(teamIds(watch.id), designed.requirement);
|
|
1209
|
+
const block = casesBlock(watch, { requirement: designed.requirement, status: 'all' });
|
|
1210
|
+
return withBlocks({
|
|
1211
|
+
blocks: [
|
|
1212
|
+
{ ...block, id: `b-design-${watch.id}-${designed.requirement}`, title: `Designed test cases for ${designed.requirement}`, tool: 'design_tests' },
|
|
1213
|
+
],
|
|
1214
|
+
facts: {
|
|
1215
|
+
runId,
|
|
1216
|
+
status: run.status,
|
|
1217
|
+
count: cases.length,
|
|
1218
|
+
designed: designed.added,
|
|
1219
|
+
alreadyThere: designed.existing,
|
|
1220
|
+
line: designedLine(designed),
|
|
1221
|
+
link: designedCasesHref(watch.id, designed),
|
|
1222
|
+
cases: cases.slice(0, 30).map((c) => ({ key: c.key, title: c.title, status: c.status })),
|
|
1223
|
+
note: 'The card lists the cases with their keys and status. Do not repeat the rows.',
|
|
1224
|
+
},
|
|
1225
|
+
}, base);
|
|
1226
|
+
}
|
|
1227
|
+
case 'list_approvals':
|
|
1228
|
+
case 'pending_approvals': {
|
|
1229
|
+
const watch = str(input.dotId) || ctx.watch ? resolveWatch(input, ctx) : undefined;
|
|
1230
|
+
const limit = typeof input.limit === 'number' ? Math.min(20, input.limit) : 10;
|
|
1231
|
+
const rows = [...list('approvals', watch ? { watchId: watch.id, status: 'pending' } : { status: 'pending' })].sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
1232
|
+
return {
|
|
1233
|
+
count: rows.length,
|
|
1234
|
+
approvals: rows.slice(0, limit).map((a) => ({
|
|
1235
|
+
approvalId: a.id,
|
|
1236
|
+
dotId: a.watchId,
|
|
1237
|
+
runId: a.runId,
|
|
1238
|
+
kind: a.action.kind,
|
|
1239
|
+
target: a.action.target,
|
|
1240
|
+
title: a.action.title,
|
|
1241
|
+
reason: a.reason,
|
|
1242
|
+
at: a.createdAt,
|
|
1243
|
+
waitingMinutes: minutesSince(a.createdAt),
|
|
1244
|
+
})),
|
|
1245
|
+
};
|
|
1246
|
+
}
|
|
1247
|
+
case 'open_prs': {
|
|
1248
|
+
const watch = str(input.dotId) || ctx.watch ? resolveWatch(input, ctx) : undefined;
|
|
1249
|
+
const limit = typeof input.limit === 'number' ? Math.max(1, Math.min(30, input.limit)) : 10;
|
|
1250
|
+
return readOpenPrs((watch ? [watch] : list('watches')).filter((w) => !w.paused && hasAppRepo(w)), limit);
|
|
1251
|
+
}
|
|
1252
|
+
case 'runs_in_progress': {
|
|
1253
|
+
const watch = str(input.dotId) || ctx.watch ? resolveWatch(input, ctx) : undefined;
|
|
1254
|
+
const scope = watch ? { watchId: watch.id } : {};
|
|
1255
|
+
const running = newestFirst(list('runs', { ...scope, status: 'running' }));
|
|
1256
|
+
const queued = list('jobs', {
|
|
1257
|
+
...scope,
|
|
1258
|
+
status: 'queued',
|
|
1259
|
+
});
|
|
1260
|
+
return {
|
|
1261
|
+
count: running.length,
|
|
1262
|
+
runs: running.slice(0, 20).map((r) => ({ ...summarizeRun(r), runningMinutes: minutesSince(r.createdAt) })),
|
|
1263
|
+
queuedCount: queued.length,
|
|
1264
|
+
queued: queued
|
|
1265
|
+
.slice(0, 20)
|
|
1266
|
+
.map((j) => ({ kind: j.kind, dotId: j.watchId, title: j.payload?.title ?? null, waitingMinutes: minutesSince(j.createdAt) })),
|
|
1267
|
+
};
|
|
1268
|
+
}
|
|
1269
|
+
case 'recent_defects': {
|
|
1270
|
+
const watch = str(input.dotId) || ctx.watch ? resolveWatch(input, ctx) : undefined;
|
|
1271
|
+
const limit = typeof input.limit === 'number' ? Math.max(1, Math.min(20, input.limit)) : 8;
|
|
1272
|
+
const all = newestFirst(watch ? list('runs', { watchId: watch.id }) : list('runs'));
|
|
1273
|
+
let since;
|
|
1274
|
+
if (input.sinceLastMerge === true) {
|
|
1275
|
+
since = all.find((r) => r.kind === 'post_merge')?.createdAt ?? new Date(Date.now() - 168 * 3600_000).toISOString();
|
|
1276
|
+
}
|
|
1277
|
+
else {
|
|
1278
|
+
const hours = typeof input.sinceHours === 'number' && Number.isFinite(input.sinceHours) ? Math.max(1, Math.min(24 * 90, input.sinceHours)) : 168;
|
|
1279
|
+
since = new Date(Date.now() - hours * 3600_000).toISOString();
|
|
1280
|
+
}
|
|
1281
|
+
const bad = all.filter((r) => r.createdAt >= since && (isDefectRun(r) || (r.status === 'failed' && !r.verdict && (r.report?.failed ?? 0) > 0)));
|
|
1282
|
+
return {
|
|
1283
|
+
count: bad.length,
|
|
1284
|
+
since,
|
|
1285
|
+
defects: bad.slice(0, limit).map((r) => ({
|
|
1286
|
+
...summarizeRun(r),
|
|
1287
|
+
failingTests: (r.report?.cases ?? [])
|
|
1288
|
+
.filter((c) => c.status === 'failed' || c.status === 'timedOut')
|
|
1289
|
+
.slice(0, 5)
|
|
1290
|
+
.map((c) => ({ title: c.title, file: c.file, error: c.error?.replace(/\u001b\[[0-9;]*m/g, '').slice(0, 200) })),
|
|
1291
|
+
risks: (r.verdict?.findings ?? [])
|
|
1292
|
+
.filter((f) => f.severity === 'high')
|
|
1293
|
+
.slice(0, 3)
|
|
1294
|
+
.map((f) => f.title),
|
|
1295
|
+
suspectedCause: r.verdict?.suspectedCause ?? null,
|
|
1296
|
+
})),
|
|
1297
|
+
};
|
|
1298
|
+
}
|
|
1299
|
+
case 'propose_report_edit':
|
|
1300
|
+
return proposeReportEdit(input, ctx);
|
|
1301
|
+
case 'list_cases': {
|
|
1302
|
+
const watch = resolveWatch(input, ctx);
|
|
1303
|
+
if (!watch)
|
|
1304
|
+
return { error: 'Which dot? More than one exists; pass dotId.' };
|
|
1305
|
+
const filters = { status: str(input.status), kind: str(input.kind), requirement: str(input.requirement) };
|
|
1306
|
+
try {
|
|
1307
|
+
const { cases } = listCases(watch, filters);
|
|
1308
|
+
// The block goes out through executeChatTool -> emitBlock, like the stats blocks, so it keeps its id in the turn's block map.
|
|
1309
|
+
return withBlocks({
|
|
1310
|
+
blocks: [casesBlock(watch, filters)],
|
|
1311
|
+
facts: {
|
|
1312
|
+
count: cases.length,
|
|
1313
|
+
cases: cases.slice(0, 30).map((c) => ({
|
|
1314
|
+
key: c.key,
|
|
1315
|
+
title: c.title,
|
|
1316
|
+
kind: c.kind,
|
|
1317
|
+
priority: c.priority,
|
|
1318
|
+
status: c.status,
|
|
1319
|
+
requirements: c.requirementRefs,
|
|
1320
|
+
specPath: c.specPath ?? null,
|
|
1321
|
+
latest: c.latest ? { result: c.latest.result, at: c.latest.at, runId: c.latest.runId ?? null } : null,
|
|
1322
|
+
})),
|
|
1323
|
+
...(cases.length ? {} : { note: 'No test cases match. Add them on the Test cases page.' }),
|
|
1324
|
+
},
|
|
1325
|
+
}, {});
|
|
1326
|
+
}
|
|
1327
|
+
catch (err) {
|
|
1328
|
+
if (err instanceof CaseInputError)
|
|
1329
|
+
return { error: err.message };
|
|
1330
|
+
throw err;
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
case 'test_results': {
|
|
1334
|
+
const watch = str(input.dotId) || ctx.watch ? resolveWatch(input, ctx) : undefined;
|
|
1335
|
+
const runs = watch ? list('runs', { watchId: watch.id }) : list('runs');
|
|
1336
|
+
return testResultsAnswer(input, runs, watch?.name ?? 'all dots', new Map(list('watches').map((w) => [w.id, w.name])));
|
|
1337
|
+
}
|
|
1338
|
+
case COMPOSE_VIEW_TOOL: {
|
|
1339
|
+
const scope = chatScope.getStore();
|
|
1340
|
+
if (!scope)
|
|
1341
|
+
return { error: 'No chat turn is open, so there is nowhere to show a view.' };
|
|
1342
|
+
// The nodes of this call may already be on the client: the stream that read them while the model wrote it carries on.
|
|
1343
|
+
const stream = claimViewStream(scope, rawInput);
|
|
1344
|
+
return composeView(rawInput, {
|
|
1345
|
+
results: (scope.results ??= new ResultRegistry()),
|
|
1346
|
+
...((scope.watch ?? ctx.watch) ? { watch: (scope.watch ?? ctx.watch) } : {}),
|
|
1347
|
+
...(scope.channel ? { channel: scope.channel } : {}),
|
|
1348
|
+
...(scope.messageId ? { messageId: scope.messageId } : {}),
|
|
1349
|
+
triage: (window) => buildTriageBlock(scope.watch ?? ctx.watch, window ? { window } : {}),
|
|
1350
|
+
nextId: () => nextViewId(scope),
|
|
1351
|
+
emit: (e) => emitChat(e),
|
|
1352
|
+
finish: (block, o) => emitBlock(block, o),
|
|
1353
|
+
hide: (ids) => {
|
|
1354
|
+
for (const id of ids)
|
|
1355
|
+
scope.blocks?.delete(id);
|
|
1356
|
+
},
|
|
1357
|
+
line: (text) => {
|
|
1358
|
+
// Two views in one turn point to the same place, so the line goes in once.
|
|
1359
|
+
if (!scope.lines?.includes(text))
|
|
1360
|
+
(scope.lines ??= []).push(text);
|
|
1361
|
+
},
|
|
1362
|
+
...(stream ? { stream } : {}),
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
case 'pass_rate': {
|
|
1366
|
+
const watch = str(input.dotId) || ctx.watch ? resolveWatch(input, ctx) : undefined;
|
|
1367
|
+
const runs = watch ? list('runs', { watchId: watch.id }) : list('runs');
|
|
1368
|
+
return withBlocks(passRateBlocks(runs, { ...(watch ? { dotId: watch.id } : {}) }), { scope: watch?.name ?? 'all dots' });
|
|
1369
|
+
}
|
|
1370
|
+
case 'flaky_by_spec': {
|
|
1371
|
+
const watch = str(input.dotId) || ctx.watch ? resolveWatch(input, ctx) : undefined;
|
|
1372
|
+
const runs = watch ? list('runs', { watchId: watch.id }) : list('runs');
|
|
1373
|
+
const days = typeof input.sinceDays === 'number' && Number.isFinite(input.sinceDays) ? input.sinceDays : 7;
|
|
1374
|
+
return withBlocks(flakyBySpecBlocks(runs, { days, ...(watch ? { dotId: watch.id } : {}) }), { scope: watch?.name ?? 'all dots' });
|
|
1375
|
+
}
|
|
1376
|
+
case 'list_runs': {
|
|
1377
|
+
const watch = str(input.dotId) || ctx.watch ? resolveWatch(input, ctx) : undefined;
|
|
1378
|
+
const hours = typeof input.sinceHours === 'number' ? input.sinceHours : 24;
|
|
1379
|
+
const since = new Date(Date.now() - hours * 3600_000).toISOString();
|
|
1380
|
+
const limit = typeof input.limit === 'number' ? Math.min(50, input.limit) : 10;
|
|
1381
|
+
let rows = newestFirst(watch ? list('runs', { watchId: watch.id }) : list('runs')).filter((r) => r.createdAt >= since);
|
|
1382
|
+
if (str(input.kind))
|
|
1383
|
+
rows = rows.filter((r) => r.kind === input.kind);
|
|
1384
|
+
if (input.onlyProblems)
|
|
1385
|
+
rows = rows.filter((r) => r.status === 'failed' || (r.verdict && !['pass', 'info'].includes(r.verdict.outcome)));
|
|
1386
|
+
return { count: rows.length, runs: rows.slice(0, limit).map(summarizeRun) };
|
|
1387
|
+
}
|
|
1388
|
+
case 'get_run': {
|
|
1389
|
+
const run = get('runs', String(input.runId ?? ''));
|
|
1390
|
+
if (!run)
|
|
1391
|
+
return { error: 'run not found' };
|
|
1392
|
+
return {
|
|
1393
|
+
...summarizeRun(run),
|
|
1394
|
+
steps: run.steps.map((s) => `${s.status}: ${s.label}${s.detail ? ` (${s.detail})` : ''}`),
|
|
1395
|
+
failing: run.report?.cases
|
|
1396
|
+
.filter((c) => c.status === 'failed' || c.status === 'timedOut')
|
|
1397
|
+
.map((c) => ({ title: c.title, file: c.file, error: c.error?.slice(0, 400) })),
|
|
1398
|
+
verdict: run.verdict,
|
|
1399
|
+
actions: run.actions.map((a) => ({ kind: a.action.kind, target: a.action.target, result: a.result })),
|
|
1400
|
+
};
|
|
1401
|
+
}
|
|
1402
|
+
case 'list_tickets_reviewed': {
|
|
1403
|
+
const watch = resolveWatch(input, ctx);
|
|
1404
|
+
const limit = typeof input.limit === 'number' ? Math.min(20, input.limit) : 5;
|
|
1405
|
+
const rows = newestFirst(watch ? list('runs', { watchId: watch.id, kind: 'ticket_review' }) : list('runs', { kind: 'ticket_review' }));
|
|
1406
|
+
return rows.slice(0, limit).map((r) => ({
|
|
1407
|
+
runId: r.id,
|
|
1408
|
+
ticket: r.trigger?.ticket?.key ?? r.trigger?.title,
|
|
1409
|
+
title: r.trigger?.title,
|
|
1410
|
+
headline: r.verdict?.headline,
|
|
1411
|
+
risks: r.verdict?.findings.map((f) => f.title),
|
|
1412
|
+
openQuestions: r.verdict?.openQuestions,
|
|
1413
|
+
commentStatus: r.actions.find((a) => a.action.kind === 'ticket_comment')?.result ?? 'none',
|
|
1414
|
+
}));
|
|
1415
|
+
}
|
|
1416
|
+
case 'rerun_tests': {
|
|
1417
|
+
const watch = resolveWatch(input, ctx);
|
|
1418
|
+
if (!watch)
|
|
1419
|
+
return { error: 'Which dot? More than one exists; pass dotId.' };
|
|
1420
|
+
const archived = archivedNote(watch);
|
|
1421
|
+
if (archived)
|
|
1422
|
+
return { queued: false, error: archived };
|
|
1423
|
+
const grep = str(input.grep);
|
|
1424
|
+
const specs = Array.isArray(input.specs) ? input.specs.filter((s) => typeof s === 'string') : undefined;
|
|
1425
|
+
const minute = new Date().toISOString().slice(0, 16);
|
|
1426
|
+
const jobId = enqueue({
|
|
1427
|
+
kind: 'rerun',
|
|
1428
|
+
watchId: watch.id,
|
|
1429
|
+
dedupeKey: `rerun:${watch.id}:${grep ?? ''}:${minute}`,
|
|
1430
|
+
payload: { grep, specs, requestedBy: 'chat' },
|
|
1431
|
+
});
|
|
1432
|
+
return jobId
|
|
1433
|
+
? { queued: true, jobId, dot: watch.name, grep: grep ?? null }
|
|
1434
|
+
: { queued: false, reason: 'An identical rerun was queued in the last minute.' };
|
|
1435
|
+
}
|
|
1436
|
+
case 'review_pr': {
|
|
1437
|
+
const watch = resolveWatch(input, ctx);
|
|
1438
|
+
if (!watch)
|
|
1439
|
+
return { error: 'Which dot? More than one exists; pass dotId.' };
|
|
1440
|
+
const repo = str(input.repo);
|
|
1441
|
+
// Every app repo of the dot counts, not only the first one (a dot of several repos, KAT-405).
|
|
1442
|
+
const appRepos = watchRepos(watch).filter((r) => r.role === 'app');
|
|
1443
|
+
if (repo && !appRepos.length && !watch.repo)
|
|
1444
|
+
return { error: `${watch.name} watches no repo, so it cannot review pull requests.` };
|
|
1445
|
+
const namedApps = appRepos.length ? appRepos.map((r) => `${r.owner}/${r.name}`) : watch.repo ? [`${watch.repo.owner}/${watch.repo.name}`] : [];
|
|
1446
|
+
if (repo && !namedApps.some((n) => n.toLowerCase() === repo.toLowerCase()))
|
|
1447
|
+
return { error: `${watch.name} watches ${namedApps.join(', ')}, not ${repo}, so it cannot review that PR.` };
|
|
1448
|
+
const parsed = parseReviewTarget({ pr: input.pr, sha: input.sha, base: input.base, head: input.head });
|
|
1449
|
+
if (!parsed.ok)
|
|
1450
|
+
return { error: parsed.error };
|
|
1451
|
+
const res = await requestReview(watch, parsed.target, { requestedBy: 'chat', ...(repo && namedApps.length > 1 ? { repo } : {}) });
|
|
1452
|
+
if (!res.ok)
|
|
1453
|
+
return { error: res.error };
|
|
1454
|
+
if ('pr' in parsed.target) {
|
|
1455
|
+
const full = repo ??
|
|
1456
|
+
(watch.repo ? `${watch.repo.owner}/${watch.repo.name}` : watchRepos(watch)[0] ? `${watchRepos(watch)[0].owner}/${watchRepos(watch)[0].name}` : '');
|
|
1457
|
+
const pr = parsed.target.pr;
|
|
1458
|
+
if (full)
|
|
1459
|
+
cardLater(async () => {
|
|
1460
|
+
const card = await buildObjectCard({ kind: 'github_pr', repo: full, number: pr }, watch);
|
|
1461
|
+
return card.ok ? card.value : null;
|
|
1462
|
+
}, 'review_pr');
|
|
1463
|
+
}
|
|
1464
|
+
return { queued: !res.duplicate, duplicate: res.duplicate, jobId: res.jobId, kind: res.jobKind, title: res.title, dot: watch.name };
|
|
1465
|
+
}
|
|
1466
|
+
case 'get_memory': {
|
|
1467
|
+
const watch = resolveWatch(input, ctx);
|
|
1468
|
+
return listMemory(watch?.id).map((m) => ({ id: m.id, kind: m.kind, text: m.text, source: m.source }));
|
|
1469
|
+
}
|
|
1470
|
+
case 'add_memory': {
|
|
1471
|
+
// A dot's memory is the editor's (POST /api/memory), so a member's chat may not write it either (KAT-364).
|
|
1472
|
+
if (chatScope.getStore()?.role === 'member')
|
|
1473
|
+
return { error: PHONE_BLOCKED_MESSAGE };
|
|
1474
|
+
const watch = resolveWatch(input, ctx);
|
|
1475
|
+
if (!watch)
|
|
1476
|
+
return { error: 'Which dot? More than one exists; pass dotId.' };
|
|
1477
|
+
const kinds = ['flaky_spec', 'env_quirk', 'convention', 'decision', 'note'];
|
|
1478
|
+
const kind = kinds.find((k) => k === input.kind) ?? 'note';
|
|
1479
|
+
const item = addMemory(watch.id, String(input.text ?? ''), kind, 'user');
|
|
1480
|
+
return { saved: true, id: item.id, kind: item.kind };
|
|
1481
|
+
}
|
|
1482
|
+
case 'probe_status': {
|
|
1483
|
+
const watch = resolveWatch(input, ctx);
|
|
1484
|
+
const rows = list('probe_state');
|
|
1485
|
+
const scoped = watch ? rows.filter((r) => !r.watchId || r.watchId === watch.id) : rows;
|
|
1486
|
+
if (!scoped.length) {
|
|
1487
|
+
const urls = watch?.probes.map((p) => p.url) ?? [];
|
|
1488
|
+
return { probes: [], note: urls.length ? `No probe results yet for ${urls.join(', ')}.` : 'No probes configured.' };
|
|
1489
|
+
}
|
|
1490
|
+
return { probes: scoped };
|
|
1491
|
+
}
|
|
1492
|
+
default:
|
|
1493
|
+
return { error: `unknown tool ${name}` };
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
function loadHistory(scope) {
|
|
1497
|
+
try {
|
|
1498
|
+
// Dot updates (KAT-143) are reports, not turns of the conversation.
|
|
1499
|
+
const rows = newestFirst(list('messages', { watchId: scope }))
|
|
1500
|
+
.filter((r) => r.kind !== 'update')
|
|
1501
|
+
.slice(0, 20)
|
|
1502
|
+
.reverse();
|
|
1503
|
+
const history = rows.map((r) => ({ role: r.role, content: r.content }));
|
|
1504
|
+
while (history.length && history[0].role !== 'user')
|
|
1505
|
+
history.shift();
|
|
1506
|
+
return history;
|
|
1507
|
+
}
|
|
1508
|
+
catch {
|
|
1509
|
+
return [];
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
function saveMessage(scope, role, content, blocks, kind, id, work) {
|
|
1513
|
+
try {
|
|
1514
|
+
return insert('messages', {
|
|
1515
|
+
...(id ? { id } : {}),
|
|
1516
|
+
watchId: scope,
|
|
1517
|
+
role,
|
|
1518
|
+
content,
|
|
1519
|
+
...(kind ? { kind } : {}),
|
|
1520
|
+
...(blocks?.length ? { blocks } : {}),
|
|
1521
|
+
...(work ? { work } : {}),
|
|
1522
|
+
}).id;
|
|
1523
|
+
}
|
|
1524
|
+
catch (err) {
|
|
1525
|
+
console.warn('[chat] could not store message', err);
|
|
1526
|
+
return undefined;
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
/** The most step lines one turn keeps. */
|
|
1530
|
+
const MAX_STEPS = 20;
|
|
1531
|
+
/**
|
|
1532
|
+
* Entry point for POST /api/chat. Loads history, streams the reply, stores both turns.
|
|
1533
|
+
* A report-scoped chat keeps its own history (scope `report:<id>`).
|
|
1534
|
+
*/
|
|
1535
|
+
export async function runChat(input, onDelta, onTool, onEvent) {
|
|
1536
|
+
const startedAt = Date.now();
|
|
1537
|
+
const page = input.reportId ? getReport(input.reportId) : undefined;
|
|
1538
|
+
const dotId = input.dotId ?? page?.watchId;
|
|
1539
|
+
const watch = dotId ? (get('watches', dotId) ?? undefined) : undefined;
|
|
1540
|
+
const scope = page ? `report:${page.id}` : (watch?.id ?? 'all');
|
|
1541
|
+
const save = input.channel !== 'call';
|
|
1542
|
+
const history = loadHistory(scope);
|
|
1543
|
+
const askId = save ? saveMessage(scope, 'user', input.text) : undefined;
|
|
1544
|
+
const origin = input.origin ??
|
|
1545
|
+
(input.channel === 'call' || page
|
|
1546
|
+
? undefined
|
|
1547
|
+
: {
|
|
1548
|
+
channel: 'web',
|
|
1549
|
+
...(askId ? { messageId: askId } : {}),
|
|
1550
|
+
text: input.text.slice(0, 200),
|
|
1551
|
+
// Asked in the all-dots chat: the result replies there (KAT-143).
|
|
1552
|
+
...(scope === 'all' ? { scope: 'all' } : {}),
|
|
1553
|
+
// Asked in a lead's chat: a member does the work, and the result replies here as "From <member>" (KAT-405).
|
|
1554
|
+
...(watch && isLead(watch) ? { scope: 'lead', leadId: watch.id } : {}),
|
|
1555
|
+
});
|
|
1556
|
+
const req = { watch, history, text: input.text };
|
|
1557
|
+
if (input.channel)
|
|
1558
|
+
req.channel = input.channel;
|
|
1559
|
+
if (page) {
|
|
1560
|
+
req.reportId = page.id;
|
|
1561
|
+
const context = reportContextFor(page.id);
|
|
1562
|
+
if (context)
|
|
1563
|
+
req.context = context;
|
|
1564
|
+
}
|
|
1565
|
+
// A question that a working view answers gets the view, picked by a cheap rule and built from stored
|
|
1566
|
+
// runs; the chat model is not called (KAT-247). "Since the last merge" and "today" pick the runs.
|
|
1567
|
+
const view = await pickChatView(input.text, { watch, channel: input.channel ?? 'chat', ...(page ? { reportId: page.id } : {}) });
|
|
1568
|
+
// The release question of a lead: Ready, Ready with risk or Not ready, built from stored rows with no model call (KAT-405).
|
|
1569
|
+
if (view?.view === 'release' && watch && isLead(watch)) {
|
|
1570
|
+
const call = releaseCallFor(watch, view.milestone);
|
|
1571
|
+
const parsed = parseChatBlock(releaseBlock(watch, call, 'release_call'));
|
|
1572
|
+
const block = parsed.ok ? parsed.block : undefined;
|
|
1573
|
+
const reply = releaseText(watch, call, input.channel, config.publicUrl, input.trust);
|
|
1574
|
+
onDelta(reply);
|
|
1575
|
+
if (block && (input.channel ?? 'chat') === 'chat')
|
|
1576
|
+
onEvent?.({ type: 'block', block });
|
|
1577
|
+
if (save)
|
|
1578
|
+
saveMessage(scope, 'assistant', reply, block && (input.channel ?? 'chat') === 'chat' ? [block] : undefined);
|
|
1579
|
+
return reply;
|
|
1580
|
+
}
|
|
1581
|
+
if (view?.view === 'triage') {
|
|
1582
|
+
const parsed = parseChatBlock(buildTriageBlock(watch, { window: triageWindowOf(input.text) }));
|
|
1583
|
+
const block = parsed.ok ? parsed.block : undefined;
|
|
1584
|
+
const reply = block?.kind === 'triage' ? triageLine(block, watch?.name) : 'The failure triage could not be built.';
|
|
1585
|
+
onDelta(reply);
|
|
1586
|
+
if (block)
|
|
1587
|
+
onEvent?.({ type: 'block', block });
|
|
1588
|
+
if (save)
|
|
1589
|
+
saveMessage(scope, 'assistant', reply, block ? [block] : undefined);
|
|
1590
|
+
return reply;
|
|
1591
|
+
}
|
|
1592
|
+
// The Reasoning card (KAT-374): summary lines the backend returned, the tool results, and the turn's model_calls rows.
|
|
1593
|
+
const steps = [];
|
|
1594
|
+
const tools = [];
|
|
1595
|
+
const callIds = [];
|
|
1596
|
+
const step = (line) => {
|
|
1597
|
+
if (steps.length >= MAX_STEPS || steps.at(-1) === line)
|
|
1598
|
+
return;
|
|
1599
|
+
steps.push(line);
|
|
1600
|
+
onEvent?.({ type: 'reasoning', text: line });
|
|
1601
|
+
};
|
|
1602
|
+
const state = {
|
|
1603
|
+
emit: (e) => {
|
|
1604
|
+
if (e.type === 'tool_result')
|
|
1605
|
+
tools.push({ name: e.name, summary: e.summary, ...(e.error ? { error: true } : {}) });
|
|
1606
|
+
onEvent?.(e);
|
|
1607
|
+
},
|
|
1608
|
+
stream: Boolean(onEvent),
|
|
1609
|
+
pending: [],
|
|
1610
|
+
question: input.text,
|
|
1611
|
+
userText: input.text,
|
|
1612
|
+
...(input.channel ? { channel: input.channel } : {}),
|
|
1613
|
+
...(origin ? { origin } : {}),
|
|
1614
|
+
...(input.role ? { role: input.role } : {}),
|
|
1615
|
+
...(watch ? { watch } : {}),
|
|
1616
|
+
messageId: newId(),
|
|
1617
|
+
};
|
|
1618
|
+
const tool = (name, toolInput) => {
|
|
1619
|
+
onTool(name, toolInput);
|
|
1620
|
+
onEvent?.({ type: 'tool', name, input: toolInput });
|
|
1621
|
+
};
|
|
1622
|
+
// A dot with Team notes streams nothing while the model writes: a copy of the notes would show in the chat before the cut. The turn's
|
|
1623
|
+
// text is held, and the cut answer goes out in one piece when the model is done (KAT-383).
|
|
1624
|
+
const held = [];
|
|
1625
|
+
const emit = teamNotes(watch) ? (t) => void held.push(t) : onDelta;
|
|
1626
|
+
try {
|
|
1627
|
+
let reply;
|
|
1628
|
+
// A dot at its daily model cap answers no new question until the reset (KAT-362). A view answered above spent no call and still went out.
|
|
1629
|
+
const capped = watch ? capState(watch) : undefined;
|
|
1630
|
+
if (watch && capped?.reached) {
|
|
1631
|
+
reply = `${capLine(capped)}. ${watch.name} used ${capped.used} of ${capped.cap} model calls today, so chat answers and new reviews wait. Raise or remove the cap in the dot settings to go on now.`;
|
|
1632
|
+
emit(reply);
|
|
1633
|
+
}
|
|
1634
|
+
else {
|
|
1635
|
+
try {
|
|
1636
|
+
const ask = () => getLlm().chat(req, emit, tool);
|
|
1637
|
+
reply = await collectModelCalls(callIds, () => withReasoning(step, () => chatScope.run(state, () => (watch ? withModelContext(watch.id, 'chat', ask) : ask()))));
|
|
1638
|
+
}
|
|
1639
|
+
catch (err) {
|
|
1640
|
+
// No model could answer (KAT-102): say so in the chat, and never make up an answer.
|
|
1641
|
+
if (!(err instanceof ModelUnavailableError))
|
|
1642
|
+
throw err;
|
|
1643
|
+
reply = `Model unavailable: ${err.reason}. Ask again once a model works; Setup shows when it is checked again.`;
|
|
1644
|
+
emit(reply);
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
// A line the turn must carry (the designed test cases and their link) goes at the end when the model left it out.
|
|
1648
|
+
const missing = (state.lines ?? []).filter((l) => !reply.includes(l.includes('](') ? l.slice(l.indexOf('](') + 2, -1) : l));
|
|
1649
|
+
// Server-built lines carry links the server wrote: the designed test cases and the view of a turn (KAT-380).
|
|
1650
|
+
for (const l of state.lines ?? [])
|
|
1651
|
+
input.trust?.addMarkdown(l);
|
|
1652
|
+
if (missing.length) {
|
|
1653
|
+
const extra = `\n\n${missing.join('\n')}`;
|
|
1654
|
+
emit(extra);
|
|
1655
|
+
reply += extra;
|
|
1656
|
+
}
|
|
1657
|
+
// The dot's team notes went into the turn as context. A copy of them in the answer is cut out before it is stored or posted (KAT-383).
|
|
1658
|
+
reply = scrubEcho(reply, teamNotes(watch));
|
|
1659
|
+
if (held.length)
|
|
1660
|
+
onDelta(reply);
|
|
1661
|
+
// Every PR, ticket, run, and defect the answer names carries its title and link (KAT-225). The text
|
|
1662
|
+
// streamed as written (a dot with Team notes streams the cut text); this final text replaces it, and it is the one stored and sent to Slack.
|
|
1663
|
+
reply = await renderRefs(reply, {
|
|
1664
|
+
...(watch ? { dotId: watch.id } : {}),
|
|
1665
|
+
format: input.refFormat ?? (input.channel === 'call' ? 'text' : 'markdown'),
|
|
1666
|
+
absolute: input.channel === 'slack' || input.channel === 'call',
|
|
1667
|
+
...(input.trust ? { trust: input.trust } : {}),
|
|
1668
|
+
});
|
|
1669
|
+
const tokens = tokensOfCalls(callIds);
|
|
1670
|
+
const work = {
|
|
1671
|
+
elapsedMs: Date.now() - startedAt,
|
|
1672
|
+
steps: steps.length || tools.length,
|
|
1673
|
+
...(steps.length ? { summary: steps } : {}),
|
|
1674
|
+
...(tools.length ? { tools } : {}),
|
|
1675
|
+
...(tokens !== undefined ? { tokens } : {}),
|
|
1676
|
+
};
|
|
1677
|
+
onEvent?.({ type: 'work', work });
|
|
1678
|
+
input.onWork?.(work);
|
|
1679
|
+
// The reply is stored under the id its link line named, so the link from Slack or a call opens this answer (KAT-366).
|
|
1680
|
+
if (save)
|
|
1681
|
+
saveMessage(scope, 'assistant', reply, state.blocks ? [...state.blocks.values()] : undefined, undefined, state.messageId, work);
|
|
1682
|
+
// A call is not stored, but a view composed in it is: the line it spoke points to the chat, where the view must be (KAT-366).
|
|
1683
|
+
else if (state.blocks && [...state.blocks.values()].some((b) => b.kind === 'view'))
|
|
1684
|
+
saveMessage(scope, 'assistant', reply, [...state.blocks.values()].filter((b) => b.kind === 'view'), 'call_view', state.messageId);
|
|
1685
|
+
return reply;
|
|
1686
|
+
}
|
|
1687
|
+
finally {
|
|
1688
|
+
// A view that streamed and was never finished (the turn failed, or the call was never made) leaves the client.
|
|
1689
|
+
for (const s of state.viewStreams ?? [])
|
|
1690
|
+
s.close();
|
|
1691
|
+
await Promise.allSettled(state.pending);
|
|
1692
|
+
if (state.runId) {
|
|
1693
|
+
try {
|
|
1694
|
+
finishRun(state.runId, 'done');
|
|
1695
|
+
}
|
|
1696
|
+
catch {
|
|
1697
|
+
/* run already gone */
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
export { runChat as chat, runChat as answerChat };
|
|
1703
|
+
//# sourceMappingURL=chat.js.map
|