@hearsay-tools/squawk 0.0.0-stage → 0.17.0-pr3058.5683
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +1846 -2
- package/dist/activity/demand.d.ts +21 -0
- package/dist/activity/demand.js +49 -0
- package/dist/activity/demand.js.map +1 -0
- package/dist/activity/publisher.d.ts +48 -0
- package/dist/activity/publisher.js +175 -0
- package/dist/activity/publisher.js.map +1 -0
- package/dist/activity/read-model.d.ts +50 -0
- package/dist/activity/read-model.js +143 -0
- package/dist/activity/read-model.js.map +1 -0
- package/dist/activity/requests.d.ts +27 -0
- package/dist/activity/requests.js +81 -0
- package/dist/activity/requests.js.map +1 -0
- package/dist/activity/text.d.ts +4 -0
- package/dist/activity/text.js +13 -0
- package/dist/activity/text.js.map +1 -0
- package/dist/activity/tool-activity.d.ts +87 -0
- package/dist/activity/tool-activity.js +250 -0
- package/dist/activity/tool-activity.js.map +1 -0
- package/dist/activity/wait.d.ts +19 -0
- package/dist/activity/wait.js +91 -0
- package/dist/activity/wait.js.map +1 -0
- package/dist/agent-config/account-identity.d.ts +28 -0
- package/dist/agent-config/account-identity.js +132 -0
- package/dist/agent-config/account-identity.js.map +1 -0
- package/dist/agent-config/catalog.d.ts +70 -0
- package/dist/agent-config/catalog.js +384 -0
- package/dist/agent-config/catalog.js.map +1 -0
- package/dist/agent-config/files.d.ts +42 -0
- package/dist/agent-config/files.js +115 -0
- package/dist/agent-config/files.js.map +1 -0
- package/dist/agent-config/model-settings/claude.d.ts +2 -0
- package/dist/agent-config/model-settings/claude.js +11 -0
- package/dist/agent-config/model-settings/claude.js.map +1 -0
- package/dist/agent-config/model-settings/codex.d.ts +2 -0
- package/dist/agent-config/model-settings/codex.js +16 -0
- package/dist/agent-config/model-settings/codex.js.map +1 -0
- package/dist/agent-config/model-settings/opencode.d.ts +2 -0
- package/dist/agent-config/model-settings/opencode.js +8 -0
- package/dist/agent-config/model-settings/opencode.js.map +1 -0
- package/dist/agent-config/model-settings/pi.d.ts +2 -0
- package/dist/agent-config/model-settings/pi.js +38 -0
- package/dist/agent-config/model-settings/pi.js.map +1 -0
- package/dist/agent-config/model-settings/shared.d.ts +12 -0
- package/dist/agent-config/model-settings/shared.js +100 -0
- package/dist/agent-config/model-settings/shared.js.map +1 -0
- package/dist/agent-config/model-settings/types.d.ts +9 -0
- package/dist/agent-config/model-settings/types.js +2 -0
- package/dist/agent-config/model-settings/types.js.map +1 -0
- package/dist/agent-config/models.d.ts +9 -0
- package/dist/agent-config/models.js +40 -0
- package/dist/agent-config/models.js.map +1 -0
- package/dist/agent-config/seed.d.ts +15 -0
- package/dist/agent-config/seed.js +94 -0
- package/dist/agent-config/seed.js.map +1 -0
- package/dist/agent-config/service.d.ts +44 -0
- package/dist/agent-config/service.js +73 -0
- package/dist/agent-config/service.js.map +1 -0
- package/dist/agent-config/validate.d.ts +18 -0
- package/dist/agent-config/validate.js +82 -0
- package/dist/agent-config/validate.js.map +1 -0
- package/dist/application-update/bin-recovery.d.ts +5 -0
- package/dist/application-update/bin-recovery.js +96 -0
- package/dist/application-update/bin-recovery.js.map +1 -0
- package/dist/application-update/discovery.d.ts +23 -0
- package/dist/application-update/discovery.js +131 -0
- package/dist/application-update/discovery.js.map +1 -0
- package/dist/application-update/helper.d.ts +58 -0
- package/dist/application-update/helper.js +363 -0
- package/dist/application-update/helper.js.map +1 -0
- package/dist/application-update/launcher.d.ts +12 -0
- package/dist/application-update/launcher.js +61 -0
- package/dist/application-update/launcher.js.map +1 -0
- package/dist/application-update/lock.d.ts +4 -0
- package/dist/application-update/lock.js +70 -0
- package/dist/application-update/lock.js.map +1 -0
- package/dist/application-update/npm-process.d.ts +17 -0
- package/dist/application-update/npm-process.js +156 -0
- package/dist/application-update/npm-process.js.map +1 -0
- package/dist/application-update/runtime-layout.d.ts +20 -0
- package/dist/application-update/runtime-layout.js +91 -0
- package/dist/application-update/runtime-layout.js.map +1 -0
- package/dist/application-update/scoped-recovery.d.ts +32 -0
- package/dist/application-update/scoped-recovery.js +210 -0
- package/dist/application-update/scoped-recovery.js.map +1 -0
- package/dist/application-update/service.d.ts +86 -0
- package/dist/application-update/service.js +523 -0
- package/dist/application-update/service.js.map +1 -0
- package/dist/archive-on-merge-setting.d.ts +15 -0
- package/dist/archive-on-merge-setting.js +15 -0
- package/dist/archive-on-merge-setting.js.map +1 -0
- package/dist/artifacts/cli.d.ts +1 -0
- package/dist/artifacts/cli.js +54 -0
- package/dist/artifacts/cli.js.map +1 -0
- package/dist/artifacts/lifecycle.d.ts +4 -0
- package/dist/artifacts/lifecycle.js +42 -0
- package/dist/artifacts/lifecycle.js.map +1 -0
- package/dist/artifacts/reports-index.d.ts +46 -0
- package/dist/artifacts/reports-index.js +279 -0
- package/dist/artifacts/reports-index.js.map +1 -0
- package/dist/artifacts/reports-watch.d.ts +43 -0
- package/dist/artifacts/reports-watch.js +129 -0
- package/dist/artifacts/reports-watch.js.map +1 -0
- package/dist/artifacts/resolve.d.ts +18 -0
- package/dist/artifacts/resolve.js +129 -0
- package/dist/artifacts/resolve.js.map +1 -0
- package/dist/artifacts/store.d.ts +40 -0
- package/dist/artifacts/store.js +513 -0
- package/dist/artifacts/store.js.map +1 -0
- package/dist/automations/automation-cli.d.ts +61 -0
- package/dist/automations/automation-cli.js +519 -0
- package/dist/automations/automation-cli.js.map +1 -0
- package/dist/automations/builtin-skill.d.ts +7 -0
- package/dist/automations/builtin-skill.js +14 -0
- package/dist/automations/builtin-skill.js.map +1 -0
- package/dist/automations/coordinator.d.ts +53 -0
- package/dist/automations/coordinator.js +136 -0
- package/dist/automations/coordinator.js.map +1 -0
- package/dist/automations/github-baseline.d.ts +3 -0
- package/dist/automations/github-baseline.js +17 -0
- package/dist/automations/github-baseline.js.map +1 -0
- package/dist/automations/github-poller.d.ts +93 -0
- package/dist/automations/github-poller.js +303 -0
- package/dist/automations/github-poller.js.map +1 -0
- package/dist/automations/project-files.d.ts +6 -0
- package/dist/automations/project-files.js +43 -0
- package/dist/automations/project-files.js.map +1 -0
- package/dist/automations/prompts.d.ts +12 -0
- package/dist/automations/prompts.js +162 -0
- package/dist/automations/prompts.js.map +1 -0
- package/dist/automations/schedule-runner.d.ts +153 -0
- package/dist/automations/schedule-runner.js +529 -0
- package/dist/automations/schedule-runner.js.map +1 -0
- package/dist/automations/scheduler.d.ts +95 -0
- package/dist/automations/scheduler.js +462 -0
- package/dist/automations/scheduler.js.map +1 -0
- package/dist/automations/service.d.ts +793 -0
- package/dist/automations/service.js +444 -0
- package/dist/automations/service.js.map +1 -0
- package/dist/automations/store.d.ts +168 -0
- package/dist/automations/store.js +805 -0
- package/dist/automations/store.js.map +1 -0
- package/dist/automations/task-template.d.ts +59 -0
- package/dist/automations/task-template.js +285 -0
- package/dist/automations/task-template.js.map +1 -0
- package/dist/automations/tool-adapter.d.ts +6 -0
- package/dist/automations/tool-adapter.js +115 -0
- package/dist/automations/tool-adapter.js.map +1 -0
- package/dist/automations/types.d.ts +1 -0
- package/dist/automations/types.js +2 -0
- package/dist/automations/types.js.map +1 -0
- package/dist/autosave-cleanup.d.ts +26 -0
- package/dist/autosave-cleanup.js +87 -0
- package/dist/autosave-cleanup.js.map +1 -0
- package/dist/autosave-git.d.ts +23 -0
- package/dist/autosave-git.js +211 -0
- package/dist/autosave-git.js.map +1 -0
- package/dist/autosave-group.d.ts +8 -0
- package/dist/autosave-group.js +61 -0
- package/dist/autosave-group.js.map +1 -0
- package/dist/autosave-holders.d.ts +34 -0
- package/dist/autosave-holders.js +228 -0
- package/dist/autosave-holders.js.map +1 -0
- package/dist/autosave-windows.d.ts +25 -0
- package/dist/autosave-windows.js +170 -0
- package/dist/autosave-windows.js.map +1 -0
- package/dist/cez.d.ts +2 -0
- package/dist/cez.js +3 -0
- package/dist/cez.js.map +1 -0
- package/dist/cezarion.d.ts +2 -0
- package/dist/cezarion.js +3 -0
- package/dist/cezarion.js.map +1 -0
- package/dist/ci-wait/client.d.ts +28 -0
- package/dist/ci-wait/client.js +123 -0
- package/dist/ci-wait/client.js.map +1 -0
- package/dist/ci-wait/controller.d.ts +824 -0
- package/dist/ci-wait/controller.js +305 -0
- package/dist/ci-wait/controller.js.map +1 -0
- package/dist/ci-wait/errors.d.ts +35 -0
- package/dist/ci-wait/errors.js +273 -0
- package/dist/ci-wait/errors.js.map +1 -0
- package/dist/ci-wait/github.d.ts +105 -0
- package/dist/ci-wait/github.js +324 -0
- package/dist/ci-wait/github.js.map +1 -0
- package/dist/ci-wait/injection.d.ts +3 -0
- package/dist/ci-wait/injection.js +13 -0
- package/dist/ci-wait/injection.js.map +1 -0
- package/dist/ci-wait/mcp.d.ts +3 -0
- package/dist/ci-wait/mcp.js +30 -0
- package/dist/ci-wait/mcp.js.map +1 -0
- package/dist/ci-wait/merge-progress.d.ts +84 -0
- package/dist/ci-wait/merge-progress.js +50 -0
- package/dist/ci-wait/merge-progress.js.map +1 -0
- package/dist/ci-wait/progress.d.ts +56 -0
- package/dist/ci-wait/progress.js +100 -0
- package/dist/ci-wait/progress.js.map +1 -0
- package/dist/ci-wait/resources.d.ts +9 -0
- package/dist/ci-wait/resources.js +46 -0
- package/dist/ci-wait/resources.js.map +1 -0
- package/dist/ci-wait/supervisor.d.ts +47 -0
- package/dist/ci-wait/supervisor.js +334 -0
- package/dist/ci-wait/supervisor.js.map +1 -0
- package/dist/ci-wait/text.d.ts +2 -0
- package/dist/ci-wait/text.js +5 -0
- package/dist/ci-wait/text.js.map +1 -0
- package/dist/ci-wait/tools.d.ts +845 -0
- package/dist/ci-wait/tools.js +232 -0
- package/dist/ci-wait/tools.js.map +1 -0
- package/dist/cli-dispatch.d.ts +33 -0
- package/dist/cli-dispatch.js +239 -0
- package/dist/cli-dispatch.js.map +1 -0
- package/dist/config.d.ts +117 -0
- package/dist/config.js +343 -0
- package/dist/config.js.map +1 -0
- package/dist/contract/active-provider.d.ts +12 -0
- package/dist/contract/activity.d.ts +1478 -0
- package/dist/contract/agent-config.d.ts +160 -0
- package/dist/contract/agent-profiles.d.ts +369 -0
- package/dist/contract/application-update.d.ts +32 -0
- package/dist/contract/artifacts.d.ts +100 -0
- package/dist/contract/ask-schema.d.ts +50 -0
- package/dist/contract/ask.d.ts +11 -0
- package/dist/contract/attachments.d.ts +35 -0
- package/dist/contract/attention.d.ts +99 -0
- package/dist/contract/automation-requests.d.ts +314 -0
- package/dist/contract/automation-schedule.d.ts +89 -0
- package/dist/contract/automation-storage.d.ts +518 -0
- package/dist/contract/automations.d.ts +1560 -0
- package/dist/contract/ci-wait.d.ts +281 -0
- package/dist/contract/config-requests.d.ts +36 -0
- package/dist/contract/control-locations.d.ts +10 -0
- package/dist/contract/conversations.d.ts +415 -0
- package/dist/contract/defined.d.ts +8 -0
- package/dist/contract/delegation.d.ts +2168 -0
- package/dist/contract/discovery.d.ts +228 -0
- package/dist/contract/effort.d.ts +18 -0
- package/dist/contract/events.d.ts +324 -0
- package/dist/contract/github-requests.d.ts +26 -0
- package/dist/contract/github.d.ts +732 -0
- package/dist/contract/handoff.d.ts +43 -0
- package/dist/contract/health.d.ts +128 -0
- package/dist/contract/home-recovery.d.ts +71 -0
- package/dist/contract/image-path.d.ts +6 -0
- package/dist/contract/index.d.ts +68 -0
- package/dist/contract/index.js +6709 -0
- package/dist/contract/live-coordination.d.ts +164 -0
- package/dist/contract/live-limits.d.ts +8 -0
- package/dist/contract/live.d.ts +123 -0
- package/dist/contract/pending-ask.d.ts +58 -0
- package/dist/contract/per-runner-optional-strings.d.ts +10 -0
- package/dist/contract/permissions.d.ts +54 -0
- package/dist/contract/preview.d.ts +568 -0
- package/dist/contract/project-limits.d.ts +35 -0
- package/dist/contract/projects.d.ts +716 -0
- package/dist/contract/provider-requests.d.ts +77 -0
- package/dist/contract/query-requests.d.ts +52 -0
- package/dist/contract/repo-requests.d.ts +12 -0
- package/dist/contract/repo.d.ts +515 -0
- package/dist/contract/report-title.d.ts +3 -0
- package/dist/contract/reports.d.ts +101 -0
- package/dist/contract/run-labels.d.ts +8 -0
- package/dist/contract/run-limits.d.ts +8 -0
- package/dist/contract/run-predicates.d.ts +87 -0
- package/dist/contract/run-requests.d.ts +33 -0
- package/dist/contract/run-search.d.ts +193 -0
- package/dist/contract/runs.d.ts +7108 -0
- package/dist/contract/scheduled-tasks.d.ts +188 -0
- package/dist/contract/self-update.d.ts +255 -0
- package/dist/contract/skill-sources.d.ts +125 -0
- package/dist/contract/skills.d.ts +56 -0
- package/dist/contract/task-options.d.ts +18 -0
- package/dist/contract/team-skill-ref.d.ts +10 -0
- package/dist/contract/tool-display.d.ts +25 -0
- package/dist/contract/ui-state-requests.d.ts +60 -0
- package/dist/contract/version.d.ts +3 -0
- package/dist/contract/voice.d.ts +213 -0
- package/dist/contract/waits.d.ts +1996 -0
- package/dist/contract/workflow-file.d.ts +37 -0
- package/dist/contract/workflow-requests.d.ts +7 -0
- package/dist/contract/workflows.d.ts +250 -0
- package/dist/contract/workspace-events.d.ts +410 -0
- package/dist/contract/workspace.d.ts +631 -0
- package/dist/contract/zoned-time.d.ts +26 -0
- package/dist/core/agent-env.d.ts +34 -0
- package/dist/core/agent-env.js +411 -0
- package/dist/core/agent-env.js.map +1 -0
- package/dist/core/agent-model-policy.d.ts +11 -0
- package/dist/core/agent-model-policy.js +38 -0
- package/dist/core/agent-model-policy.js.map +1 -0
- package/dist/core/agent-profiles.d.ts +50 -0
- package/dist/core/agent-profiles.js +105 -0
- package/dist/core/agent-profiles.js.map +1 -0
- package/dist/core/agent-runner.d.ts +425 -0
- package/dist/core/agent-runner.js +98 -0
- package/dist/core/agent-runner.js.map +1 -0
- package/dist/core/ask.d.ts +26 -0
- package/dist/core/ask.js +35 -0
- package/dist/core/ask.js.map +1 -0
- package/dist/core/backend-detect.d.ts +18 -0
- package/dist/core/backend-detect.js +216 -0
- package/dist/core/backend-detect.js.map +1 -0
- package/dist/core/claude-cli-runner.d.ts +85 -0
- package/dist/core/claude-cli-runner.js +899 -0
- package/dist/core/claude-cli-runner.js.map +1 -0
- package/dist/core/claude-executable.d.ts +2 -0
- package/dist/core/claude-executable.js +15 -0
- package/dist/core/claude-executable.js.map +1 -0
- package/dist/core/claude-model-catalog.d.ts +16 -0
- package/dist/core/claude-model-catalog.js +198 -0
- package/dist/core/claude-model-catalog.js.map +1 -0
- package/dist/core/claude-stream.d.ts +38 -0
- package/dist/core/claude-stream.js +77 -0
- package/dist/core/claude-stream.js.map +1 -0
- package/dist/core/claude-ui-mapper.d.ts +88 -0
- package/dist/core/claude-ui-mapper.js +851 -0
- package/dist/core/claude-ui-mapper.js.map +1 -0
- package/dist/core/codex-app-server-runner.d.ts +76 -0
- package/dist/core/codex-app-server-runner.js +1271 -0
- package/dist/core/codex-app-server-runner.js.map +1 -0
- package/dist/core/codex-app-server-transport.d.ts +52 -0
- package/dist/core/codex-app-server-transport.js +200 -0
- package/dist/core/codex-app-server-transport.js.map +1 -0
- package/dist/core/codex-approvals.d.ts +12 -0
- package/dist/core/codex-approvals.js +180 -0
- package/dist/core/codex-approvals.js.map +1 -0
- package/dist/core/codex-hook-mapper.d.ts +15 -0
- package/dist/core/codex-hook-mapper.js +73 -0
- package/dist/core/codex-hook-mapper.js.map +1 -0
- package/dist/core/codex-model-catalog.d.ts +10 -0
- package/dist/core/codex-model-catalog.js +110 -0
- package/dist/core/codex-model-catalog.js.map +1 -0
- package/dist/core/codex-native-frames.d.ts +35 -0
- package/dist/core/codex-native-frames.js +80 -0
- package/dist/core/codex-native-frames.js.map +1 -0
- package/dist/core/codex-permissions.d.ts +6 -0
- package/dist/core/codex-permissions.js +27 -0
- package/dist/core/codex-permissions.js.map +1 -0
- package/dist/core/codex-stream-error.d.ts +2 -0
- package/dist/core/codex-stream-error.js +12 -0
- package/dist/core/codex-stream-error.js.map +1 -0
- package/dist/core/codex-turn-outcome.d.ts +10 -0
- package/dist/core/codex-turn-outcome.js +35 -0
- package/dist/core/codex-turn-outcome.js.map +1 -0
- package/dist/core/codex-ui-mapper-state.d.ts +84 -0
- package/dist/core/codex-ui-mapper-state.js +8 -0
- package/dist/core/codex-ui-mapper-state.js.map +1 -0
- package/dist/core/codex-ui-mapper.d.ts +44 -0
- package/dist/core/codex-ui-mapper.js +799 -0
- package/dist/core/codex-ui-mapper.js.map +1 -0
- package/dist/core/cursor-cli-flags.d.ts +19 -0
- package/dist/core/cursor-cli-flags.js +47 -0
- package/dist/core/cursor-cli-flags.js.map +1 -0
- package/dist/core/cursor-config-options.d.ts +18 -0
- package/dist/core/cursor-config-options.js +31 -0
- package/dist/core/cursor-config-options.js.map +1 -0
- package/dist/core/cursor-model-catalog.d.ts +50 -0
- package/dist/core/cursor-model-catalog.js +306 -0
- package/dist/core/cursor-model-catalog.js.map +1 -0
- package/dist/core/cursor-print-capabilities.d.ts +14 -0
- package/dist/core/cursor-print-capabilities.js +169 -0
- package/dist/core/cursor-print-capabilities.js.map +1 -0
- package/dist/core/cursor-print-process.d.ts +31 -0
- package/dist/core/cursor-print-process.js +117 -0
- package/dist/core/cursor-print-process.js.map +1 -0
- package/dist/core/cursor-print-runner.d.ts +43 -0
- package/dist/core/cursor-print-runner.js +574 -0
- package/dist/core/cursor-print-runner.js.map +1 -0
- package/dist/core/cursor-print-tool-catalog.d.ts +3 -0
- package/dist/core/cursor-print-tool-catalog.js +13 -0
- package/dist/core/cursor-print-tool-catalog.js.map +1 -0
- package/dist/core/cursor-print-ui-mapper.d.ts +47 -0
- package/dist/core/cursor-print-ui-mapper.js +574 -0
- package/dist/core/cursor-print-ui-mapper.js.map +1 -0
- package/dist/core/cursor-provider-error.d.ts +9 -0
- package/dist/core/cursor-provider-error.js +15 -0
- package/dist/core/cursor-provider-error.js.map +1 -0
- package/dist/core/cursor-runner.d.ts +46 -0
- package/dist/core/cursor-runner.js +144 -0
- package/dist/core/cursor-runner.js.map +1 -0
- package/dist/core/defined.d.ts +1 -0
- package/dist/core/defined.js +2 -0
- package/dist/core/defined.js.map +1 -0
- package/dist/core/disclaim-spawn.d.ts +11 -0
- package/dist/core/disclaim-spawn.js +60 -0
- package/dist/core/disclaim-spawn.js.map +1 -0
- package/dist/core/edit-write-key.d.ts +2 -0
- package/dist/core/edit-write-key.js +6 -0
- package/dist/core/edit-write-key.js.map +1 -0
- package/dist/core/hook-items.d.ts +14 -0
- package/dist/core/hook-items.js +13 -0
- package/dist/core/hook-items.js.map +1 -0
- package/dist/core/host-model-catalog.d.ts +7 -0
- package/dist/core/host-model-catalog.js +21 -0
- package/dist/core/host-model-catalog.js.map +1 -0
- package/dist/core/input-submissions.d.ts +14 -0
- package/dist/core/input-submissions.js +26 -0
- package/dist/core/input-submissions.js.map +1 -0
- package/dist/core/managed-session.d.ts +6 -0
- package/dist/core/managed-session.js +116 -0
- package/dist/core/managed-session.js.map +1 -0
- package/dist/core/model-discovery-options.d.ts +8 -0
- package/dist/core/model-discovery-options.js +2 -0
- package/dist/core/model-discovery-options.js.map +1 -0
- package/dist/core/model-identity.d.ts +108 -0
- package/dist/core/model-identity.js +137 -0
- package/dist/core/model-identity.js.map +1 -0
- package/dist/core/model-presets.d.ts +10 -0
- package/dist/core/model-presets.js +77 -0
- package/dist/core/model-presets.js.map +1 -0
- package/dist/core/model-probe-teardown.d.ts +4 -0
- package/dist/core/model-probe-teardown.js +20 -0
- package/dist/core/model-probe-teardown.js.map +1 -0
- package/dist/core/ndjson.d.ts +6 -0
- package/dist/core/ndjson.js +23 -0
- package/dist/core/ndjson.js.map +1 -0
- package/dist/core/omp-model-catalog.d.ts +39 -0
- package/dist/core/omp-model-catalog.js +183 -0
- package/dist/core/omp-model-catalog.js.map +1 -0
- package/dist/core/omp-runner.d.ts +86 -0
- package/dist/core/omp-runner.js +818 -0
- package/dist/core/omp-runner.js.map +1 -0
- package/dist/core/omp-subagent-rows.d.ts +21 -0
- package/dist/core/omp-subagent-rows.js +99 -0
- package/dist/core/omp-subagent-rows.js.map +1 -0
- package/dist/core/omp-ui-mapper.d.ts +112 -0
- package/dist/core/omp-ui-mapper.js +807 -0
- package/dist/core/omp-ui-mapper.js.map +1 -0
- package/dist/core/opencode-model-catalog.d.ts +27 -0
- package/dist/core/opencode-model-catalog.js +189 -0
- package/dist/core/opencode-model-catalog.js.map +1 -0
- package/dist/core/opencode-permissions.d.ts +52 -0
- package/dist/core/opencode-permissions.js +101 -0
- package/dist/core/opencode-permissions.js.map +1 -0
- package/dist/core/opencode-runtime-config.d.ts +9 -0
- package/dist/core/opencode-runtime-config.js +26 -0
- package/dist/core/opencode-runtime-config.js.map +1 -0
- package/dist/core/opencode-server-runner.d.ts +75 -0
- package/dist/core/opencode-server-runner.js +1690 -0
- package/dist/core/opencode-server-runner.js.map +1 -0
- package/dist/core/opencode-session-error.d.ts +19 -0
- package/dist/core/opencode-session-error.js +42 -0
- package/dist/core/opencode-session-error.js.map +1 -0
- package/dist/core/opencode-task-scope.d.ts +9 -0
- package/dist/core/opencode-task-scope.js +19 -0
- package/dist/core/opencode-task-scope.js.map +1 -0
- package/dist/core/opencode-ui-mapper-state.d.ts +82 -0
- package/dist/core/opencode-ui-mapper-state.js +7 -0
- package/dist/core/opencode-ui-mapper-state.js.map +1 -0
- package/dist/core/opencode-ui-mapper.d.ts +59 -0
- package/dist/core/opencode-ui-mapper.js +724 -0
- package/dist/core/opencode-ui-mapper.js.map +1 -0
- package/dist/core/opencode-usage.d.ts +32 -0
- package/dist/core/opencode-usage.js +105 -0
- package/dist/core/opencode-usage.js.map +1 -0
- package/dist/core/pi-model-catalog.d.ts +34 -0
- package/dist/core/pi-model-catalog.js +247 -0
- package/dist/core/pi-model-catalog.js.map +1 -0
- package/dist/core/pi-rpc-turn.d.ts +179 -0
- package/dist/core/pi-rpc-turn.js +370 -0
- package/dist/core/pi-rpc-turn.js.map +1 -0
- package/dist/core/pi-rpc-wire.d.ts +31 -0
- package/dist/core/pi-rpc-wire.js +88 -0
- package/dist/core/pi-rpc-wire.js.map +1 -0
- package/dist/core/pi-runner.d.ts +58 -0
- package/dist/core/pi-runner.js +587 -0
- package/dist/core/pi-runner.js.map +1 -0
- package/dist/core/pi-ui-mapper.d.ts +52 -0
- package/dist/core/pi-ui-mapper.js +408 -0
- package/dist/core/pi-ui-mapper.js.map +1 -0
- package/dist/core/process-usage.d.ts +74 -0
- package/dist/core/process-usage.js +219 -0
- package/dist/core/process-usage.js.map +1 -0
- package/dist/core/product-env.d.ts +21 -0
- package/dist/core/product-env.js +38 -0
- package/dist/core/product-env.js.map +1 -0
- package/dist/core/provider-auth.d.ts +168 -0
- package/dist/core/provider-auth.js +661 -0
- package/dist/core/provider-auth.js.map +1 -0
- package/dist/core/provider-availability.d.ts +3 -0
- package/dist/core/provider-availability.js +14 -0
- package/dist/core/provider-availability.js.map +1 -0
- package/dist/core/runner-factory.d.ts +11 -0
- package/dist/core/runner-factory.js +36 -0
- package/dist/core/runner-factory.js.map +1 -0
- package/dist/core/runner-facts.d.ts +135 -0
- package/dist/core/runner-facts.js +59 -0
- package/dist/core/runner-facts.js.map +1 -0
- package/dist/core/runner-model-catalog.d.ts +34 -0
- package/dist/core/runner-model-catalog.js +109 -0
- package/dist/core/runner-model-catalog.js.map +1 -0
- package/dist/core/runner-runtime.d.ts +40 -0
- package/dist/core/runner-runtime.js +88 -0
- package/dist/core/runner-runtime.js.map +1 -0
- package/dist/core/runner-stderr.d.ts +2 -0
- package/dist/core/runner-stderr.js +20 -0
- package/dist/core/runner-stderr.js.map +1 -0
- package/dist/core/runner-tool-grants.d.ts +19 -0
- package/dist/core/runner-tool-grants.js +54 -0
- package/dist/core/runner-tool-grants.js.map +1 -0
- package/dist/core/secret-redaction.d.ts +20 -0
- package/dist/core/secret-redaction.js +95 -0
- package/dist/core/secret-redaction.js.map +1 -0
- package/dist/core/session-process.d.ts +43 -0
- package/dist/core/session-process.js +363 -0
- package/dist/core/session-process.js.map +1 -0
- package/dist/core/shell-env.d.ts +17 -0
- package/dist/core/shell-env.js +51 -0
- package/dist/core/shell-env.js.map +1 -0
- package/dist/core/ui-events.d.ts +356 -0
- package/dist/core/ui-events.js +13 -0
- package/dist/core/ui-events.js.map +1 -0
- package/dist/core/usage-limit.d.ts +42 -0
- package/dist/core/usage-limit.js +152 -0
- package/dist/core/usage-limit.js.map +1 -0
- package/dist/core/usage.d.ts +11 -0
- package/dist/core/usage.js +15 -0
- package/dist/core/usage.js.map +1 -0
- package/dist/core/v1-text-coalescer.d.ts +36 -0
- package/dist/core/v1-text-coalescer.js +72 -0
- package/dist/core/v1-text-coalescer.js.map +1 -0
- package/dist/data-gitignore.d.ts +6 -0
- package/dist/data-gitignore.js +80 -0
- package/dist/data-gitignore.js.map +1 -0
- package/dist/delegation/capacity.d.ts +15 -0
- package/dist/delegation/capacity.js +30 -0
- package/dist/delegation/capacity.js.map +1 -0
- package/dist/delegation/cli.d.ts +5 -0
- package/dist/delegation/cli.js +572 -0
- package/dist/delegation/cli.js.map +1 -0
- package/dist/delegation/context.d.ts +40 -0
- package/dist/delegation/context.js +398 -0
- package/dist/delegation/context.js.map +1 -0
- package/dist/delegation/conversation-send.d.ts +64 -0
- package/dist/delegation/conversation-send.js +74 -0
- package/dist/delegation/conversation-send.js.map +1 -0
- package/dist/delegation/conversations.d.ts +2186 -0
- package/dist/delegation/conversations.js +285 -0
- package/dist/delegation/conversations.js.map +1 -0
- package/dist/delegation/credentials.d.ts +21 -0
- package/dist/delegation/credentials.js +66 -0
- package/dist/delegation/credentials.js.map +1 -0
- package/dist/delegation/destroy-observation.d.ts +42 -0
- package/dist/delegation/destroy-observation.js +115 -0
- package/dist/delegation/destroy-observation.js.map +1 -0
- package/dist/delegation/effort.d.ts +1 -0
- package/dist/delegation/effort.js +11 -0
- package/dist/delegation/effort.js.map +1 -0
- package/dist/delegation/execution-identity.d.ts +257 -0
- package/dist/delegation/execution-identity.js +253 -0
- package/dist/delegation/execution-identity.js.map +1 -0
- package/dist/delegation/execution-settings.d.ts +11 -0
- package/dist/delegation/execution-settings.js +65 -0
- package/dist/delegation/execution-settings.js.map +1 -0
- package/dist/delegation/idle-test-app.d.ts +4 -0
- package/dist/delegation/idle-test-app.js +186 -0
- package/dist/delegation/idle-test-app.js.map +1 -0
- package/dist/delegation/input.d.ts +12 -0
- package/dist/delegation/input.js +50 -0
- package/dist/delegation/input.js.map +1 -0
- package/dist/delegation/park-notice.d.ts +45 -0
- package/dist/delegation/park-notice.js +81 -0
- package/dist/delegation/park-notice.js.map +1 -0
- package/dist/delegation/policy.d.ts +27 -0
- package/dist/delegation/policy.js +110 -0
- package/dist/delegation/policy.js.map +1 -0
- package/dist/delegation/process-liveness.d.ts +98 -0
- package/dist/delegation/process-liveness.js +213 -0
- package/dist/delegation/process-liveness.js.map +1 -0
- package/dist/delegation/provision.d.ts +36 -0
- package/dist/delegation/provision.js +218 -0
- package/dist/delegation/provision.js.map +1 -0
- package/dist/delegation/questions.d.ts +24 -0
- package/dist/delegation/questions.js +56 -0
- package/dist/delegation/questions.js.map +1 -0
- package/dist/delegation/readiness.d.ts +13 -0
- package/dist/delegation/readiness.js +19 -0
- package/dist/delegation/readiness.js.map +1 -0
- package/dist/delegation/results.d.ts +13 -0
- package/dist/delegation/results.js +153 -0
- package/dist/delegation/results.js.map +1 -0
- package/dist/delegation/retry-backoff.d.ts +31 -0
- package/dist/delegation/retry-backoff.js +26 -0
- package/dist/delegation/retry-backoff.js.map +1 -0
- package/dist/delegation/routes.d.ts +1017 -0
- package/dist/delegation/routes.js +85 -0
- package/dist/delegation/routes.js.map +1 -0
- package/dist/delegation/scratch-cleanup.d.ts +33 -0
- package/dist/delegation/scratch-cleanup.js +155 -0
- package/dist/delegation/scratch-cleanup.js.map +1 -0
- package/dist/delegation/service.d.ts +264 -0
- package/dist/delegation/service.js +807 -0
- package/dist/delegation/service.js.map +1 -0
- package/dist/delegation/task-discovery.d.ts +6 -0
- package/dist/delegation/task-discovery.js +27 -0
- package/dist/delegation/task-discovery.js.map +1 -0
- package/dist/delegation/transport.d.ts +995 -0
- package/dist/delegation/transport.js +22 -0
- package/dist/delegation/transport.js.map +1 -0
- package/dist/delegation/wait.d.ts +7 -0
- package/dist/delegation/wait.js +44 -0
- package/dist/delegation/wait.js.map +1 -0
- package/dist/delegation/windows-test-app.d.ts +5 -0
- package/dist/delegation/windows-test-app.js +64 -0
- package/dist/delegation/windows-test-app.js.map +1 -0
- package/dist/delegation/windows-workspace-identity.d.ts +3 -0
- package/dist/delegation/windows-workspace-identity.js +23 -0
- package/dist/delegation/windows-workspace-identity.js.map +1 -0
- package/dist/delegation/worker-cleanup.d.ts +53 -0
- package/dist/delegation/worker-cleanup.js +330 -0
- package/dist/delegation/worker-cleanup.js.map +1 -0
- package/dist/delegation/workspace.d.ts +40 -0
- package/dist/delegation/workspace.js +592 -0
- package/dist/delegation/workspace.js.map +1 -0
- package/dist/detached-process.d.ts +134 -0
- package/dist/detached-process.js +460 -0
- package/dist/detached-process.js.map +1 -0
- package/dist/discovery/catalog.d.ts +16 -0
- package/dist/discovery/catalog.js +31 -0
- package/dist/discovery/catalog.js.map +1 -0
- package/dist/discovery/cli.d.ts +8 -0
- package/dist/discovery/cli.js +122 -0
- package/dist/discovery/cli.js.map +1 -0
- package/dist/git-command.d.ts +7 -0
- package/dist/git-command.js +8 -0
- package/dist/git-command.js.map +1 -0
- package/dist/git-diff-base.d.ts +41 -0
- package/dist/git-diff-base.js +116 -0
- package/dist/git-diff-base.js.map +1 -0
- package/dist/git-index-copy.d.ts +4 -0
- package/dist/git-index-copy.js +10 -0
- package/dist/git-index-copy.js.map +1 -0
- package/dist/git-private-index.d.ts +11 -0
- package/dist/git-private-index.js +51 -0
- package/dist/git-private-index.js.map +1 -0
- package/dist/git-refs.d.ts +11 -0
- package/dist/git-refs.js +14 -0
- package/dist/git-refs.js.map +1 -0
- package/dist/git-worktree-lock-helper.d.ts +7 -0
- package/dist/git-worktree-lock-helper.js +178 -0
- package/dist/git-worktree-lock-helper.js.map +1 -0
- package/dist/git-worktree-lock.d.ts +17 -0
- package/dist/git-worktree-lock.js +90 -0
- package/dist/git-worktree-lock.js.map +1 -0
- package/dist/git-worktree-release.d.ts +20 -0
- package/dist/git-worktree-release.js +27 -0
- package/dist/git-worktree-release.js.map +1 -0
- package/dist/git-worktree.d.ts +148 -0
- package/dist/git-worktree.js +858 -0
- package/dist/git-worktree.js.map +1 -0
- package/dist/handoff.d.ts +72 -0
- package/dist/handoff.js +213 -0
- package/dist/handoff.js.map +1 -0
- package/dist/headless-run.d.ts +48 -0
- package/dist/headless-run.js +158 -0
- package/dist/headless-run.js.map +1 -0
- package/dist/home/boundary.d.ts +5 -0
- package/dist/home/boundary.js +76 -0
- package/dist/home/boundary.js.map +1 -0
- package/dist/home/changes.d.ts +3 -0
- package/dist/home/changes.js +11 -0
- package/dist/home/changes.js.map +1 -0
- package/dist/home/command.d.ts +9 -0
- package/dist/home/command.js +37 -0
- package/dist/home/command.js.map +1 -0
- package/dist/home/deprecation.d.ts +9 -0
- package/dist/home/deprecation.js +17 -0
- package/dist/home/deprecation.js.map +1 -0
- package/dist/home/display.d.ts +4 -0
- package/dist/home/display.js +11 -0
- package/dist/home/display.js.map +1 -0
- package/dist/home/entry-authorization.d.ts +17 -0
- package/dist/home/entry-authorization.js +39 -0
- package/dist/home/entry-authorization.js.map +1 -0
- package/dist/home/entry.d.ts +16 -0
- package/dist/home/entry.js +96 -0
- package/dist/home/entry.js.map +1 -0
- package/dist/home/file-location.d.ts +14 -0
- package/dist/home/file-location.js +24 -0
- package/dist/home/file-location.js.map +1 -0
- package/dist/home/home.d.ts +42 -0
- package/dist/home/home.js +70 -0
- package/dist/home/home.js.map +1 -0
- package/dist/home/identity.d.ts +22 -0
- package/dist/home/identity.js +25 -0
- package/dist/home/identity.js.map +1 -0
- package/dist/home/install-marker.d.ts +27 -0
- package/dist/home/install-marker.js +78 -0
- package/dist/home/install-marker.js.map +1 -0
- package/dist/home/kind-roots.d.ts +10 -0
- package/dist/home/kind-roots.js +36 -0
- package/dist/home/kind-roots.js.map +1 -0
- package/dist/home/leases.d.ts +14 -0
- package/dist/home/leases.js +55 -0
- package/dist/home/leases.js.map +1 -0
- package/dist/home/legacy-account-paths.d.ts +4 -0
- package/dist/home/legacy-account-paths.js +41 -0
- package/dist/home/legacy-account-paths.js.map +1 -0
- package/dist/home/legacy-merge.d.ts +3 -0
- package/dist/home/legacy-merge.js +45 -0
- package/dist/home/legacy-merge.js.map +1 -0
- package/dist/home/legacy-recovery.d.ts +5 -0
- package/dist/home/legacy-recovery.js +154 -0
- package/dist/home/legacy-recovery.js.map +1 -0
- package/dist/home/legacy-source.d.ts +15 -0
- package/dist/home/legacy-source.js +57 -0
- package/dist/home/legacy-source.js.map +1 -0
- package/dist/home/migrate.d.ts +19 -0
- package/dist/home/migrate.js +322 -0
- package/dist/home/migrate.js.map +1 -0
- package/dist/home/migration-account-source.d.ts +2 -0
- package/dist/home/migration-account-source.js +17 -0
- package/dist/home/migration-account-source.js.map +1 -0
- package/dist/home/migration-accounts.d.ts +23 -0
- package/dist/home/migration-accounts.js +174 -0
- package/dist/home/migration-accounts.js.map +1 -0
- package/dist/home/migration-boot.d.ts +23 -0
- package/dist/home/migration-boot.js +84 -0
- package/dist/home/migration-boot.js.map +1 -0
- package/dist/home/migration-claim.d.ts +4 -0
- package/dist/home/migration-claim.js +69 -0
- package/dist/home/migration-claim.js.map +1 -0
- package/dist/home/migration-cli.d.ts +3 -0
- package/dist/home/migration-cli.js +64 -0
- package/dist/home/migration-cli.js.map +1 -0
- package/dist/home/migration-files.d.ts +14 -0
- package/dist/home/migration-files.js +192 -0
- package/dist/home/migration-files.js.map +1 -0
- package/dist/home/migration-guard.d.ts +2 -0
- package/dist/home/migration-guard.js +20 -0
- package/dist/home/migration-guard.js.map +1 -0
- package/dist/home/migration-plan.d.ts +20 -0
- package/dist/home/migration-plan.js +71 -0
- package/dist/home/migration-plan.js.map +1 -0
- package/dist/home/migration-record.d.ts +79 -0
- package/dist/home/migration-record.js +39 -0
- package/dist/home/migration-record.js.map +1 -0
- package/dist/home/private-files.d.ts +13 -0
- package/dist/home/private-files.js +113 -0
- package/dist/home/private-files.js.map +1 -0
- package/dist/home/process-identity.d.ts +2 -0
- package/dist/home/process-identity.js +8 -0
- package/dist/home/process-identity.js.map +1 -0
- package/dist/home/protected-roots.d.ts +25 -0
- package/dist/home/protected-roots.js +91 -0
- package/dist/home/protected-roots.js.map +1 -0
- package/dist/home/recovery-watch.d.ts +3 -0
- package/dist/home/recovery-watch.js +75 -0
- package/dist/home/recovery-watch.js.map +1 -0
- package/dist/home/recovery.d.ts +10 -0
- package/dist/home/recovery.js +94 -0
- package/dist/home/recovery.js.map +1 -0
- package/dist/home/registry-content.d.ts +2 -0
- package/dist/home/registry-content.js +17 -0
- package/dist/home/registry-content.js.map +1 -0
- package/dist/home/reset-detection.d.ts +5 -0
- package/dist/home/reset-detection.js +22 -0
- package/dist/home/reset-detection.js.map +1 -0
- package/dist/home/resolve.d.ts +10 -0
- package/dist/home/resolve.js +28 -0
- package/dist/home/resolve.js.map +1 -0
- package/dist/home/store.d.ts +48 -0
- package/dist/home/store.js +324 -0
- package/dist/home/store.js.map +1 -0
- package/dist/home/switch.d.ts +2 -0
- package/dist/home/switch.js +3 -0
- package/dist/home/switch.js.map +1 -0
- package/dist/home/version-log.d.ts +48 -0
- package/dist/home/version-log.js +216 -0
- package/dist/home/version-log.js.map +1 -0
- package/dist/home/write-lock.d.ts +6 -0
- package/dist/home/write-lock.js +96 -0
- package/dist/home/write-lock.js.map +1 -0
- package/dist/hsq.d.ts +2 -0
- package/dist/hsq.js +3 -0
- package/dist/hsq.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +123 -0
- package/dist/index.js.map +1 -0
- package/dist/install-as-command.d.ts +32 -0
- package/dist/install-as-command.js +55 -0
- package/dist/install-as-command.js.map +1 -0
- package/dist/jobs/cli.d.ts +2 -0
- package/dist/jobs/cli.js +53 -0
- package/dist/jobs/cli.js.map +1 -0
- package/dist/jobs/command.d.ts +9 -0
- package/dist/jobs/command.js +19 -0
- package/dist/jobs/command.js.map +1 -0
- package/dist/jobs/job.d.ts +77 -0
- package/dist/jobs/job.js +252 -0
- package/dist/jobs/job.js.map +1 -0
- package/dist/merge-observations.d.ts +13 -0
- package/dist/merge-observations.js +55 -0
- package/dist/merge-observations.js.map +1 -0
- package/dist/merged-streams.d.ts +13 -0
- package/dist/merged-streams.js +33 -0
- package/dist/merged-streams.js.map +1 -0
- package/dist/open-url.d.ts +2 -0
- package/dist/open-url.js +10 -0
- package/dist/open-url.js.map +1 -0
- package/dist/pack-check.d.ts +17 -0
- package/dist/pack-check.js +27 -0
- package/dist/pack-check.js.map +1 -0
- package/dist/paths.d.ts +118 -0
- package/dist/paths.js +192 -0
- package/dist/paths.js.map +1 -0
- package/dist/planner.d.ts +30 -0
- package/dist/planner.js +270 -0
- package/dist/planner.js.map +1 -0
- package/dist/preview/agent-address.d.ts +34 -0
- package/dist/preview/agent-address.js +52 -0
- package/dist/preview/agent-address.js.map +1 -0
- package/dist/preview/agent-marker.d.ts +17 -0
- package/dist/preview/agent-marker.js +71 -0
- package/dist/preview/agent-marker.js.map +1 -0
- package/dist/preview/browser-launch.d.ts +28 -0
- package/dist/preview/browser-launch.js +64 -0
- package/dist/preview/browser-launch.js.map +1 -0
- package/dist/preview/cdp-proxy.d.ts +54 -0
- package/dist/preview/cdp-proxy.js +152 -0
- package/dist/preview/cdp-proxy.js.map +1 -0
- package/dist/preview/cdp.d.ts +49 -0
- package/dist/preview/cdp.js +167 -0
- package/dist/preview/cdp.js.map +1 -0
- package/dist/preview/chromium-launch-progress.d.ts +10 -0
- package/dist/preview/chromium-launch-progress.js +48 -0
- package/dist/preview/chromium-launch-progress.js.map +1 -0
- package/dist/preview/chromium.d.ts +91 -0
- package/dist/preview/chromium.js +500 -0
- package/dist/preview/chromium.js.map +1 -0
- package/dist/preview/density.d.ts +17 -0
- package/dist/preview/density.js +62 -0
- package/dist/preview/density.js.map +1 -0
- package/dist/preview/dev-server.d.ts +68 -0
- package/dist/preview/dev-server.js +318 -0
- package/dist/preview/dev-server.js.map +1 -0
- package/dist/preview/host.d.ts +215 -0
- package/dist/preview/host.js +954 -0
- package/dist/preview/host.js.map +1 -0
- package/dist/preview/registration.d.ts +68 -0
- package/dist/preview/registration.js +111 -0
- package/dist/preview/registration.js.map +1 -0
- package/dist/preview/run-tools.d.ts +9 -0
- package/dist/preview/run-tools.js +72 -0
- package/dist/preview/run-tools.js.map +1 -0
- package/dist/preview/session.d.ts +139 -0
- package/dist/preview/session.js +479 -0
- package/dist/preview/session.js.map +1 -0
- package/dist/preview/url.d.ts +6 -0
- package/dist/preview/url.js +17 -0
- package/dist/preview/url.js.map +1 -0
- package/dist/process-wait/exit-file.d.ts +29 -0
- package/dist/process-wait/exit-file.js +67 -0
- package/dist/process-wait/exit-file.js.map +1 -0
- package/dist/process-wait/supervisor.d.ts +30 -0
- package/dist/process-wait/supervisor.js +71 -0
- package/dist/process-wait/supervisor.js.map +1 -0
- package/dist/project-commands.d.ts +14 -0
- package/dist/project-commands.js +91 -0
- package/dist/project-commands.js.map +1 -0
- package/dist/project-migration/answer.d.ts +23 -0
- package/dist/project-migration/answer.js +37 -0
- package/dist/project-migration/answer.js.map +1 -0
- package/dist/project-migration/auto.d.ts +73 -0
- package/dist/project-migration/auto.js +161 -0
- package/dist/project-migration/auto.js.map +1 -0
- package/dist/project-migration/cli.d.ts +18 -0
- package/dist/project-migration/cli.js +57 -0
- package/dist/project-migration/cli.js.map +1 -0
- package/dist/project-migration/containment.d.ts +52 -0
- package/dist/project-migration/containment.js +200 -0
- package/dist/project-migration/containment.js.map +1 -0
- package/dist/project-migration/durable.d.ts +25 -0
- package/dist/project-migration/durable.js +69 -0
- package/dist/project-migration/durable.js.map +1 -0
- package/dist/project-migration/engine.d.ts +47 -0
- package/dist/project-migration/engine.js +103 -0
- package/dist/project-migration/engine.js.map +1 -0
- package/dist/project-migration/fresh.d.ts +4 -0
- package/dist/project-migration/fresh.js +25 -0
- package/dist/project-migration/fresh.js.map +1 -0
- package/dist/project-migration/journal.d.ts +139 -0
- package/dist/project-migration/journal.js +69 -0
- package/dist/project-migration/journal.js.map +1 -0
- package/dist/project-migration/live.d.ts +48 -0
- package/dist/project-migration/live.js +148 -0
- package/dist/project-migration/live.js.map +1 -0
- package/dist/project-migration/offline.d.ts +25 -0
- package/dist/project-migration/offline.js +54 -0
- package/dist/project-migration/offline.js.map +1 -0
- package/dist/project-migration/plan.d.ts +91 -0
- package/dist/project-migration/plan.js +466 -0
- package/dist/project-migration/plan.js.map +1 -0
- package/dist/project-migration/recovery.d.ts +28 -0
- package/dist/project-migration/recovery.js +78 -0
- package/dist/project-migration/recovery.js.map +1 -0
- package/dist/project-migration/steps.d.ts +18 -0
- package/dist/project-migration/steps.js +312 -0
- package/dist/project-migration/steps.js.map +1 -0
- package/dist/project-paths.d.ts +61 -0
- package/dist/project-paths.js +185 -0
- package/dist/project-paths.js.map +1 -0
- package/dist/release/compatibility.d.ts +12 -0
- package/dist/release/compatibility.js +62 -0
- package/dist/release/compatibility.js.map +1 -0
- package/dist/release/manifests.d.ts +112 -0
- package/dist/release/manifests.js +104 -0
- package/dist/release/manifests.js.map +1 -0
- package/dist/release/publication.d.ts +3 -0
- package/dist/release/publication.js +34 -0
- package/dist/release/publication.js.map +1 -0
- package/dist/release/snapshot.d.ts +67 -0
- package/dist/release/snapshot.js +97 -0
- package/dist/release/snapshot.js.map +1 -0
- package/dist/release/stable.d.ts +38 -0
- package/dist/release/stable.js +55 -0
- package/dist/release/stable.js.map +1 -0
- package/dist/remote-settings-cli.d.ts +24 -0
- package/dist/remote-settings-cli.js +156 -0
- package/dist/remote-settings-cli.js.map +1 -0
- package/dist/remote-settings.d.ts +41 -0
- package/dist/remote-settings.js +110 -0
- package/dist/remote-settings.js.map +1 -0
- package/dist/runs/agent-tmpdir-removal.d.ts +15 -0
- package/dist/runs/agent-tmpdir-removal.js +60 -0
- package/dist/runs/agent-tmpdir-removal.js.map +1 -0
- package/dist/runs/agent-tmpdir.d.ts +119 -0
- package/dist/runs/agent-tmpdir.js +855 -0
- package/dist/runs/agent-tmpdir.js.map +1 -0
- package/dist/runs/archive-family.d.ts +33 -0
- package/dist/runs/archive-family.js +90 -0
- package/dist/runs/archive-family.js.map +1 -0
- package/dist/runs/arm-repo-handle.d.ts +10 -0
- package/dist/runs/arm-repo-handle.js +45 -0
- package/dist/runs/arm-repo-handle.js.map +1 -0
- package/dist/runs/auto-name.d.ts +77 -0
- package/dist/runs/auto-name.js +179 -0
- package/dist/runs/auto-name.js.map +1 -0
- package/dist/runs/claim-families.d.ts +37 -0
- package/dist/runs/claim-families.js +101 -0
- package/dist/runs/claim-families.js.map +1 -0
- package/dist/runs/client-request.d.ts +11 -0
- package/dist/runs/client-request.js +26 -0
- package/dist/runs/client-request.js.map +1 -0
- package/dist/runs/cold-reads.d.ts +40 -0
- package/dist/runs/cold-reads.js +56 -0
- package/dist/runs/cold-reads.js.map +1 -0
- package/dist/runs/conversation-identity.d.ts +19 -0
- package/dist/runs/conversation-identity.js +31 -0
- package/dist/runs/conversation-identity.js.map +1 -0
- package/dist/runs/delegation-state.d.ts +409 -0
- package/dist/runs/delegation-state.js +22 -0
- package/dist/runs/delegation-state.js.map +1 -0
- package/dist/runs/event-history-agent-context.d.ts +15 -0
- package/dist/runs/event-history-agent-context.js +119 -0
- package/dist/runs/event-history-agent-context.js.map +1 -0
- package/dist/runs/event-history-context.d.ts +15 -0
- package/dist/runs/event-history-context.js +109 -0
- package/dist/runs/event-history-context.js.map +1 -0
- package/dist/runs/event-history.d.ts +53 -0
- package/dist/runs/event-history.js +563 -0
- package/dist/runs/event-history.js.map +1 -0
- package/dist/runs/exit-events.d.ts +27 -0
- package/dist/runs/exit-events.js +46 -0
- package/dist/runs/exit-events.js.map +1 -0
- package/dist/runs/history-compressor.d.ts +29 -0
- package/dist/runs/history-compressor.js +116 -0
- package/dist/runs/history-compressor.js.map +1 -0
- package/dist/runs/history-file.d.ts +36 -0
- package/dist/runs/history-file.js +464 -0
- package/dist/runs/history-file.js.map +1 -0
- package/dist/runs/history-stream.d.ts +2 -0
- package/dist/runs/history-stream.js +62 -0
- package/dist/runs/history-stream.js.map +1 -0
- package/dist/runs/legacy-index.d.ts +49 -0
- package/dist/runs/legacy-index.js +156 -0
- package/dist/runs/legacy-index.js.map +1 -0
- package/dist/runs/legacy-run-rows.d.ts +12 -0
- package/dist/runs/legacy-run-rows.js +33 -0
- package/dist/runs/legacy-run-rows.js.map +1 -0
- package/dist/runs/legacy-waits.d.ts +64 -0
- package/dist/runs/legacy-waits.js +335 -0
- package/dist/runs/legacy-waits.js.map +1 -0
- package/dist/runs/live-item-snapshots.d.ts +15 -0
- package/dist/runs/live-item-snapshots.js +95 -0
- package/dist/runs/live-item-snapshots.js.map +1 -0
- package/dist/runs/loaded-run-exits.d.ts +6 -0
- package/dist/runs/loaded-run-exits.js +32 -0
- package/dist/runs/loaded-run-exits.js.map +1 -0
- package/dist/runs/loaded-run-healing.d.ts +3 -0
- package/dist/runs/loaded-run-healing.js +17 -0
- package/dist/runs/loaded-run-healing.js.map +1 -0
- package/dist/runs/outcome-commit.d.ts +119 -0
- package/dist/runs/outcome-commit.js +155 -0
- package/dist/runs/outcome-commit.js.map +1 -0
- package/dist/runs/owned-run-create.d.ts +33 -0
- package/dist/runs/owned-run-create.js +98 -0
- package/dist/runs/owned-run-create.js.map +1 -0
- package/dist/runs/project-run-connection.d.ts +76 -0
- package/dist/runs/project-run-connection.js +287 -0
- package/dist/runs/project-run-connection.js.map +1 -0
- package/dist/runs/project-run-sources.d.ts +21 -0
- package/dist/runs/project-run-sources.js +86 -0
- package/dist/runs/project-run-sources.js.map +1 -0
- package/dist/runs/pull-requests.d.ts +14 -0
- package/dist/runs/pull-requests.js +96 -0
- package/dist/runs/pull-requests.js.map +1 -0
- package/dist/runs/raw-record.d.ts +40 -0
- package/dist/runs/raw-record.js +133 -0
- package/dist/runs/raw-record.js.map +1 -0
- package/dist/runs/record-write-rules.d.ts +129 -0
- package/dist/runs/record-write-rules.js +326 -0
- package/dist/runs/record-write-rules.js.map +1 -0
- package/dist/runs/report-rows.d.ts +36 -0
- package/dist/runs/report-rows.js +102 -0
- package/dist/runs/report-rows.js.map +1 -0
- package/dist/runs/retention.d.ts +91 -0
- package/dist/runs/retention.js +193 -0
- package/dist/runs/retention.js.map +1 -0
- package/dist/runs/review-gate.d.ts +18 -0
- package/dist/runs/review-gate.js +22 -0
- package/dist/runs/review-gate.js.map +1 -0
- package/dist/runs/run-claims.d.ts +30 -0
- package/dist/runs/run-claims.js +52 -0
- package/dist/runs/run-claims.js.map +1 -0
- package/dist/runs/run-database-peer-page.d.ts +12 -0
- package/dist/runs/run-database-peer-page.js +25 -0
- package/dist/runs/run-database-peer-page.js.map +1 -0
- package/dist/runs/run-database-row.d.ts +39 -0
- package/dist/runs/run-database-row.js +83 -0
- package/dist/runs/run-database-row.js.map +1 -0
- package/dist/runs/run-database-version.d.ts +14 -0
- package/dist/runs/run-database-version.js +19 -0
- package/dist/runs/run-database-version.js.map +1 -0
- package/dist/runs/run-database.d.ts +389 -0
- package/dist/runs/run-database.js +957 -0
- package/dist/runs/run-database.js.map +1 -0
- package/dist/runs/run-exits.d.ts +4510 -0
- package/dist/runs/run-exits.js +191 -0
- package/dist/runs/run-exits.js.map +1 -0
- package/dist/runs/run-index.d.ts +36 -0
- package/dist/runs/run-index.js +262 -0
- package/dist/runs/run-index.js.map +1 -0
- package/dist/runs/run-references.d.ts +61 -0
- package/dist/runs/run-references.js +421 -0
- package/dist/runs/run-references.js.map +1 -0
- package/dist/runs/run-row.d.ts +14 -0
- package/dist/runs/run-row.js +56 -0
- package/dist/runs/run-row.js.map +1 -0
- package/dist/runs/run-summary-page.d.ts +36 -0
- package/dist/runs/run-summary-page.js +46 -0
- package/dist/runs/run-summary-page.js.map +1 -0
- package/dist/runs/run-title.d.ts +8 -0
- package/dist/runs/run-title.js +28 -0
- package/dist/runs/run-title.js.map +1 -0
- package/dist/runs/store-open-error.d.ts +24 -0
- package/dist/runs/store-open-error.js +62 -0
- package/dist/runs/store-open-error.js.map +1 -0
- package/dist/runs/store.d.ts +3974 -0
- package/dist/runs/store.js +3516 -0
- package/dist/runs/store.js.map +1 -0
- package/dist/runs/task-markers.d.ts +29 -0
- package/dist/runs/task-markers.js +78 -0
- package/dist/runs/task-markers.js.map +1 -0
- package/dist/runs/task-refs.d.ts +39 -0
- package/dist/runs/task-refs.js +107 -0
- package/dist/runs/task-refs.js.map +1 -0
- package/dist/runs/transcript-facts-fold.d.ts +64 -0
- package/dist/runs/transcript-facts-fold.js +109 -0
- package/dist/runs/transcript-facts-fold.js.map +1 -0
- package/dist/runs/transcript-facts-load.d.ts +11 -0
- package/dist/runs/transcript-facts-load.js +66 -0
- package/dist/runs/transcript-facts-load.js.map +1 -0
- package/dist/runs/transcript-facts-queue.d.ts +30 -0
- package/dist/runs/transcript-facts-queue.js +227 -0
- package/dist/runs/transcript-facts-queue.js.map +1 -0
- package/dist/runs/transcript-facts-worker.d.ts +15 -0
- package/dist/runs/transcript-facts-worker.js +26 -0
- package/dist/runs/transcript-facts-worker.js.map +1 -0
- package/dist/runs/transcript-facts.d.ts +40 -0
- package/dist/runs/transcript-facts.js +193 -0
- package/dist/runs/transcript-facts.js.map +1 -0
- package/dist/runs/ui-event-sink.d.ts +59 -0
- package/dist/runs/ui-event-sink.js +197 -0
- package/dist/runs/ui-event-sink.js.map +1 -0
- package/dist/runs/wait-capacity.d.ts +8 -0
- package/dist/runs/wait-capacity.js +28 -0
- package/dist/runs/wait-capacity.js.map +1 -0
- package/dist/runs/wait-phases.d.ts +4 -0
- package/dist/runs/wait-phases.js +11 -0
- package/dist/runs/wait-phases.js.map +1 -0
- package/dist/runs/wait-registration-write.d.ts +5 -0
- package/dist/runs/wait-registration-write.js +35 -0
- package/dist/runs/wait-registration-write.js.map +1 -0
- package/dist/runs/webhook.d.ts +193 -0
- package/dist/runs/webhook.js +362 -0
- package/dist/runs/webhook.js.map +1 -0
- package/dist/runs/worker-deletion-proof.d.ts +13 -0
- package/dist/runs/worker-deletion-proof.js +13 -0
- package/dist/runs/worker-deletion-proof.js.map +1 -0
- package/dist/runs/worker-execution.d.ts +22 -0
- package/dist/runs/worker-execution.js +30 -0
- package/dist/runs/worker-execution.js.map +1 -0
- package/dist/runs/worker-resource-holders.d.ts +17 -0
- package/dist/runs/worker-resource-holders.js +34 -0
- package/dist/runs/worker-resource-holders.js.map +1 -0
- package/dist/runs/worker-result-commit.d.ts +26 -0
- package/dist/runs/worker-result-commit.js +121 -0
- package/dist/runs/worker-result-commit.js.map +1 -0
- package/dist/self-update/checkouts.d.ts +46 -0
- package/dist/self-update/checkouts.js +284 -0
- package/dist/self-update/checkouts.js.map +1 -0
- package/dist/self-update/cli.d.ts +19 -0
- package/dist/self-update/cli.js +243 -0
- package/dist/self-update/cli.js.map +1 -0
- package/dist/self-update/installer.d.ts +24 -0
- package/dist/self-update/installer.js +139 -0
- package/dist/self-update/installer.js.map +1 -0
- package/dist/self-update/launcher.d.ts +30 -0
- package/dist/self-update/launcher.js +172 -0
- package/dist/self-update/launcher.js.map +1 -0
- package/dist/self-update/layout.d.ts +108 -0
- package/dist/self-update/layout.js +276 -0
- package/dist/self-update/layout.js.map +1 -0
- package/dist/self-update/pulls.d.ts +30 -0
- package/dist/self-update/pulls.js +128 -0
- package/dist/self-update/pulls.js.map +1 -0
- package/dist/self-update/registry.d.ts +37 -0
- package/dist/self-update/registry.js +86 -0
- package/dist/self-update/registry.js.map +1 -0
- package/dist/self-update/restart.d.ts +25 -0
- package/dist/self-update/restart.js +79 -0
- package/dist/self-update/restart.js.map +1 -0
- package/dist/self-update/semver.d.ts +19 -0
- package/dist/self-update/semver.js +79 -0
- package/dist/self-update/semver.js.map +1 -0
- package/dist/self-update/service.d.ts +115 -0
- package/dist/self-update/service.js +332 -0
- package/dist/self-update/service.js.map +1 -0
- package/dist/self-update/supervisor.d.ts +10 -0
- package/dist/self-update/supervisor.js +52 -0
- package/dist/self-update/supervisor.js.map +1 -0
- package/dist/serve-boot.d.ts +60 -0
- package/dist/serve-boot.js +142 -0
- package/dist/serve-boot.js.map +1 -0
- package/dist/serve-command.d.ts +71 -0
- package/dist/serve-command.js +168 -0
- package/dist/serve-command.js.map +1 -0
- package/dist/serve-listener.d.ts +12 -0
- package/dist/serve-listener.js +52 -0
- package/dist/serve-listener.js.map +1 -0
- package/dist/server/activity-project-topic.d.ts +44 -0
- package/dist/server/activity-project-topic.js +137 -0
- package/dist/server/activity-project-topic.js.map +1 -0
- package/dist/server/activity-routes.d.ts +317 -0
- package/dist/server/activity-routes.js +117 -0
- package/dist/server/activity-routes.js.map +1 -0
- package/dist/server/activity-sources.d.ts +26 -0
- package/dist/server/activity-sources.js +45 -0
- package/dist/server/activity-sources.js.map +1 -0
- package/dist/server/activity-topic.d.ts +33 -0
- package/dist/server/activity-topic.js +48 -0
- package/dist/server/activity-topic.js.map +1 -0
- package/dist/server/app-type.d.ts +6 -0
- package/dist/server/app-type.js +2 -0
- package/dist/server/app-type.js.map +1 -0
- package/dist/server/archive-on-merge.d.ts +45 -0
- package/dist/server/archive-on-merge.js +242 -0
- package/dist/server/archive-on-merge.js.map +1 -0
- package/dist/server/automation-log-retry.d.ts +27 -0
- package/dist/server/automation-log-retry.js +88 -0
- package/dist/server/automation-log-retry.js.map +1 -0
- package/dist/server/capabilities.d.ts +69 -0
- package/dist/server/capabilities.js +111 -0
- package/dist/server/capabilities.js.map +1 -0
- package/dist/server/checkout-progress.d.ts +10 -0
- package/dist/server/checkout-progress.js +27 -0
- package/dist/server/checkout-progress.js.map +1 -0
- package/dist/server/checkout.d.ts +153 -0
- package/dist/server/checkout.js +323 -0
- package/dist/server/checkout.js.map +1 -0
- package/dist/server/cockpit-ownership.d.ts +112 -0
- package/dist/server/cockpit-ownership.js +420 -0
- package/dist/server/cockpit-ownership.js.map +1 -0
- package/dist/server/cold-repo-handles.d.ts +17 -0
- package/dist/server/cold-repo-handles.js +82 -0
- package/dist/server/cold-repo-handles.js.map +1 -0
- package/dist/server/completion-actions.d.ts +37 -0
- package/dist/server/completion-actions.js +261 -0
- package/dist/server/completion-actions.js.map +1 -0
- package/dist/server/config-merge.d.ts +11 -0
- package/dist/server/config-merge.js +74 -0
- package/dist/server/config-merge.js.map +1 -0
- package/dist/server/create-project.d.ts +71 -0
- package/dist/server/create-project.js +467 -0
- package/dist/server/create-project.js.map +1 -0
- package/dist/server/forge/draft-pr.d.ts +8 -0
- package/dist/server/forge/draft-pr.js +123 -0
- package/dist/server/forge/draft-pr.js.map +1 -0
- package/dist/server/forge/github-filters.d.ts +22 -0
- package/dist/server/forge/github-filters.js +128 -0
- package/dist/server/forge/github-filters.js.map +1 -0
- package/dist/server/forge/github-merge-requirements.d.ts +18 -0
- package/dist/server/forge/github-merge-requirements.js +148 -0
- package/dist/server/forge/github-merge-requirements.js.map +1 -0
- package/dist/server/forge/github-merge-state.d.ts +27 -0
- package/dist/server/forge/github-merge-state.js +248 -0
- package/dist/server/forge/github-merge-state.js.map +1 -0
- package/dist/server/forge/github-timeline.d.ts +117 -0
- package/dist/server/forge/github-timeline.js +273 -0
- package/dist/server/forge/github-timeline.js.map +1 -0
- package/dist/server/forge/github.d.ts +393 -0
- package/dist/server/forge/github.js +2881 -0
- package/dist/server/forge/github.js.map +1 -0
- package/dist/server/forge/index.d.ts +47 -0
- package/dist/server/forge/index.js +81 -0
- package/dist/server/forge/index.js.map +1 -0
- package/dist/server/forge/repo-handle-discovery.d.ts +16 -0
- package/dist/server/forge/repo-handle-discovery.js +2 -0
- package/dist/server/forge/repo-handle-discovery.js.map +1 -0
- package/dist/server/forge/types.d.ts +278 -0
- package/dist/server/forge/types.js +2 -0
- package/dist/server/forge/types.js.map +1 -0
- package/dist/server/fs-browse.d.ts +82 -0
- package/dist/server/fs-browse.js +217 -0
- package/dist/server/fs-browse.js.map +1 -0
- package/dist/server/git-changes.d.ts +205 -0
- package/dist/server/git-changes.js +531 -0
- package/dist/server/git-changes.js.map +1 -0
- package/dist/server/git.d.ts +52 -0
- package/dist/server/git.js +174 -0
- package/dist/server/git.js.map +1 -0
- package/dist/server/home-recovery-routes.d.ts +171 -0
- package/dist/server/home-recovery-routes.js +56 -0
- package/dist/server/home-recovery-routes.js.map +1 -0
- package/dist/server/launch-key.d.ts +5 -0
- package/dist/server/launch-key.js +35 -0
- package/dist/server/launch-key.js.map +1 -0
- package/dist/server/live-routes.d.ts +73 -0
- package/dist/server/live-routes.js +161 -0
- package/dist/server/live-routes.js.map +1 -0
- package/dist/server/open-in-app.d.ts +57 -0
- package/dist/server/open-in-app.js +178 -0
- package/dist/server/open-in-app.js.map +1 -0
- package/dist/server/open-in-targets.d.ts +53 -0
- package/dist/server/open-in-targets.js +112 -0
- package/dist/server/open-in-targets.js.map +1 -0
- package/dist/server/open-in-terminal.d.ts +73 -0
- package/dist/server/open-in-terminal.js +160 -0
- package/dist/server/open-in-terminal.js.map +1 -0
- package/dist/server/preview-cdp-socket.d.ts +21 -0
- package/dist/server/preview-cdp-socket.js +112 -0
- package/dist/server/preview-cdp-socket.js.map +1 -0
- package/dist/server/preview-socket.d.ts +36 -0
- package/dist/server/preview-socket.js +142 -0
- package/dist/server/preview-socket.js.map +1 -0
- package/dist/server/preview-sockets.d.ts +14 -0
- package/dist/server/preview-sockets.js +18 -0
- package/dist/server/preview-sockets.js.map +1 -0
- package/dist/server/project-context.d.ts +218 -0
- package/dist/server/project-context.js +525 -0
- package/dist/server/project-context.js.map +1 -0
- package/dist/server/project-run-files.d.ts +13 -0
- package/dist/server/project-run-files.js +52 -0
- package/dist/server/project-run-files.js.map +1 -0
- package/dist/server/provider-action-gate.d.ts +5 -0
- package/dist/server/provider-action-gate.js +28 -0
- package/dist/server/provider-action-gate.js.map +1 -0
- package/dist/server/provider-auth-runtime.d.ts +34 -0
- package/dist/server/provider-auth-runtime.js +118 -0
- package/dist/server/provider-auth-runtime.js.map +1 -0
- package/dist/server/repo-branch-forge.d.ts +61 -0
- package/dist/server/repo-branch-forge.js +158 -0
- package/dist/server/repo-branch-forge.js.map +1 -0
- package/dist/server/repo-branch-sweep.d.ts +21 -0
- package/dist/server/repo-branch-sweep.js +64 -0
- package/dist/server/repo-branch-sweep.js.map +1 -0
- package/dist/server/repo-branches.d.ts +103 -0
- package/dist/server/repo-branches.js +804 -0
- package/dist/server/repo-branches.js.map +1 -0
- package/dist/server/repo-pull.d.ts +18 -0
- package/dist/server/repo-pull.js +97 -0
- package/dist/server/repo-pull.js.map +1 -0
- package/dist/server/reports-api.d.ts +90 -0
- package/dist/server/reports-api.js +21 -0
- package/dist/server/reports-api.js.map +1 -0
- package/dist/server/reports-topic.d.ts +5 -0
- package/dist/server/reports-topic.js +37 -0
- package/dist/server/reports-topic.js.map +1 -0
- package/dist/server/run-checkout.d.ts +56 -0
- package/dist/server/run-checkout.js +141 -0
- package/dist/server/run-checkout.js.map +1 -0
- package/dist/server/run-event-feed.d.ts +20 -0
- package/dist/server/run-event-feed.js +203 -0
- package/dist/server/run-event-feed.js.map +1 -0
- package/dist/server/self-update-routes.d.ts +308 -0
- package/dist/server/self-update-routes.js +80 -0
- package/dist/server/self-update-routes.js.map +1 -0
- package/dist/server/server-deps.d.ts +40 -0
- package/dist/server/server-deps.js +132 -0
- package/dist/server/server-deps.js.map +1 -0
- package/dist/server/server.d.ts +35332 -0
- package/dist/server/server.js +5046 -0
- package/dist/server/server.js.map +1 -0
- package/dist/server/skill-sources.d.ts +264 -0
- package/dist/server/skill-sources.js +130 -0
- package/dist/server/skill-sources.js.map +1 -0
- package/dist/server/start-run-options.d.ts +84 -0
- package/dist/server/start-run-options.js +114 -0
- package/dist/server/start-run-options.js.map +1 -0
- package/dist/server/static-ui.d.ts +49 -0
- package/dist/server/static-ui.js +99 -0
- package/dist/server/static-ui.js.map +1 -0
- package/dist/server/upgrade-router.d.ts +23 -0
- package/dist/server/upgrade-router.js +53 -0
- package/dist/server/upgrade-router.js.map +1 -0
- package/dist/server/validators.d.ts +80 -0
- package/dist/server/validators.js +85 -0
- package/dist/server/validators.js.map +1 -0
- package/dist/server/workflow-files.d.ts +9 -0
- package/dist/server/workflow-files.js +27 -0
- package/dist/server/workflow-files.js.map +1 -0
- package/dist/server/workspace-config-patch.d.ts +11 -0
- package/dist/server/workspace-config-patch.js +75 -0
- package/dist/server/workspace-config-patch.js.map +1 -0
- package/dist/server/workspace-live-routes.d.ts +222 -0
- package/dist/server/workspace-live-routes.js +8 -0
- package/dist/server/workspace-live-routes.js.map +1 -0
- package/dist/server/workspace-projects.d.ts +51 -0
- package/dist/server/workspace-projects.js +161 -0
- package/dist/server/workspace-projects.js.map +1 -0
- package/dist/server/workspace-run-feed.d.ts +66 -0
- package/dist/server/workspace-run-feed.js +171 -0
- package/dist/server/workspace-run-feed.js.map +1 -0
- package/dist/server/ws.d.ts +80 -0
- package/dist/server/ws.js +280 -0
- package/dist/server/ws.js.map +1 -0
- package/dist/server/wsl.d.ts +25 -0
- package/dist/server/wsl.js +92 -0
- package/dist/server/wsl.js.map +1 -0
- package/dist/server-command.d.ts +53 -0
- package/dist/server-command.js +180 -0
- package/dist/server-command.js.map +1 -0
- package/dist/server-install/engine.d.ts +51 -0
- package/dist/server-install/engine.js +358 -0
- package/dist/server-install/engine.js.map +1 -0
- package/dist/server-install/group-migrate.d.ts +26 -0
- package/dist/server-install/group-migrate.js +241 -0
- package/dist/server-install/group-migrate.js.map +1 -0
- package/dist/server-install/migrate-cli.d.ts +12 -0
- package/dist/server-install/migrate-cli.js +44 -0
- package/dist/server-install/migrate-cli.js.map +1 -0
- package/dist/server-install/migrate-journal.d.ts +15 -0
- package/dist/server-install/migrate-journal.js +48 -0
- package/dist/server-install/migrate-journal.js.map +1 -0
- package/dist/server-install/migration-report.d.ts +3 -0
- package/dist/server-install/migration-report.js +21 -0
- package/dist/server-install/migration-report.js.map +1 -0
- package/dist/server-install/npx-cache.d.ts +34 -0
- package/dist/server-install/npx-cache.js +142 -0
- package/dist/server-install/npx-cache.js.map +1 -0
- package/dist/server-install/platforms/macosx-launchd-files.d.ts +11 -0
- package/dist/server-install/platforms/macosx-launchd-files.js +60 -0
- package/dist/server-install/platforms/macosx-launchd-files.js.map +1 -0
- package/dist/server-install/platforms/macosx-migration.d.ts +17 -0
- package/dist/server-install/platforms/macosx-migration.js +239 -0
- package/dist/server-install/platforms/macosx-migration.js.map +1 -0
- package/dist/server-install/platforms/macosx-ngrok.d.ts +12 -0
- package/dist/server-install/platforms/macosx-ngrok.js +490 -0
- package/dist/server-install/platforms/macosx-ngrok.js.map +1 -0
- package/dist/server-install/platforms/service-env.d.ts +4 -0
- package/dist/server-install/platforms/service-env.js +17 -0
- package/dist/server-install/platforms/service-env.js.map +1 -0
- package/dist/server-install/platforms/ubuntu-vps-files.d.ts +18 -0
- package/dist/server-install/platforms/ubuntu-vps-files.js +43 -0
- package/dist/server-install/platforms/ubuntu-vps-files.js.map +1 -0
- package/dist/server-install/platforms/ubuntu-vps-migration.d.ts +26 -0
- package/dist/server-install/platforms/ubuntu-vps-migration.js +391 -0
- package/dist/server-install/platforms/ubuntu-vps-migration.js.map +1 -0
- package/dist/server-install/platforms/ubuntu-vps-names.d.ts +30 -0
- package/dist/server-install/platforms/ubuntu-vps-names.js +50 -0
- package/dist/server-install/platforms/ubuntu-vps-names.js.map +1 -0
- package/dist/server-install/platforms/ubuntu-vps-redeploy.d.ts +42 -0
- package/dist/server-install/platforms/ubuntu-vps-redeploy.js +127 -0
- package/dist/server-install/platforms/ubuntu-vps-redeploy.js.map +1 -0
- package/dist/server-install/platforms/ubuntu-vps.d.ts +57 -0
- package/dist/server-install/platforms/ubuntu-vps.js +945 -0
- package/dist/server-install/platforms/ubuntu-vps.js.map +1 -0
- package/dist/server-install/state.d.ts +51 -0
- package/dist/server-install/state.js +178 -0
- package/dist/server-install/state.js.map +1 -0
- package/dist/server-install/steps.d.ts +108 -0
- package/dist/server-install/steps.js +348 -0
- package/dist/server-install/steps.js.map +1 -0
- package/dist/server-install/strategies.d.ts +6 -0
- package/dist/server-install/strategies.js +24 -0
- package/dist/server-install/strategies.js.map +1 -0
- package/dist/server-install/types.d.ts +316 -0
- package/dist/server-install/types.js +118 -0
- package/dist/server-install/types.js.map +1 -0
- package/dist/server-install/ui.d.ts +36 -0
- package/dist/server-install/ui.js +131 -0
- package/dist/server-install/ui.js.map +1 -0
- package/dist/session-context.d.ts +66 -0
- package/dist/session-context.js +266 -0
- package/dist/session-context.js.map +1 -0
- package/dist/skills-provision.d.ts +64 -0
- package/dist/skills-provision.js +325 -0
- package/dist/skills-provision.js.map +1 -0
- package/dist/skills-remote.d.ts +144 -0
- package/dist/skills-remote.js +847 -0
- package/dist/skills-remote.js.map +1 -0
- package/dist/skills.d.ts +81 -0
- package/dist/skills.js +288 -0
- package/dist/skills.js.map +1 -0
- package/dist/subcommands.d.ts +12 -0
- package/dist/subcommands.js +59 -0
- package/dist/subcommands.js.map +1 -0
- package/dist/task-cli/cli.d.ts +39 -0
- package/dist/task-cli/cli.js +773 -0
- package/dist/task-cli/cli.js.map +1 -0
- package/dist/task-cli/discovery.d.ts +32 -0
- package/dist/task-cli/discovery.js +133 -0
- package/dist/task-cli/discovery.js.map +1 -0
- package/dist/task-cli/hint.d.ts +20 -0
- package/dist/task-cli/hint.js +15 -0
- package/dist/task-cli/hint.js.map +1 -0
- package/dist/task-cli/http.d.ts +42 -0
- package/dist/task-cli/http.js +110 -0
- package/dist/task-cli/http.js.map +1 -0
- package/dist/task-cli/projections.d.ts +68 -0
- package/dist/task-cli/projections.js +76 -0
- package/dist/task-cli/projections.js.map +1 -0
- package/dist/task-cli/watch.d.ts +119 -0
- package/dist/task-cli/watch.js +494 -0
- package/dist/task-cli/watch.js.map +1 -0
- package/dist/ui-state.d.ts +41 -0
- package/dist/ui-state.js +93 -0
- package/dist/ui-state.js.map +1 -0
- package/dist/update-check.d.ts +2 -0
- package/dist/update-check.js +28 -0
- package/dist/update-check.js.map +1 -0
- package/dist/use-command-notice.d.ts +1 -0
- package/dist/use-command-notice.js +6 -0
- package/dist/use-command-notice.js.map +1 -0
- package/dist/voice/decide.d.ts +35 -0
- package/dist/voice/decide.js +329 -0
- package/dist/voice/decide.js.map +1 -0
- package/dist/voice/decision-log.d.ts +48 -0
- package/dist/voice/decision-log.js +117 -0
- package/dist/voice/decision-log.js.map +1 -0
- package/dist/voice/engine.d.ts +119 -0
- package/dist/voice/engine.js +61 -0
- package/dist/voice/engine.js.map +1 -0
- package/dist/voice/fake-engine.d.ts +10 -0
- package/dist/voice/fake-engine.js +192 -0
- package/dist/voice/fake-engine.js.map +1 -0
- package/dist/voice/jev-engine.d.ts +17 -0
- package/dist/voice/jev-engine.js +110 -0
- package/dist/voice/jev-engine.js.map +1 -0
- package/dist/voice/pending-ask.d.ts +17 -0
- package/dist/voice/pending-ask.js +51 -0
- package/dist/voice/pending-ask.js.map +1 -0
- package/dist/voice/routes.d.ts +115 -0
- package/dist/voice/routes.js +92 -0
- package/dist/voice/routes.js.map +1 -0
- package/dist/voice/sentence.d.ts +21 -0
- package/dist/voice/sentence.js +61 -0
- package/dist/voice/sentence.js.map +1 -0
- package/dist/voice/thresholds.d.ts +28 -0
- package/dist/voice/thresholds.js +29 -0
- package/dist/voice/thresholds.js.map +1 -0
- package/dist/webhooks/cli.d.ts +18 -0
- package/dist/webhooks/cli.js +127 -0
- package/dist/webhooks/cli.js.map +1 -0
- package/dist/webhooks/prompt.d.ts +5 -0
- package/dist/webhooks/prompt.js +26 -0
- package/dist/webhooks/prompt.js.map +1 -0
- package/dist/workflows/active-run.d.ts +46 -0
- package/dist/workflows/active-run.js +46 -0
- package/dist/workflows/active-run.js.map +1 -0
- package/dist/workflows/attachment-path.d.ts +6 -0
- package/dist/workflows/attachment-path.js +40 -0
- package/dist/workflows/attachment-path.js.map +1 -0
- package/dist/workflows/completion-policy.d.ts +98 -0
- package/dist/workflows/completion-policy.js +77 -0
- package/dist/workflows/completion-policy.js.map +1 -0
- package/dist/workflows/continuation-admission.d.ts +1229 -0
- package/dist/workflows/continuation-admission.js +277 -0
- package/dist/workflows/continuation-admission.js.map +1 -0
- package/dist/workflows/execution-account.d.ts +13 -0
- package/dist/workflows/execution-account.js +18 -0
- package/dist/workflows/execution-account.js.map +1 -0
- package/dist/workflows/grouped-command.d.ts +31 -0
- package/dist/workflows/grouped-command.js +147 -0
- package/dist/workflows/grouped-command.js.map +1 -0
- package/dist/workflows/input-acknowledgement.d.ts +233 -0
- package/dist/workflows/input-acknowledgement.js +41 -0
- package/dist/workflows/input-acknowledgement.js.map +1 -0
- package/dist/workflows/line-order.d.ts +5 -0
- package/dist/workflows/line-order.js +25 -0
- package/dist/workflows/line-order.js.map +1 -0
- package/dist/workflows/load.d.ts +19 -0
- package/dist/workflows/load.js +74 -0
- package/dist/workflows/load.js.map +1 -0
- package/dist/workflows/outcome-batches.d.ts +32 -0
- package/dist/workflows/outcome-batches.js +55 -0
- package/dist/workflows/outcome-batches.js.map +1 -0
- package/dist/workflows/pasted-content.d.ts +60 -0
- package/dist/workflows/pasted-content.js +69 -0
- package/dist/workflows/pasted-content.js.map +1 -0
- package/dist/workflows/question-routing.d.ts +16 -0
- package/dist/workflows/question-routing.js +28 -0
- package/dist/workflows/question-routing.js.map +1 -0
- package/dist/workflows/run/attachment-writer.d.ts +34 -0
- package/dist/workflows/run/attachment-writer.js +98 -0
- package/dist/workflows/run/attachment-writer.js.map +1 -0
- package/dist/workflows/run/auto-resume.d.ts +91 -0
- package/dist/workflows/run/auto-resume.js +247 -0
- package/dist/workflows/run/auto-resume.js.map +1 -0
- package/dist/workflows/run/cezar-tools.d.ts +45 -0
- package/dist/workflows/run/cezar-tools.js +117 -0
- package/dist/workflows/run/cezar-tools.js.map +1 -0
- package/dist/workflows/run/completion-gate.d.ts +77 -0
- package/dist/workflows/run/completion-gate.js +93 -0
- package/dist/workflows/run/completion-gate.js.map +1 -0
- package/dist/workflows/run/compose.d.ts +181 -0
- package/dist/workflows/run/compose.js +413 -0
- package/dist/workflows/run/compose.js.map +1 -0
- package/dist/workflows/run/constants.d.ts +48 -0
- package/dist/workflows/run/constants.js +49 -0
- package/dist/workflows/run/constants.js.map +1 -0
- package/dist/workflows/run/continuation-acceptance.d.ts +70 -0
- package/dist/workflows/run/continuation-acceptance.js +188 -0
- package/dist/workflows/run/continuation-acceptance.js.map +1 -0
- package/dist/workflows/run/continuation-session.d.ts +130 -0
- package/dist/workflows/run/continuation-session.js +597 -0
- package/dist/workflows/run/continuation-session.js.map +1 -0
- package/dist/workflows/run/drop-active.d.ts +4 -0
- package/dist/workflows/run/drop-active.js +11 -0
- package/dist/workflows/run/drop-active.js.map +1 -0
- package/dist/workflows/run/family-reconciler.d.ts +78 -0
- package/dist/workflows/run/family-reconciler.js +308 -0
- package/dist/workflows/run/family-reconciler.js.map +1 -0
- package/dist/workflows/run/human-waits.d.ts +85 -0
- package/dist/workflows/run/human-waits.js +163 -0
- package/dist/workflows/run/human-waits.js.map +1 -0
- package/dist/workflows/run/inbox-claims.d.ts +38 -0
- package/dist/workflows/run/inbox-claims.js +108 -0
- package/dist/workflows/run/inbox-claims.js.map +1 -0
- package/dist/workflows/run/input-batch.d.ts +15 -0
- package/dist/workflows/run/input-batch.js +32 -0
- package/dist/workflows/run/input-batch.js.map +1 -0
- package/dist/workflows/run/input-delivery.d.ts +202 -0
- package/dist/workflows/run/input-delivery.js +818 -0
- package/dist/workflows/run/input-delivery.js.map +1 -0
- package/dist/workflows/run/input-receipts.d.ts +65 -0
- package/dist/workflows/run/input-receipts.js +221 -0
- package/dist/workflows/run/input-receipts.js.map +1 -0
- package/dist/workflows/run/input-timers.d.ts +10 -0
- package/dist/workflows/run/input-timers.js +32 -0
- package/dist/workflows/run/input-timers.js.map +1 -0
- package/dist/workflows/run/launch-context.d.ts +62 -0
- package/dist/workflows/run/launch-context.js +119 -0
- package/dist/workflows/run/launch-context.js.map +1 -0
- package/dist/workflows/run/launch-read-resolver.d.ts +510 -0
- package/dist/workflows/run/launch-read-resolver.js +658 -0
- package/dist/workflows/run/launch-read-resolver.js.map +1 -0
- package/dist/workflows/run/launch-settings.d.ts +10 -0
- package/dist/workflows/run/launch-settings.js +11 -0
- package/dist/workflows/run/launch-settings.js.map +1 -0
- package/dist/workflows/run/live-session-state.d.ts +49 -0
- package/dist/workflows/run/live-session-state.js +83 -0
- package/dist/workflows/run/live-session-state.js.map +1 -0
- package/dist/workflows/run/maintenance-claims.d.ts +47 -0
- package/dist/workflows/run/maintenance-claims.js +83 -0
- package/dist/workflows/run/maintenance-claims.js.map +1 -0
- package/dist/workflows/run/memory-guard.d.ts +25 -0
- package/dist/workflows/run/memory-guard.js +64 -0
- package/dist/workflows/run/memory-guard.js.map +1 -0
- package/dist/workflows/run/opening.d.ts +30 -0
- package/dist/workflows/run/opening.js +6 -0
- package/dist/workflows/run/opening.js.map +1 -0
- package/dist/workflows/run/pending-ask-facts.d.ts +10 -0
- package/dist/workflows/run/pending-ask-facts.js +19 -0
- package/dist/workflows/run/pending-ask-facts.js.map +1 -0
- package/dist/workflows/run/prompts.d.ts +74 -0
- package/dist/workflows/run/prompts.js +140 -0
- package/dist/workflows/run/prompts.js.map +1 -0
- package/dist/workflows/run/queued-input.d.ts +108 -0
- package/dist/workflows/run/queued-input.js +249 -0
- package/dist/workflows/run/queued-input.js.map +1 -0
- package/dist/workflows/run/queued-revival.d.ts +44 -0
- package/dist/workflows/run/queued-revival.js +124 -0
- package/dist/workflows/run/queued-revival.js.map +1 -0
- package/dist/workflows/run/readonly-collections.d.ts +24 -0
- package/dist/workflows/run/readonly-collections.js +33 -0
- package/dist/workflows/run/readonly-collections.js.map +1 -0
- package/dist/workflows/run/repo-root-lease.d.ts +34 -0
- package/dist/workflows/run/repo-root-lease.js +71 -0
- package/dist/workflows/run/repo-root-lease.js.map +1 -0
- package/dist/workflows/run/resolved-wait-input-batch.d.ts +7 -0
- package/dist/workflows/run/resolved-wait-input-batch.js +23 -0
- package/dist/workflows/run/resolved-wait-input-batch.js.map +1 -0
- package/dist/workflows/run/run-cancel.d.ts +36 -0
- package/dist/workflows/run/run-cancel.js +130 -0
- package/dist/workflows/run/run-cancel.js.map +1 -0
- package/dist/workflows/run/run-finish.d.ts +72 -0
- package/dist/workflows/run/run-finish.js +256 -0
- package/dist/workflows/run/run-finish.js.map +1 -0
- package/dist/workflows/run/run-guards.d.ts +15 -0
- package/dist/workflows/run/run-guards.js +39 -0
- package/dist/workflows/run/run-guards.js.map +1 -0
- package/dist/workflows/run/run-line.d.ts +187 -0
- package/dist/workflows/run/run-line.js +438 -0
- package/dist/workflows/run/run-line.js.map +1 -0
- package/dist/workflows/run/run-recovery.d.ts +116 -0
- package/dist/workflows/run/run-recovery.js +422 -0
- package/dist/workflows/run/run-recovery.js.map +1 -0
- package/dist/workflows/run/run-settlement.d.ts +177 -0
- package/dist/workflows/run/run-settlement.js +504 -0
- package/dist/workflows/run/run-settlement.js.map +1 -0
- package/dist/workflows/run/run-skills.d.ts +114 -0
- package/dist/workflows/run/run-skills.js +233 -0
- package/dist/workflows/run/run-skills.js.map +1 -0
- package/dist/workflows/run/run-start.d.ts +64 -0
- package/dist/workflows/run/run-start.js +126 -0
- package/dist/workflows/run/run-start.js.map +1 -0
- package/dist/workflows/run/run-workspace.d.ts +25 -0
- package/dist/workflows/run/run-workspace.js +66 -0
- package/dist/workflows/run/run-workspace.js.map +1 -0
- package/dist/workflows/run/session-events.d.ts +155 -0
- package/dist/workflows/run/session-events.js +282 -0
- package/dist/workflows/run/session-events.js.map +1 -0
- package/dist/workflows/run/step-decisions.d.ts +37 -0
- package/dist/workflows/run/step-decisions.js +43 -0
- package/dist/workflows/run/step-decisions.js.map +1 -0
- package/dist/workflows/run/step-loop.d.ts +143 -0
- package/dist/workflows/run/step-loop.js +488 -0
- package/dist/workflows/run/step-loop.js.map +1 -0
- package/dist/workflows/run/step-session.d.ts +65 -0
- package/dist/workflows/run/step-session.js +280 -0
- package/dist/workflows/run/step-session.js.map +1 -0
- package/dist/workflows/run/steps.d.ts +13 -0
- package/dist/workflows/run/steps.js +14 -0
- package/dist/workflows/run/steps.js.map +1 -0
- package/dist/workflows/run/subagent-waits.d.ts +138 -0
- package/dist/workflows/run/subagent-waits.js +305 -0
- package/dist/workflows/run/subagent-waits.js.map +1 -0
- package/dist/workflows/run/tracked-work.d.ts +12 -0
- package/dist/workflows/run/tracked-work.js +29 -0
- package/dist/workflows/run/tracked-work.js.map +1 -0
- package/dist/workflows/run/turn-bookkeeping.d.ts +67 -0
- package/dist/workflows/run/turn-bookkeeping.js +173 -0
- package/dist/workflows/run/turn-bookkeeping.js.map +1 -0
- package/dist/workflows/run/turn-boundary.d.ts +215 -0
- package/dist/workflows/run/turn-boundary.js +329 -0
- package/dist/workflows/run/turn-boundary.js.map +1 -0
- package/dist/workflows/run/turn-markers.d.ts +62 -0
- package/dist/workflows/run/turn-markers.js +86 -0
- package/dist/workflows/run/turn-markers.js.map +1 -0
- package/dist/workflows/run/turn-nudges.d.ts +104 -0
- package/dist/workflows/run/turn-nudges.js +244 -0
- package/dist/workflows/run/turn-nudges.js.map +1 -0
- package/dist/workflows/run/turn-settler.d.ts +161 -0
- package/dist/workflows/run/turn-settler.js +469 -0
- package/dist/workflows/run/turn-settler.js.map +1 -0
- package/dist/workflows/run/types.d.ts +293 -0
- package/dist/workflows/run/types.js +2 -0
- package/dist/workflows/run/types.js.map +1 -0
- package/dist/workflows/run/usage-accounting.d.ts +49 -0
- package/dist/workflows/run/usage-accounting.js +213 -0
- package/dist/workflows/run/usage-accounting.js.map +1 -0
- package/dist/workflows/run/wait-engine.d.ts +1210 -0
- package/dist/workflows/run/wait-engine.js +456 -0
- package/dist/workflows/run/wait-engine.js.map +1 -0
- package/dist/workflows/run/wait-kinds.d.ts +336 -0
- package/dist/workflows/run/wait-kinds.js +471 -0
- package/dist/workflows/run/wait-kinds.js.map +1 -0
- package/dist/workflows/run/wait-registration-tool.d.ts +3 -0
- package/dist/workflows/run/wait-registration-tool.js +68 -0
- package/dist/workflows/run/wait-registration-tool.js.map +1 -0
- package/dist/workflows/run/wait-registration.d.ts +7 -0
- package/dist/workflows/run/wait-registration.js +39 -0
- package/dist/workflows/run/wait-registration.js.map +1 -0
- package/dist/workflows/run/wait-registry.d.ts +120 -0
- package/dist/workflows/run/wait-registry.js +365 -0
- package/dist/workflows/run/wait-registry.js.map +1 -0
- package/dist/workflows/run/wait-watches.d.ts +13 -0
- package/dist/workflows/run/wait-watches.js +12 -0
- package/dist/workflows/run/wait-watches.js.map +1 -0
- package/dist/workflows/run/wake-admission.d.ts +150 -0
- package/dist/workflows/run/wake-admission.js +397 -0
- package/dist/workflows/run/wake-admission.js.map +1 -0
- package/dist/workflows/run/wake-ledger.d.ts +61 -0
- package/dist/workflows/run/wake-ledger.js +78 -0
- package/dist/workflows/run/wake-ledger.js.map +1 -0
- package/dist/workflows/run/wake-retries.d.ts +31 -0
- package/dist/workflows/run/wake-retries.js +93 -0
- package/dist/workflows/run/wake-retries.js.map +1 -0
- package/dist/workflows/run/worker-executions.d.ts +54 -0
- package/dist/workflows/run/worker-executions.js +149 -0
- package/dist/workflows/run/worker-executions.js.map +1 -0
- package/dist/workflows/run/worker-orphans.d.ts +91 -0
- package/dist/workflows/run/worker-orphans.js +275 -0
- package/dist/workflows/run/worker-orphans.js.map +1 -0
- package/dist/workflows/run/worker-questions.d.ts +169 -0
- package/dist/workflows/run/worker-questions.js +464 -0
- package/dist/workflows/run/worker-questions.js.map +1 -0
- package/dist/workflows/run/workflow-position.d.ts +7 -0
- package/dist/workflows/run/workflow-position.js +9 -0
- package/dist/workflows/run/workflow-position.js.map +1 -0
- package/dist/workflows/run/worktree-autosave.d.ts +48 -0
- package/dist/workflows/run/worktree-autosave.js +175 -0
- package/dist/workflows/run/worktree-autosave.js.map +1 -0
- package/dist/workflows/run-scheduling.d.ts +38 -0
- package/dist/workflows/run-scheduling.js +76 -0
- package/dist/workflows/run-scheduling.js.map +1 -0
- package/dist/workflows/run-states.d.ts +73 -0
- package/dist/workflows/run-states.js +233 -0
- package/dist/workflows/run-states.js.map +1 -0
- package/dist/workflows/run.d.ts +275 -0
- package/dist/workflows/run.js +626 -0
- package/dist/workflows/run.js.map +1 -0
- package/dist/workflows/turn-end.d.ts +119 -0
- package/dist/workflows/turn-end.js +74 -0
- package/dist/workflows/turn-end.js.map +1 -0
- package/dist/workflows/types.d.ts +83 -0
- package/dist/workflows/types.js +192 -0
- package/dist/workflows/types.js.map +1 -0
- package/dist/workflows/wait-scope.d.ts +75 -0
- package/dist/workflows/wait-scope.js +115 -0
- package/dist/workflows/wait-scope.js.map +1 -0
- package/dist/workflows/waits.d.ts +58 -0
- package/dist/workflows/waits.js +89 -0
- package/dist/workflows/waits.js.map +1 -0
- package/dist/workspace/agent-accounts.d.ts +135 -0
- package/dist/workspace/agent-accounts.js +353 -0
- package/dist/workspace/agent-accounts.js.map +1 -0
- package/dist/workspace/agent-profiles.d.ts +125 -0
- package/dist/workspace/agent-profiles.js +190 -0
- package/dist/workspace/agent-profiles.js.map +1 -0
- package/dist/workspace/backup-cli.d.ts +9 -0
- package/dist/workspace/backup-cli.js +147 -0
- package/dist/workspace/backup-cli.js.map +1 -0
- package/dist/workspace/backups.d.ts +36 -0
- package/dist/workspace/backups.js +249 -0
- package/dist/workspace/backups.js.map +1 -0
- package/dist/workspace/config.d.ts +179 -0
- package/dist/workspace/config.js +396 -0
- package/dist/workspace/config.js.map +1 -0
- package/dist/workspace/migrations.d.ts +37 -0
- package/dist/workspace/migrations.js +149 -0
- package/dist/workspace/migrations.js.map +1 -0
- package/dist/workspace/projects-cli.d.ts +27 -0
- package/dist/workspace/projects-cli.js +175 -0
- package/dist/workspace/projects-cli.js.map +1 -0
- package/dist/workspace/projects.d.ts +131 -0
- package/dist/workspace/projects.js +274 -0
- package/dist/workspace/projects.js.map +1 -0
- package/dist/workspace/root-probe-cache.d.ts +10 -0
- package/dist/workspace/root-probe-cache.js +43 -0
- package/dist/workspace/root-probe-cache.js.map +1 -0
- package/dist/workspace/semaphore.d.ts +147 -0
- package/dist/workspace/semaphore.js +196 -0
- package/dist/workspace/semaphore.js.map +1 -0
- package/dist/workspace/ui-state.d.ts +17 -0
- package/dist/workspace/ui-state.js +59 -0
- package/dist/workspace/ui-state.js.map +1 -0
- package/dist/workspace/webhooks.d.ts +40 -0
- package/dist/workspace/webhooks.js +146 -0
- package/dist/workspace/webhooks.js.map +1 -0
- package/dist/worktree-manifest.d.ts +165 -0
- package/dist/worktree-manifest.js +535 -0
- package/dist/worktree-manifest.js.map +1 -0
- package/dist/worktree-setup.d.ts +88 -0
- package/dist/worktree-setup.js +170 -0
- package/dist/worktree-setup.js.map +1 -0
- package/package.json +80 -4
- package/scripts/benchmark-family-reconcile.ts +119 -0
- package/scripts/benchmark-home.ts +32 -0
- package/scripts/benchmark-project-probes.mjs +158 -0
- package/scripts/benchmark-run-store-fixture.ts +432 -0
- package/scripts/benchmark-run-store.ts +767 -0
- package/scripts/benchmark-stuck-destroys.ts +144 -0
- package/scripts/benchmark-workspace-events.ts +339 -0
- package/scripts/check-pack.mjs +48 -0
- package/scripts/inline-contract.mjs +165 -0
- package/scripts/install-as-command.mjs +125 -0
- package/scripts/measure-transcript-facts-health.mjs +103 -0
- package/scripts/measure-transcript-facts-lag.mjs +69 -0
- package/scripts/mock-autonomous.mjs +13 -0
- package/scripts/mock-ci-tool.mjs +159 -0
- package/scripts/mock-claude-model-refusal.mjs +76 -0
- package/scripts/mock-claude-subagents.mjs +54 -0
- package/scripts/mock-claude.mjs +800 -0
- package/scripts/mock-codex-app-server.mjs +767 -0
- package/scripts/mock-codex-coalesce-steer.mjs +59 -0
- package/scripts/mock-codex-reverse-steer.mjs +143 -0
- package/scripts/mock-codex-subagents.mjs +52 -0
- package/scripts/mock-controls.d.mts +11 -0
- package/scripts/mock-controls.mjs +13 -0
- package/scripts/mock-cursor-hang-stdin-late.mjs +35 -0
- package/scripts/mock-cursor-hang-stdin.mjs +30 -0
- package/scripts/mock-cursor-print.mjs +237 -0
- package/scripts/mock-deferred-turn-start.mjs +16 -0
- package/scripts/mock-delivery-control.d.mts +23 -0
- package/scripts/mock-delivery-control.mjs +80 -0
- package/scripts/mock-env-record.mjs +11 -0
- package/scripts/mock-failure-control.d.mts +13 -0
- package/scripts/mock-failure-control.mjs +68 -0
- package/scripts/mock-lifecycle.mjs +51 -0
- package/scripts/mock-multi-pr.mjs +10 -0
- package/scripts/mock-nudge.mjs +43 -0
- package/scripts/mock-omp-rpc.mjs +536 -0
- package/scripts/mock-opencode-serve.mjs +1000 -0
- package/scripts/mock-pi-rpc.mjs +421 -0
- package/scripts/mock-publish.mjs +13 -0
- package/scripts/mock-runner-crash.mjs +64 -0
- package/scripts/mock-scenario.d.mts +71 -0
- package/scripts/mock-scenario.mjs +295 -0
- package/scripts/mock-session-context.d.mts +5 -0
- package/scripts/mock-session-context.mjs +56 -0
- package/scripts/mock-silent-tail.mjs +93 -0
- package/scripts/mock-turn-messages.mjs +5 -0
- package/scripts/mock-wire-evidence.config.mjs +12 -0
- package/scripts/mock-wire-recorder.mjs +66 -0
- package/scripts/mock-wire-tools.mjs +7 -0
- package/scripts/omp-ci-wait.mjs +29 -0
- package/scripts/opencode-task-scope.d.mts +18 -0
- package/scripts/opencode-task-scope.mjs +59 -0
- package/scripts/parity-rows.ts +160 -0
- package/scripts/pi-ci-wait.mjs +17 -0
- package/scripts/pi-default-tools.mjs +22 -0
- package/scripts/pi-project-skills.mjs +59 -0
- package/scripts/pi-retry-degraded-service.mjs +16 -0
- package/scripts/probe-cursor-print.mjs +699 -0
- package/scripts/probe-session-env.ts +8 -0
- package/scripts/product-env.mjs +6 -0
- package/scripts/product-identity.mjs +12 -0
- package/scripts/record-mock-wires.mjs +16 -0
- package/scripts/seed-archive-profile.ts +53 -0
- package/scripts/sync-readme.mjs +20 -0
- package/scripts/test-process-usage.mjs +47 -0
- package/scripts/transcript-facts-fixture.mjs +53 -0
- package/scripts/transcript-facts-timing.mjs +42 -0
- package/scripts/voice-replay.ts +50 -0
- package/scripts/windows-workspace-identity.cs +208 -0
- package/scripts/windows-workspace-identity.ps1 +10 -0
- package/web/dist/assets/activity-motion-0wHCeUs7.css +1 -0
- package/web/dist/assets/activity-motion-SGth5vR4.js +2 -0
- package/web/dist/assets/activity-route-D8otXwOA.js +1 -0
- package/web/dist/assets/agent-accounts-6EuZhy-N.js +18 -0
- package/web/dist/assets/alert-dialog-vFjLgfK4.js +1 -0
- package/web/dist/assets/app-window-CSHwDX9N.js +1 -0
- package/web/dist/assets/arrow-right-Dh_Lx3um.js +1 -0
- package/web/dist/assets/bundle-mjs-BT31bpU6.js +1 -0
- package/web/dist/assets/button-BEK7XKhS.js +3 -0
- package/web/dist/assets/centered-state-CPgI8cCO.js +1 -0
- package/web/dist/assets/cez-idle-BT0U4GN_.js +1 -0
- package/web/dist/assets/chunk-BO2N2NFS-DE6qKn3r.js +131 -0
- package/web/dist/assets/commit-diff-panel-DzyWgn1Z.js +1 -0
- package/web/dist/assets/control-locations-CexZsd3M.js +1 -0
- package/web/dist/assets/core-BCsw8oQw.js +12 -0
- package/web/dist/assets/createLucideIcon-BHp8sXmF.js +1 -0
- package/web/dist/assets/css-BsVw1vtW.js +1 -0
- package/web/dist/assets/design-icons-DmYTv2r1.js +1 -0
- package/web/dist/assets/diff-DBDiZl58.js +3 -0
- package/web/dist/assets/diff-controls-DXyB0aDl.js +1 -0
- package/web/dist/assets/diff-view-DiKAwVMH.js +11 -0
- package/web/dist/assets/diff-woXpYk--.js +1 -0
- package/web/dist/assets/dropdown-menu-DYflHa4m.js +1 -0
- package/web/dist/assets/engine-javascript-FQvzRocB.js +141 -0
- package/web/dist/assets/es2015-BO600Xfi.js +41 -0
- package/web/dist/assets/eye-B8okNWXU.js +1 -0
- package/web/dist/assets/file-preview-DTSRkmMo.js +2 -0
- package/web/dist/assets/git-toolbar-U4s46kUd.js +1 -0
- package/web/dist/assets/github-UaBp2yRF.js +1 -0
- package/web/dist/assets/github-sidebar-model-czjLsgVJ.js +1 -0
- package/web/dist/assets/global-activity-route-B2N2Nz0H.js +1 -0
- package/web/dist/assets/go-rLFTqkRN.js +1 -0
- package/web/dist/assets/highlighted-body-OFNGDK62-BL-MfFgl.js +1 -0
- package/web/dist/assets/hourglass-B0lPAJvT.js +1 -0
- package/web/dist/assets/html-BY76lXLH.js +1 -0
- package/web/dist/assets/image-preview-XPCPclpG.js +1 -0
- package/web/dist/assets/index-B18AqW0Z.css +1 -0
- package/web/dist/assets/index-xj-3O89_.js +14 -0
- package/web/dist/assets/index.global-D3Ga_D3Y.js +1 -0
- package/web/dist/assets/info-DA1x_4bz.js +1 -0
- package/web/dist/assets/javascript-BgS3c2Ky.js +1 -0
- package/web/dist/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/web/dist/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/web/dist/assets/json-qhed-kSA.js +1 -0
- package/web/dist/assets/jsonc-CYpm1nAK.js +1 -0
- package/web/dist/assets/jsx-Bz0zcwM4.js +1 -0
- package/web/dist/assets/lib-BxQXEXDF.js +1 -0
- package/web/dist/assets/live-protocol-BDY7JJpT.js +64 -0
- package/web/dist/assets/live-refetch-C-Ony2Yr.js +1 -0
- package/web/dist/assets/live-worker-BWdeGzQX.ts +34 -0
- package/web/dist/assets/loader-circle-CZkGEdoI.js +1 -0
- package/web/dist/assets/markdown-BBDDg-4t.js +1 -0
- package/web/dist/assets/markdown-BYOwaDjH.js +1 -0
- package/web/dist/assets/markdown-UdZ_toya.js +6 -0
- package/web/dist/assets/mermaid-GHXKKRXX-C4jxqUUb.js +1 -0
- package/web/dist/assets/mermaid.min-3O9YjB2l.js +1 -0
- package/web/dist/assets/message-square-B-MD-8rD.js +1 -0
- package/web/dist/assets/nav-row-styles-BE99gD3_.js +1 -0
- package/web/dist/assets/popover-CpULuQSm.js +1 -0
- package/web/dist/assets/poppins-devanagari-400-normal-CJDn6rn8.woff2 +0 -0
- package/web/dist/assets/poppins-devanagari-400-normal-CqVvlrh5.woff +0 -0
- package/web/dist/assets/poppins-devanagari-500-normal-BIdkeU1p.woff2 +0 -0
- package/web/dist/assets/poppins-devanagari-500-normal-DMPDjHtT.woff +0 -0
- package/web/dist/assets/poppins-devanagari-600-normal-ClASKHrr.woff +0 -0
- package/web/dist/assets/poppins-devanagari-600-normal-STEjXBNN.woff2 +0 -0
- package/web/dist/assets/poppins-latin-400-normal-BOb3E3N0.woff +0 -0
- package/web/dist/assets/poppins-latin-400-normal-cpxAROuN.woff2 +0 -0
- package/web/dist/assets/poppins-latin-500-normal-C8OXljZJ.woff2 +0 -0
- package/web/dist/assets/poppins-latin-500-normal-DGXqpDMm.woff +0 -0
- package/web/dist/assets/poppins-latin-600-normal-BJdTmd5m.woff +0 -0
- package/web/dist/assets/poppins-latin-600-normal-zEkxB9Mr.woff2 +0 -0
- package/web/dist/assets/poppins-latin-ext-400-normal-DaBSavcJ.woff +0 -0
- package/web/dist/assets/poppins-latin-ext-400-normal-by3JarPu.woff2 +0 -0
- package/web/dist/assets/poppins-latin-ext-500-normal-CK-6C4Hw.woff2 +0 -0
- package/web/dist/assets/poppins-latin-ext-500-normal-CgAe2rWW.woff +0 -0
- package/web/dist/assets/poppins-latin-ext-600-normal-CAhIAdZj.woff2 +0 -0
- package/web/dist/assets/poppins-latin-ext-600-normal-Df5ffKXP.woff +0 -0
- package/web/dist/assets/preview-pane-connected-CgRK7GzB.js +3 -0
- package/web/dist/assets/project-router-CimAoN1H.js +1 -0
- package/web/dist/assets/project-scope-context-DMpvJqIm.js +4 -0
- package/web/dist/assets/python-gzcpVVnB.js +1 -0
- package/web/dist/assets/queries-BIOp8jLx.js +1 -0
- package/web/dist/assets/react-runtime-CCIEwYL0.js +9 -0
- package/web/dist/assets/repo-git-DVXNOJDT.css +1 -0
- package/web/dist/assets/repo-git-XkyTfP9G.js +1 -0
- package/web/dist/assets/report-reader-CZjYS5Aw.js +1 -0
- package/web/dist/assets/report-title-D4Mbu2Er.js +1 -0
- package/web/dist/assets/reports-B7QAzibP.css +1 -0
- package/web/dist/assets/reports-D_2sFE1y.js +1 -0
- package/web/dist/assets/rolldown-runtime-QTnfLwEv.js +1 -0
- package/web/dist/assets/run-header-BwPDVPpg.js +2 -0
- package/web/dist/assets/run-header-CC7VUTg9.css +1 -0
- package/web/dist/assets/rust-Cfkwpbl8.js +1 -0
- package/web/dist/assets/shellscript-CLZ0U2zV.js +1 -0
- package/web/dist/assets/skill-empty-hint-YDmkqgo-.js +1 -0
- package/web/dist/assets/skills-BkXe4p4q.js +1 -0
- package/web/dist/assets/skills-DFBiAuJG.js +1 -0
- package/web/dist/assets/skills-workflows-C21NystW.css +1 -0
- package/web/dist/assets/sortable.esm-zp_qkYGV.js +5 -0
- package/web/dist/assets/sql-BsFa4tDR.js +1 -0
- package/web/dist/assets/tab-link-BIweqlIZ.js +1 -0
- package/web/dist/assets/task-changes-USNHNm_T.js +1 -0
- package/web/dist/assets/task-commits-kHqlOJe_.js +1 -0
- package/web/dist/assets/task-files-wTWwLR39.js +1 -0
- package/web/dist/assets/task-github-item-GzaQjkF1.js +1 -0
- package/web/dist/assets/task-thread-BZX863UN.js +9 -0
- package/web/dist/assets/task-thread-DtQ6uGfQ.css +1 -0
- package/web/dist/assets/textarea-FAUkHz1K.js +1 -0
- package/web/dist/assets/thread-state-CBzx2hmX.js +4 -0
- package/web/dist/assets/toaster-pthKGWiE.js +1 -0
- package/web/dist/assets/toml-CcmNWLt0.js +1 -0
- package/web/dist/assets/tsx-udAQXfEw.js +1 -0
- package/web/dist/assets/typescript-bsJCZSQ-.js +1 -0
- package/web/dist/assets/use-hand-to-agent-state-Bav9eT3b.js +1 -0
- package/web/dist/assets/use-hand-to-agent-state-C7esPJ-N.css +1 -0
- package/web/dist/assets/useInfiniteQuery-CQI4BO4C.js +1 -0
- package/web/dist/assets/useQuery-Bkq-x4Id.js +1 -0
- package/web/dist/assets/workflows-D871Gx--.js +7 -0
- package/web/dist/assets/ws-DspW9wBH.js +1 -0
- package/web/dist/assets/yaml-rwi0_p6S.js +1 -0
- package/web/dist/assets/zoomable-image-DNc6ORaF.js +1 -0
- package/web/dist/cezarion-lockup-dark.svg +9 -0
- package/web/dist/cezarion-lockup-light.svg +9 -0
- package/web/dist/cezarion-mark-dark.svg +4 -0
- package/web/dist/cezarion-mark-light.svg +4 -0
- package/web/dist/index.html +90 -0
- package/web/dist/live-worker.js +4 -0
|
@@ -0,0 +1,2881 @@
|
|
|
1
|
+
import { productEnv } from '../../core/product-env.js';
|
|
2
|
+
import { createDraftPr } from './draft-pr.js';
|
|
3
|
+
export { createDraftPr, buildPrBody } from './draft-pr.js';
|
|
4
|
+
import { defined } from '../../core/defined.js';
|
|
5
|
+
import { activeWorkflowRunsArgs, parseActiveWorkflowRuns } from '../../ci-wait/github.js';
|
|
6
|
+
import { pullRequestKey } from '../../runs/pull-requests.js';
|
|
7
|
+
import { matchesGithubPullRequest, mergedPullRequestObservations } from '../../merge-observations.js';
|
|
8
|
+
import { randomUUID } from 'node:crypto';
|
|
9
|
+
import { execFile } from 'node:child_process';
|
|
10
|
+
import { promisify } from 'node:util';
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
import { GITHUB_COMMENT_COUNTS_MAX, GITHUB_SEARCH_MAX, REFERENCE_STATUS_MAX } from '../../contract/index.js';
|
|
13
|
+
import { fetchIssueProjects, fetchRepositoryProjects, fetchViewerLogin } from './github-filters.js';
|
|
14
|
+
import { fetchGithubMergeRequirements } from './github-merge-requirements.js';
|
|
15
|
+
import { mergePrSchema, normalizeMergeState } from './github-merge-state.js';
|
|
16
|
+
import { fetchTimelinePages, normalizeEvents, TIMELINE_PER_PAGE } from './github-timeline.js';
|
|
17
|
+
export { normalizeMergeState } from './github-merge-state.js';
|
|
18
|
+
export { fetchTimelinePages, ghTimelineEventSchema, normalizeEvents, TIMELINE_BUDGET_MS, TIMELINE_EVENT_CAP, TIMELINE_EVENT_KINDS, TIMELINE_MAX_PAGES, TIMELINE_MIN_PAGE_MS, } from './github-timeline.js';
|
|
19
|
+
import { refNumberFromUrl } from '../../contract/index.js';
|
|
20
|
+
/**
|
|
21
|
+
* The GitHub forge driver — all `gh`-CLI logic in one place, moved here from
|
|
22
|
+
* `src/server/github.ts` (tab listing) and `src/server/pr.ts` (draft PRs)
|
|
23
|
+
* behind the `ForgeDriver` seam. Those modules remain as thin delegates.
|
|
24
|
+
* `/api/github`'s response shape is this driver's serialization and is
|
|
25
|
+
* protected by BACKWARD_COMPATIBILITY.md — additive changes only.
|
|
26
|
+
*/
|
|
27
|
+
const exec = promisify(execFile);
|
|
28
|
+
export const GH_PR_DIFF_FILE_CAP = 300;
|
|
29
|
+
export const GH_PR_PATCH_CAP = 512 * 1024;
|
|
30
|
+
export const GH_PR_DIFF_JSON_CAP = 4 * 1024 * 1024;
|
|
31
|
+
const ghPrFileSchema = z.object({
|
|
32
|
+
filename: z.string().min(1),
|
|
33
|
+
previous_filename: z.string().optional(),
|
|
34
|
+
status: z.enum(['added', 'modified', 'removed', 'renamed', 'copied', 'changed']),
|
|
35
|
+
additions: z.number().int().nonnegative(),
|
|
36
|
+
deletions: z.number().int().nonnegative(),
|
|
37
|
+
patch: z.string().optional(),
|
|
38
|
+
});
|
|
39
|
+
const ghPrHeadSchema = z.object({ headRefOid: z.string().regex(/^[0-9a-f]{40}$/i) });
|
|
40
|
+
export class GithubPrNotFoundError extends Error {
|
|
41
|
+
}
|
|
42
|
+
const prDiffCache = new Map();
|
|
43
|
+
/** Fetch no more than the three GitHub pages represented by the public 300-file response cap. */
|
|
44
|
+
export async function fetchPrFilePages(runPage) {
|
|
45
|
+
const rows = [];
|
|
46
|
+
for (let page = 1; page <= GH_PR_DIFF_FILE_CAP / 100; page++) {
|
|
47
|
+
const next = z.array(z.unknown()).parse(JSON.parse(await runPage(page)));
|
|
48
|
+
rows.push(...next);
|
|
49
|
+
if (next.length < 100)
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
return rows;
|
|
53
|
+
}
|
|
54
|
+
export async function fetchGithubPrDiff(repoRoot, number, refresh = false) {
|
|
55
|
+
if (productEnv('DRY_RUN') === '1')
|
|
56
|
+
return mockGithubPrDiff(number);
|
|
57
|
+
try {
|
|
58
|
+
const head = ghPrHeadSchema.parse(JSON.parse(await gh(repoRoot, ['pr', 'view', String(number), '--json', 'headRefOid']))).headRefOid;
|
|
59
|
+
const key = `${repoRoot}\0${number}\0${head}`;
|
|
60
|
+
const hit = prDiffCache.get(key);
|
|
61
|
+
if (!refresh && hit && Date.now() - hit.at < CACHE_MS)
|
|
62
|
+
return hit.data;
|
|
63
|
+
const rows = z.array(ghPrFileSchema).parse(await fetchPrFilePages((page) => gh(repoRoot, ['api', `repos/{owner}/{repo}/pulls/${number}/files?per_page=100&page=${page}`], 30_000)));
|
|
64
|
+
const limited = rows.slice(0, GH_PR_DIFF_FILE_CAP);
|
|
65
|
+
// A full third page may have a successor. Without fetching a 301st file, conservatively call
|
|
66
|
+
// the response partial rather than claiming completeness we cannot prove.
|
|
67
|
+
let responseTruncated = rows.length >= GH_PR_DIFF_FILE_CAP;
|
|
68
|
+
const reasons = responseTruncated ? [`Only the first ${GH_PR_DIFF_FILE_CAP} files are shown.`] : [];
|
|
69
|
+
const files = limited.map((row) => {
|
|
70
|
+
let patch = row.patch;
|
|
71
|
+
let truncated = false;
|
|
72
|
+
let patchUnavailableReason;
|
|
73
|
+
if (patch !== undefined && Buffer.byteLength(patch, 'utf8') > GH_PR_PATCH_CAP) {
|
|
74
|
+
patch = undefined;
|
|
75
|
+
truncated = true;
|
|
76
|
+
patchUnavailableReason = 'too-large';
|
|
77
|
+
responseTruncated = true;
|
|
78
|
+
}
|
|
79
|
+
else if (patch === undefined) {
|
|
80
|
+
patchUnavailableReason = row.additions === 0 && row.deletions === 0 ? 'binary' : 'not-provided';
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
path: row.filename,
|
|
84
|
+
...(row.previous_filename ? { previousPath: row.previous_filename } : {}),
|
|
85
|
+
status: row.status,
|
|
86
|
+
additions: row.additions,
|
|
87
|
+
deletions: row.deletions,
|
|
88
|
+
...(patch !== undefined ? { patch } : {}),
|
|
89
|
+
...(patchUnavailableReason ? { patchUnavailableReason } : {}),
|
|
90
|
+
...(truncated ? { truncated: true } : {}),
|
|
91
|
+
};
|
|
92
|
+
});
|
|
93
|
+
let kept = files;
|
|
94
|
+
while (kept.length > 0 &&
|
|
95
|
+
Buffer.byteLength(JSON.stringify({ available: true, number, headSha: head, files: kept }), 'utf8') >
|
|
96
|
+
GH_PR_DIFF_JSON_CAP) {
|
|
97
|
+
kept = kept.slice(0, -1);
|
|
98
|
+
responseTruncated = true;
|
|
99
|
+
}
|
|
100
|
+
if (kept.length < files.length)
|
|
101
|
+
reasons.push('The response size limit omitted some files.');
|
|
102
|
+
if (files.some((file) => file.truncated))
|
|
103
|
+
reasons.push('One or more patches exceeded the per-file limit.');
|
|
104
|
+
const data = {
|
|
105
|
+
available: true,
|
|
106
|
+
number,
|
|
107
|
+
headSha: head,
|
|
108
|
+
files: kept,
|
|
109
|
+
additions: rows.reduce((sum, row) => sum + row.additions, 0),
|
|
110
|
+
deletions: rows.reduce((sum, row) => sum + row.deletions, 0),
|
|
111
|
+
truncated: responseTruncated,
|
|
112
|
+
...(reasons.length ? { reason: reasons.join(' ') } : {}),
|
|
113
|
+
};
|
|
114
|
+
prDiffCache.set(key, { at: Date.now(), data });
|
|
115
|
+
while (prDiffCache.size > 50)
|
|
116
|
+
prDiffCache.delete(prDiffCache.keys().next().value);
|
|
117
|
+
return data;
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
121
|
+
if (/HTTP 404|Could not resolve to a PullRequest|no pull requests found/i.test(message)) {
|
|
122
|
+
throw new GithubPrNotFoundError(`Pull request #${number} was not found`);
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
available: false,
|
|
126
|
+
reason: /ENOENT/.test(message)
|
|
127
|
+
? 'gh CLI not found — install it and run `gh auth login`'
|
|
128
|
+
: firstLine(message),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function mockGithubPrDiff(number) {
|
|
133
|
+
return {
|
|
134
|
+
available: true,
|
|
135
|
+
number,
|
|
136
|
+
headSha: '0123456789abcdef0123456789abcdef01234567',
|
|
137
|
+
additions: 15,
|
|
138
|
+
deletions: 4,
|
|
139
|
+
truncated: true,
|
|
140
|
+
reason: 'One or more patches were not provided by GitHub.',
|
|
141
|
+
files: [
|
|
142
|
+
{ path: 'src/session.ts', status: 'modified', additions: 8, deletions: 3, patch: '@@ -1,3 +1,4 @@\n-old\n+new\n context' },
|
|
143
|
+
{ path: 'src/new-name.ts', previousPath: 'src/old-name.ts', status: 'renamed', additions: 7, deletions: 1, patch: '@@ -1 +1 @@\n-old name\n+new name' },
|
|
144
|
+
{ path: 'assets/logo.png', status: 'modified', additions: 0, deletions: 0, patchUnavailableReason: 'binary' },
|
|
145
|
+
{ path: 'generated/output.txt', status: 'modified', additions: 0, deletions: 0, patchUnavailableReason: 'too-large', truncated: true },
|
|
146
|
+
],
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
// `gh … --json` output — validated at the boundary, extras stripped.
|
|
150
|
+
const ghAuthor = z.object({ login: z.string() }).nullish();
|
|
151
|
+
// `color` is the 6-hex GitHub label color (no `#`), '' when gh omits it.
|
|
152
|
+
const ghLabel = z.object({ name: z.string(), color: z.string().default('') });
|
|
153
|
+
const ghIssueSchema = z.object({
|
|
154
|
+
number: z.number(),
|
|
155
|
+
title: z.string(),
|
|
156
|
+
author: ghAuthor,
|
|
157
|
+
createdAt: z.string(),
|
|
158
|
+
labels: z.array(ghLabel).default([]),
|
|
159
|
+
assignees: z.array(z.object({ login: z.string() })).default([]),
|
|
160
|
+
body: z.string().nullish(),
|
|
161
|
+
url: z.string(),
|
|
162
|
+
});
|
|
163
|
+
// One check run's `gh --json statusCheckRollup` entry — every field optional/nullish because
|
|
164
|
+
// gh's shape varies by check provider (exported so #400's unit tests can build fixtures).
|
|
165
|
+
export const ghCheckRunSchema = z.object({
|
|
166
|
+
state: z.string().nullish(),
|
|
167
|
+
status: z.string().nullish(),
|
|
168
|
+
conclusion: z.string().nullish(),
|
|
169
|
+
});
|
|
170
|
+
const ghStatusCheckRollup = z.array(ghCheckRunSchema).nullish();
|
|
171
|
+
const ghPrViewSchema = z.object({
|
|
172
|
+
number: z.number(),
|
|
173
|
+
url: z.string(),
|
|
174
|
+
state: z.string().default('OPEN'),
|
|
175
|
+
mergedAt: z.unknown().optional(),
|
|
176
|
+
isDraft: z.boolean().default(false),
|
|
177
|
+
statusCheckRollup: ghStatusCheckRollup,
|
|
178
|
+
});
|
|
179
|
+
const ghMergeResultSchema = z.object({
|
|
180
|
+
merged: z.boolean(),
|
|
181
|
+
message: z.string().nullish(),
|
|
182
|
+
sha: z.string().nullish(),
|
|
183
|
+
});
|
|
184
|
+
/** Exported for unit tests (#400) — collapses a zod-validated `statusCheckRollup` array down to
|
|
185
|
+
* the single enum the GitHub tab (list rows + detail badge) renders. */
|
|
186
|
+
export function rollupToChecks(rollup) {
|
|
187
|
+
if (!rollup || rollup.length === 0)
|
|
188
|
+
return null;
|
|
189
|
+
const states = rollup.map((r) => (r.conclusion || r.state || r.status || '').toUpperCase());
|
|
190
|
+
if (states.some((s) => ['FAILURE', 'ERROR', 'TIMED_OUT', 'ACTION_REQUIRED'].includes(s)))
|
|
191
|
+
return 'failing';
|
|
192
|
+
if (states.some((s) => ['PENDING', 'IN_PROGRESS', 'QUEUED', 'EXPECTED', ''].includes(s)))
|
|
193
|
+
return 'pending';
|
|
194
|
+
return 'passing';
|
|
195
|
+
}
|
|
196
|
+
// Keep account-directory and host selection attached to shared work. Never key by owner/name:
|
|
197
|
+
// separate local roots (including different remotes/accounts) must not share results.
|
|
198
|
+
function githubContext(repoRoot) {
|
|
199
|
+
const env = { GH_HOST: process.env.GH_HOST, GH_CONFIG_DIR: process.env.GH_CONFIG_DIR };
|
|
200
|
+
return { key: JSON.stringify([repoRoot, env.GH_HOST, env.GH_CONFIG_DIR]), env };
|
|
201
|
+
}
|
|
202
|
+
async function gh(repoRoot, args, timeout = 15_000, signal, context) {
|
|
203
|
+
const { stdout } = await exec('gh', args, {
|
|
204
|
+
cwd: repoRoot,
|
|
205
|
+
...(context ? { env: { ...process.env, ...context.env } } : {}),
|
|
206
|
+
timeout,
|
|
207
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
208
|
+
...(signal ? { signal } : {}),
|
|
209
|
+
});
|
|
210
|
+
return stdout;
|
|
211
|
+
}
|
|
212
|
+
/** `gh api graphql` argv. `-f` sends every value as a String, and GitHub rejects a String where
|
|
213
|
+
* the query declares `Int!` ("Could not coerce value to Int", open-mercato/cezar#1366), so a
|
|
214
|
+
* number goes through `-F`. A string never does: `-F` would read `@file` and coerce
|
|
215
|
+
* `true`/numerals, which is not what an opaque cursor or a login means. */
|
|
216
|
+
export function ghGraphqlArgs(query, variables = {}) {
|
|
217
|
+
const args = ['api', 'graphql', '-f', `query=${query}`];
|
|
218
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
219
|
+
args.push(typeof value === 'number' ? '-F' : '-f', `${key}=${value}`);
|
|
220
|
+
}
|
|
221
|
+
return args;
|
|
222
|
+
}
|
|
223
|
+
/** `owner/name` → `{ owner, name }`, or null when the handle isn't a clean two-part slug. */
|
|
224
|
+
export function parseOwnerName(nameWithOwner) {
|
|
225
|
+
const [owner, name, ...rest] = nameWithOwner.trim().split('/');
|
|
226
|
+
return owner && name && rest.length === 0 ? { owner, name } : null;
|
|
227
|
+
}
|
|
228
|
+
// Paged open lists defer comment counts; hydrate them through the counts route after paint.
|
|
229
|
+
/** GitHub GraphQL rejects `first` above 100 (EXCESSIVE_PAGINATION); larger limits walk pages. */
|
|
230
|
+
export const GH_GRAPHQL_PAGE_MAX = 100;
|
|
231
|
+
// Labels up to 100, which is what `gh issue list --json labels` read before the GraphQL page.
|
|
232
|
+
const LIST_NODE_FIELDS = 'number title author { login } createdAt url body labels(first: 100) { nodes { name color } }';
|
|
233
|
+
const listQuery = (side) => `
|
|
234
|
+
query ($owner: String!, $name: String!, $first: Int!, $after: String) {
|
|
235
|
+
repository(owner: $owner, name: $name) {
|
|
236
|
+
${side}(first: $first, after: $after, states: OPEN, orderBy: {field: CREATED_AT, direction: DESC}) {
|
|
237
|
+
totalCount
|
|
238
|
+
pageInfo { hasNextPage endCursor }
|
|
239
|
+
nodes { ${LIST_NODE_FIELDS} ${side === 'issues' ? 'assignees(first: 20) { nodes { login } }' : 'isDraft additions deletions'} }
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}`;
|
|
243
|
+
/** A GraphQL connection (`{ nodes }`) read as the flat array `gh … --json` used to return. */
|
|
244
|
+
const connectionOf = (item) => z.object({ nodes: z.array(item.nullable()).default([]) }).nullish()
|
|
245
|
+
.transform((connection) => (connection?.nodes ?? []).filter((node) => node != null));
|
|
246
|
+
const ghListNodeSchema = z.object({
|
|
247
|
+
number: z.number(),
|
|
248
|
+
title: z.string(),
|
|
249
|
+
author: ghAuthor,
|
|
250
|
+
createdAt: z.string(),
|
|
251
|
+
url: z.string(),
|
|
252
|
+
body: z.string().nullish(),
|
|
253
|
+
labels: connectionOf(ghLabel),
|
|
254
|
+
assignees: connectionOf(z.object({ login: z.string() })),
|
|
255
|
+
isDraft: z.boolean().nullish(),
|
|
256
|
+
additions: z.number().nullish(),
|
|
257
|
+
deletions: z.number().nullish(),
|
|
258
|
+
});
|
|
259
|
+
const ghListConnectionSchema = z.object({
|
|
260
|
+
totalCount: z.number().int().nonnegative(),
|
|
261
|
+
pageInfo: z.object({ hasNextPage: z.boolean(), endCursor: z.string().nullish() }),
|
|
262
|
+
nodes: z.array(ghListNodeSchema.nullable()),
|
|
263
|
+
});
|
|
264
|
+
function parseListConnection(out, side) {
|
|
265
|
+
const parsed = JSON.parse(out);
|
|
266
|
+
const message = parsed.errors?.map((e) => e.message).filter(Boolean).join(' ');
|
|
267
|
+
if (message)
|
|
268
|
+
throw new Error(message);
|
|
269
|
+
if (!parsed.data?.repository)
|
|
270
|
+
throw new Error('GitHub did not resolve this repository');
|
|
271
|
+
return ghListConnectionSchema.parse(parsed.data.repository[side]);
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* One side of the open list: up to `limit` rows from `cursor`, in requests of at most
|
|
275
|
+
* `GH_GRAPHQL_PAGE_MAX`. `nextCursor` is where the rows stop, or `null` once the side is
|
|
276
|
+
* exhausted. `done` asks for `first: 0`, which still answers `totalCount`, so an exhausted side
|
|
277
|
+
* keeps a fresh total without ever restarting from its first page (open-mercato/cezar#1366
|
|
278
|
+
* restarted it while the other side paged).
|
|
279
|
+
*/
|
|
280
|
+
export async function fetchListConnection(run, owner, name, side, limit, cursor, done = false) {
|
|
281
|
+
const query = listQuery(side);
|
|
282
|
+
if (done) {
|
|
283
|
+
const page = parseListConnection(await run(query, { owner, name, first: 0 }), side);
|
|
284
|
+
return { totalCount: page.totalCount, nodes: [], nextCursor: null };
|
|
285
|
+
}
|
|
286
|
+
const nodes = [];
|
|
287
|
+
let after = cursor;
|
|
288
|
+
for (;;) {
|
|
289
|
+
const first = Math.min(GH_GRAPHQL_PAGE_MAX, limit - nodes.length);
|
|
290
|
+
const page = parseListConnection(await run(query, { owner, name, first, ...(after ? { after } : {}) }), side);
|
|
291
|
+
const rows = page.nodes.filter((node) => node !== null);
|
|
292
|
+
nodes.push(...rows);
|
|
293
|
+
const next = page.pageInfo.hasNextPage ? page.pageInfo.endCursor ?? null : null;
|
|
294
|
+
// An empty page that claims more would loop forever; read it as the end of the side.
|
|
295
|
+
if (next === null || rows.length === 0)
|
|
296
|
+
return { totalCount: page.totalCount, nodes, nextCursor: null };
|
|
297
|
+
if (next === after)
|
|
298
|
+
throw new Error('GitHub list cursor did not advance');
|
|
299
|
+
// A short page with more behind it still ends this request: the cursor resumes it exactly.
|
|
300
|
+
if (nodes.length >= limit || rows.length < first)
|
|
301
|
+
return { totalCount: page.totalCount, nodes, nextCursor: next };
|
|
302
|
+
after = next;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
/* Reads degrade to `available: false` with a hint — never an error (plan rule
|
|
306
|
+
7): no `gh`, no remote, offline all land on the same quiet path. A short
|
|
307
|
+
cache keeps tab switches from hammering the GitHub API; a cached fetch with
|
|
308
|
+
a bigger limit than asked serves fine (it's a superset). Keyed by repository/host/account context
|
|
309
|
+
(multi-project workspace, step 2.6) plus the page asked for: one project's — possibly private —
|
|
310
|
+
issues/PRs must never be served under another project's scope, and page 2 is never page 1.
|
|
311
|
+
Bounded like `commentsCache` so an unbounded workspace can't grow it without limit. */
|
|
312
|
+
let listRequestOrder = 0;
|
|
313
|
+
/** Per context, the order of the newest page-1 refresh. A refresh drops the context's later
|
|
314
|
+
* pages (their cursors were cut from the old first page), and a later page that started before
|
|
315
|
+
* it may answer its caller but never re-enters the cache. Bounded with `listCache`. */
|
|
316
|
+
const listRefreshOrder = new Map();
|
|
317
|
+
const listCache = new Map();
|
|
318
|
+
const LIST_CACHE_MAX = 50;
|
|
319
|
+
const CACHE_MS = 60_000;
|
|
320
|
+
/** Which page key holds a projects generation, so `fetchGithubProjects` never scans the cache.
|
|
321
|
+
* Installed and evicted in step with `listCache`, so it shares its bound. */
|
|
322
|
+
const projectGenerations = new Map();
|
|
323
|
+
/** Board discovery (the `projectsV2` walk) per context: page 2 pays for membership only. A
|
|
324
|
+
* failed discovery is dropped, so the next page or refresh asks again. */
|
|
325
|
+
const repositoryProjectsCache = new Map();
|
|
326
|
+
function dropListEntry(key) {
|
|
327
|
+
const generation = listCache.get(key)?.data.projectsGeneration;
|
|
328
|
+
if (generation && projectGenerations.get(generation) === key)
|
|
329
|
+
projectGenerations.delete(generation);
|
|
330
|
+
listCache.delete(key);
|
|
331
|
+
}
|
|
332
|
+
function discoverRepositoryProjects(contextKey, refresh, start) {
|
|
333
|
+
const hit = repositoryProjectsCache.get(contextKey);
|
|
334
|
+
if (!refresh && hit && Date.now() - hit.at < CACHE_MS)
|
|
335
|
+
return hit.projects;
|
|
336
|
+
const projects = start();
|
|
337
|
+
const entry = { at: Date.now(), projects };
|
|
338
|
+
repositoryProjectsCache.delete(contextKey);
|
|
339
|
+
repositoryProjectsCache.set(contextKey, entry);
|
|
340
|
+
projects.catch(() => {
|
|
341
|
+
if (repositoryProjectsCache.get(contextKey) === entry)
|
|
342
|
+
repositoryProjectsCache.delete(contextKey);
|
|
343
|
+
});
|
|
344
|
+
while (repositoryProjectsCache.size > LIST_CACHE_MAX) {
|
|
345
|
+
const oldest = repositoryProjectsCache.keys().next().value;
|
|
346
|
+
if (oldest === undefined)
|
|
347
|
+
break;
|
|
348
|
+
repositoryProjectsCache.delete(oldest);
|
|
349
|
+
}
|
|
350
|
+
return projects;
|
|
351
|
+
}
|
|
352
|
+
export const GH_MAX_LIMIT = 1000;
|
|
353
|
+
function listPageKey(contextKey, page) {
|
|
354
|
+
return JSON.stringify([contextKey, page.issuesCursor ?? null, page.prsCursor ?? null, !!page.issuesDone, !!page.prsDone]);
|
|
355
|
+
}
|
|
356
|
+
function isFirstGithubPage(page) {
|
|
357
|
+
return !page.issuesCursor && !page.prsCursor && !page.issuesDone && !page.prsDone;
|
|
358
|
+
}
|
|
359
|
+
function cachedGithubPage(hit, refresh, capped) {
|
|
360
|
+
if (!refresh && hit && Date.now() - hit.at < CACHE_MS && hit.limit >= capped)
|
|
361
|
+
return hit.data;
|
|
362
|
+
return undefined;
|
|
363
|
+
}
|
|
364
|
+
function rememberListRefresh(contextKey, order, key) {
|
|
365
|
+
listRefreshOrder.delete(contextKey);
|
|
366
|
+
listRefreshOrder.set(contextKey, order);
|
|
367
|
+
while (listRefreshOrder.size > LIST_CACHE_MAX) {
|
|
368
|
+
const oldest = listRefreshOrder.keys().next().value;
|
|
369
|
+
if (oldest === undefined)
|
|
370
|
+
break;
|
|
371
|
+
listRefreshOrder.delete(oldest);
|
|
372
|
+
}
|
|
373
|
+
for (const [cached, entry] of [...listCache]) {
|
|
374
|
+
if (entry.contextKey === contextKey && cached !== key)
|
|
375
|
+
dropListEntry(cached);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
function githubProjectGraphql(repoRoot, context) {
|
|
379
|
+
// Optional project queries have one shared wall-clock budget. A missing scope, a cap,
|
|
380
|
+
// or malformed metadata must never turn an otherwise working list into unavailable.
|
|
381
|
+
const deadline = Date.now() + 15_000;
|
|
382
|
+
return (query, variables) => {
|
|
383
|
+
const remaining = deadline - Date.now();
|
|
384
|
+
if (remaining <= 0)
|
|
385
|
+
return Promise.reject(new Error('Project lookup timed out'));
|
|
386
|
+
return gh(repoRoot, ghGraphqlArgs(query, variables), Math.min(remaining, 8_000), undefined, context);
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
function recordLabelColor(labelColors, label) {
|
|
390
|
+
if (label.color && !labelColors[label.name])
|
|
391
|
+
labelColors[label.name] = label.color;
|
|
392
|
+
}
|
|
393
|
+
function flattenGithubIssue(issue, recordColor) {
|
|
394
|
+
issue.labels.forEach(recordColor);
|
|
395
|
+
return {
|
|
396
|
+
kind: 'issue',
|
|
397
|
+
number: issue.number,
|
|
398
|
+
title: issue.title,
|
|
399
|
+
author: issue.author?.login ?? '?',
|
|
400
|
+
createdAt: issue.createdAt,
|
|
401
|
+
labels: issue.labels.map((label) => label.name),
|
|
402
|
+
body: (issue.body ?? '').slice(0, 8_000),
|
|
403
|
+
url: issue.url,
|
|
404
|
+
comments: 0,
|
|
405
|
+
assignees: issue.assignees.map((assignee) => assignee.login),
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
function flattenGithubPr(pr, recordColor) {
|
|
409
|
+
pr.labels.forEach(recordColor);
|
|
410
|
+
const isDraft = pr.isDraft ?? false;
|
|
411
|
+
return {
|
|
412
|
+
kind: 'pr',
|
|
413
|
+
number: pr.number,
|
|
414
|
+
title: pr.title,
|
|
415
|
+
author: pr.author?.login ?? '?',
|
|
416
|
+
createdAt: pr.createdAt,
|
|
417
|
+
labels: [...pr.labels.map((label) => label.name), ...(isDraft ? ['draft'] : [])],
|
|
418
|
+
body: (pr.body ?? '').slice(0, 8_000),
|
|
419
|
+
url: pr.url,
|
|
420
|
+
comments: 0,
|
|
421
|
+
isDraft,
|
|
422
|
+
additions: pr.additions ?? 0,
|
|
423
|
+
deletions: pr.deletions ?? 0,
|
|
424
|
+
// Hydrated lazily by `fetchGithubChecks` for on-screen rows (#664) — the list no
|
|
425
|
+
// longer pays for the CI rollup of every open PR.
|
|
426
|
+
checks: null,
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
function applyPreviousProjectIds(issues, previous) {
|
|
430
|
+
for (const issue of issues) {
|
|
431
|
+
const known = previous?.issues.find((row) => row.number === issue.number)?.projectIds;
|
|
432
|
+
if (known !== undefined)
|
|
433
|
+
issue.projectIds = known;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
function githubListData(input) {
|
|
437
|
+
return {
|
|
438
|
+
available: true,
|
|
439
|
+
repo: input.repo,
|
|
440
|
+
syncedAt: new Date().toISOString(),
|
|
441
|
+
issues: input.issues,
|
|
442
|
+
prs: input.prs,
|
|
443
|
+
labelColors: input.labelColors,
|
|
444
|
+
...(input.viewerLogin ? { viewerLogin: input.viewerLogin } : {}),
|
|
445
|
+
projectsState: 'refreshing',
|
|
446
|
+
projectsGeneration: input.generation,
|
|
447
|
+
...(input.previous?.projects ? { projects: input.previous.projects } : {}),
|
|
448
|
+
issuesTotal: input.issuesTotal,
|
|
449
|
+
prsTotal: input.prsTotal,
|
|
450
|
+
issuesNextCursor: input.issuesNextCursor,
|
|
451
|
+
prsNextCursor: input.prsNextCursor,
|
|
452
|
+
commentsDeferred: true,
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
function applyReadyProjectMembership(current, metadata) {
|
|
456
|
+
current.data = {
|
|
457
|
+
...current.data,
|
|
458
|
+
projectsState: metadata.state,
|
|
459
|
+
projects: metadata.projects,
|
|
460
|
+
issues: current.data.issues.map((issue) => ({ ...issue, projectIds: metadata.membership?.[issue.number] ?? [] })),
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
function attachProjectMetadata(projectLookup, generation, key) {
|
|
464
|
+
return projectLookup.then((result) => {
|
|
465
|
+
const metadata = 'membership' in result
|
|
466
|
+
? { generation, state: 'ready', projects: result.projects, membership: result.membership }
|
|
467
|
+
: { generation, state: 'unavailable', reason: result.projectsReason };
|
|
468
|
+
const current = listCache.get(key);
|
|
469
|
+
if (current?.data.projectsGeneration === generation) {
|
|
470
|
+
if (metadata.state === 'ready')
|
|
471
|
+
applyReadyProjectMembership(current, metadata);
|
|
472
|
+
else
|
|
473
|
+
current.data = { ...current.data, projectsState: metadata.state, projectsReason: metadata.reason };
|
|
474
|
+
}
|
|
475
|
+
return metadata;
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
function storeGithubPage(key, contextKey, order, capped, data, projects, generation) {
|
|
479
|
+
// Concurrent refreshes may finish in reverse order. An older list must not evict
|
|
480
|
+
// the newer generation (and invalidate the newer client's metadata follow-up).
|
|
481
|
+
if ((listCache.get(key)?.order ?? -1) < order && order >= (listRefreshOrder.get(contextKey) ?? 0)) {
|
|
482
|
+
dropListEntry(key); // re-insert so this key becomes the newest
|
|
483
|
+
listCache.set(key, { contextKey, order, at: Date.now(), limit: capped, data, projects });
|
|
484
|
+
projectGenerations.set(generation, key);
|
|
485
|
+
}
|
|
486
|
+
while (listCache.size > LIST_CACHE_MAX) {
|
|
487
|
+
const oldest = listCache.keys().next().value;
|
|
488
|
+
if (oldest === undefined)
|
|
489
|
+
break;
|
|
490
|
+
dropListEntry(oldest);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
async function loadGithubPage(repoRoot, context, capped, page, key, hit, order, refresh) {
|
|
494
|
+
// No `statusCheckRollup` (#664): it is hydrated lazily per on-screen PR row via
|
|
495
|
+
// `fetchGithubChecks`. A big aggregate list still gets a little more wall clock.
|
|
496
|
+
const timeout = capped > 100 ? 30_000 : 15_000;
|
|
497
|
+
const ownerName = await resolveRepoHandleStrict(repoRoot, context);
|
|
498
|
+
if (!ownerName)
|
|
499
|
+
return { available: false, reason: 'Could not resolve this repository on GitHub.', issues: [], prs: [] };
|
|
500
|
+
const runGraphql = (query, variables) => gh(repoRoot, ghGraphqlArgs(query, variables), timeout, undefined, context);
|
|
501
|
+
const projectGraphql = githubProjectGraphql(repoRoot, context);
|
|
502
|
+
const firstPage = isFirstGithubPage(page);
|
|
503
|
+
const issueSide = fetchListConnection(runGraphql, ownerName.owner, ownerName.name, 'issues', capped, page.issuesCursor, page.issuesDone);
|
|
504
|
+
const projectLookup = fetchIssueProjects(projectGraphql, ownerName.owner, ownerName.name, async () => (await issueSide).nodes.map((node) => node.number), () => discoverRepositoryProjects(context.key, refresh, () => fetchRepositoryProjects(projectGraphql, ownerName.owner, ownerName.name)));
|
|
505
|
+
const [issuesPage, prsPage, viewerLogin] = await Promise.all([
|
|
506
|
+
issueSide,
|
|
507
|
+
fetchListConnection(runGraphql, ownerName.owner, ownerName.name, 'pullRequests', capped, page.prsCursor, page.prsDone),
|
|
508
|
+
// The viewer is the same on every page; only the first one asks.
|
|
509
|
+
firstPage
|
|
510
|
+
? fetchViewerLogin((query) => gh(repoRoot, ghGraphqlArgs(query), 8_000, undefined, context))
|
|
511
|
+
: Promise.resolve(undefined),
|
|
512
|
+
]);
|
|
513
|
+
// One repo-wide label→color map, filled as we flatten each item's labels.
|
|
514
|
+
const labelColors = {};
|
|
515
|
+
const recordColor = (label) => recordLabelColor(labelColors, label);
|
|
516
|
+
const issues = issuesPage.nodes.map((issue) => flattenGithubIssue(issue, recordColor));
|
|
517
|
+
const prs = prsPage.nodes.map((pr) => flattenGithubPr(pr, recordColor));
|
|
518
|
+
// Only verified values from the same repository may survive a refresh.
|
|
519
|
+
const previous = hit?.data.repo === `${ownerName.owner}/${ownerName.name}` ? hit.data : undefined;
|
|
520
|
+
applyPreviousProjectIds(issues, previous);
|
|
521
|
+
const generation = randomUUID();
|
|
522
|
+
const data = githubListData({
|
|
523
|
+
repo: `${ownerName.owner}/${ownerName.name}`,
|
|
524
|
+
issues,
|
|
525
|
+
prs,
|
|
526
|
+
labelColors,
|
|
527
|
+
viewerLogin,
|
|
528
|
+
generation,
|
|
529
|
+
previous,
|
|
530
|
+
issuesTotal: issuesPage.totalCount,
|
|
531
|
+
prsTotal: prsPage.totalCount,
|
|
532
|
+
issuesNextCursor: issuesPage.nextCursor,
|
|
533
|
+
prsNextCursor: prsPage.nextCursor,
|
|
534
|
+
});
|
|
535
|
+
const projects = attachProjectMetadata(projectLookup, generation, key);
|
|
536
|
+
storeGithubPage(key, context.key, order, capped, data, projects, generation);
|
|
537
|
+
return data;
|
|
538
|
+
}
|
|
539
|
+
export async function fetchGithub(repoRoot, refresh = false, limit = 30, page = {}) {
|
|
540
|
+
if (productEnv('DRY_RUN') === '1')
|
|
541
|
+
return mockGithubPage(limit, page);
|
|
542
|
+
const context = githubContext(repoRoot);
|
|
543
|
+
const capped = Math.min(Math.max(limit, 1), GH_MAX_LIMIT);
|
|
544
|
+
const key = listPageKey(context.key, page);
|
|
545
|
+
const hit = listCache.get(key);
|
|
546
|
+
const cached = cachedGithubPage(hit, refresh, capped);
|
|
547
|
+
if (cached)
|
|
548
|
+
return cached;
|
|
549
|
+
const order = ++listRequestOrder;
|
|
550
|
+
if (refresh && isFirstGithubPage(page))
|
|
551
|
+
rememberListRefresh(context.key, order, key);
|
|
552
|
+
try {
|
|
553
|
+
return await loadGithubPage(repoRoot, context, capped, page, key, hit, order, refresh);
|
|
554
|
+
}
|
|
555
|
+
catch (err) {
|
|
556
|
+
return { available: false, reason: ghFailureReason(err), issues: [], prs: [] };
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
/** A `gh` failure as one readable line. GitHub's own words come on stderr; the Node message
|
|
560
|
+
* starts with the whole argv, and a GraphQL argv is a multi-line query, so its first line is
|
|
561
|
+
* a fragment of that query rather than the reason (open-mercato/cezar#1366). */
|
|
562
|
+
export function ghFailureReason(err) {
|
|
563
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
564
|
+
if (/ENOENT/.test(message))
|
|
565
|
+
return 'gh CLI not found — install it and run `gh auth login`';
|
|
566
|
+
const failure = (err ?? {});
|
|
567
|
+
const stderr = typeof failure.stderr === 'string' ? failure.stderr : '';
|
|
568
|
+
if (stderr.trim()) {
|
|
569
|
+
const lines = stderr.split('\n').map((line) => line.trim()).filter(Boolean);
|
|
570
|
+
return lines.find((line) => !/^Command failed:/i.test(line)) ?? lines[0];
|
|
571
|
+
}
|
|
572
|
+
if (failure.killed === true || typeof failure.signal === 'string')
|
|
573
|
+
return 'GitHub did not answer in time. Refresh to try again.';
|
|
574
|
+
// Without stderr, everything after "Command failed:" is the argv, which is query text.
|
|
575
|
+
if (/^Command failed:/i.test(message))
|
|
576
|
+
return 'GitHub request failed. Refresh to try again.';
|
|
577
|
+
return message.split('\n').map((line) => line.trim()).find(Boolean) ?? 'gh failed';
|
|
578
|
+
}
|
|
579
|
+
/** Bounded lookup already started by the list. Never launch work for an expired generation. */
|
|
580
|
+
export async function fetchGithubProjects(repoRoot, generation) {
|
|
581
|
+
const key = projectGenerations.get(generation);
|
|
582
|
+
const entry = key === undefined ? undefined : listCache.get(key);
|
|
583
|
+
// A generation from another repository, host or account never resolves here, even when
|
|
584
|
+
// its UUID is known: the context is part of what the generation was issued for.
|
|
585
|
+
if (!entry || entry.contextKey !== githubContext(repoRoot).key || entry.data.projectsGeneration !== generation) {
|
|
586
|
+
return { generation, state: 'unavailable', reason: 'Project metadata expired. Refresh to try again.' };
|
|
587
|
+
}
|
|
588
|
+
return entry.projects;
|
|
589
|
+
}
|
|
590
|
+
function firstLine(s) {
|
|
591
|
+
return s.split('\n').find((l) => l.trim().length > 0)?.trim() ?? 'gh failed';
|
|
592
|
+
}
|
|
593
|
+
// On-demand cross-state search stays separate from the hot open list.
|
|
594
|
+
/** Hits per search. `gh search`'s own default is 30; 50 matches the page size the incremental-
|
|
595
|
+
* loading spec settled on and keeps one search to a single `gh` round-trip. */
|
|
596
|
+
export const GH_SEARCH_MAX = GITHUB_SEARCH_MAX;
|
|
597
|
+
/** The `--json` field set each search path requests. Neither path asks for `additions`/`deletions`
|
|
598
|
+
* or `statusCheckRollup` — the same reason the list tier stopped (#664), those are the expensive
|
|
599
|
+
* per-PR lookups.
|
|
600
|
+
*
|
|
601
|
+
* `isDraft` is added for PRs only, and that is a hard requirement rather than a nicety: `gh search
|
|
602
|
+
* issues` does not define the field at all and rejects the whole call with `Unknown JSON field:
|
|
603
|
+
* "isDraft"`, which would turn every text query on the Issues tab into the "could not be searched"
|
|
604
|
+
* degrade. `gh {issue,pr} view` is the opposite — it tolerates the field for both kinds — so only
|
|
605
|
+
* the search path needs to discriminate. */
|
|
606
|
+
const SEARCH_FIELDS = 'number,title,author,createdAt,labels,body,url';
|
|
607
|
+
/** `gh search {prs,issues} --json` field list for `kind`. See `SEARCH_FIELDS` on why `isDraft`
|
|
608
|
+
* cannot be sent to the issues search. */
|
|
609
|
+
function searchJsonFields(kind) {
|
|
610
|
+
return kind === 'pr'
|
|
611
|
+
? `${SEARCH_FIELDS},isDraft,commentsCount`
|
|
612
|
+
: `${SEARCH_FIELDS},assignees,commentsCount`;
|
|
613
|
+
}
|
|
614
|
+
/** `gh search issues|prs` returns `commentsCount` where the list tier gets its counts from a
|
|
615
|
+
* separate GraphQL walk. Same value, different spelling — normalized into `ForgeItem.comments`. */
|
|
616
|
+
const ghSearchHitSchema = ghIssueSchema.extend({
|
|
617
|
+
assignees: z.array(z.object({ login: z.string() })).optional(),
|
|
618
|
+
isDraft: z.boolean().default(false),
|
|
619
|
+
commentsCount: z.number().default(0),
|
|
620
|
+
});
|
|
621
|
+
/** `gh {issue,pr} view <n> --json …` — the exact-number path. Shares `ghIssueSchema`'s core;
|
|
622
|
+
* `isDraft`/`additions`/`deletions` are PR-only and default harmlessly for issues. */
|
|
623
|
+
const ghViewHitSchema = ghIssueSchema.extend({
|
|
624
|
+
state: z.string().optional(),
|
|
625
|
+
mergedAt: z.unknown().optional(),
|
|
626
|
+
assignees: z.array(z.object({ login: z.string() })).optional(),
|
|
627
|
+
isDraft: z.boolean().default(false),
|
|
628
|
+
additions: z.number().default(0),
|
|
629
|
+
deletions: z.number().default(0),
|
|
630
|
+
});
|
|
631
|
+
/** Flatten one validated hit into the `ForgeItem` the tab's rows already render. `checks: null`
|
|
632
|
+
* is what the list tier ships too since #664 — the glyph hydrates lazily via `/api/github/checks`
|
|
633
|
+
* — so a searched row and a listed row are indistinguishable to the UI. */
|
|
634
|
+
function toSearchItem(kind, hit, labelColors) {
|
|
635
|
+
for (const label of hit.labels) {
|
|
636
|
+
if (label.color && !labelColors[label.name])
|
|
637
|
+
labelColors[label.name] = label.color;
|
|
638
|
+
}
|
|
639
|
+
const item = {
|
|
640
|
+
kind,
|
|
641
|
+
number: hit.number,
|
|
642
|
+
title: hit.title,
|
|
643
|
+
author: hit.author?.login ?? '?',
|
|
644
|
+
createdAt: hit.createdAt,
|
|
645
|
+
labels: hit.labels.map((l) => l.name),
|
|
646
|
+
body: (hit.body ?? '').slice(0, 8_000),
|
|
647
|
+
url: hit.url,
|
|
648
|
+
comments: hit.commentsCount,
|
|
649
|
+
...(kind === 'issue' && hit.assignees ? { assignees: hit.assignees.map(a => a.login) } : {}),
|
|
650
|
+
};
|
|
651
|
+
if (kind === 'pr') {
|
|
652
|
+
item.isDraft = hit.isDraft;
|
|
653
|
+
// Draft is a label on list rows as well as a flag — keep the parity so filtering by the
|
|
654
|
+
// `draft` chip behaves the same on a searched row.
|
|
655
|
+
if (hit.isDraft)
|
|
656
|
+
item.labels = [...item.labels, 'draft'];
|
|
657
|
+
if ('additions' in hit)
|
|
658
|
+
item.additions = hit.additions;
|
|
659
|
+
if ('deletions' in hit)
|
|
660
|
+
item.deletions = hit.deletions;
|
|
661
|
+
item.checks = null;
|
|
662
|
+
}
|
|
663
|
+
return item;
|
|
664
|
+
}
|
|
665
|
+
/** `gh {pr,issue} view <n>` diagnostics that name the REQUESTED item as absent. A bare
|
|
666
|
+
* "not found" / 404 or "Could not resolve to a Repository" also fails a view, but says the
|
|
667
|
+
* repository (or auth) is the problem, not that the number does not exist. */
|
|
668
|
+
const MISSING_ITEM_RE = /could not resolve to an? (?:pullrequest|issue|issueorpullrequest|pull request|issue or pull request)\b|no (?:pull requests|issues) found/i;
|
|
669
|
+
/** The exact `gh view` call plus its parse. `null` only for a confirmed-missing item; anything
|
|
670
|
+
* else (transport, auth, repository, unparseable output) throws. */
|
|
671
|
+
async function viewGithubHit(repoRoot, kind, number) {
|
|
672
|
+
try {
|
|
673
|
+
const out = await gh(repoRoot, [
|
|
674
|
+
kind === 'pr' ? 'pr' : 'issue',
|
|
675
|
+
'view',
|
|
676
|
+
String(number),
|
|
677
|
+
'--json',
|
|
678
|
+
kind === 'pr' ? `${SEARCH_FIELDS},isDraft,additions,deletions,state,mergedAt` : `${SEARCH_FIELDS},assignees`,
|
|
679
|
+
]);
|
|
680
|
+
const hit = ghViewHitSchema.parse(JSON.parse(out));
|
|
681
|
+
if (kind === 'pr' && hit.state === 'MERGED') {
|
|
682
|
+
const handle = await resolveRepoHandle(repoRoot);
|
|
683
|
+
if (handle)
|
|
684
|
+
mergedPullRequestObservations.observe({ owner: handle.owner, repo: handle.name, number }, hit);
|
|
685
|
+
}
|
|
686
|
+
return hit;
|
|
687
|
+
}
|
|
688
|
+
catch (err) {
|
|
689
|
+
if (MISSING_ITEM_RE.test(err instanceof Error ? err.message : String(err)))
|
|
690
|
+
return null;
|
|
691
|
+
throw err;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
/** `view` exposes comment bodies rather than a count. Read the single issue resource
|
|
695
|
+
* (PRs share this endpoint) instead of paginating a potentially enormous thread.
|
|
696
|
+
* gh resolves placeholders in repoRoot, just as it resolves the preceding view call. */
|
|
697
|
+
async function finishViewHit(repoRoot, kind, number, hit, labelColors) {
|
|
698
|
+
const commentsCount = z.number().int().nonnegative().parse(JSON.parse(await gh(repoRoot, ['api', `repos/{owner}/{repo}/issues/${number}`, '--jq', '.comments'], 8_000)));
|
|
699
|
+
return toSearchItem(kind, { ...hit, commentsCount }, labelColors);
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* One issue or PR by number, in any state (#692). `null` when `gh` confirms the item is absent;
|
|
703
|
+
* throws on every other failure. `labelColors` is an optional out-param.
|
|
704
|
+
*/
|
|
705
|
+
export async function viewGithubItem(repoRoot, kind, number, labelColors = {}) {
|
|
706
|
+
const hit = await viewGithubHit(repoRoot, kind, number);
|
|
707
|
+
return hit ? finishViewHit(repoRoot, kind, number, hit, labelColors) : null;
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* Search any state: exact numbers try a direct lookup, then text fallback; text search omits
|
|
711
|
+
* state restriction. Return availability and a reason on failure instead of throwing.
|
|
712
|
+
*/
|
|
713
|
+
export async function searchGithubItems(repoRoot, kind, query, limit = GH_SEARCH_MAX) {
|
|
714
|
+
const trimmed = query.trim();
|
|
715
|
+
if (trimmed === '')
|
|
716
|
+
return { available: true, items: [] };
|
|
717
|
+
const capped = Math.min(Math.max(limit, 1), GH_SEARCH_MAX);
|
|
718
|
+
if (productEnv('DRY_RUN') === '1') {
|
|
719
|
+
const mock = mockGithub();
|
|
720
|
+
const pool = kind === 'issue' ? mock.issues : mock.prs;
|
|
721
|
+
const queryText = trimmed.toLowerCase();
|
|
722
|
+
const numeric = queryText.replace(/^#/, '');
|
|
723
|
+
const idOnly = /^\d+$/.test(numeric);
|
|
724
|
+
// The fixture obeys `limit` and flags `truncated` on the live path's own rule (#838). The
|
|
725
|
+
// catalog is a handful of rows, so the cap never bites in the demo itself — but offline mode
|
|
726
|
+
// is the only place anyone developing without `gh` exercises cap-and-truncate at all, and a
|
|
727
|
+
// dry-run that returned the whole pool unflagged would hide a regression in it.
|
|
728
|
+
const items = pool
|
|
729
|
+
.filter((item) => idOnly
|
|
730
|
+
? String(item.number).includes(numeric)
|
|
731
|
+
: `#${item.number} ${item.title} ${item.author} ${item.body}`.toLowerCase().includes(queryText))
|
|
732
|
+
.slice(0, capped)
|
|
733
|
+
// Search rows defer checks to the same hydration endpoint as live results. Copy rather
|
|
734
|
+
// than mutate: the mock checks endpoint still reads the fixture's passing/failing values.
|
|
735
|
+
.map(item => item.kind === 'pr' ? { ...item, checks: null } : item);
|
|
736
|
+
return { available: true, items, truncated: items.length >= capped };
|
|
737
|
+
}
|
|
738
|
+
const labelColors = {};
|
|
739
|
+
const numeric = trimmed.replace(/^#/, '');
|
|
740
|
+
try {
|
|
741
|
+
if (/^\d+$/.test(numeric)) {
|
|
742
|
+
// `Number()` before interpolation: the regex already guarantees digits, but the number is
|
|
743
|
+
// user input reaching an argv, so it is normalized rather than passed through verbatim.
|
|
744
|
+
const number = Number(numeric);
|
|
745
|
+
// Any failure of the exact view call or its parse (and a missing number) falls through to
|
|
746
|
+
// the text search below. The comment count is outside that guard: its failure reports
|
|
747
|
+
// unavailable rather than an invented zero.
|
|
748
|
+
let hit = null;
|
|
749
|
+
try {
|
|
750
|
+
hit = await viewGithubHit(repoRoot, kind, number);
|
|
751
|
+
}
|
|
752
|
+
catch {
|
|
753
|
+
// fall through
|
|
754
|
+
}
|
|
755
|
+
if (hit) {
|
|
756
|
+
const item = await finishViewHit(repoRoot, kind, number, hit, labelColors);
|
|
757
|
+
return await hydrateSearchProjects(repoRoot, kind, { available: true, items: [item], labelColors });
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
// The memoized handle first (usually a hit). Its `null` is deliberately ambiguous — it swallows
|
|
761
|
+
// "gh is missing" and "no remote" alike — so on a miss, ask once more directly and let the real
|
|
762
|
+
// failure reach the catch below, where it becomes the same honest hint the list tier gives.
|
|
763
|
+
// Only a genuinely unparseable slug reaches the "no remote" reason.
|
|
764
|
+
const handle = (await resolveRepoHandle(repoRoot)) ??
|
|
765
|
+
parseOwnerName(await gh(repoRoot, ['repo', 'view', '--json', 'nameWithOwner', '--jq', '.nameWithOwner']));
|
|
766
|
+
if (!handle) {
|
|
767
|
+
return { available: false, reason: 'no GitHub remote to search', items: [] };
|
|
768
|
+
}
|
|
769
|
+
const out = await gh(repoRoot, [
|
|
770
|
+
'search',
|
|
771
|
+
kind === 'pr' ? 'prs' : 'issues',
|
|
772
|
+
'--repo',
|
|
773
|
+
`${handle.owner}/${handle.name}`,
|
|
774
|
+
'--limit',
|
|
775
|
+
String(capped),
|
|
776
|
+
'--json',
|
|
777
|
+
searchJsonFields(kind),
|
|
778
|
+
// End-of-flags, then the query: `gh` parses a leading `-`/`--` as a flag otherwise, so a
|
|
779
|
+
// flag-shaped query would degrade the tab to "GitHub could not be searched: unknown flag",
|
|
780
|
+
// and the exact text `--web` would match `gh search`'s own `-w, --web` and open a browser
|
|
781
|
+
// on the cockpit's host. Behind `--` it is search text, which is all the box ever means.
|
|
782
|
+
'--',
|
|
783
|
+
trimmed,
|
|
784
|
+
]);
|
|
785
|
+
const hits = z.array(ghSearchHitSchema).parse(JSON.parse(out));
|
|
786
|
+
// Older gh/GHES search treats repeated repo qualifiers as OR. Never hydrate a foreign
|
|
787
|
+
// hit's number with this repository's memberships or expose it as a local detail row.
|
|
788
|
+
const prefix = `/${handle.owner}/${handle.name}/${kind === 'pr' ? 'pull' : 'issues'}/`.toLowerCase();
|
|
789
|
+
if (hits.some(hit => {
|
|
790
|
+
try {
|
|
791
|
+
return new URL(hit.url).pathname.toLowerCase() !== `${prefix}${hit.number}`;
|
|
792
|
+
}
|
|
793
|
+
catch {
|
|
794
|
+
return true;
|
|
795
|
+
}
|
|
796
|
+
}))
|
|
797
|
+
return { available: false, reason: 'GitHub returned results outside this repository. Remove repository qualifiers and try again.', items: [] };
|
|
798
|
+
return await hydrateSearchProjects(repoRoot, kind, {
|
|
799
|
+
available: true,
|
|
800
|
+
items: hits.map((hit) => toSearchItem(kind, hit, labelColors)),
|
|
801
|
+
truncated: hits.length >= capped,
|
|
802
|
+
labelColors,
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
catch (err) {
|
|
806
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
807
|
+
const reason = /ENOENT/.test(message)
|
|
808
|
+
? 'gh CLI not found — install it and run `gh auth login`'
|
|
809
|
+
: firstLine(message);
|
|
810
|
+
return { available: false, reason, items: [] };
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
/** The list's metadata window contains only open issues. Search must look up its own window,
|
|
814
|
+
* including closed issues, with the same bounds and unknown-on-failure semantics (#90). */
|
|
815
|
+
async function hydrateSearchProjects(repoRoot, kind, data) {
|
|
816
|
+
if (kind !== 'issue' || data.items.length === 0)
|
|
817
|
+
return data;
|
|
818
|
+
const handle = await resolveRepoHandle(repoRoot);
|
|
819
|
+
if (!handle)
|
|
820
|
+
return { ...data, projectsReason: 'Project boards unavailable for this repository.' };
|
|
821
|
+
const deadline = Date.now() + 15_000;
|
|
822
|
+
const run = (query, variables) => {
|
|
823
|
+
const remaining = deadline - Date.now();
|
|
824
|
+
if (remaining <= 0)
|
|
825
|
+
return Promise.reject(new Error('Project lookup timed out'));
|
|
826
|
+
const args = ghGraphqlArgs(query, variables);
|
|
827
|
+
return gh(repoRoot, args, Math.min(remaining, 8_000));
|
|
828
|
+
};
|
|
829
|
+
const projects = await fetchIssueProjects(run, handle.owner, handle.name, data.items.map(item => item.number));
|
|
830
|
+
if ('projectsReason' in projects)
|
|
831
|
+
return { ...data, projectsReason: projects.projectsReason };
|
|
832
|
+
return { ...data, items: data.items.map(item => ({ ...item, projectIds: projects.membership[item.number] ?? [] })) };
|
|
833
|
+
}
|
|
834
|
+
/** The dry-run catalog, paged like the real list: numeric offset cursors per side, done flags
|
|
835
|
+
* honored, exact totals, and counts deferred to the counts route (hearsay-tools/cezarion#1183). */
|
|
836
|
+
function mockGithubPage(limit, page) {
|
|
837
|
+
const full = mockGithub();
|
|
838
|
+
const side = (rows, cursor, done) => {
|
|
839
|
+
const start = done ? rows.length : Number(cursor ?? 0);
|
|
840
|
+
const slice = rows.slice(start, start + Math.max(limit, 1));
|
|
841
|
+
const end = start + slice.length;
|
|
842
|
+
return { rows: slice.map((row) => ({ ...row, comments: 0 })), next: end < rows.length ? String(end) : null };
|
|
843
|
+
};
|
|
844
|
+
const issues = side(full.issues, page.issuesCursor, page.issuesDone);
|
|
845
|
+
const prs = side(full.prs, page.prsCursor, page.prsDone);
|
|
846
|
+
const firstPage = !page.issuesCursor && !page.prsCursor && !page.issuesDone && !page.prsDone;
|
|
847
|
+
const { viewerLogin, ...rest } = full;
|
|
848
|
+
return {
|
|
849
|
+
...rest,
|
|
850
|
+
...(firstPage && viewerLogin ? { viewerLogin } : {}),
|
|
851
|
+
issues: issues.rows,
|
|
852
|
+
prs: prs.rows,
|
|
853
|
+
issuesTotal: full.issues.length,
|
|
854
|
+
prsTotal: full.prs.length,
|
|
855
|
+
issuesNextCursor: issues.next,
|
|
856
|
+
prsNextCursor: prs.next,
|
|
857
|
+
commentsDeferred: true,
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
/** CEZ_DRY_RUN=1 — a small fixed catalog so the GitHub tab is demoable offline. */
|
|
861
|
+
function mockGithub() {
|
|
862
|
+
const mk = (over) => ({
|
|
863
|
+
author: 'mock',
|
|
864
|
+
createdAt: new Date(Date.now() - over.number * 3_600_000).toISOString(),
|
|
865
|
+
labels: [],
|
|
866
|
+
url: `https://github.com/mock/repo/${over.kind === 'pr' ? 'pull' : 'issues'}/${over.number}`,
|
|
867
|
+
comments: 0,
|
|
868
|
+
...over,
|
|
869
|
+
});
|
|
870
|
+
return {
|
|
871
|
+
available: true,
|
|
872
|
+
repo: 'mock/repo',
|
|
873
|
+
viewerLogin: 'octocat',
|
|
874
|
+
projects: [{ id: 'mock-delivery', title: 'Delivery', url: 'https://github.com/users/mock/projects/1' }],
|
|
875
|
+
syncedAt: new Date().toISOString(),
|
|
876
|
+
issues: [
|
|
877
|
+
mk({ kind: 'issue', assignees: ['octocat'], projectIds: ['mock-delivery'], number: 142, title: 'Login form drops session on refresh', labels: ['bug', 'auth'], comments: 3, body: 'Repro: log in, hit reload — you land back on /login. The session cookie is set correctly, but the client store rehydrates before the cookie check resolves, so the auth guard redirects.' }),
|
|
878
|
+
mk({ kind: 'issue', assignees: ['ada'], projectIds: [], number: 139, title: 'Add --json flag to cez CLI output', labels: ['enhancement', 'cli'], comments: 1, body: 'For scripting it would help if `cez list` and `cez status` could emit machine-readable JSON instead of the table view.' }),
|
|
879
|
+
mk({ kind: 'issue', assignees: [], projectIds: ['mock-delivery'], number: 135, title: 'Flaky e2e: worktree cleanup race on cancel', labels: ['bug', 'flaky-test'], comments: 6, body: 'Cancelling a run while the agent holds a file lock leaves a dangling worktree. The next run on the same branch then fails with "worktree already exists".' }),
|
|
880
|
+
],
|
|
881
|
+
prs: [
|
|
882
|
+
mk({ kind: 'pr', number: 128, title: 'Fix flaky auth test in CI', labels: ['tests'], checks: 'passing', additions: 6, deletions: 3, body: 'Loosens the timing assertion in refresh.test.ts to a realistic budget.' }),
|
|
883
|
+
mk({ kind: 'pr', number: 124, title: 'Rate limit /api/runs', labels: ['server', 'draft'], isDraft: true, checks: 'failing', additions: 118, deletions: 7, comments: 4, body: 'Draft: token-bucket middleware on the runs router. Still needs the config surface and README docs before review.' }),
|
|
884
|
+
],
|
|
885
|
+
labelColors: {
|
|
886
|
+
bug: 'd73a4a',
|
|
887
|
+
auth: '5319e7',
|
|
888
|
+
enhancement: 'a2eeef',
|
|
889
|
+
cli: '0e8a16',
|
|
890
|
+
'flaky-test': 'fbca04',
|
|
891
|
+
tests: 'c5def5',
|
|
892
|
+
server: '1d76db',
|
|
893
|
+
draft: '6a737d',
|
|
894
|
+
},
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
// ---- comment threads (#499 Phase 2) ----------------------------------------
|
|
898
|
+
// A lazy per-thread fetch behind `GET /api/github/comments/:kind/:number`: the
|
|
899
|
+
// conversation comments (issues endpoint — GitHub serves PR conversation
|
|
900
|
+
// comments there too), plus submitted reviews for PRs, normalized into one
|
|
901
|
+
// chronological `ForgeComment[]`. Its own bounded 60 s cache keeps an open
|
|
902
|
+
// detail view from re-fetching on every focus. Degrades to `available: false`
|
|
903
|
+
// exactly like the list fetch — never a 5xx.
|
|
904
|
+
const ghCommentUser = z.object({ login: z.string(), avatar_url: z.string().nullish() }).nullish();
|
|
905
|
+
const ghIssueCommentSchema = z.object({
|
|
906
|
+
id: z.number(),
|
|
907
|
+
user: ghCommentUser,
|
|
908
|
+
created_at: z.string(),
|
|
909
|
+
body: z.string().nullish(),
|
|
910
|
+
html_url: z.string(),
|
|
911
|
+
});
|
|
912
|
+
const ghReviewSchema = z.object({
|
|
913
|
+
id: z.number(),
|
|
914
|
+
user: ghCommentUser,
|
|
915
|
+
body: z.string().nullish(),
|
|
916
|
+
state: z.string(),
|
|
917
|
+
submitted_at: z.string().nullish(),
|
|
918
|
+
html_url: z.string(),
|
|
919
|
+
});
|
|
920
|
+
const REVIEW_STATE = {
|
|
921
|
+
APPROVED: 'approved',
|
|
922
|
+
CHANGES_REQUESTED: 'changes_requested',
|
|
923
|
+
COMMENTED: 'commented',
|
|
924
|
+
DISMISSED: 'dismissed',
|
|
925
|
+
};
|
|
926
|
+
/** First 200 thread entries, then `truncated`; each body sliced to 8 000 chars (same cap as
|
|
927
|
+
* item bodies). */
|
|
928
|
+
export const THREAD_ENTRY_CAP = 200;
|
|
929
|
+
const COMMENT_BODY_CAP = 8_000;
|
|
930
|
+
/** `gh api …/issues/{n}/comments` JSON → `ForgeComment[]`. Exported for unit tests. */
|
|
931
|
+
export function normalizeComments(raw) {
|
|
932
|
+
return z.array(ghIssueCommentSchema).parse(raw).map((c) => defined({
|
|
933
|
+
id: c.id,
|
|
934
|
+
author: c.user?.login ?? '?',
|
|
935
|
+
avatarUrl: c.user?.avatar_url ?? undefined,
|
|
936
|
+
createdAt: c.created_at,
|
|
937
|
+
body: (c.body ?? '').slice(0, COMMENT_BODY_CAP),
|
|
938
|
+
kind: 'comment',
|
|
939
|
+
url: c.html_url,
|
|
940
|
+
}));
|
|
941
|
+
}
|
|
942
|
+
/** `gh api …/pulls/{n}/reviews` JSON → `ForgeComment[]`. Reviews with an empty body AND state
|
|
943
|
+
* COMMENTED/PENDING carry no signal in a flat thread and are dropped; the rest map to
|
|
944
|
+
* `kind: 'review'` (Q4). Exported for unit tests. */
|
|
945
|
+
export function normalizeReviews(raw) {
|
|
946
|
+
return z
|
|
947
|
+
.array(ghReviewSchema)
|
|
948
|
+
.parse(raw)
|
|
949
|
+
.filter((r) => {
|
|
950
|
+
const state = r.state.toUpperCase();
|
|
951
|
+
const emptyBody = (r.body ?? '').trim().length === 0;
|
|
952
|
+
return !(emptyBody && (state === 'COMMENTED' || state === 'PENDING'));
|
|
953
|
+
})
|
|
954
|
+
.map((r) => defined({
|
|
955
|
+
id: r.id,
|
|
956
|
+
author: r.user?.login ?? '?',
|
|
957
|
+
avatarUrl: r.user?.avatar_url ?? undefined,
|
|
958
|
+
createdAt: r.submitted_at ?? '',
|
|
959
|
+
body: (r.body ?? '').slice(0, COMMENT_BODY_CAP),
|
|
960
|
+
kind: 'review',
|
|
961
|
+
reviewState: REVIEW_STATE[r.state.toUpperCase()],
|
|
962
|
+
url: r.html_url,
|
|
963
|
+
}));
|
|
964
|
+
}
|
|
965
|
+
/** Merge comment/review lists chronologically (oldest first) and apply the entry cap. Exported
|
|
966
|
+
* for unit tests. */
|
|
967
|
+
export function mergeThread(parts, cap = THREAD_ENTRY_CAP) {
|
|
968
|
+
const all = parts.flat().sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
969
|
+
const truncated = all.length > cap;
|
|
970
|
+
return { comments: truncated ? all.slice(0, cap) : all, truncated };
|
|
971
|
+
}
|
|
972
|
+
// Per-thread cache: keyed `repoRoot␀kind#number` (the root scopes the key — two projects each
|
|
973
|
+
// having a PR #42 must not collide; step 2.6), same 60 s TTL as the list cache but BOUNDED — a
|
|
974
|
+
// long browsing session can't grow it without limit (Map preserves insertion order → oldest
|
|
975
|
+
// first).
|
|
976
|
+
const commentsCache = new Map();
|
|
977
|
+
const COMMENTS_CACHE_MAX = 50;
|
|
978
|
+
function cacheComments(key, data) {
|
|
979
|
+
commentsCache.delete(key); // re-insert so this key becomes the newest
|
|
980
|
+
commentsCache.set(key, { at: Date.now(), data });
|
|
981
|
+
while (commentsCache.size > COMMENTS_CACHE_MAX) {
|
|
982
|
+
const oldest = commentsCache.keys().next().value;
|
|
983
|
+
if (oldest === undefined)
|
|
984
|
+
break;
|
|
985
|
+
commentsCache.delete(oldest);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
/** Test-only: drop the per-thread cache so cases don't leak state into each other. */
|
|
989
|
+
export function __clearCommentsCacheForTests() {
|
|
990
|
+
commentsCache.clear();
|
|
991
|
+
}
|
|
992
|
+
// Single-item cache (#692): same key shape, TTL and LRU bound as `commentsCache`. Only a real
|
|
993
|
+
// answer (an item or a confirmed absence) is cached, never an unavailable one.
|
|
994
|
+
const itemCache = new Map();
|
|
995
|
+
function itemCacheKey(repoRoot, kind, number) {
|
|
996
|
+
return `${repoRoot}\0${kind}#${number}`;
|
|
997
|
+
}
|
|
998
|
+
export function forgetGithubItem(repoRoot, kind, number) {
|
|
999
|
+
itemCache.delete(itemCacheKey(repoRoot, kind, number));
|
|
1000
|
+
}
|
|
1001
|
+
/** Test-only: drop the single-item cache. */
|
|
1002
|
+
export function __clearGithubItemCacheForTests() {
|
|
1003
|
+
itemCache.clear();
|
|
1004
|
+
}
|
|
1005
|
+
export async function fetchGithubItem(repoRoot, kind, number, refresh = false) {
|
|
1006
|
+
if (productEnv('DRY_RUN') === '1') {
|
|
1007
|
+
const mock = mockGithub();
|
|
1008
|
+
const item = (kind === 'issue' ? mock.issues : mock.prs).find((i) => i.number === number);
|
|
1009
|
+
return item
|
|
1010
|
+
? { available: true, item, labelColors: mock.labelColors ?? {} }
|
|
1011
|
+
: { available: true, item: null };
|
|
1012
|
+
}
|
|
1013
|
+
const key = itemCacheKey(repoRoot, kind, number);
|
|
1014
|
+
const hit = itemCache.get(key);
|
|
1015
|
+
if (!refresh && hit && Date.now() - hit.at < CACHE_MS)
|
|
1016
|
+
return hit.data;
|
|
1017
|
+
try {
|
|
1018
|
+
const forge = await detectGithub(repoRoot);
|
|
1019
|
+
if (!forge.available)
|
|
1020
|
+
return { available: false, reason: forge.reason ?? 'GitHub is unavailable' };
|
|
1021
|
+
const labelColors = {};
|
|
1022
|
+
const item = await viewGithubItem(repoRoot, kind, number, labelColors);
|
|
1023
|
+
const data = {
|
|
1024
|
+
available: true, item, ...(item ? { labelColors } : {}),
|
|
1025
|
+
};
|
|
1026
|
+
itemCache.delete(key); // re-insert so this key becomes the newest
|
|
1027
|
+
itemCache.set(key, { at: Date.now(), data });
|
|
1028
|
+
while (itemCache.size > COMMENTS_CACHE_MAX) {
|
|
1029
|
+
const oldest = itemCache.keys().next().value;
|
|
1030
|
+
if (oldest === undefined)
|
|
1031
|
+
break;
|
|
1032
|
+
itemCache.delete(oldest);
|
|
1033
|
+
}
|
|
1034
|
+
return data;
|
|
1035
|
+
}
|
|
1036
|
+
catch (err) {
|
|
1037
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1038
|
+
return {
|
|
1039
|
+
available: false,
|
|
1040
|
+
reason: /ENOENT/.test(message)
|
|
1041
|
+
? 'gh CLI not found — install it and run `gh auth login`'
|
|
1042
|
+
: firstLine(message),
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
const repoHandleCache = new Map();
|
|
1047
|
+
const repoDiscoveryFlights = new Map();
|
|
1048
|
+
/** Each subscriber owns a lease, not the subprocess. Only the last cancellation aborts it.
|
|
1049
|
+
* Keep the rejecting promise intact so strict callers retain gh's diagnostic. */
|
|
1050
|
+
function sharedRepoHandle(repoRoot, signal, context = githubContext(repoRoot)) {
|
|
1051
|
+
if (signal?.aborted)
|
|
1052
|
+
return Promise.reject(signal.reason);
|
|
1053
|
+
const memo = repoHandleCache.get(context.key);
|
|
1054
|
+
if (memo !== undefined)
|
|
1055
|
+
return Promise.resolve(memo);
|
|
1056
|
+
let flight = repoDiscoveryFlights.get(context.key);
|
|
1057
|
+
if (!flight) {
|
|
1058
|
+
const controller = new AbortController();
|
|
1059
|
+
const created = {
|
|
1060
|
+
controller, owners: 0,
|
|
1061
|
+
promise: gh(repoRoot, ['repo', 'view', '--json', 'nameWithOwner', '--jq', '.nameWithOwner'], 15_000, controller.signal, context)
|
|
1062
|
+
.then(parseOwnerName),
|
|
1063
|
+
};
|
|
1064
|
+
created.promise = created.promise.then(handle => {
|
|
1065
|
+
// A disposed owner may complete late; it must not overwrite a replacement discovery.
|
|
1066
|
+
if (!controller.signal.aborted && repoDiscoveryFlights.get(context.key) === created) {
|
|
1067
|
+
repoHandleCache.set(context.key, handle);
|
|
1068
|
+
}
|
|
1069
|
+
return handle;
|
|
1070
|
+
}).finally(() => {
|
|
1071
|
+
if (repoDiscoveryFlights.get(context.key) === created)
|
|
1072
|
+
repoDiscoveryFlights.delete(context.key);
|
|
1073
|
+
});
|
|
1074
|
+
flight = created;
|
|
1075
|
+
repoDiscoveryFlights.set(context.key, flight);
|
|
1076
|
+
}
|
|
1077
|
+
const owned = flight;
|
|
1078
|
+
owned.owners++;
|
|
1079
|
+
return new Promise((resolve, reject) => {
|
|
1080
|
+
let released = false;
|
|
1081
|
+
const release = () => {
|
|
1082
|
+
if (released)
|
|
1083
|
+
return false;
|
|
1084
|
+
released = true;
|
|
1085
|
+
signal?.removeEventListener('abort', cancel);
|
|
1086
|
+
if (--owned.owners === 0 && repoDiscoveryFlights.get(context.key) === owned) {
|
|
1087
|
+
repoDiscoveryFlights.delete(context.key);
|
|
1088
|
+
owned.controller.abort();
|
|
1089
|
+
}
|
|
1090
|
+
return true;
|
|
1091
|
+
};
|
|
1092
|
+
const cancel = () => { if (release())
|
|
1093
|
+
reject(signal?.reason); };
|
|
1094
|
+
signal?.addEventListener('abort', cancel, { once: true });
|
|
1095
|
+
owned.promise.then(handle => { if (release())
|
|
1096
|
+
resolve(handle); }, error => { if (release())
|
|
1097
|
+
reject(error); });
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
/** Test-only: drop the memoized repo handles. */
|
|
1101
|
+
export function __clearRepoHandleCacheForTests() {
|
|
1102
|
+
repoHandleCache.clear();
|
|
1103
|
+
for (const flight of repoDiscoveryFlights.values())
|
|
1104
|
+
flight.controller.abort();
|
|
1105
|
+
repoDiscoveryFlights.clear();
|
|
1106
|
+
}
|
|
1107
|
+
/** Local absence cannot improve through network retries. Authentication, timeouts and remote
|
|
1108
|
+
* errors deliberately stay retryable, including 404s for temporarily inaccessible repositories. */
|
|
1109
|
+
function isPermanentRepoFailure(error) {
|
|
1110
|
+
if (!(error instanceof Error))
|
|
1111
|
+
return false;
|
|
1112
|
+
if ('code' in error && error.code === 'ENOENT')
|
|
1113
|
+
return true;
|
|
1114
|
+
const stderr = 'stderr' in error && typeof error.stderr === 'string' ? error.stderr : '';
|
|
1115
|
+
return /not a git repository|no git remotes found|none of the git remotes configured for this repository point to a known GitHub host/i.test(`${error.message}\n${stderr}`);
|
|
1116
|
+
}
|
|
1117
|
+
/** Share the existing memo with ordinary discovery, but retain failure classification (#102).
|
|
1118
|
+
* An aborted owner must never publish its late result into this shared cache. */
|
|
1119
|
+
export async function discoverRepoHandle(repoRoot, signal) {
|
|
1120
|
+
const context = githubContext(repoRoot);
|
|
1121
|
+
if (signal?.aborted)
|
|
1122
|
+
return { status: 'cancelled' };
|
|
1123
|
+
try {
|
|
1124
|
+
const handle = await sharedRepoHandle(repoRoot, signal, context);
|
|
1125
|
+
if (signal?.aborted)
|
|
1126
|
+
return { status: 'cancelled' };
|
|
1127
|
+
return handle ? { status: 'resolved', handle } : { status: 'unknown' };
|
|
1128
|
+
}
|
|
1129
|
+
catch (error) {
|
|
1130
|
+
if (signal?.aborted)
|
|
1131
|
+
return { status: 'cancelled' };
|
|
1132
|
+
if (isPermanentRepoFailure(error)) {
|
|
1133
|
+
// Do not replace a successful discovery another caller completed while this one failed.
|
|
1134
|
+
const recovered = repoHandleCache.get(context.key);
|
|
1135
|
+
if (recovered)
|
|
1136
|
+
return { status: 'resolved', handle: recovered };
|
|
1137
|
+
// The live arming loop stops on unknown. Do not memoize local failures here: later cold
|
|
1138
|
+
// index/status requests must still discover an installed gh or a newly added remote.
|
|
1139
|
+
return { status: 'unknown' };
|
|
1140
|
+
}
|
|
1141
|
+
return { status: 'retryable' };
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
/** Compatible fail-open lookup for existing API and cold-index callers. */
|
|
1145
|
+
export async function resolveRepoHandle(repoRoot, signal) {
|
|
1146
|
+
const result = await discoverRepoHandle(repoRoot, signal);
|
|
1147
|
+
return result.status === 'resolved' ? result.handle : null;
|
|
1148
|
+
}
|
|
1149
|
+
/** SHAs per rollup query. Aliases resolve independently, so a chunk that fails costs only its own
|
|
1150
|
+
* glyphs — but an unbounded alias list would eventually blow the query size limit. */
|
|
1151
|
+
export const COMMIT_CHECKS_CHUNK = 50;
|
|
1152
|
+
/** One aliased `object(oid:)` per SHA. `oid` requires a FULL 40-char SHA in both literal and
|
|
1153
|
+
* variable form (`Could not coerce value "babda63" to GitObjectID`), which constrains fixtures
|
|
1154
|
+
* rather than production data — the timeline always supplies full SHAs. */
|
|
1155
|
+
function commitChecksQuery(shas) {
|
|
1156
|
+
const aliases = shas
|
|
1157
|
+
.map((sha, i) => ` c${i}: object(oid: "${sha}") { ... on Commit { statusCheckRollup { state } } }`)
|
|
1158
|
+
.join('\n');
|
|
1159
|
+
return `query ($owner: String!, $name: String!) {\n repository(owner: $owner, name: $name) {\n${aliases}\n }\n}`;
|
|
1160
|
+
}
|
|
1161
|
+
const ghCommitChecksSchema = z.record(z.string(), z.object({ statusCheckRollup: z.object({ state: z.string().nullish() }).nullish() }).nullish());
|
|
1162
|
+
/**
|
|
1163
|
+
* Rolled-up CI state per commit SHA, as a `sha → checks` map.
|
|
1164
|
+
*
|
|
1165
|
+
* Batched and aliased so a 40-commit PR costs one subprocess, not forty. Verified against the live
|
|
1166
|
+
* API: each alias resolves independently and an unknown SHA comes back `null` rather than erroring
|
|
1167
|
+
* the batch, so partial results degrade cleanly.
|
|
1168
|
+
*
|
|
1169
|
+
* Degrades to an empty map on any failure — exactly as `fetchCommentCounts` does for counts. The
|
|
1170
|
+
* caller then leaves `checks` **absent**, which the UI renders as no glyph. Exported for tests;
|
|
1171
|
+
* `runGraphql` is injected so this is testable without shelling out.
|
|
1172
|
+
*/
|
|
1173
|
+
export async function fetchCommitChecks(runGraphql, owner, name, shas, chunkSize = COMMIT_CHECKS_CHUNK) {
|
|
1174
|
+
const out = {};
|
|
1175
|
+
if (shas.length === 0)
|
|
1176
|
+
return out;
|
|
1177
|
+
for (let i = 0; i < shas.length; i += chunkSize) {
|
|
1178
|
+
const chunk = shas.slice(i, i + chunkSize);
|
|
1179
|
+
try {
|
|
1180
|
+
const raw = JSON.parse(await runGraphql(commitChecksQuery(chunk), { owner, name }));
|
|
1181
|
+
const repository = ghCommitChecksSchema.parse(raw?.data?.repository ?? {});
|
|
1182
|
+
chunk.forEach((sha, index) => {
|
|
1183
|
+
const node = repository[`c${index}`];
|
|
1184
|
+
if (!node)
|
|
1185
|
+
return; // unknown SHA → alias resolved null; leave `checks` absent
|
|
1186
|
+
// Adapt the single rollup state into the array shape `rollupToChecks` expects, so the
|
|
1187
|
+
// existing FAILURE/PENDING/SUCCESS vocabulary is reused rather than duplicated.
|
|
1188
|
+
out[sha] = node.statusCheckRollup
|
|
1189
|
+
? rollupToChecks([{ state: node.statusCheckRollup.state, status: null, conclusion: null }])
|
|
1190
|
+
: null; // no CI configured — distinct from absent
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
catch {
|
|
1194
|
+
// A failed chunk costs only its own glyphs; the rest still resolve.
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
return out;
|
|
1198
|
+
}
|
|
1199
|
+
/** PR numbers per checks query. Aliases resolve independently (a failed chunk costs only its own
|
|
1200
|
+
* glyphs); bounded so an unbounded number list can't blow the query size limit. Also the route's
|
|
1201
|
+
* hard cap on how many PRs one request may ask about. */
|
|
1202
|
+
export const GH_CHECKS_MAX = 100;
|
|
1203
|
+
/** One aliased `pullRequest(number:)` per PR; the rolled-up CI state lives on the head commit. */
|
|
1204
|
+
function prChecksQuery(numbers) {
|
|
1205
|
+
const aliases = numbers
|
|
1206
|
+
.map((n, i) => ` p${i}: pullRequest(number: ${n}) { commits(last: 1) { nodes { commit { statusCheckRollup { state } } } } }`)
|
|
1207
|
+
.join('\n');
|
|
1208
|
+
return `query ($owner: String!, $name: String!) {\n repository(owner: $owner, name: $name) {\n${aliases}\n }\n}`;
|
|
1209
|
+
}
|
|
1210
|
+
const ghPrChecksSchema = z.record(z.string(), z
|
|
1211
|
+
.object({
|
|
1212
|
+
commits: z.object({
|
|
1213
|
+
nodes: z.array(z.object({
|
|
1214
|
+
commit: z.object({
|
|
1215
|
+
statusCheckRollup: z.object({ state: z.string().nullish() }).nullish(),
|
|
1216
|
+
}),
|
|
1217
|
+
})),
|
|
1218
|
+
}),
|
|
1219
|
+
})
|
|
1220
|
+
.nullish());
|
|
1221
|
+
/**
|
|
1222
|
+
* Rolled-up CI state per PR number, as a `number → glyph` map.
|
|
1223
|
+
*
|
|
1224
|
+
* Batched and aliased so a 100-PR window costs one subprocess, not a hundred. Each alias resolves
|
|
1225
|
+
* independently and an unknown number comes back `null` (alias resolved null → left absent), so
|
|
1226
|
+
* partial results degrade cleanly. Degrades to an empty map on any failure — exactly as
|
|
1227
|
+
* `fetchCommitChecks` does. Exported for tests; `runGraphql` is injected so this is testable
|
|
1228
|
+
* without shelling out.
|
|
1229
|
+
*/
|
|
1230
|
+
export async function fetchPrChecks(runGraphql, owner, name, numbers, chunkSize = GH_CHECKS_MAX) {
|
|
1231
|
+
const out = {};
|
|
1232
|
+
if (numbers.length === 0)
|
|
1233
|
+
return out;
|
|
1234
|
+
for (let i = 0; i < numbers.length; i += chunkSize) {
|
|
1235
|
+
const chunk = numbers.slice(i, i + chunkSize);
|
|
1236
|
+
try {
|
|
1237
|
+
const raw = JSON.parse(await runGraphql(prChecksQuery(chunk), { owner, name }));
|
|
1238
|
+
const repository = ghPrChecksSchema.parse(raw?.data?.repository ?? {});
|
|
1239
|
+
chunk.forEach((number, index) => {
|
|
1240
|
+
const node = repository[`p${index}`];
|
|
1241
|
+
if (!node)
|
|
1242
|
+
return; // unknown PR → alias resolved null; leave the glyph absent
|
|
1243
|
+
const rollup = node.commits.nodes[0]?.commit.statusCheckRollup;
|
|
1244
|
+
// Adapt the single rollup state into the array shape `rollupToChecks` expects, reusing the
|
|
1245
|
+
// FAILURE/PENDING/SUCCESS vocabulary rather than duplicating it.
|
|
1246
|
+
out[number] = rollup
|
|
1247
|
+
? rollupToChecks([{ state: rollup.state, status: null, conclusion: null }]) ?? null
|
|
1248
|
+
: null; // no CI configured — distinct from absent
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
catch {
|
|
1252
|
+
// A failed chunk costs only its own glyphs; the rest still resolve.
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
return out;
|
|
1256
|
+
}
|
|
1257
|
+
const itemDetailSchema = z.object({
|
|
1258
|
+
body: z.string().nullish(),
|
|
1259
|
+
additions: z.number().int().nonnegative().optional(),
|
|
1260
|
+
deletions: z.number().int().nonnegative().optional(),
|
|
1261
|
+
});
|
|
1262
|
+
/** One item's body (and a PR's diffstat), or `undefined` on any failure. */
|
|
1263
|
+
async function fetchItemDetail(repoRoot, kind, number) {
|
|
1264
|
+
try {
|
|
1265
|
+
const out = await gh(repoRoot, [kind, 'view', String(number), '--json', kind === 'pr' ? 'body,additions,deletions' : 'body']);
|
|
1266
|
+
const parsed = itemDetailSchema.parse(JSON.parse(out));
|
|
1267
|
+
return {
|
|
1268
|
+
body: (parsed.body ?? '').slice(0, COMMENT_BODY_CAP),
|
|
1269
|
+
...(kind === 'pr' && parsed.additions !== undefined ? { additions: parsed.additions } : {}),
|
|
1270
|
+
...(kind === 'pr' && parsed.deletions !== undefined ? { deletions: parsed.deletions } : {}),
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1273
|
+
catch {
|
|
1274
|
+
return undefined;
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
// Per-item comment counts (hearsay-tools/cezarion#1183): the list defers them, and the cockpit
|
|
1278
|
+
// asks for its loaded rows after paint. Keyed `context␀kind␀number`, same TTL and bound as checks.
|
|
1279
|
+
const commentCountsCache = new Map();
|
|
1280
|
+
const commentCountsSchema = z.object({
|
|
1281
|
+
data: z.object({
|
|
1282
|
+
repository: z.record(z.string(), z.object({ comments: z.object({ totalCount: z.number().int().nonnegative() }) }).nullable()),
|
|
1283
|
+
}),
|
|
1284
|
+
});
|
|
1285
|
+
/**
|
|
1286
|
+
* Comment counts for the given issue or PR numbers, in one aliased GraphQL query. A number GitHub
|
|
1287
|
+
* does not know is absent (unknown), never 0. Degrades to `{ available: false, reason }` — never a
|
|
1288
|
+
* throw. Numbers are de-duplicated, validated and capped at `GITHUB_COMMENT_COUNTS_MAX`.
|
|
1289
|
+
*/
|
|
1290
|
+
export async function fetchGithubCommentCounts(repoRoot, kind, numbers, refresh = false) {
|
|
1291
|
+
if (productEnv('DRY_RUN') === '1')
|
|
1292
|
+
return mockGithubCommentCounts(kind, numbers);
|
|
1293
|
+
const context = githubContext(repoRoot);
|
|
1294
|
+
const wanted = [...new Set(numbers)].filter((n) => Number.isSafeInteger(n) && n > 0).slice(0, GITHUB_COMMENT_COUNTS_MAX);
|
|
1295
|
+
const counts = {};
|
|
1296
|
+
const misses = [];
|
|
1297
|
+
const now = Date.now();
|
|
1298
|
+
const keyOf = (n) => `${context.key}\0${kind}\0${n}`;
|
|
1299
|
+
for (const n of wanted) {
|
|
1300
|
+
const hit = commentCountsCache.get(keyOf(n));
|
|
1301
|
+
if (!refresh && hit && now - hit.at < CACHE_MS)
|
|
1302
|
+
counts[n] = hit.count;
|
|
1303
|
+
else
|
|
1304
|
+
misses.push(n);
|
|
1305
|
+
}
|
|
1306
|
+
if (misses.length === 0)
|
|
1307
|
+
return { available: true, counts };
|
|
1308
|
+
try {
|
|
1309
|
+
const handle = await resolveRepoHandleStrict(repoRoot, context);
|
|
1310
|
+
if (!handle)
|
|
1311
|
+
return { available: false, reason: 'Could not resolve this repository on GitHub.' };
|
|
1312
|
+
// Integers only in the aliases; owner and name travel as variables.
|
|
1313
|
+
const fields = misses.map((n) => `n${n}: issueOrPullRequest(number: ${n}) {
|
|
1314
|
+
... on Issue { comments { totalCount } }
|
|
1315
|
+
... on PullRequest { comments { totalCount } }
|
|
1316
|
+
}`).join('\n');
|
|
1317
|
+
// A number GitHub does not know comes back NOT_FOUND and makes `gh` exit 1 with the rest of
|
|
1318
|
+
// the batch on stdout; `refStatusGraphql` keeps that partial answer.
|
|
1319
|
+
const raw = await refStatusGraphql(repoRoot, context)(`query($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { ${fields} } }`, { owner: handle.owner, name: handle.name });
|
|
1320
|
+
const rows = commentCountsSchema.parse(JSON.parse(raw)).data.repository;
|
|
1321
|
+
for (const n of misses) {
|
|
1322
|
+
const count = rows[`n${n}`]?.comments.totalCount;
|
|
1323
|
+
if (count === undefined)
|
|
1324
|
+
continue; // unknown number: absent, and asked again next time
|
|
1325
|
+
counts[n] = count;
|
|
1326
|
+
commentCountsCache.delete(keyOf(n));
|
|
1327
|
+
commentCountsCache.set(keyOf(n), { at: now, count });
|
|
1328
|
+
}
|
|
1329
|
+
while (commentCountsCache.size > CHECKS_CACHE_MAX) {
|
|
1330
|
+
const oldest = commentCountsCache.keys().next().value;
|
|
1331
|
+
if (oldest === undefined)
|
|
1332
|
+
break;
|
|
1333
|
+
commentCountsCache.delete(oldest);
|
|
1334
|
+
}
|
|
1335
|
+
return { available: true, counts };
|
|
1336
|
+
}
|
|
1337
|
+
catch (err) {
|
|
1338
|
+
return { available: false, reason: ghFailureReason(err) };
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
/** CEZ_DRY_RUN=1 — the catalog's own counts, so the offline list shows its badges. */
|
|
1342
|
+
function mockGithubCommentCounts(kind, numbers) {
|
|
1343
|
+
const rows = kind === 'pr' ? mockGithub().prs : mockGithub().issues;
|
|
1344
|
+
const counts = {};
|
|
1345
|
+
for (const n of numbers) {
|
|
1346
|
+
const row = rows.find((item) => item.number === n);
|
|
1347
|
+
if (row)
|
|
1348
|
+
counts[n] = row.comments;
|
|
1349
|
+
}
|
|
1350
|
+
return { available: true, counts };
|
|
1351
|
+
}
|
|
1352
|
+
// Per-PR checks cache: keyed `repoRoot␀number`, same 60 s TTL as the list cache but BOUNDED. Lets
|
|
1353
|
+
// a repeated visible-window hydration within a minute serve cached glyphs instead of re-querying.
|
|
1354
|
+
const checksCache = new Map();
|
|
1355
|
+
const CHECKS_CACHE_MAX = 500;
|
|
1356
|
+
/** Test hook — the per-PR checks cache would otherwise leak state across cases in one process. */
|
|
1357
|
+
export function __clearChecksCacheForTests() {
|
|
1358
|
+
checksCache.clear();
|
|
1359
|
+
}
|
|
1360
|
+
/**
|
|
1361
|
+
* Lazy checks glyphs for the given PR numbers (route-facing). Resolves the repo handle once,
|
|
1362
|
+
* serves fresh cache entries, queries only the misses, and degrades to `{ available: false,
|
|
1363
|
+
* reason }` when `gh` or the handle is unavailable — never a throw, never a 5xx (plan rule 7).
|
|
1364
|
+
* Numbers are de-duplicated, validated, and capped at `GH_CHECKS_MAX`.
|
|
1365
|
+
*/
|
|
1366
|
+
export async function fetchGithubChecks(repoRoot, numbers) {
|
|
1367
|
+
if (productEnv('DRY_RUN') === '1')
|
|
1368
|
+
return mockGithubChecks(numbers);
|
|
1369
|
+
const wanted = [...new Set(numbers)].filter((n) => Number.isInteger(n) && n > 0).slice(0, GH_CHECKS_MAX);
|
|
1370
|
+
const checks = {};
|
|
1371
|
+
const misses = [];
|
|
1372
|
+
const now = Date.now();
|
|
1373
|
+
for (const n of wanted) {
|
|
1374
|
+
const hit = checksCache.get(`${repoRoot}\0${n}`);
|
|
1375
|
+
if (hit && now - hit.at < CACHE_MS)
|
|
1376
|
+
checks[n] = hit.glyph;
|
|
1377
|
+
else
|
|
1378
|
+
misses.push(n);
|
|
1379
|
+
}
|
|
1380
|
+
if (misses.length === 0)
|
|
1381
|
+
return { available: true, checks };
|
|
1382
|
+
try {
|
|
1383
|
+
const repoOut = await gh(repoRoot, ['repo', 'view', '--json', 'nameWithOwner', '--jq', '.nameWithOwner']);
|
|
1384
|
+
const ownerName = parseOwnerName(repoOut);
|
|
1385
|
+
if (!ownerName)
|
|
1386
|
+
return { available: false, reason: 'repository handle unavailable' };
|
|
1387
|
+
const runGraphql = (query, variables) => {
|
|
1388
|
+
const args = ghGraphqlArgs(query, variables);
|
|
1389
|
+
return gh(repoRoot, args);
|
|
1390
|
+
};
|
|
1391
|
+
const fetched = await fetchPrChecks(runGraphql, ownerName.owner, ownerName.name, misses);
|
|
1392
|
+
for (const n of misses) {
|
|
1393
|
+
// Absent (unknown/closed PR) caches as `null` so a nonexistent number isn't re-queried.
|
|
1394
|
+
const glyph = n in fetched ? fetched[n] : null;
|
|
1395
|
+
checks[n] = glyph;
|
|
1396
|
+
checksCache.set(`${repoRoot}\0${n}`, { at: now, glyph });
|
|
1397
|
+
}
|
|
1398
|
+
while (checksCache.size > CHECKS_CACHE_MAX) {
|
|
1399
|
+
const oldest = checksCache.keys().next().value;
|
|
1400
|
+
if (oldest === undefined)
|
|
1401
|
+
break;
|
|
1402
|
+
checksCache.delete(oldest);
|
|
1403
|
+
}
|
|
1404
|
+
return { available: true, checks };
|
|
1405
|
+
}
|
|
1406
|
+
catch (err) {
|
|
1407
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1408
|
+
return {
|
|
1409
|
+
available: false,
|
|
1410
|
+
reason: /ENOENT/.test(message)
|
|
1411
|
+
? 'gh CLI not found — install it and run `gh auth login`'
|
|
1412
|
+
: firstLine(message),
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
/** CEZ_DRY_RUN=1 — glyphs straight from the mock catalog so the offline demo shows checks. */
|
|
1417
|
+
function mockGithubChecks(numbers) {
|
|
1418
|
+
const byNumber = new Map(mockGithub().prs.map((p) => [p.number, (p.checks ?? null)]));
|
|
1419
|
+
const checks = {};
|
|
1420
|
+
for (const n of numbers)
|
|
1421
|
+
checks[n] = byNumber.get(n) ?? null;
|
|
1422
|
+
return { available: true, checks };
|
|
1423
|
+
}
|
|
1424
|
+
/** Numbers per kind in one ref-status query — the same bound, and for the same reasons, as
|
|
1425
|
+
* `GH_CHECKS_MAX`: aliases resolve independently, and the query size stays finite. Taken from the
|
|
1426
|
+
* contract rather than restated, because the cockpit caps its batches by the same number and a
|
|
1427
|
+
* client that guessed high would have every chip in a batch 400 instead of losing its tail. */
|
|
1428
|
+
export const GH_REF_STATUS_MAX = REFERENCE_STATUS_MAX;
|
|
1429
|
+
/** A requested reviewer, as either connection spells them. `login` covers `User` and `Bot`, `slug`
|
|
1430
|
+
* covers `Team`; `reviewerKey` folds them into one comparable string. */
|
|
1431
|
+
const reviewerRefSchema = z
|
|
1432
|
+
.object({
|
|
1433
|
+
__typename: z.string().nullish(),
|
|
1434
|
+
login: z.string().nullish(),
|
|
1435
|
+
slug: z.string().nullish(),
|
|
1436
|
+
})
|
|
1437
|
+
.nullish();
|
|
1438
|
+
/** One reviewer as a comparable key, or `null` when GitHub named nobody we can match on — which
|
|
1439
|
+
* must never compare equal to another unnamed reviewer, hence `null` rather than a shared "". */
|
|
1440
|
+
function reviewerKey(ref) {
|
|
1441
|
+
const name = ref?.login ?? ref?.slug;
|
|
1442
|
+
return name ? `${ref?.__typename ?? '?'}:${name}` : null;
|
|
1443
|
+
}
|
|
1444
|
+
const ghRefStatusPrSchema = z
|
|
1445
|
+
.object({
|
|
1446
|
+
state: z.string(),
|
|
1447
|
+
url: z.unknown().optional(),
|
|
1448
|
+
mergedAt: z.unknown().optional(),
|
|
1449
|
+
isDraft: z.boolean().nullish(),
|
|
1450
|
+
reviewDecision: z.string().nullish(),
|
|
1451
|
+
/** `MERGEABLE` | `CONFLICTING` | `UNKNOWN` — GitHub computes it in the background, so
|
|
1452
|
+
* `UNKNOWN` is normal on a PR that was just pushed and must never read as "clean". */
|
|
1453
|
+
mergeable: z.string().nullish(),
|
|
1454
|
+
commits: z
|
|
1455
|
+
.object({
|
|
1456
|
+
nodes: z.array(z.object({
|
|
1457
|
+
commit: z.object({
|
|
1458
|
+
committedDate: z.string().nullish(),
|
|
1459
|
+
/** `totalCount` says whether the head is a merge; the first parent is the branch's
|
|
1460
|
+
* previous tip, which is the newest commit that is actual WORK when it is. */
|
|
1461
|
+
parents: z
|
|
1462
|
+
.object({
|
|
1463
|
+
totalCount: z.number(),
|
|
1464
|
+
nodes: z.array(z.object({ committedDate: z.string().nullish() }).nullish()).nullish(),
|
|
1465
|
+
})
|
|
1466
|
+
.nullish(),
|
|
1467
|
+
statusCheckRollup: z.object({ state: z.string().nullish() }).nullish(),
|
|
1468
|
+
}),
|
|
1469
|
+
})),
|
|
1470
|
+
})
|
|
1471
|
+
.nullish(),
|
|
1472
|
+
reviews: z.object({ nodes: z.array(z.object({ submittedAt: z.string().nullish() })) }).nullish(),
|
|
1473
|
+
/** Who is on the hook RIGHT NOW. The reviewers are carried, not just the count, because a
|
|
1474
|
+
* request's date has to be matched to a request that still stands — see `reviewRequestedAt`. */
|
|
1475
|
+
reviewRequests: z
|
|
1476
|
+
.object({
|
|
1477
|
+
totalCount: z.number(),
|
|
1478
|
+
nodes: z.array(z.object({ requestedReviewer: reviewerRefSchema }).nullish()).nullish(),
|
|
1479
|
+
})
|
|
1480
|
+
.nullish(),
|
|
1481
|
+
timelineItems: z
|
|
1482
|
+
.object({
|
|
1483
|
+
nodes: z
|
|
1484
|
+
.array(z.object({ createdAt: z.string().nullish(), requestedReviewer: reviewerRefSchema }).nullish())
|
|
1485
|
+
.nullish(),
|
|
1486
|
+
})
|
|
1487
|
+
.nullish(),
|
|
1488
|
+
})
|
|
1489
|
+
.nullish();
|
|
1490
|
+
/**
|
|
1491
|
+
* When was the newest STILL-STANDING review request made?
|
|
1492
|
+
*
|
|
1493
|
+
* The two connections answer different halves and neither answers both: `reviewRequests` says who
|
|
1494
|
+
* is on the hook right now but carries no date, while a `ReviewRequestedEvent` carries the date and
|
|
1495
|
+
* survives the request being withdrawn. Reading the newest event on its own therefore dates a
|
|
1496
|
+
* request that may no longer exist — and a request added after a review and then removed would make
|
|
1497
|
+
* a live rejection read as answered, which is the exact bug the precedence above exists to fix.
|
|
1498
|
+
* Matching the two by reviewer is what keeps the date attached to a request that is really there.
|
|
1499
|
+
*
|
|
1500
|
+
* `null` when nothing matches, which the precedence reads as an undated request and treats
|
|
1501
|
+
* conservatively: the review stands rather than being dismissed on a guess.
|
|
1502
|
+
*/
|
|
1503
|
+
function standingReviewRequestedAt(requests, timeline) {
|
|
1504
|
+
const standing = new Set();
|
|
1505
|
+
for (const node of requests?.nodes ?? []) {
|
|
1506
|
+
const key = reviewerKey(node?.requestedReviewer);
|
|
1507
|
+
if (key)
|
|
1508
|
+
standing.add(key);
|
|
1509
|
+
}
|
|
1510
|
+
if (standing.size === 0)
|
|
1511
|
+
return null;
|
|
1512
|
+
let newest = null;
|
|
1513
|
+
for (const node of timeline?.nodes ?? []) {
|
|
1514
|
+
if (!node?.createdAt)
|
|
1515
|
+
continue;
|
|
1516
|
+
const key = reviewerKey(node.requestedReviewer);
|
|
1517
|
+
if (!key || !standing.has(key))
|
|
1518
|
+
continue;
|
|
1519
|
+
if (!newest || isAfter(node.createdAt, newest))
|
|
1520
|
+
newest = node.createdAt;
|
|
1521
|
+
}
|
|
1522
|
+
return newest;
|
|
1523
|
+
}
|
|
1524
|
+
const ghRefStatusIssueSchema = z.object({ state: z.string(), stateReason: z.string().nullish() }).nullish();
|
|
1525
|
+
// The aliases hold two DIFFERENT shapes, so the record stays unvalidated here and each alias is
|
|
1526
|
+
// parsed by its own schema below. A union at this level would be wrong, not merely loose: zod
|
|
1527
|
+
// strips unknown keys, so a PR node matched by the issue arm would come back with `isDraft` and
|
|
1528
|
+
// `commits` silently removed.
|
|
1529
|
+
const ghRefStatusSchema = z.record(z.string(), z.unknown());
|
|
1530
|
+
/**
|
|
1531
|
+
* Query issueOrPullRequest once per number and use __typename for the actual kind. Include
|
|
1532
|
+
* branch-work and review timestamps for freshness decisions.
|
|
1533
|
+
*/
|
|
1534
|
+
function refStatusQuery(numbers) {
|
|
1535
|
+
const aliases = numbers
|
|
1536
|
+
.map((n, i) => ` r${i}: issueOrPullRequest(number: ${n}) { __typename ... on PullRequest { url mergedAt state isDraft reviewDecision mergeable commits(last: 1) { nodes { commit { committedDate parents(first: 1) { totalCount nodes { committedDate } } statusCheckRollup { state } } } } reviews(last: 1, states: CHANGES_REQUESTED) { nodes { submittedAt } } reviewRequests(first: 20) { totalCount nodes { requestedReviewer { __typename ... on User { login } ... on Bot { login } ... on Team { slug } } } } timelineItems(last: 20, itemTypes: [REVIEW_REQUESTED_EVENT]) { nodes { ... on ReviewRequestedEvent { createdAt requestedReviewer { __typename ... on User { login } ... on Bot { login } ... on Team { slug } } } } } } ... on Issue { state stateReason } }`)
|
|
1537
|
+
.join('\n');
|
|
1538
|
+
return `query ($owner: String!, $name: String!) {\n repository(owner: $owner, name: $name) {\nnameWithOwner\n${aliases}\n }\n}`;
|
|
1539
|
+
}
|
|
1540
|
+
/**
|
|
1541
|
+
* Derive whose action is pending: terminal, draft, pending checks, current requested changes,
|
|
1542
|
+
* failing checks, review, then ready. Use dated review/work evidence; mergeability remains a
|
|
1543
|
+
* separate axis.
|
|
1544
|
+
*/
|
|
1545
|
+
export function derivePrReferenceStatus(pr) {
|
|
1546
|
+
const state = pr.state.toUpperCase();
|
|
1547
|
+
if (state === 'MERGED')
|
|
1548
|
+
return 'merged';
|
|
1549
|
+
if (state === 'CLOSED')
|
|
1550
|
+
return 'closed';
|
|
1551
|
+
if (pr.isDraft)
|
|
1552
|
+
return 'draft';
|
|
1553
|
+
if (pr.checks === 'pending')
|
|
1554
|
+
return 'checks-pending';
|
|
1555
|
+
const decision = (pr.reviewDecision ?? '').toUpperCase();
|
|
1556
|
+
const changesRequested = decision === 'CHANGES_REQUESTED';
|
|
1557
|
+
// Has the author already answered the review — by asking for another look, or by pushing?
|
|
1558
|
+
// A standing request counts as the ask only if it POSTDATES the review; one made before it is a
|
|
1559
|
+
// reviewer who has not got to the PR yet, and on a PR where someone else rejected it that
|
|
1560
|
+
// request would otherwise mask the rejection indefinitely. With no review date at all it still
|
|
1561
|
+
// counts — that is the empty-`reviews` case above, where it is the only signal there is.
|
|
1562
|
+
const reRequested = pr.reviewRequested === true && (!pr.changesRequestedAt || isAfter(pr.reviewRequestedAt, pr.changesRequestedAt));
|
|
1563
|
+
// A push counts as the answer, judged by the newest commit that is actual WORK. GitHub's "Update
|
|
1564
|
+
// branch" button (and this cockpit's own "Resolve conflicts") writes `Merge branch 'main' into
|
|
1565
|
+
// <branch>` dated NOW, newer than any review while addressing none of it: the click people make
|
|
1566
|
+
// reflexively on a stale PR must not wipe a rejection off the chip. Two parents is what tells
|
|
1567
|
+
// that commit apart from work.
|
|
1568
|
+
//
|
|
1569
|
+
// Which is why a merge is not simply DISQUALIFYING: it is transparent. Reading only the head
|
|
1570
|
+
// would let a merge landing on top of a genuine fix erase that fix's answer and flip the chip
|
|
1571
|
+
// back to red, blaming an author who already responded — the same misattribution this whole
|
|
1572
|
+
// function exists to prevent, just pointed the other way. The merge's FIRST parent is the
|
|
1573
|
+
// branch's previous tip, so it carries the date of the work the merge sat on top of.
|
|
1574
|
+
const workCommittedAt = (pr.headParentCount ?? 1) < 2 ? pr.headCommittedAt : pr.headFirstParentCommittedAt;
|
|
1575
|
+
const pushed = isAfter(workCommittedAt, pr.changesRequestedAt);
|
|
1576
|
+
const answered = reRequested || pushed;
|
|
1577
|
+
if (changesRequested && !answered)
|
|
1578
|
+
return 'changes-requested';
|
|
1579
|
+
if (pr.checks === 'failing')
|
|
1580
|
+
return 'checks-failing';
|
|
1581
|
+
// `APPROVED` is the forge saying the review requirement IS MET, and it outranks a pending
|
|
1582
|
+
// request: a reviewer left on the list after someone else approved is a courtesy ask, not an
|
|
1583
|
+
// unmet gate. (A repo needing two approvals reports `REVIEW_REQUIRED` until it has both, so
|
|
1584
|
+
// `APPROVED` never arrives early.) Without this, an approved, green, mergeable pull request
|
|
1585
|
+
// reads "waiting for review" for as long as anyone stays listed — which is indefinitely.
|
|
1586
|
+
if (decision === 'APPROVED')
|
|
1587
|
+
return 'ready';
|
|
1588
|
+
if (changesRequested || decision === 'REVIEW_REQUIRED' || pr.reviewRequested === true) {
|
|
1589
|
+
return 'review-required';
|
|
1590
|
+
}
|
|
1591
|
+
return 'ready';
|
|
1592
|
+
}
|
|
1593
|
+
export function mergeabilityOf(state, mergeable) {
|
|
1594
|
+
if (state.toUpperCase() !== 'OPEN')
|
|
1595
|
+
return undefined;
|
|
1596
|
+
switch (mergeable?.toUpperCase()) {
|
|
1597
|
+
case 'MERGEABLE':
|
|
1598
|
+
return 'mergeable';
|
|
1599
|
+
case 'CONFLICTING':
|
|
1600
|
+
return 'conflicting';
|
|
1601
|
+
default:
|
|
1602
|
+
// Includes a field GitHub omitted entirely: not being told is not being told.
|
|
1603
|
+
return 'unknown';
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
/** Did `later` happen AFTER `earlier` — a commit, or a re-request, landing past the review?
|
|
1607
|
+
* Unparseable or missing dates answer `false`, which is the conservative direction for both
|
|
1608
|
+
* callers: they only ever use a `true` to demote a review, so no answer must keep the review
|
|
1609
|
+
* current rather than silently dismiss one. */
|
|
1610
|
+
function isAfter(later, earlier) {
|
|
1611
|
+
const a = later ? Date.parse(later) : NaN;
|
|
1612
|
+
const b = earlier ? Date.parse(earlier) : NaN;
|
|
1613
|
+
return Number.isFinite(a) && Number.isFinite(b) && a > b;
|
|
1614
|
+
}
|
|
1615
|
+
/**
|
|
1616
|
+
* An issue's two signals as one word. `NOT_PLANNED` is kept apart from `completed` because they
|
|
1617
|
+
* are opposite outcomes — "we did it" vs "we won't" — and a task whose issue was declined must
|
|
1618
|
+
* not read as a task that landed.
|
|
1619
|
+
*/
|
|
1620
|
+
export function deriveIssueReferenceStatus(issue) {
|
|
1621
|
+
if (issue.state.toUpperCase() !== 'CLOSED')
|
|
1622
|
+
return 'open';
|
|
1623
|
+
return (issue.stateReason ?? '').toUpperCase() === 'NOT_PLANNED' ? 'not-planned' : 'completed';
|
|
1624
|
+
}
|
|
1625
|
+
function refStatusGraphqlClean(raw, repository, owner, name) {
|
|
1626
|
+
return (!raw.errors || (Array.isArray(raw.errors) && raw.errors.length === 0)) &&
|
|
1627
|
+
typeof repository.nameWithOwner === 'string' && repository.nameWithOwner.toLowerCase() === `${owner}/${name}`.toLowerCase();
|
|
1628
|
+
}
|
|
1629
|
+
function prHeadCommit(pr) {
|
|
1630
|
+
return pr.commits?.nodes[0]?.commit;
|
|
1631
|
+
}
|
|
1632
|
+
function prRollupChecks(head) {
|
|
1633
|
+
const rollup = head?.statusCheckRollup;
|
|
1634
|
+
// Adapt the single rollup state into the array shape `rollupToChecks` expects,
|
|
1635
|
+
// reusing the FAILURE/PENDING/SUCCESS vocabulary rather than duplicating it.
|
|
1636
|
+
return rollup ? rollupToChecks([{ state: rollup.state, status: null, conclusion: null }]) ?? null : null;
|
|
1637
|
+
}
|
|
1638
|
+
function resolvePrReference(pr, raw, repository, owner, name, number) {
|
|
1639
|
+
if (refStatusGraphqlClean(raw, repository, owner, name)) {
|
|
1640
|
+
mergedPullRequestObservations.observe({ owner, repo: name, number }, { ...pr, number });
|
|
1641
|
+
}
|
|
1642
|
+
const head = prHeadCommit(pr);
|
|
1643
|
+
const mergeability = mergeabilityOf(pr.state, pr.mergeable);
|
|
1644
|
+
return {
|
|
1645
|
+
kind: 'pr',
|
|
1646
|
+
status: derivePrReferenceStatus({
|
|
1647
|
+
state: pr.state,
|
|
1648
|
+
isDraft: pr.isDraft,
|
|
1649
|
+
reviewDecision: pr.reviewDecision,
|
|
1650
|
+
checks: prRollupChecks(head),
|
|
1651
|
+
headCommittedAt: head?.committedDate,
|
|
1652
|
+
// Two parents = "Update branch", which is dated now and answers nothing on its own —
|
|
1653
|
+
// so the first parent's date, the work it sat on top of, is carried with it.
|
|
1654
|
+
headParentCount: head?.parents?.totalCount,
|
|
1655
|
+
headFirstParentCommittedAt: head?.parents?.nodes?.[0]?.committedDate,
|
|
1656
|
+
// `reviews(last: 1, states: CHANGES_REQUESTED)` — the timestamp only. WHETHER changes
|
|
1657
|
+
// are requested stays `reviewDecision`'s answer, which is the one that accounts for
|
|
1658
|
+
// dismissed and superseded reviews.
|
|
1659
|
+
changesRequestedAt: pr.reviews?.nodes[0]?.submittedAt,
|
|
1660
|
+
reviewRequested: (pr.reviewRequests?.totalCount ?? 0) > 0,
|
|
1661
|
+
// WHEN that standing request was made. `reviewRequests` carries no date of its own,
|
|
1662
|
+
// and without one an old request looks exactly like a re-request.
|
|
1663
|
+
reviewRequestedAt: standingReviewRequestedAt(pr.reviewRequests, pr.timelineItems),
|
|
1664
|
+
}),
|
|
1665
|
+
// The tri-state, not a boolean: `unknown` has to survive as far as the cache, which
|
|
1666
|
+
// is what decides to ask again in seconds rather than in a minute.
|
|
1667
|
+
...(mergeability ? { mergeable: mergeability } : {}),
|
|
1668
|
+
};
|
|
1669
|
+
}
|
|
1670
|
+
function applyRefStatusNode(out, node, raw, repository, owner, name, number) {
|
|
1671
|
+
// Absent alias → the number is not in this repository at all. Left out of the map, which
|
|
1672
|
+
// is what the route reports as "not found" and the chip paints as neutral.
|
|
1673
|
+
if (!node)
|
|
1674
|
+
return;
|
|
1675
|
+
if (node.__typename === 'PullRequest') {
|
|
1676
|
+
const pr = ghRefStatusPrSchema.parse(node);
|
|
1677
|
+
if (!pr)
|
|
1678
|
+
return;
|
|
1679
|
+
out.resolved[number] = resolvePrReference(pr, raw, repository, owner, name, number);
|
|
1680
|
+
return;
|
|
1681
|
+
}
|
|
1682
|
+
if (node.__typename === 'Issue') {
|
|
1683
|
+
const issue = ghRefStatusIssueSchema.parse(node);
|
|
1684
|
+
if (!issue)
|
|
1685
|
+
return;
|
|
1686
|
+
out.resolved[number] = { kind: 'issue', status: deriveIssueReferenceStatus(issue) };
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
async function fetchRefStatusChunk(runGraphql, owner, name, chunk, out) {
|
|
1690
|
+
try {
|
|
1691
|
+
const raw = JSON.parse(await runGraphql(refStatusQuery(chunk), { owner, name }));
|
|
1692
|
+
const repository = ghRefStatusSchema.parse(raw?.data?.repository ?? {});
|
|
1693
|
+
chunk.forEach((number, index) => {
|
|
1694
|
+
applyRefStatusNode(out, repository[`r${index}`], raw, repository, owner, name, number);
|
|
1695
|
+
});
|
|
1696
|
+
}
|
|
1697
|
+
catch (err) {
|
|
1698
|
+
// A failed chunk costs only its own numbers; the rest still resolve. They are recorded as
|
|
1699
|
+
// FAILED rather than left absent, so nothing downstream mistakes them for "no such number".
|
|
1700
|
+
out.failed.push(...chunk);
|
|
1701
|
+
out.reason ??= firstLine(err instanceof Error ? err.message : String(err));
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
/**
|
|
1705
|
+
* Status per NUMBER, resolved to whatever that number actually is.
|
|
1706
|
+
*
|
|
1707
|
+
* Batched and aliased like `fetchPrChecks`: one subprocess per chunk, each alias resolving
|
|
1708
|
+
* independently, and a number the forge does not have simply staying absent from `resolved` (its
|
|
1709
|
+
* chip then renders neutral, as it did before this seam existed). A failed chunk costs only its
|
|
1710
|
+
* own numbers, and says so rather than letting them look absent. Exported for tests; `runGraphql`
|
|
1711
|
+
* is injected so this is testable without shelling out.
|
|
1712
|
+
*/
|
|
1713
|
+
export async function fetchRefStatuses(runGraphql, owner, name, numbers, chunkSize = GH_REF_STATUS_MAX) {
|
|
1714
|
+
const out = { resolved: {}, failed: [] };
|
|
1715
|
+
if (numbers.length === 0)
|
|
1716
|
+
return out;
|
|
1717
|
+
for (let i = 0; i < numbers.length; i += chunkSize) {
|
|
1718
|
+
await fetchRefStatusChunk(runGraphql, owner, name, numbers.slice(i, i + chunkSize), out);
|
|
1719
|
+
}
|
|
1720
|
+
return out;
|
|
1721
|
+
}
|
|
1722
|
+
// Per-reference cache: keyed `repoRoot␀number` — by NUMBER, not by kind, because the kind is now
|
|
1723
|
+
// something the forge answers rather than something the caller asserts. Same 60 s TTL and bounded
|
|
1724
|
+
// shape as the checks cache; `null` is a cached "this repository has no such number", so a
|
|
1725
|
+
// transcript-scraped number from another repo is not re-queried on every table repaint.
|
|
1726
|
+
//
|
|
1727
|
+
// `unknownSince` is when this reference FIRST came back with its mergeability still being
|
|
1728
|
+
// computed, carried across refreshes so the fast recheck below is bounded to that first window
|
|
1729
|
+
// rather than restarting on every answer that is still `unknown`.
|
|
1730
|
+
const refStatusCache = new Map();
|
|
1731
|
+
const REF_STATUS_CACHE_MAX = 500;
|
|
1732
|
+
/** Test-only: drop the per-reference cache so cases don't leak state into each other. */
|
|
1733
|
+
export function __clearRefStatusCacheForTests() {
|
|
1734
|
+
refStatusCache.clear();
|
|
1735
|
+
refStatusFlights.clear();
|
|
1736
|
+
}
|
|
1737
|
+
/** Test-only: warm the cache the way the lazy route would have, so a reader can be tested
|
|
1738
|
+
* without a forge behind it. */
|
|
1739
|
+
export function __seedRefStatusCacheForTests(repoRoot, entries) {
|
|
1740
|
+
for (const [number, resolved] of entries) {
|
|
1741
|
+
refStatusCache.set(refStatusKey(repoRoot, number), { at: Date.now(), resolved });
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
/**
|
|
1745
|
+
* Invalidate one reference after a known mutation. Let the next forge read establish status
|
|
1746
|
+
* rather than caching a guessed result.
|
|
1747
|
+
*/
|
|
1748
|
+
export function forgetRefStatus(repoRoot, number) {
|
|
1749
|
+
refStatusCache.delete(refStatusKey(repoRoot, number));
|
|
1750
|
+
refStatusFlights.delete(refStatusKey(repoRoot, number));
|
|
1751
|
+
}
|
|
1752
|
+
/**
|
|
1753
|
+
* Everything the cache ALREADY knows about these numbers. Never spawns `gh`, never awaits.
|
|
1754
|
+
*
|
|
1755
|
+
* This is what lets a status ride along with the rows that carry the references, instead of the
|
|
1756
|
+
* cockpit fetching it separately a moment later: the run index reads whatever is warm and ships
|
|
1757
|
+
* it, and a cold entry is simply absent — the lazy `/github/ref-status` route stays the thing that
|
|
1758
|
+
* actually goes and asks.
|
|
1759
|
+
*
|
|
1760
|
+
* Because it cannot cost anything, the caller may pass a SUPERSET of the numbers it will really
|
|
1761
|
+
* display. That matters: deciding which of a run's references a chip shows is the cockpit's rule
|
|
1762
|
+
* (#407, #526), deliberately not duplicated server-side, and a cache read does not need to know —
|
|
1763
|
+
* it can look up every number a run mentions and let the client pick.
|
|
1764
|
+
*/
|
|
1765
|
+
export function readCachedRefStatuses(repoRoot, numbers) {
|
|
1766
|
+
const out = { prs: {}, issues: {} };
|
|
1767
|
+
const now = Date.now();
|
|
1768
|
+
for (const number of new Set(numbers)) {
|
|
1769
|
+
const hit = refStatusCache.get(refStatusKey(repoRoot, number));
|
|
1770
|
+
if (!hit || !hit.resolved || now - hit.at >= refStatusTtl(hit.resolved, hit.unknownSince, now))
|
|
1771
|
+
continue;
|
|
1772
|
+
out[hit.resolved.kind === 'pr' ? 'prs' : 'issues'][number] = hit.resolved.status;
|
|
1773
|
+
}
|
|
1774
|
+
return out;
|
|
1775
|
+
}
|
|
1776
|
+
// The `#N` in a forge URL. Defined beside the run search matcher, which reads it too (#864).
|
|
1777
|
+
export { refNumberFromUrl } from '../../contract/index.js';
|
|
1778
|
+
/** A closed issue or an abandoned PR can be REOPENED, so this is long rather than forever — but
|
|
1779
|
+
* it is the rare event, and re-asking a hundred settled references every minute to catch it is
|
|
1780
|
+
* the wrong trade. */
|
|
1781
|
+
const REF_STATUS_CLOSED_TTL = 10 * 60_000;
|
|
1782
|
+
/** A merged pull request is merged forever: GitHub has no un-merge. Capped at a day only so a
|
|
1783
|
+
* long-lived server eventually re-reads rather than trusting a value from another era. */
|
|
1784
|
+
const REF_STATUS_MERGED_TTL = 24 * 60 * 60_000;
|
|
1785
|
+
/**
|
|
1786
|
+
* How long a cached answer stays fresh — by how changeable that answer IS.
|
|
1787
|
+
*
|
|
1788
|
+
* One TTL for everything gets both ends wrong: it re-asks about a merged PR (which cannot change)
|
|
1789
|
+
* every minute, and it is the only thing standing between a running CI job and a stale chip. What
|
|
1790
|
+
* a reader wants rechecked is precisely what is still moving.
|
|
1791
|
+
*
|
|
1792
|
+
* A number the repository does not have keeps the short TTL: it is usually a wrong number, but it
|
|
1793
|
+
* is also what a reference to a not-yet-created PR looks like, and re-asking is cheap.
|
|
1794
|
+
*/
|
|
1795
|
+
function refStatusTtl(entry, unknownSince, now = Date.now()) {
|
|
1796
|
+
if (!entry)
|
|
1797
|
+
return CACHE_MS;
|
|
1798
|
+
// Mergeability GitHub has not finished computing is not an answer to cache for a minute. It is
|
|
1799
|
+
// the normal reply for the first seconds after a push, and holding it that long is what let a
|
|
1800
|
+
// conflicting pull request read "Ready to merge" until the page was reloaded. Ask again in
|
|
1801
|
+
// seconds instead — and only while it is still plausibly being computed, so a repository that
|
|
1802
|
+
// answers `UNKNOWN` indefinitely settles back to the ordinary cadence rather than spawning `gh`
|
|
1803
|
+
// every few seconds forever.
|
|
1804
|
+
if (entry.mergeable === 'unknown' &&
|
|
1805
|
+
unknownSince !== undefined &&
|
|
1806
|
+
now - unknownSince < MERGEABILITY_UNKNOWN_WINDOW_MS) {
|
|
1807
|
+
return MERGEABILITY_UNKNOWN_TTL_MS;
|
|
1808
|
+
}
|
|
1809
|
+
switch (entry.status) {
|
|
1810
|
+
case 'merged':
|
|
1811
|
+
return REF_STATUS_MERGED_TTL;
|
|
1812
|
+
case 'closed':
|
|
1813
|
+
case 'completed':
|
|
1814
|
+
case 'not-planned':
|
|
1815
|
+
return REF_STATUS_CLOSED_TTL;
|
|
1816
|
+
default:
|
|
1817
|
+
return CACHE_MS;
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
/** How long a status can be trusted to stay put — `null` when it can never change again. The
|
|
1821
|
+
* cadence half of `refStatusTtl`, and deliberately the same function: a value the cache would
|
|
1822
|
+
* still be serving is a value there is no point asking for, and a value it would NOT serve —
|
|
1823
|
+
* mergeability still being computed — is one the cockpit should come back for just as soon. */
|
|
1824
|
+
function refStatusRecheckAfter(entry, unknownSince, now = Date.now()) {
|
|
1825
|
+
if (entry?.status === 'merged')
|
|
1826
|
+
return null; // GitHub has no un-merge
|
|
1827
|
+
return refStatusTtl(entry, unknownSince, now);
|
|
1828
|
+
}
|
|
1829
|
+
/** How long the WHOLE answer holds — the soonest any single reference in it could differ. `null`
|
|
1830
|
+
* only when every one of them is immutable, which is what tells the cockpit to stop scheduling.
|
|
1831
|
+
* Taking the per-reference values rather than the entries, because one of them may be on the fast
|
|
1832
|
+
* mergeability cadence and the batch has to travel at the speed of its most impatient member. */
|
|
1833
|
+
function batchRecheckAfter(rechecks) {
|
|
1834
|
+
let soonest = null;
|
|
1835
|
+
for (const after of rechecks) {
|
|
1836
|
+
if (after === null)
|
|
1837
|
+
continue;
|
|
1838
|
+
soonest = soonest === null ? after : Math.min(soonest, after);
|
|
1839
|
+
}
|
|
1840
|
+
return soonest;
|
|
1841
|
+
}
|
|
1842
|
+
/**
|
|
1843
|
+
* How long a still-computing mergeability holds, and for how long that fast cadence applies.
|
|
1844
|
+
*
|
|
1845
|
+
* Five seconds because that is the shape of the thing being waited for: GitHub kicks off the
|
|
1846
|
+
* merge-base computation when asked and usually has it by the next request. Bounded to a minute
|
|
1847
|
+
* because a value that is STILL unknown after that is not a computation in flight any more — it is
|
|
1848
|
+
* a repository that will not answer, and re-asking it every five seconds forever costs a `gh`
|
|
1849
|
+
* subprocess a second for nothing.
|
|
1850
|
+
*/
|
|
1851
|
+
const MERGEABILITY_UNKNOWN_TTL_MS = 5_000;
|
|
1852
|
+
const MERGEABILITY_UNKNOWN_WINDOW_MS = 60_000;
|
|
1853
|
+
/** A forge that could not be reached is worth retrying, and worth not hammering: a workspace with
|
|
1854
|
+
* no `gh` installed would otherwise spawn a subprocess a minute, forever, to be told the same
|
|
1855
|
+
* thing. Five minutes is the same order as the cockpit's own reconnect cadence. */
|
|
1856
|
+
const REF_STATUS_RETRY_MS = 5 * 60_000;
|
|
1857
|
+
/**
|
|
1858
|
+
* Use failed gh GraphQL stdout only when it contains usable data.repository. Preserve successful
|
|
1859
|
+
* aliases in partial responses; rethrow malformed or unresolved-repository failures.
|
|
1860
|
+
*/
|
|
1861
|
+
function refStatusGraphql(repoRoot, context = githubContext(repoRoot)) {
|
|
1862
|
+
return async (query, variables) => {
|
|
1863
|
+
const args = ghGraphqlArgs(query, variables);
|
|
1864
|
+
try {
|
|
1865
|
+
return await gh(repoRoot, args, 15_000, undefined, context);
|
|
1866
|
+
}
|
|
1867
|
+
catch (err) {
|
|
1868
|
+
const stdout = err.stdout;
|
|
1869
|
+
if (typeof stdout === 'string' && hasResolvedRepository(stdout))
|
|
1870
|
+
return stdout;
|
|
1871
|
+
throw err;
|
|
1872
|
+
}
|
|
1873
|
+
};
|
|
1874
|
+
}
|
|
1875
|
+
/**
|
|
1876
|
+
* The repo handle, memoized in the same map `resolveRepoHandle` uses — but rethrowing instead of
|
|
1877
|
+
* answering `null`.
|
|
1878
|
+
*
|
|
1879
|
+
* The memo is the point: without it every recheck tick spawned a second subprocess to re-learn an
|
|
1880
|
+
* `owner/name` that changes approximately never, doubling the cost of the one query that actually
|
|
1881
|
+
* carries information. The difference from `resolveRepoHandle` is the failure: this route turns a
|
|
1882
|
+
* `gh` error into the payload's `reason` — "gh CLI not found — install it and run `gh auth login`"
|
|
1883
|
+
* is what the chip's tooltip now shows a user — and a `null` would erase which failure it was.
|
|
1884
|
+
*/
|
|
1885
|
+
async function resolveRepoHandleStrict(repoRoot, context = githubContext(repoRoot)) {
|
|
1886
|
+
return sharedRepoHandle(repoRoot, undefined, context);
|
|
1887
|
+
}
|
|
1888
|
+
/** Is this a reply we can still read aliases out of? `repository: null` says the handle did not
|
|
1889
|
+
* resolve, which is a real failure and must not be read as "the numbers are all missing". */
|
|
1890
|
+
function hasResolvedRepository(stdout) {
|
|
1891
|
+
try {
|
|
1892
|
+
const parsed = JSON.parse(stdout);
|
|
1893
|
+
const repository = parsed?.data?.repository;
|
|
1894
|
+
return typeof repository === 'object' && repository !== null;
|
|
1895
|
+
}
|
|
1896
|
+
catch {
|
|
1897
|
+
return false;
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
// Ownership is per reference, before discovery starts. Overlapping requests join just the
|
|
1901
|
+
// numbers they need; extra numbers form their own batch without a batching timer or new delay.
|
|
1902
|
+
// At capacity, untracked work answers its callers without caching: it has no ownership token
|
|
1903
|
+
// to protect publication against invalidation or newer requests. Retained ownership is at most 500.
|
|
1904
|
+
const refStatusFlights = new Map();
|
|
1905
|
+
async function sharedRefStatusMisses(repoRoot, numbers, context) {
|
|
1906
|
+
const pending = new Map();
|
|
1907
|
+
const owned = [];
|
|
1908
|
+
for (const n of numbers) {
|
|
1909
|
+
const flight = refStatusFlights.get(refStatusKey(repoRoot, n, context));
|
|
1910
|
+
if (flight)
|
|
1911
|
+
pending.set(n, flight);
|
|
1912
|
+
else
|
|
1913
|
+
owned.push(n);
|
|
1914
|
+
}
|
|
1915
|
+
if (owned.length) {
|
|
1916
|
+
const batch = (async () => {
|
|
1917
|
+
const handle = await resolveRepoHandleStrict(repoRoot, context);
|
|
1918
|
+
if (!handle)
|
|
1919
|
+
throw new Error('repository handle unavailable');
|
|
1920
|
+
return fetchRefStatuses(refStatusGraphql(repoRoot, context), handle.owner, handle.name, owned);
|
|
1921
|
+
})();
|
|
1922
|
+
for (const n of owned) {
|
|
1923
|
+
const key = refStatusKey(repoRoot, n, context);
|
|
1924
|
+
const tracked = refStatusFlights.size < REF_STATUS_CACHE_MAX;
|
|
1925
|
+
const promise = batch.then(result => {
|
|
1926
|
+
if (result.failed.includes(n))
|
|
1927
|
+
return { reason: result.reason ?? 'GitHub could not be reached' };
|
|
1928
|
+
// Date fresh results at arrival; keep the original unknown-mergeability retry window.
|
|
1929
|
+
const storedAt = Date.now();
|
|
1930
|
+
const entry = result.resolved[n] ?? null;
|
|
1931
|
+
const unknownSince = entry?.mergeable === 'unknown'
|
|
1932
|
+
? (refStatusCache.get(key)?.unknownSince ?? storedAt) : undefined;
|
|
1933
|
+
const value = { resolved: entry, ...(unknownSince === undefined ? {} : { unknownSince }) };
|
|
1934
|
+
if (tracked && refStatusFlights.get(key) === promise) {
|
|
1935
|
+
refStatusCache.set(key, { at: storedAt, ...value });
|
|
1936
|
+
while (refStatusCache.size > REF_STATUS_CACHE_MAX)
|
|
1937
|
+
refStatusCache.delete(refStatusCache.keys().next().value);
|
|
1938
|
+
}
|
|
1939
|
+
return value;
|
|
1940
|
+
}).finally(() => {
|
|
1941
|
+
if (refStatusFlights.get(key) === promise)
|
|
1942
|
+
refStatusFlights.delete(key);
|
|
1943
|
+
});
|
|
1944
|
+
if (tracked)
|
|
1945
|
+
refStatusFlights.set(key, promise);
|
|
1946
|
+
pending.set(n, promise);
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
return Promise.all(numbers.map(async (n) => [n, await pending.get(n)]));
|
|
1950
|
+
}
|
|
1951
|
+
/**
|
|
1952
|
+
* Route-facing reference status. Resolves the repo handle once, serves fresh cache entries,
|
|
1953
|
+
* queries only the misses, and degrades to `{ available: false, reason }` when `gh` or the handle
|
|
1954
|
+
* is unavailable — never a throw, never a 5xx.
|
|
1955
|
+
*
|
|
1956
|
+
* The two request lists are merged into ONE set of numbers: issues and pull requests share a
|
|
1957
|
+
* repository's numbering space, so a number is one thing and asking about it twice would be asking
|
|
1958
|
+
* the same question twice. What comes back is filed by what each number turned out to BE, which is
|
|
1959
|
+
* why a chip whose kind the cockpit guessed wrong still gets the right status.
|
|
1960
|
+
*/
|
|
1961
|
+
export async function fetchGithubRefStatus(repoRoot, input) {
|
|
1962
|
+
const context = githubContext(repoRoot);
|
|
1963
|
+
const asPrs = sanitizeRefNumbers(input.prs);
|
|
1964
|
+
const asIssues = sanitizeRefNumbers(input.issues);
|
|
1965
|
+
if (productEnv('DRY_RUN') === '1')
|
|
1966
|
+
return mockGithubRefStatus(asPrs, asIssues);
|
|
1967
|
+
const wanted = [...new Set([...asPrs, ...asIssues])];
|
|
1968
|
+
const resolved = { prs: {}, issues: {} };
|
|
1969
|
+
// How soon each reference in the answer could differ — what the batch's cadence is the minimum
|
|
1970
|
+
// of. Per reference rather than per batch because mergeability still being computed is worth
|
|
1971
|
+
// coming back for in seconds while everything else holds for a minute.
|
|
1972
|
+
const rechecks = [];
|
|
1973
|
+
// The second axis, and a list rather than a map because it is nearly always empty: only the
|
|
1974
|
+
// pull requests the forge actively called CONFLICTING are named (see `mergeabilityOf`).
|
|
1975
|
+
const conflicts = [];
|
|
1976
|
+
const file = (number, entry, unknownSince) => {
|
|
1977
|
+
rechecks.push(refStatusRecheckAfter(entry, unknownSince));
|
|
1978
|
+
if (!entry)
|
|
1979
|
+
return;
|
|
1980
|
+
resolved[entry.kind === 'pr' ? 'prs' : 'issues'][number] = entry.status;
|
|
1981
|
+
if (entry.mergeable === 'conflicting')
|
|
1982
|
+
conflicts.push(number);
|
|
1983
|
+
};
|
|
1984
|
+
const misses = [];
|
|
1985
|
+
const now = Date.now();
|
|
1986
|
+
for (const n of wanted) {
|
|
1987
|
+
const hit = refStatusCache.get(refStatusKey(repoRoot, n, context));
|
|
1988
|
+
if (!hit || now - hit.at >= refStatusTtl(hit.resolved, hit.unknownSince, now))
|
|
1989
|
+
misses.push(n);
|
|
1990
|
+
else
|
|
1991
|
+
file(n, hit.resolved, hit.unknownSince);
|
|
1992
|
+
}
|
|
1993
|
+
if (misses.length === 0) {
|
|
1994
|
+
return {
|
|
1995
|
+
available: true,
|
|
1996
|
+
prs: resolved.prs,
|
|
1997
|
+
issues: resolved.issues,
|
|
1998
|
+
conflicts,
|
|
1999
|
+
recheckAfterMs: batchRecheckAfter(rechecks),
|
|
2000
|
+
};
|
|
2001
|
+
}
|
|
2002
|
+
try {
|
|
2003
|
+
const entries = await sharedRefStatusMisses(repoRoot, misses, context);
|
|
2004
|
+
let failure;
|
|
2005
|
+
for (const [n, result] of entries) {
|
|
2006
|
+
if ('reason' in result)
|
|
2007
|
+
failure ??= result.reason;
|
|
2008
|
+
else
|
|
2009
|
+
file(n, result.resolved, result.unknownSince);
|
|
2010
|
+
}
|
|
2011
|
+
// Anything unasked makes the whole answer `unavailable`, deliberately. The alternative is a
|
|
2012
|
+
// payload where a number we could not reach is indistinguishable from one that does not exist,
|
|
2013
|
+
// and the cockpit would paint "not found on this repository" over a perfectly good PR — the
|
|
2014
|
+
// exact defect this route was reported for. The successes are cached either way, so the next
|
|
2015
|
+
// request costs only the numbers that failed and usually answers in full.
|
|
2016
|
+
if (failure !== undefined) {
|
|
2017
|
+
return {
|
|
2018
|
+
available: false,
|
|
2019
|
+
reason: failure,
|
|
2020
|
+
recheckAfterMs: REF_STATUS_RETRY_MS,
|
|
2021
|
+
};
|
|
2022
|
+
}
|
|
2023
|
+
return {
|
|
2024
|
+
available: true,
|
|
2025
|
+
prs: resolved.prs,
|
|
2026
|
+
issues: resolved.issues,
|
|
2027
|
+
conflicts,
|
|
2028
|
+
recheckAfterMs: batchRecheckAfter(rechecks),
|
|
2029
|
+
};
|
|
2030
|
+
}
|
|
2031
|
+
catch (err) {
|
|
2032
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2033
|
+
return {
|
|
2034
|
+
available: false,
|
|
2035
|
+
reason: /ENOENT/.test(message)
|
|
2036
|
+
? 'gh CLI not found — install it and run `gh auth login`'
|
|
2037
|
+
: firstLine(message),
|
|
2038
|
+
recheckAfterMs: REF_STATUS_RETRY_MS,
|
|
2039
|
+
};
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
/** NUL separator, as everywhere else here: two projects each having a #42 must not collide. */
|
|
2043
|
+
function refStatusKey(repoRoot, number, context = githubContext(repoRoot)) {
|
|
2044
|
+
return `${context.key}\0#${number}`;
|
|
2045
|
+
}
|
|
2046
|
+
function sanitizeRefNumbers(numbers) {
|
|
2047
|
+
return [...new Set(numbers ?? [])].filter((n) => Number.isInteger(n) && n > 0).slice(0, GH_REF_STATUS_MAX);
|
|
2048
|
+
}
|
|
2049
|
+
/** CEZ_DRY_RUN=1 — statuses derived from the mock catalog so the offline demo paints real chips. */
|
|
2050
|
+
function mockGithubRefStatus(prs, issues) {
|
|
2051
|
+
const catalog = mockGithub();
|
|
2052
|
+
const byPr = new Map(catalog.prs.map((p) => [p.number, p]));
|
|
2053
|
+
const byIssue = new Set(catalog.issues.map((i) => i.number));
|
|
2054
|
+
const out = { available: true, prs: {}, issues: {}, recheckAfterMs: CACHE_MS };
|
|
2055
|
+
for (const n of prs) {
|
|
2056
|
+
const pr = byPr.get(n);
|
|
2057
|
+
if (pr) {
|
|
2058
|
+
out.prs[n] = derivePrReferenceStatus({
|
|
2059
|
+
state: 'OPEN',
|
|
2060
|
+
isDraft: pr.isDraft,
|
|
2061
|
+
reviewDecision: null,
|
|
2062
|
+
checks: (pr.checks ?? null),
|
|
2063
|
+
});
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
for (const n of issues)
|
|
2067
|
+
if (byIssue.has(n))
|
|
2068
|
+
out.issues[n] = 'open';
|
|
2069
|
+
return out;
|
|
2070
|
+
}
|
|
2071
|
+
/**
|
|
2072
|
+
* The conversation thread for one issue/PR, lazily. `{owner}`/`{repo}` in the gh api paths are
|
|
2073
|
+
* filled from the worktree's remote by gh itself, so no extra handle lookup. Everything degrades:
|
|
2074
|
+
* gh missing/offline → `{ available: false, reason }`, a 404 → a "not found" hint — never a throw.
|
|
2075
|
+
*
|
|
2076
|
+
* Since #525 the thread is sourced from `/issues/{n}/timeline`, which returns comments AND events
|
|
2077
|
+
* in one stream: `commented` rows go through the unchanged `normalizeComments`, the rest through
|
|
2078
|
+
* `normalizeEvents`. `comments[]` therefore keeps its exact pre-#525 shape, contents and cap
|
|
2079
|
+
* (BACKWARD_COMPATIBILITY.md) — see the top-up below for the one case that needed defending.
|
|
2080
|
+
*/
|
|
2081
|
+
function commentsCacheKey(repoRoot, kind, number) {
|
|
2082
|
+
// NUL separator: cannot appear in a filesystem path, so roots can never alias.
|
|
2083
|
+
return `${repoRoot}\0${kind}#${number}`;
|
|
2084
|
+
}
|
|
2085
|
+
function cachedGithubComments(key, refresh) {
|
|
2086
|
+
const hit = commentsCache.get(key);
|
|
2087
|
+
if (!refresh && hit && Date.now() - hit.at < CACHE_MS)
|
|
2088
|
+
return hit.data;
|
|
2089
|
+
return undefined;
|
|
2090
|
+
}
|
|
2091
|
+
function legacyIssueComments(repoRoot, number) {
|
|
2092
|
+
// PR conversation comments live on the issues endpoint too — always use it for the body thread.
|
|
2093
|
+
return () => gh(repoRoot, ['api', `repos/{owner}/{repo}/issues/${number}/comments`, '--paginate']);
|
|
2094
|
+
}
|
|
2095
|
+
function parseUnknownRows(raw) {
|
|
2096
|
+
return z.array(z.unknown()).parse(JSON.parse(raw));
|
|
2097
|
+
}
|
|
2098
|
+
function commentedTimelineRows(rows) {
|
|
2099
|
+
return rows.filter((row) => row?.event === 'commented');
|
|
2100
|
+
}
|
|
2101
|
+
async function fetchIssueTimelinePages(repoRoot, number) {
|
|
2102
|
+
return fetchTimelinePages((page, timeoutMs) => gh(repoRoot, [
|
|
2103
|
+
'api',
|
|
2104
|
+
'-H',
|
|
2105
|
+
'Accept: application/vnd.github+json',
|
|
2106
|
+
`repos/{owner}/{repo}/issues/${number}/timeline?per_page=${TIMELINE_PER_PAGE}&page=${page}`,
|
|
2107
|
+
], timeoutMs));
|
|
2108
|
+
}
|
|
2109
|
+
async function topUpTimelineComments(commentRows, stoppedShort, legacyComments) {
|
|
2110
|
+
// Comments top-up. The two ENTRY caps are independent, but the FETCH budget is not: today's
|
|
2111
|
+
// call is an unbounded --paginate over a comments-only endpoint, so every comment is fetched
|
|
2112
|
+
// and mergeThread picks the oldest 200 from the complete set. Under the timeline those same
|
|
2113
|
+
// 200 slots are filled from at most 1000 rows in which events compete with comments — so on
|
|
2114
|
+
// a 1500-row thread with 250 interleaved comments, comments[] would return ~167 where today
|
|
2115
|
+
// it returns 200. Same §2 defect class as a combined entry cap, arriving via the source set.
|
|
2116
|
+
//
|
|
2117
|
+
// The trigger deliberately over-fires: it cannot distinguish "comments were cut off" from
|
|
2118
|
+
// "this thread just has few comments", because the second is only knowable by fetching them.
|
|
2119
|
+
// So on an event-heavy thread with 30 comments it re-fetches 30 complete comments. Accepted
|
|
2120
|
+
// — it fires only past ~1000 timeline rows, behind the 60 s LRU, and the alternative is a §2
|
|
2121
|
+
// regression. In the other direction the threshold is provably sound: with ≥200 commented
|
|
2122
|
+
// rows already in the oldest-first prefix, mergeThread's oldest-200 cut lies inside it.
|
|
2123
|
+
if (!(stoppedShort && commentRows.length < THREAD_ENTRY_CAP))
|
|
2124
|
+
return commentRows;
|
|
2125
|
+
// Its OWN nested catch. The inner handler's remedy is the fallback — which is this very
|
|
2126
|
+
// call — and the outer handler returns { available: false, comments: [] }, emptying a
|
|
2127
|
+
// thread that was about to render. So a thrown top-up is swallowed: keep the commented
|
|
2128
|
+
// rows the timeline already returned and carry on. That is at worst the short list the
|
|
2129
|
+
// safeguard existed to avoid, never worse than not having attempted it.
|
|
2130
|
+
try {
|
|
2131
|
+
return parseUnknownRows(await legacyComments());
|
|
2132
|
+
}
|
|
2133
|
+
catch {
|
|
2134
|
+
// keep the timeline's own commented rows
|
|
2135
|
+
return commentRows;
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
async function loadGithubThreadSource(repoRoot, number, legacyComments) {
|
|
2139
|
+
const source = {
|
|
2140
|
+
commentRows: [],
|
|
2141
|
+
events: undefined,
|
|
2142
|
+
eventsTruncated: false,
|
|
2143
|
+
stoppedShort: false,
|
|
2144
|
+
};
|
|
2145
|
+
try {
|
|
2146
|
+
const pages = await fetchIssueTimelinePages(repoRoot, number);
|
|
2147
|
+
source.stoppedShort = pages.stoppedShort;
|
|
2148
|
+
source.commentRows = commentedTimelineRows(pages.rows);
|
|
2149
|
+
const normalized = normalizeEvents(pages.rows);
|
|
2150
|
+
source.events = normalized.events;
|
|
2151
|
+
source.eventsTruncated = normalized.truncated;
|
|
2152
|
+
source.commentRows = await topUpTimelineComments(source.commentRows, source.stoppedShort, legacyComments);
|
|
2153
|
+
return source;
|
|
2154
|
+
}
|
|
2155
|
+
catch (timelineErr) {
|
|
2156
|
+
// Scoped INSIDE the existing outer catch on purpose: the outer handler's /404|not found/i
|
|
2157
|
+
// branch would otherwise turn a timeline 404 into an empty thread. ENOENT is the one failure
|
|
2158
|
+
// the fallback cannot rescue — nothing will work, and a second spawn fails identically.
|
|
2159
|
+
const message = timelineErr instanceof Error ? timelineErr.message : String(timelineErr);
|
|
2160
|
+
if (/ENOENT/.test(message))
|
|
2161
|
+
throw timelineErr;
|
|
2162
|
+
// Every other endpoint-level failure substitutes the legacy comments call. It is a
|
|
2163
|
+
// substitution, not a retry — a different endpoint, which typically still answers. (A 403
|
|
2164
|
+
// attaches to the token rather than the endpoint, so it cannot succeed either; not
|
|
2165
|
+
// special-cased, since rate-limit replies are immediate and it costs one fast spawn.)
|
|
2166
|
+
source.commentRows = parseUnknownRows(await legacyComments());
|
|
2167
|
+
source.events = undefined;
|
|
2168
|
+
return source;
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
function applyCommitChecks(events, checks) {
|
|
2172
|
+
for (const event of events) {
|
|
2173
|
+
if (event.kind !== 'committed' || !event.sha)
|
|
2174
|
+
continue;
|
|
2175
|
+
if (event.sha in checks) {
|
|
2176
|
+
delete event.checks;
|
|
2177
|
+
Object.assign(event, defined({ checks: checks[event.sha] }));
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
async function attachCommitChecks(repoRoot, events) {
|
|
2182
|
+
// Per-commit CI (#525 Phase 2). One extra subprocess per opened thread that contains commits,
|
|
2183
|
+
// behind the same 60 s LRU — a per-thread-open cost, not per-render. Every failure path here
|
|
2184
|
+
// leaves `checks` ABSENT rather than null, so commits simply render unglyphed: the fetch
|
|
2185
|
+
// degrades to "no data" and the render decides what absence looks like.
|
|
2186
|
+
const commitShas = (events ?? []).flatMap((event) => (event.kind === 'committed' && event.sha ? [event.sha] : []));
|
|
2187
|
+
if (commitShas.length === 0)
|
|
2188
|
+
return;
|
|
2189
|
+
const handle = await resolveRepoHandle(repoRoot);
|
|
2190
|
+
if (!handle)
|
|
2191
|
+
return;
|
|
2192
|
+
const runGraphql = (query, variables) => {
|
|
2193
|
+
const args = ghGraphqlArgs(query, variables);
|
|
2194
|
+
return gh(repoRoot, args);
|
|
2195
|
+
};
|
|
2196
|
+
applyCommitChecks(events ?? [], await fetchCommitChecks(runGraphql, handle.owner, handle.name, commitShas));
|
|
2197
|
+
}
|
|
2198
|
+
async function mergeGithubThread(repoRoot, kind, number, commentRows) {
|
|
2199
|
+
const parts = [normalizeComments(commentRows)];
|
|
2200
|
+
if (kind === 'pr') {
|
|
2201
|
+
const reviewsOut = await gh(repoRoot, ['api', `repos/{owner}/{repo}/pulls/${number}/reviews`, '--paginate']);
|
|
2202
|
+
parts.push(normalizeReviews(JSON.parse(reviewsOut)));
|
|
2203
|
+
}
|
|
2204
|
+
return mergeThread(parts);
|
|
2205
|
+
}
|
|
2206
|
+
function commentsFailureReason(err) {
|
|
2207
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2208
|
+
if (/ENOENT/.test(message))
|
|
2209
|
+
return 'gh CLI not found — install it and run `gh auth login`';
|
|
2210
|
+
if (/404|not found/i.test(message))
|
|
2211
|
+
return 'not found on GitHub — it may be closed or deleted';
|
|
2212
|
+
return firstLine(message);
|
|
2213
|
+
}
|
|
2214
|
+
async function loadGithubComments(repoRoot, kind, number, key) {
|
|
2215
|
+
const legacyComments = legacyIssueComments(repoRoot, number);
|
|
2216
|
+
// The detail tier (hearsay-tools/cezarion#1183): the list no longer has to carry a body, so
|
|
2217
|
+
// the pane asks for it here, in parallel with the thread. It is attached only to a thread
|
|
2218
|
+
// that loaded, and a failed view never costs the thread.
|
|
2219
|
+
const detail = fetchItemDetail(repoRoot, kind, number);
|
|
2220
|
+
try {
|
|
2221
|
+
const source = await loadGithubThreadSource(repoRoot, number, legacyComments);
|
|
2222
|
+
await attachCommitChecks(repoRoot, source.events);
|
|
2223
|
+
const { comments, truncated } = await mergeGithubThread(repoRoot, kind, number, source.commentRows);
|
|
2224
|
+
const data = defined({
|
|
2225
|
+
available: true,
|
|
2226
|
+
comments,
|
|
2227
|
+
// OR-folded exactly as before, so the pre-existing >200-comments trigger is preserved
|
|
2228
|
+
// rather than replaced.
|
|
2229
|
+
truncated: truncated || source.eventsTruncated || source.stoppedShort || undefined,
|
|
2230
|
+
});
|
|
2231
|
+
if (source.events)
|
|
2232
|
+
data.events = source.events;
|
|
2233
|
+
const itemDetail = await detail;
|
|
2234
|
+
if (itemDetail)
|
|
2235
|
+
data.detail = itemDetail;
|
|
2236
|
+
cacheComments(key, data);
|
|
2237
|
+
return data;
|
|
2238
|
+
}
|
|
2239
|
+
catch (err) {
|
|
2240
|
+
return { available: false, reason: commentsFailureReason(err), comments: [] };
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
export async function fetchGithubComments(repoRoot, kind, number, refresh = false) {
|
|
2244
|
+
if (productEnv('DRY_RUN') === '1')
|
|
2245
|
+
return mockGithubComments(kind, number);
|
|
2246
|
+
const key = commentsCacheKey(repoRoot, kind, number);
|
|
2247
|
+
const cached = cachedGithubComments(key, refresh);
|
|
2248
|
+
if (cached)
|
|
2249
|
+
return cached;
|
|
2250
|
+
return loadGithubComments(repoRoot, kind, number, key);
|
|
2251
|
+
}
|
|
2252
|
+
/** CEZ_DRY_RUN=1 — a small fixed thread (one image-bearing comment, plus a review for PRs) so
|
|
2253
|
+
* the whole feature is demoable and e2e-testable offline. */
|
|
2254
|
+
function mockGithubComments(kind, number) {
|
|
2255
|
+
const base = Date.now() - 3_600_000;
|
|
2256
|
+
const at = (offset) => new Date(base + offset).toISOString();
|
|
2257
|
+
const comments = [
|
|
2258
|
+
{
|
|
2259
|
+
id: 1,
|
|
2260
|
+
author: 'ada',
|
|
2261
|
+
avatarUrl: 'https://avatars.githubusercontent.com/u/1?v=4',
|
|
2262
|
+
createdAt: at(0),
|
|
2263
|
+
body: 'Thanks for the report — I can reproduce. Which browser were you on?\n\n```\nchrome 126, macOS\n```',
|
|
2264
|
+
kind: 'comment',
|
|
2265
|
+
url: 'https://github.com/mock/repo/issues/1#issuecomment-1',
|
|
2266
|
+
},
|
|
2267
|
+
{
|
|
2268
|
+
id: 2,
|
|
2269
|
+
author: 'lin',
|
|
2270
|
+
createdAt: at(600_000),
|
|
2271
|
+
body: 'Here is the failing screen:\n\n',
|
|
2272
|
+
kind: 'comment',
|
|
2273
|
+
url: 'https://github.com/mock/repo/issues/1#issuecomment-2',
|
|
2274
|
+
},
|
|
2275
|
+
];
|
|
2276
|
+
if (kind === 'pr') {
|
|
2277
|
+
comments.push({
|
|
2278
|
+
id: 3,
|
|
2279
|
+
author: 'grace',
|
|
2280
|
+
avatarUrl: 'https://avatars.githubusercontent.com/u/3?v=4',
|
|
2281
|
+
createdAt: at(1_200_000),
|
|
2282
|
+
body: 'Looks good overall — please add a regression test before this lands.',
|
|
2283
|
+
kind: 'review',
|
|
2284
|
+
reviewState: 'changes_requested',
|
|
2285
|
+
url: 'https://github.com/mock/repo/pull/1#pullrequestreview-3',
|
|
2286
|
+
});
|
|
2287
|
+
}
|
|
2288
|
+
// Timeline events (#525) so the whole feature is demoable and e2e-testable offline. Deliberately
|
|
2289
|
+
// covers the cases that are easy to get wrong rather than one of each: a multi-commit run by ONE
|
|
2290
|
+
// author (exercises the client-side grouping) with MIXED check states (passing/failing/pending
|
|
2291
|
+
// plus one `null` = no CI configured), a label change, a cross-reference, and — for PRs — a
|
|
2292
|
+
// merge. SHAs are full 40-char because the rollup query's `oid` rejects abbreviated ones, and a
|
|
2293
|
+
// fixture that cheated there would not survive being pasted into a real query.
|
|
2294
|
+
const sha = (seed) => seed.repeat(40).slice(0, 40);
|
|
2295
|
+
const events = [
|
|
2296
|
+
{
|
|
2297
|
+
id: 'evt-100',
|
|
2298
|
+
kind: 'labeled',
|
|
2299
|
+
actor: 'ada',
|
|
2300
|
+
avatarUrl: 'https://avatars.githubusercontent.com/u/1?v=4',
|
|
2301
|
+
createdAt: at(300_000),
|
|
2302
|
+
label: { name: 'bug', color: 'd73a4a' },
|
|
2303
|
+
},
|
|
2304
|
+
{
|
|
2305
|
+
id: `evt-${sha('a')}`,
|
|
2306
|
+
kind: 'committed',
|
|
2307
|
+
actor: 'Lin Zhao',
|
|
2308
|
+
createdAt: at(900_000),
|
|
2309
|
+
sha: sha('a'),
|
|
2310
|
+
message: 'fix(session): keep the refresh token on reload',
|
|
2311
|
+
checks: 'passing',
|
|
2312
|
+
},
|
|
2313
|
+
{
|
|
2314
|
+
id: `evt-${sha('b')}`,
|
|
2315
|
+
kind: 'committed',
|
|
2316
|
+
actor: 'Lin Zhao',
|
|
2317
|
+
createdAt: at(960_000),
|
|
2318
|
+
sha: sha('b'),
|
|
2319
|
+
message: 'test(session): cover the reload path',
|
|
2320
|
+
checks: 'failing',
|
|
2321
|
+
},
|
|
2322
|
+
{
|
|
2323
|
+
id: `evt-${sha('c')}`,
|
|
2324
|
+
kind: 'committed',
|
|
2325
|
+
actor: 'Lin Zhao',
|
|
2326
|
+
createdAt: at(1_020_000),
|
|
2327
|
+
sha: sha('c'),
|
|
2328
|
+
message: 'chore: appease the linter',
|
|
2329
|
+
checks: 'pending',
|
|
2330
|
+
},
|
|
2331
|
+
{
|
|
2332
|
+
id: `evt-${sha('d')}`,
|
|
2333
|
+
kind: 'committed',
|
|
2334
|
+
actor: 'Lin Zhao',
|
|
2335
|
+
createdAt: at(1_080_000),
|
|
2336
|
+
sha: sha('d'),
|
|
2337
|
+
message: 'docs: note the new behavior',
|
|
2338
|
+
checks: null, // no CI configured — renders no glyph, distinct from absent
|
|
2339
|
+
},
|
|
2340
|
+
{
|
|
2341
|
+
id: 'evt-101',
|
|
2342
|
+
kind: 'cross-referenced',
|
|
2343
|
+
actor: 'grace',
|
|
2344
|
+
avatarUrl: 'https://avatars.githubusercontent.com/u/3?v=4',
|
|
2345
|
+
createdAt: at(1_500_000),
|
|
2346
|
+
refNumber: 42,
|
|
2347
|
+
refTitle: 'Session handling rewrite',
|
|
2348
|
+
refIsPr: true,
|
|
2349
|
+
url: 'https://github.com/mock/repo/pull/42',
|
|
2350
|
+
},
|
|
2351
|
+
];
|
|
2352
|
+
if (kind === 'pr') {
|
|
2353
|
+
events.push({
|
|
2354
|
+
id: 'evt-102',
|
|
2355
|
+
kind: 'merged',
|
|
2356
|
+
actor: 'grace',
|
|
2357
|
+
avatarUrl: 'https://avatars.githubusercontent.com/u/3?v=4',
|
|
2358
|
+
createdAt: at(1_800_000),
|
|
2359
|
+
});
|
|
2360
|
+
}
|
|
2361
|
+
// The detail tier (hearsay-tools/cezarion#1183): the catalog row's own body and diffstat.
|
|
2362
|
+
const row = number === undefined ? undefined
|
|
2363
|
+
: (kind === 'pr' ? mockGithub().prs : mockGithub().issues).find((item) => item.number === number);
|
|
2364
|
+
const detail = row ? {
|
|
2365
|
+
body: row.body ?? '',
|
|
2366
|
+
...(kind === 'pr' ? { additions: row.additions ?? 0, deletions: row.deletions ?? 0 } : {}),
|
|
2367
|
+
} : undefined;
|
|
2368
|
+
return { available: true, comments, events, ...(detail ? { detail } : {}) };
|
|
2369
|
+
}
|
|
2370
|
+
// ---- the driver -------------------------------------------------------------
|
|
2371
|
+
/**
|
|
2372
|
+
* Cached availability probe so `GET /api/health` never pays a full listing. Keyed by repo root:
|
|
2373
|
+
* every registered project reads its own slot, so the boot project's constant health refresh can't
|
|
2374
|
+
* evict another project's answer (a single slot made `GET /automations` flap between "still being
|
|
2375
|
+
* checked" and available). One in-flight probe per root dedupes a burst of reads.
|
|
2376
|
+
*/
|
|
2377
|
+
const DETECT_CACHE_MAX = 64;
|
|
2378
|
+
const detectCache = new Map();
|
|
2379
|
+
const detectInflight = new Map();
|
|
2380
|
+
async function probeGithub(repoRoot) {
|
|
2381
|
+
let result;
|
|
2382
|
+
try {
|
|
2383
|
+
await gh(repoRoot, ['repo', 'view', '--json', 'nameWithOwner'], 5_000);
|
|
2384
|
+
result = { available: true };
|
|
2385
|
+
}
|
|
2386
|
+
catch (err) {
|
|
2387
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2388
|
+
result = {
|
|
2389
|
+
available: false,
|
|
2390
|
+
reason: /ENOENT/.test(message)
|
|
2391
|
+
? 'gh CLI not found — install it and run `gh auth login`'
|
|
2392
|
+
: firstLine(message),
|
|
2393
|
+
};
|
|
2394
|
+
}
|
|
2395
|
+
detectCache.delete(repoRoot); // re-insert so Map order tracks recency
|
|
2396
|
+
detectCache.set(repoRoot, { at: Date.now(), result });
|
|
2397
|
+
while (detectCache.size > DETECT_CACHE_MAX) {
|
|
2398
|
+
const oldest = detectCache.keys().next().value;
|
|
2399
|
+
if (oldest === undefined)
|
|
2400
|
+
break;
|
|
2401
|
+
detectCache.delete(oldest);
|
|
2402
|
+
}
|
|
2403
|
+
return result;
|
|
2404
|
+
}
|
|
2405
|
+
async function detectGithub(repoRoot) {
|
|
2406
|
+
if (productEnv('DRY_RUN') === '1')
|
|
2407
|
+
return { available: true };
|
|
2408
|
+
const hit = detectCache.get(repoRoot);
|
|
2409
|
+
if (hit && Date.now() - hit.at < CACHE_MS)
|
|
2410
|
+
return hit.result;
|
|
2411
|
+
const pending = detectInflight.get(repoRoot);
|
|
2412
|
+
if (pending)
|
|
2413
|
+
return pending;
|
|
2414
|
+
const probe = probeGithub(repoRoot).finally(() => detectInflight.delete(repoRoot));
|
|
2415
|
+
detectInflight.set(repoRoot, probe);
|
|
2416
|
+
return probe;
|
|
2417
|
+
}
|
|
2418
|
+
/**
|
|
2419
|
+
* Non-blocking availability for `GET /api/health` (#major-health-latency): serves the last-known
|
|
2420
|
+
* probe immediately (stale-while-revalidate) and only returns `null` on a cold start, before the
|
|
2421
|
+
* first probe has ever warmed the cache. It NEVER shells out to `gh` on the request that reads it,
|
|
2422
|
+
* so health stays under the bookmarklet's 800 ms port budget (a `gh repo view` round-trip is
|
|
2423
|
+
* ~500–650 ms on its own). `null` is contract-safe — the whole `forge` field is additive, so
|
|
2424
|
+
* "unknown until warm" is a valid answer.
|
|
2425
|
+
*
|
|
2426
|
+
* Serving the stale value while revalidating is what keeps the GitHub nav item from flickering:
|
|
2427
|
+
* without it, every time the 60 s cache expired this returned `null` for one 5 s health poll,
|
|
2428
|
+
* dropping `forge.available` and blinking the sidebar item out until the background probe warmed.
|
|
2429
|
+
*/
|
|
2430
|
+
export function detectGithubCached(repoRoot) {
|
|
2431
|
+
if (productEnv('DRY_RUN') === '1')
|
|
2432
|
+
return { available: true };
|
|
2433
|
+
const hit = detectCache.get(repoRoot);
|
|
2434
|
+
const cached = hit?.result ?? null;
|
|
2435
|
+
const fresh = hit !== undefined && Date.now() - hit.at < CACHE_MS;
|
|
2436
|
+
if (!fresh) {
|
|
2437
|
+
void detectGithub(repoRoot).catch(() => { }); // revalidate off the request path
|
|
2438
|
+
}
|
|
2439
|
+
return cached; // last-known value while revalidating; null only until the first probe warms
|
|
2440
|
+
}
|
|
2441
|
+
const mergeStateCache = new Map();
|
|
2442
|
+
const mergeInflight = new Set();
|
|
2443
|
+
const MERGE_CACHE_MS = 15_000;
|
|
2444
|
+
async function fetchPrMergeState(repoRoot, repoRef, number, refresh = false) {
|
|
2445
|
+
if (productEnv('DRY_RUN') === '1') {
|
|
2446
|
+
return {
|
|
2447
|
+
available: true,
|
|
2448
|
+
mergeState: normalizeMergeState({
|
|
2449
|
+
number,
|
|
2450
|
+
title: 'Dry-run pull request',
|
|
2451
|
+
url: `https://github.com/mock/repo/pull/${number}`,
|
|
2452
|
+
state: 'OPEN',
|
|
2453
|
+
isDraft: false,
|
|
2454
|
+
headRefName: 'feat/dry-run',
|
|
2455
|
+
baseRefName: 'main',
|
|
2456
|
+
headRefOid: '0123456789abcdef0123456789abcdef01234567',
|
|
2457
|
+
mergeable: 'MERGEABLE',
|
|
2458
|
+
mergeStateStatus: 'CLEAN',
|
|
2459
|
+
reviewDecision: 'APPROVED',
|
|
2460
|
+
statusCheckRollup: [{ name: 'test', conclusion: 'SUCCESS', detailsUrl: 'https://github.com/mock/repo/actions' }],
|
|
2461
|
+
}, { allow_merge_commit: true, allow_squash_merge: true, allow_rebase_merge: true }, { readable: true, requiredChecks: ['test'] }),
|
|
2462
|
+
};
|
|
2463
|
+
}
|
|
2464
|
+
if (!repoRef)
|
|
2465
|
+
return { available: false, reason: 'GitHub remote could not be resolved' };
|
|
2466
|
+
const key = `${repoRoot}:${number}`;
|
|
2467
|
+
const hit = mergeStateCache.get(key);
|
|
2468
|
+
if (!refresh && hit && Date.now() - hit.at < MERGE_CACHE_MS)
|
|
2469
|
+
return hit.value;
|
|
2470
|
+
try {
|
|
2471
|
+
const prOut = await gh(repoRoot, [
|
|
2472
|
+
'pr', 'view', String(number), '--json',
|
|
2473
|
+
'number,title,url,state,isDraft,headRefName,baseRefName,headRefOid,mergeable,mergeStateStatus,reviewDecision,statusCheckRollup,mergedAt',
|
|
2474
|
+
]);
|
|
2475
|
+
const parsedPr = mergePrSchema.parse(JSON.parse(prOut));
|
|
2476
|
+
if (!matchesGithubPullRequest({ ...repoRef, number }, parsedPr))
|
|
2477
|
+
return { available: false, reason: 'GitHub returned a different pull request.' };
|
|
2478
|
+
mergedPullRequestObservations.observe({ ...repoRef, number }, parsedPr);
|
|
2479
|
+
const [policyOut, requirements] = await Promise.all([
|
|
2480
|
+
gh(repoRoot, ['api', `repos/${repoRef.owner}/${repoRef.repo}`]),
|
|
2481
|
+
fetchGithubMergeRequirements((args) => gh(repoRoot, args), repoRef.owner, repoRef.repo, parsedPr.baseRefName, number),
|
|
2482
|
+
]);
|
|
2483
|
+
const value = {
|
|
2484
|
+
available: true,
|
|
2485
|
+
mergeState: normalizeMergeState(parsedPr, JSON.parse(policyOut), requirements),
|
|
2486
|
+
};
|
|
2487
|
+
mergeStateCache.set(key, { at: Date.now(), value });
|
|
2488
|
+
return value;
|
|
2489
|
+
}
|
|
2490
|
+
catch (error) {
|
|
2491
|
+
return { available: false, reason: firstLine(error instanceof Error ? error.message : String(error)) };
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
export function evictGithubProjectCaches(repoRoot) {
|
|
2495
|
+
const contextKey = githubContext(repoRoot).key;
|
|
2496
|
+
listCache.forEach((entry, key) => {
|
|
2497
|
+
if (entry.contextKey === contextKey)
|
|
2498
|
+
dropListEntry(key);
|
|
2499
|
+
});
|
|
2500
|
+
repositoryProjectsCache.delete(contextKey);
|
|
2501
|
+
commentCountsCache.forEach((_value, key) => {
|
|
2502
|
+
if (key.startsWith(`${contextKey}\0`))
|
|
2503
|
+
commentCountsCache.delete(key);
|
|
2504
|
+
});
|
|
2505
|
+
mergeStateCache.forEach((_value, key) => {
|
|
2506
|
+
if (key.startsWith(`${repoRoot}:`))
|
|
2507
|
+
mergeStateCache.delete(key);
|
|
2508
|
+
});
|
|
2509
|
+
commentsCache.forEach((_value, key) => {
|
|
2510
|
+
if (key.startsWith(`${repoRoot}\0`))
|
|
2511
|
+
commentsCache.delete(key);
|
|
2512
|
+
});
|
|
2513
|
+
itemCache.forEach((_value, key) => {
|
|
2514
|
+
if (key.startsWith(`${repoRoot}\0`))
|
|
2515
|
+
itemCache.delete(key);
|
|
2516
|
+
});
|
|
2517
|
+
}
|
|
2518
|
+
function mergeCancelledResult() {
|
|
2519
|
+
return { merged: false, status: 409, error: 'Automatic merging was cancelled.', code: 'cancelled' };
|
|
2520
|
+
}
|
|
2521
|
+
function mergeConflictResult(error, code, current) {
|
|
2522
|
+
return { merged: false, status: 409, error, code, current };
|
|
2523
|
+
}
|
|
2524
|
+
function mergePullRequestPreflight(current, input) {
|
|
2525
|
+
if (current.headSha !== input.expectedHeadSha) {
|
|
2526
|
+
return mergeConflictResult('The pull request head changed. Review the new commits before merging.', 'stale-head', current);
|
|
2527
|
+
}
|
|
2528
|
+
if (input.expectedHeadRef !== undefined && current.headRef !== input.expectedHeadRef) {
|
|
2529
|
+
return mergeConflictResult('The pull request branch changed.', 'stale-branch', current);
|
|
2530
|
+
}
|
|
2531
|
+
if (input.requirePassingChecks && (current.checks.length === 0 || current.checks.some(check => check.state !== 'passing'))) {
|
|
2532
|
+
return mergeConflictResult('Automatic merging requires at least one check and every current-head check passing.', 'checks-not-passing', current);
|
|
2533
|
+
}
|
|
2534
|
+
if (!current.methods.includes(input.method)) {
|
|
2535
|
+
return mergeConflictResult('That merge method is no longer enabled.', 'disabled-method', current);
|
|
2536
|
+
}
|
|
2537
|
+
if (!mergePreflightAllowed(current, !input.requirePassingChecks && input.overrideRules)) {
|
|
2538
|
+
return mergeConflictResult(current.blockers[0]?.message ?? 'The pull request is not eligible to merge.', current.eligibility, current);
|
|
2539
|
+
}
|
|
2540
|
+
return null;
|
|
2541
|
+
}
|
|
2542
|
+
async function mergeActiveCiConflict(repoRoot, repoRef, current, input) {
|
|
2543
|
+
if (!(input.requirePassingChecks && productEnv('DRY_RUN') !== '1'))
|
|
2544
|
+
return null;
|
|
2545
|
+
if (!repoRef)
|
|
2546
|
+
return { merged: false, status: 404, error: 'GitHub repository not found.' };
|
|
2547
|
+
const active = parseActiveWorkflowRuns(await gh(repoRoot, activeWorkflowRunsArgs({
|
|
2548
|
+
repository: `${repoRef.owner}/${repoRef.repo}`, headSha: current.headSha,
|
|
2549
|
+
})));
|
|
2550
|
+
if (input.canProceed?.() === false)
|
|
2551
|
+
return mergeCancelledResult();
|
|
2552
|
+
if (active)
|
|
2553
|
+
return mergeConflictResult('Waiting for active CI workflows on the current PR head.', 'checks-active', current);
|
|
2554
|
+
return null;
|
|
2555
|
+
}
|
|
2556
|
+
async function executeGithubMerge(repoRoot, repoRef, number, input, current) {
|
|
2557
|
+
if (productEnv('DRY_RUN') === '1') {
|
|
2558
|
+
evictGithubProjectCaches(repoRoot);
|
|
2559
|
+
return { merged: true, number, url: current.url, method: input.method, mergeCommitSha: 'abcdef0123456789abcdef0123456789abcdef01' };
|
|
2560
|
+
}
|
|
2561
|
+
if (!repoRef)
|
|
2562
|
+
return { merged: false, status: 404, error: 'GitHub repository not found.' };
|
|
2563
|
+
if (input.canProceed?.() === false)
|
|
2564
|
+
return mergeCancelledResult();
|
|
2565
|
+
const out = await gh(repoRoot, [
|
|
2566
|
+
'api', '--method', 'PUT', `repos/${repoRef.owner}/${repoRef.repo}/pulls/${number}/merge`,
|
|
2567
|
+
'-f', `merge_method=${input.method}`, '-f', `sha=${input.expectedHeadSha}`,
|
|
2568
|
+
]);
|
|
2569
|
+
// Once GitHub accepted the mutation, report its real outcome even if intent changed in flight.
|
|
2570
|
+
const result = ghMergeResultSchema.parse(JSON.parse(out));
|
|
2571
|
+
if (!result.merged)
|
|
2572
|
+
return mergeConflictResult(result.message ?? 'GitHub refused the merge.', 'github-blocked', current);
|
|
2573
|
+
evictGithubProjectCaches(repoRoot);
|
|
2574
|
+
return { merged: true, number, url: current.url, method: input.method, ...(result.sha ? { mergeCommitSha: result.sha } : {}) };
|
|
2575
|
+
}
|
|
2576
|
+
function mergePullRequestFailure(error) {
|
|
2577
|
+
const message = firstLine(error instanceof Error ? error.message : String(error));
|
|
2578
|
+
const status = /403|permission|forbidden/i.test(message) ? 403 : /404|not found/i.test(message) ? 404 : 502;
|
|
2579
|
+
return { merged: false, status, error: status === 403 ? 'GitHub permission denied.' : status === 404 ? 'Pull request or repository not found.' : 'GitHub could not complete the merge.' };
|
|
2580
|
+
}
|
|
2581
|
+
async function mergePullRequest(repoRoot, repoRef, number, input) {
|
|
2582
|
+
const key = `${repoRoot}:${number}`;
|
|
2583
|
+
if (mergeInflight.has(key))
|
|
2584
|
+
return { merged: false, status: 409, error: 'A merge is already in progress.', code: 'concurrent' };
|
|
2585
|
+
mergeInflight.add(key);
|
|
2586
|
+
try {
|
|
2587
|
+
if (input.canProceed?.() === false)
|
|
2588
|
+
return mergeCancelledResult();
|
|
2589
|
+
const fresh = await fetchPrMergeState(repoRoot, repoRef, number, true);
|
|
2590
|
+
if (input.canProceed?.() === false)
|
|
2591
|
+
return mergeCancelledResult();
|
|
2592
|
+
if (!fresh.available)
|
|
2593
|
+
return { merged: false, status: 502, error: fresh.reason };
|
|
2594
|
+
const current = fresh.mergeState;
|
|
2595
|
+
const preflight = mergePullRequestPreflight(current, input);
|
|
2596
|
+
if (preflight)
|
|
2597
|
+
return preflight;
|
|
2598
|
+
const ciConflict = await mergeActiveCiConflict(repoRoot, repoRef, current, input);
|
|
2599
|
+
if (ciConflict)
|
|
2600
|
+
return ciConflict;
|
|
2601
|
+
return await executeGithubMerge(repoRoot, repoRef, number, input, current);
|
|
2602
|
+
}
|
|
2603
|
+
catch (error) {
|
|
2604
|
+
return mergePullRequestFailure(error);
|
|
2605
|
+
}
|
|
2606
|
+
finally {
|
|
2607
|
+
mergeInflight.delete(key);
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
export function mergePreflightAllowed(current, overrideRules = false) {
|
|
2611
|
+
return current.canMerge || (overrideRules && current.canOverride);
|
|
2612
|
+
}
|
|
2613
|
+
function readyCancelledResult() {
|
|
2614
|
+
return { available: false, reason: 'Marking the pull request ready was cancelled.' };
|
|
2615
|
+
}
|
|
2616
|
+
function readyPullRequestInputInvalid(number, input) {
|
|
2617
|
+
return !Number.isSafeInteger(number) || number < 1 || !/^[0-9a-f]{40}$/.test(input.expectedHeadSha) || !input.expectedHeadRef;
|
|
2618
|
+
}
|
|
2619
|
+
function readyPullRequestIdentityConflict(pr, repoRef, number, input) {
|
|
2620
|
+
if (!matchesGithubPullRequest({ ...repoRef, number }, pr))
|
|
2621
|
+
return { available: false, reason: 'GitHub returned a different pull request.' };
|
|
2622
|
+
if (pr.headRefOid !== input.expectedHeadSha)
|
|
2623
|
+
return { available: false, reason: 'The pull request head changed.' };
|
|
2624
|
+
if (pr.headRefName !== input.expectedHeadRef)
|
|
2625
|
+
return { available: false, reason: 'The pull request branch changed.' };
|
|
2626
|
+
if (pr.state !== 'OPEN')
|
|
2627
|
+
return { available: false, reason: 'The pull request is no longer open.' };
|
|
2628
|
+
return null;
|
|
2629
|
+
}
|
|
2630
|
+
async function markDraftPullRequestReady(repoRoot, repoRef, number, input) {
|
|
2631
|
+
if (input.canProceed?.() === false)
|
|
2632
|
+
return readyCancelledResult();
|
|
2633
|
+
await gh(repoRoot, ['pr', 'ready', String(number), '--repo', `github.com/${repoRef.owner}/${repoRef.repo}`]);
|
|
2634
|
+
evictGithubProjectCaches(repoRoot);
|
|
2635
|
+
if (input.canProceed?.() === false)
|
|
2636
|
+
return readyCancelledResult();
|
|
2637
|
+
return null;
|
|
2638
|
+
}
|
|
2639
|
+
async function readyPullRequestOwnership(repoRoot, repoRef, number, input) {
|
|
2640
|
+
if (!repoRef)
|
|
2641
|
+
return { available: false, reason: 'GitHub remote could not be resolved' };
|
|
2642
|
+
const pr = mergePrSchema.pick({ number: true, url: true, state: true, isDraft: true, headRefName: true, headRefOid: true }).parse(JSON.parse(await gh(repoRoot, [
|
|
2643
|
+
'pr', 'view', String(number), '--json', 'number,url,state,isDraft,headRefName,headRefOid',
|
|
2644
|
+
])));
|
|
2645
|
+
if (input.canProceed?.() === false)
|
|
2646
|
+
return readyCancelledResult();
|
|
2647
|
+
const identity = readyPullRequestIdentityConflict(pr, repoRef, number, input);
|
|
2648
|
+
if (identity)
|
|
2649
|
+
return identity;
|
|
2650
|
+
if (!pr.isDraft)
|
|
2651
|
+
return null;
|
|
2652
|
+
return markDraftPullRequestReady(repoRoot, repoRef, number, input);
|
|
2653
|
+
}
|
|
2654
|
+
async function refreshReadyPullRequestState(repoRoot, repoRef, number, input) {
|
|
2655
|
+
const fresh = await fetchPrMergeState(repoRoot, repoRef, number, true);
|
|
2656
|
+
if (input.canProceed?.() === false)
|
|
2657
|
+
return readyCancelledResult();
|
|
2658
|
+
if (fresh.available && fresh.mergeState.headSha !== input.expectedHeadSha)
|
|
2659
|
+
return { available: false, reason: 'The pull request head changed.' };
|
|
2660
|
+
if (fresh.available && fresh.mergeState.headRef !== input.expectedHeadRef)
|
|
2661
|
+
return { available: false, reason: 'The pull request branch changed.' };
|
|
2662
|
+
return fresh;
|
|
2663
|
+
}
|
|
2664
|
+
/** gh has no compare-and-swap ready operation. Check ownership immediately before marking ready,
|
|
2665
|
+
* then refresh again; the later merge has its own head guard and GitHub's atomic SHA check. */
|
|
2666
|
+
async function readyPullRequest(repoRoot, repoRef, number, input) {
|
|
2667
|
+
if (readyPullRequestInputInvalid(number, input)) {
|
|
2668
|
+
return { available: false, reason: 'A valid pull request, head SHA and task branch are required.' };
|
|
2669
|
+
}
|
|
2670
|
+
const key = `${repoRoot}:${number}`;
|
|
2671
|
+
if (mergeInflight.has(key))
|
|
2672
|
+
return { available: false, reason: 'A pull request mutation is already in progress.' };
|
|
2673
|
+
mergeInflight.add(key);
|
|
2674
|
+
try {
|
|
2675
|
+
if (input.canProceed?.() === false)
|
|
2676
|
+
return readyCancelledResult();
|
|
2677
|
+
if (productEnv('DRY_RUN') !== '1') {
|
|
2678
|
+
const ownership = await readyPullRequestOwnership(repoRoot, repoRef, number, input);
|
|
2679
|
+
if (ownership)
|
|
2680
|
+
return ownership;
|
|
2681
|
+
}
|
|
2682
|
+
return await refreshReadyPullRequestState(repoRoot, repoRef, number, input);
|
|
2683
|
+
}
|
|
2684
|
+
catch {
|
|
2685
|
+
return { available: false, reason: 'GitHub could not mark the pull request ready for review.' };
|
|
2686
|
+
}
|
|
2687
|
+
finally {
|
|
2688
|
+
mergeInflight.delete(key);
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
const GH_PR_STATES = {
|
|
2692
|
+
MERGED: 'merged',
|
|
2693
|
+
CLOSED: 'closed',
|
|
2694
|
+
};
|
|
2695
|
+
export function createGithubDriver(repoRoot, repoRef) {
|
|
2696
|
+
return {
|
|
2697
|
+
kind: 'github',
|
|
2698
|
+
discoverRepository: (signal) => discoverRepoHandle(repoRoot, signal),
|
|
2699
|
+
resolveRepository: (signal) => resolveRepoHandle(repoRoot, signal),
|
|
2700
|
+
mergedPullRequests: (identities, signal) => lookupMergedPullRequests(repoRoot, identities, signal),
|
|
2701
|
+
list: (opts) => fetchGithub(repoRoot, opts?.refresh, opts?.limit, opts),
|
|
2702
|
+
projects: (generation) => fetchGithubProjects(repoRoot, generation),
|
|
2703
|
+
item: (kind, number, opts) => fetchGithubItem(repoRoot, kind, number, opts?.refresh),
|
|
2704
|
+
comments: (kind, number, opts) => fetchGithubComments(repoRoot, kind, number, opts?.refresh),
|
|
2705
|
+
commentCounts: (kind, numbers, opts) => fetchGithubCommentCounts(repoRoot, kind, numbers, opts?.refresh),
|
|
2706
|
+
checks: (numbers) => fetchGithubChecks(repoRoot, numbers),
|
|
2707
|
+
refStatus: (input) => fetchGithubRefStatus(repoRoot, input),
|
|
2708
|
+
cachedRefStatuses: (numbers) => readCachedRefStatuses(repoRoot, numbers),
|
|
2709
|
+
branchPushed: (numbers) => {
|
|
2710
|
+
// A push is the event that changes what the chips say about this task's pull requests —
|
|
2711
|
+
// its checks start again, and its MERGEABILITY is recomputed from scratch. Both are cached
|
|
2712
|
+
// per number, so without this the cockpit would keep showing the pre-push answer (up to a
|
|
2713
|
+
// minute of "Ready to merge" for a branch that has just been rewritten) about a push the
|
|
2714
|
+
// user watched this server make.
|
|
2715
|
+
for (const number of numbers)
|
|
2716
|
+
forgetRefStatus(repoRoot, number);
|
|
2717
|
+
},
|
|
2718
|
+
detect: () => detectGithub(repoRoot),
|
|
2719
|
+
detectCached: () => detectGithubCached(repoRoot),
|
|
2720
|
+
listIssues: async (opts) => (await fetchGithub(repoRoot, opts?.refresh, opts?.limit, opts)).issues,
|
|
2721
|
+
listPRs: async (opts) => (await fetchGithub(repoRoot, opts?.refresh, opts?.limit, opts)).prs,
|
|
2722
|
+
// The open-only list tier's escape hatch (#730) — this is the only path that can reach a
|
|
2723
|
+
// closed or merged item.
|
|
2724
|
+
searchItems: (kind, query, opts) => searchGithubItems(repoRoot, kind, query, opts?.limit),
|
|
2725
|
+
viewItem: (kind, number) => viewGithubItem(repoRoot, kind, number),
|
|
2726
|
+
prDiff: (number, opts) => fetchGithubPrDiff(repoRoot, number, opts?.refresh),
|
|
2727
|
+
createPR: async (input) => {
|
|
2728
|
+
const outcome = await createDraftPr(input);
|
|
2729
|
+
if (outcome.ok) {
|
|
2730
|
+
// A number the cockpit asked about BEFORE the pull request existed is cached as "this
|
|
2731
|
+
// repository has no such number" — which is exactly what a `CEZ:PR=901` marker declared
|
|
2732
|
+
// ahead of the push looks like. It exists now.
|
|
2733
|
+
const createdNumber = refNumberFromUrl(outcome.url);
|
|
2734
|
+
if (createdNumber !== null)
|
|
2735
|
+
forgetRefStatus(repoRoot, createdNumber);
|
|
2736
|
+
}
|
|
2737
|
+
return outcome;
|
|
2738
|
+
},
|
|
2739
|
+
// Null covers everything from "no PR yet" to "gh missing" — the callers
|
|
2740
|
+
// (Create PR → View PR flip) treat all of it as "nothing to link".
|
|
2741
|
+
prStatus: async (branch) => {
|
|
2742
|
+
if (productEnv('DRY_RUN') === '1')
|
|
2743
|
+
return null;
|
|
2744
|
+
try {
|
|
2745
|
+
const out = await gh(repoRoot, ['pr', 'view', branch, '--json', 'number,url,state,isDraft,statusCheckRollup,mergedAt']);
|
|
2746
|
+
const pr = ghPrViewSchema.parse(JSON.parse(out));
|
|
2747
|
+
if (repoRef)
|
|
2748
|
+
mergedPullRequestObservations.observe({ ...repoRef, number: pr.number }, pr);
|
|
2749
|
+
return {
|
|
2750
|
+
number: pr.number,
|
|
2751
|
+
url: pr.url,
|
|
2752
|
+
state: GH_PR_STATES[pr.state.toUpperCase()] ?? 'open',
|
|
2753
|
+
isDraft: pr.isDraft,
|
|
2754
|
+
checks: rollupToChecks(pr.statusCheckRollup) ?? null,
|
|
2755
|
+
};
|
|
2756
|
+
}
|
|
2757
|
+
catch {
|
|
2758
|
+
return null;
|
|
2759
|
+
}
|
|
2760
|
+
},
|
|
2761
|
+
prMergeState: (number, opts) => fetchPrMergeState(repoRoot, repoRef, number, opts?.refresh),
|
|
2762
|
+
readyPR: (number, input) => readyPullRequest(repoRoot, repoRef, number, input),
|
|
2763
|
+
mergePR: async (number, input) => {
|
|
2764
|
+
const result = await mergePullRequest(repoRoot, repoRef, number, input);
|
|
2765
|
+
if (result.merged) {
|
|
2766
|
+
// We just changed this pull request, so what the ref-status cache holds about it is now
|
|
2767
|
+
// known-stale — and its TTL would keep every chip showing the PRE-merge status for up to
|
|
2768
|
+
// a minute after the user watched this server merge it. Forget it; the next reader asks
|
|
2769
|
+
// the forge, gets `merged`, and then stops polling it at all.
|
|
2770
|
+
forgetRefStatus(repoRoot, number);
|
|
2771
|
+
forgetGithubItem(repoRoot, 'pr', number);
|
|
2772
|
+
}
|
|
2773
|
+
return result;
|
|
2774
|
+
},
|
|
2775
|
+
viewUrl: (kind, ref) => {
|
|
2776
|
+
if (!repoRef)
|
|
2777
|
+
return null;
|
|
2778
|
+
const base = `https://github.com/${repoRef.owner}/${repoRef.repo}`;
|
|
2779
|
+
// Branch names may contain '/' — encode per segment, keep the slashes.
|
|
2780
|
+
const path = String(ref).split('/').map(encodeURIComponent).join('/');
|
|
2781
|
+
switch (kind) {
|
|
2782
|
+
case 'repo':
|
|
2783
|
+
return base;
|
|
2784
|
+
case 'issue':
|
|
2785
|
+
return `${base}/issues/${path}`;
|
|
2786
|
+
case 'pr':
|
|
2787
|
+
return `${base}/pull/${path}`;
|
|
2788
|
+
case 'branch':
|
|
2789
|
+
return `${base}/tree/${path}`;
|
|
2790
|
+
case 'commit':
|
|
2791
|
+
return `${base}/commit/${path}`;
|
|
2792
|
+
}
|
|
2793
|
+
},
|
|
2794
|
+
};
|
|
2795
|
+
}
|
|
2796
|
+
/** A missing alias must not poison unrelated families. Discard partial data and requery only
|
|
2797
|
+
* unaffected aliases once; archive authority still comes from an error-free response. */
|
|
2798
|
+
async function mergedPullRequestChunk(runGraphql, initial) {
|
|
2799
|
+
let chunk = initial;
|
|
2800
|
+
for (let attempt = 0; attempt < 2 && chunk.length; attempt++) {
|
|
2801
|
+
const query = `query($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { nameWithOwner ${chunk.map(pr => `pr${pr.number}: pullRequest(number: ${pr.number}) { number url state mergedAt }`).join(' ')} } }`;
|
|
2802
|
+
let response;
|
|
2803
|
+
let failure;
|
|
2804
|
+
try {
|
|
2805
|
+
response = JSON.parse(await runGraphql(query, { owner: chunk[0].owner, name: chunk[0].repo }));
|
|
2806
|
+
}
|
|
2807
|
+
catch (error) {
|
|
2808
|
+
// gh exits 1 on GraphQL errors, with the structured envelope on stdout. Neither stderr
|
|
2809
|
+
// nor timeout/abort output is evidence that one particular alias was missing.
|
|
2810
|
+
const rejected = z.object({ code: z.literal(1), stdout: z.string() }).safeParse(error);
|
|
2811
|
+
if (!rejected.success)
|
|
2812
|
+
throw error;
|
|
2813
|
+
try {
|
|
2814
|
+
response = JSON.parse(rejected.data.stdout);
|
|
2815
|
+
}
|
|
2816
|
+
catch {
|
|
2817
|
+
throw error;
|
|
2818
|
+
}
|
|
2819
|
+
failure = error;
|
|
2820
|
+
}
|
|
2821
|
+
const partial = z.object({
|
|
2822
|
+
errors: z.array(z.object({ type: z.literal('NOT_FOUND'), path: z.tuple([z.literal('repository'), z.string()]) })).nonempty(),
|
|
2823
|
+
data: z.object({ repository: z.object({ nameWithOwner: z.string() }) }),
|
|
2824
|
+
}).safeParse(response);
|
|
2825
|
+
const aliases = new Set(chunk.map(pr => `pr${pr.number}`));
|
|
2826
|
+
if (attempt === 0 && partial.success &&
|
|
2827
|
+
partial.data.data.repository.nameWithOwner.toLowerCase() === `${chunk[0].owner}/${chunk[0].repo}`.toLowerCase() &&
|
|
2828
|
+
partial.data.errors.every(error => aliases.has(error.path[1]))) {
|
|
2829
|
+
const missing = new Set(partial.data.errors.map(error => error.path[1]));
|
|
2830
|
+
chunk = chunk.filter(pr => !missing.has(`pr${pr.number}`));
|
|
2831
|
+
continue;
|
|
2832
|
+
}
|
|
2833
|
+
if (failure)
|
|
2834
|
+
throw failure;
|
|
2835
|
+
return response;
|
|
2836
|
+
}
|
|
2837
|
+
return undefined;
|
|
2838
|
+
}
|
|
2839
|
+
/** Strict archive authority: no partial GraphQL success, redirects, guessed identity or date. */
|
|
2840
|
+
export async function fetchMergedPullRequests(runGraphql, identities) {
|
|
2841
|
+
const repositories = new Map();
|
|
2842
|
+
for (const identity of new Map(identities.map(pr => [pullRequestKey(pr), pr])).values()) {
|
|
2843
|
+
const key = `${identity.owner}/${identity.repo}`;
|
|
2844
|
+
repositories.set(key, [...(repositories.get(key) ?? []), identity]);
|
|
2845
|
+
}
|
|
2846
|
+
const merged = [];
|
|
2847
|
+
for (const [repository, prs] of repositories) {
|
|
2848
|
+
for (let offset = 0; offset < prs.length; offset += 25) {
|
|
2849
|
+
const chunk = prs.slice(offset, offset + 25);
|
|
2850
|
+
const response = await mergedPullRequestChunk(runGraphql, chunk);
|
|
2851
|
+
const parsed = z.object({ errors: z.array(z.unknown()).optional(), data: z.object({ repository: z.object({ nameWithOwner: z.string() }).passthrough().nullable() }) }).safeParse(response);
|
|
2852
|
+
if (!parsed.success || parsed.data.errors?.length || parsed.data.data.repository?.nameWithOwner.toLowerCase() !== repository.toLowerCase())
|
|
2853
|
+
continue;
|
|
2854
|
+
for (const pr of chunk) {
|
|
2855
|
+
const fact = mergedPullRequestObservations.observe(pr, parsed.data.data.repository[`pr${pr.number}`]);
|
|
2856
|
+
if (fact)
|
|
2857
|
+
merged.push(fact);
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
return merged;
|
|
2862
|
+
}
|
|
2863
|
+
export async function lookupMergedPullRequests(repoRoot, identities, signal) {
|
|
2864
|
+
signal.throwIfAborted();
|
|
2865
|
+
if (productEnv('DRY_RUN') === '1')
|
|
2866
|
+
return [];
|
|
2867
|
+
const unique = [...new Map(identities.map(pr => [pullRequestKey(pr), pr])).values()];
|
|
2868
|
+
const known = unique.flatMap(pr => mergedPullRequestObservations.get(pr) ?? []);
|
|
2869
|
+
const missing = unique.filter(pr => !mergedPullRequestObservations.get(pr));
|
|
2870
|
+
if (missing.length === 0)
|
|
2871
|
+
return known;
|
|
2872
|
+
const context = githubContext(repoRoot);
|
|
2873
|
+
const fetched = await fetchMergedPullRequests(async (query, variables) => {
|
|
2874
|
+
const args = ['api', 'graphql', '--hostname', 'github.com', '-f', `query=${query}`];
|
|
2875
|
+
for (const [key, value] of Object.entries(variables))
|
|
2876
|
+
args.push('-f', `${key}=${value}`);
|
|
2877
|
+
return gh(repoRoot, args, 15_000, signal, context);
|
|
2878
|
+
}, missing);
|
|
2879
|
+
return [...known, ...fetched];
|
|
2880
|
+
}
|
|
2881
|
+
//# sourceMappingURL=github.js.map
|