@mastra/factory 0.0.0-esbuild-bundle-worker-20260807173433
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1008 -0
- package/LICENSE.md +30 -0
- package/README.md +27 -0
- package/dist/auth.d.ts +183 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +507 -0
- package/dist/auth.js.map +1 -0
- package/dist/capabilities/connection.d.ts +9 -0
- package/dist/capabilities/connection.d.ts.map +1 -0
- package/dist/capabilities/connection.js +1 -0
- package/dist/capabilities/intake.d.ts +141 -0
- package/dist/capabilities/intake.d.ts.map +1 -0
- package/dist/capabilities/intake.js +1 -0
- package/dist/capabilities/version-control.d.ts +247 -0
- package/dist/capabilities/version-control.d.ts.map +1 -0
- package/dist/capabilities/version-control.js +1 -0
- package/dist/factory.d.ts +159 -0
- package/dist/factory.d.ts.map +1 -0
- package/dist/factory.js +491 -0
- package/dist/factory.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/integrations/base.d.ts +254 -0
- package/dist/integrations/base.d.ts.map +1 -0
- package/dist/integrations/base.js +1 -0
- package/dist/integrations/github/config.d.ts +60 -0
- package/dist/integrations/github/config.d.ts.map +1 -0
- package/dist/integrations/github/config.js +42 -0
- package/dist/integrations/github/config.js.map +1 -0
- package/dist/integrations/github/integration.d.ts +220 -0
- package/dist/integrations/github/integration.d.ts.map +1 -0
- package/dist/integrations/github/integration.js +1162 -0
- package/dist/integrations/github/integration.js.map +1 -0
- package/dist/integrations/github/issue-triage.d.ts +27 -0
- package/dist/integrations/github/issue-triage.d.ts.map +1 -0
- package/dist/integrations/github/issue-triage.js +82 -0
- package/dist/integrations/github/issue-triage.js.map +1 -0
- package/dist/integrations/github/pat.d.ts +38 -0
- package/dist/integrations/github/pat.d.ts.map +1 -0
- package/dist/integrations/github/pat.js +59 -0
- package/dist/integrations/github/pat.js.map +1 -0
- package/dist/integrations/github/provenance.d.ts +27 -0
- package/dist/integrations/github/provenance.d.ts.map +1 -0
- package/dist/integrations/github/provenance.js +75 -0
- package/dist/integrations/github/provenance.js.map +1 -0
- package/dist/integrations/github/reconcile-worker.d.ts +41 -0
- package/dist/integrations/github/reconcile-worker.d.ts.map +1 -0
- package/dist/integrations/github/reconcile-worker.js +125 -0
- package/dist/integrations/github/reconcile-worker.js.map +1 -0
- package/dist/integrations/github/routes.d.ts +69 -0
- package/dist/integrations/github/routes.d.ts.map +1 -0
- package/dist/integrations/github/routes.js +1335 -0
- package/dist/integrations/github/routes.js.map +1 -0
- package/dist/integrations/github/rules.d.ts +105 -0
- package/dist/integrations/github/rules.d.ts.map +1 -0
- package/dist/integrations/github/rules.js +551 -0
- package/dist/integrations/github/rules.js.map +1 -0
- package/dist/integrations/github/sandbox-release.d.ts +53 -0
- package/dist/integrations/github/sandbox-release.d.ts.map +1 -0
- package/dist/integrations/github/sandbox-release.js +115 -0
- package/dist/integrations/github/sandbox-release.js.map +1 -0
- package/dist/integrations/github/sandbox.d.ts +281 -0
- package/dist/integrations/github/sandbox.d.ts.map +1 -0
- package/dist/integrations/github/sandbox.js +547 -0
- package/dist/integrations/github/sandbox.js.map +1 -0
- package/dist/integrations/github/session-subscriptions.d.ts +26 -0
- package/dist/integrations/github/session-subscriptions.d.ts.map +1 -0
- package/dist/integrations/github/session-subscriptions.js +192 -0
- package/dist/integrations/github/session-subscriptions.js.map +1 -0
- package/dist/integrations/github/subscriptions.d.ts +54 -0
- package/dist/integrations/github/subscriptions.d.ts.map +1 -0
- package/dist/integrations/github/subscriptions.js +62 -0
- package/dist/integrations/github/subscriptions.js.map +1 -0
- package/dist/integrations/github/token-refresh.d.ts +12 -0
- package/dist/integrations/github/token-refresh.d.ts.map +1 -0
- package/dist/integrations/github/token-refresh.js +24 -0
- package/dist/integrations/github/token-refresh.js.map +1 -0
- package/dist/integrations/github/webhook.d.ts +107 -0
- package/dist/integrations/github/webhook.d.ts.map +1 -0
- package/dist/integrations/github/webhook.js +371 -0
- package/dist/integrations/github/webhook.js.map +1 -0
- package/dist/integrations/linear/agent-tools.d.ts +34 -0
- package/dist/integrations/linear/agent-tools.d.ts.map +1 -0
- package/dist/integrations/linear/agent-tools.js +85 -0
- package/dist/integrations/linear/agent-tools.js.map +1 -0
- package/dist/integrations/linear/integration.d.ts +235 -0
- package/dist/integrations/linear/integration.d.ts.map +1 -0
- package/dist/integrations/linear/integration.js +676 -0
- package/dist/integrations/linear/integration.js.map +1 -0
- package/dist/integrations/linear/routes.d.ts +61 -0
- package/dist/integrations/linear/routes.d.ts.map +1 -0
- package/dist/integrations/linear/routes.js +240 -0
- package/dist/integrations/linear/routes.js.map +1 -0
- package/dist/integrations/linear/rules.d.ts +44 -0
- package/dist/integrations/linear/rules.d.ts.map +1 -0
- package/dist/integrations/linear/rules.js +133 -0
- package/dist/integrations/linear/rules.js.map +1 -0
- package/dist/integrations/linear/storage.d.ts +65 -0
- package/dist/integrations/linear/storage.d.ts.map +1 -0
- package/dist/integrations/linear/storage.js +1 -0
- package/dist/integrations/platform/api-client.d.ts +26 -0
- package/dist/integrations/platform/api-client.d.ts.map +1 -0
- package/dist/integrations/platform/api-client.js +156 -0
- package/dist/integrations/platform/api-client.js.map +1 -0
- package/dist/integrations/platform/github/event-worker.d.ts +44 -0
- package/dist/integrations/platform/github/event-worker.d.ts.map +1 -0
- package/dist/integrations/platform/github/event-worker.js +349 -0
- package/dist/integrations/platform/github/event-worker.js.map +1 -0
- package/dist/integrations/platform/github/integration.d.ts +64 -0
- package/dist/integrations/platform/github/integration.d.ts.map +1 -0
- package/dist/integrations/platform/github/integration.js +1006 -0
- package/dist/integrations/platform/github/integration.js.map +1 -0
- package/dist/integrations/platform/linear/integration.d.ts +46 -0
- package/dist/integrations/platform/linear/integration.d.ts.map +1 -0
- package/dist/integrations/platform/linear/integration.js +483 -0
- package/dist/integrations/platform/linear/integration.js.map +1 -0
- package/dist/integrations/slack/connect-route.d.ts +46 -0
- package/dist/integrations/slack/connect-route.d.ts.map +1 -0
- package/dist/integrations/slack/connect-route.js +186 -0
- package/dist/integrations/slack/connect-route.js.map +1 -0
- package/dist/integrations/slack/integration.d.ts +62 -0
- package/dist/integrations/slack/integration.d.ts.map +1 -0
- package/dist/integrations/slack/integration.js +68 -0
- package/dist/integrations/slack/integration.js.map +1 -0
- package/dist/integrations/slack/slack.d.ts +200 -0
- package/dist/integrations/slack/slack.d.ts.map +1 -0
- package/dist/integrations/slack/slack.js +411 -0
- package/dist/integrations/slack/slack.js.map +1 -0
- package/dist/integrations/workos/integration.d.ts +39 -0
- package/dist/integrations/workos/integration.d.ts.map +1 -0
- package/dist/integrations/workos/integration.js +91 -0
- package/dist/integrations/workos/integration.js.map +1 -0
- package/dist/routes/config.d.ts +235 -0
- package/dist/routes/config.d.ts.map +1 -0
- package/dist/routes/config.js +972 -0
- package/dist/routes/config.js.map +1 -0
- package/dist/routes/custom-provider-source.d.ts +52 -0
- package/dist/routes/custom-provider-source.d.ts.map +1 -0
- package/dist/routes/custom-provider-source.js +124 -0
- package/dist/routes/custom-provider-source.js.map +1 -0
- package/dist/routes/fs.d.ts +151 -0
- package/dist/routes/fs.d.ts.map +1 -0
- package/dist/routes/fs.js +708 -0
- package/dist/routes/fs.js.map +1 -0
- package/dist/routes/intake.d.ts +23 -0
- package/dist/routes/intake.d.ts.map +1 -0
- package/dist/routes/intake.js +179 -0
- package/dist/routes/intake.js.map +1 -0
- package/dist/routes/oauth.d.ts +43 -0
- package/dist/routes/oauth.d.ts.map +1 -0
- package/dist/routes/oauth.js +463 -0
- package/dist/routes/oauth.js.map +1 -0
- package/dist/routes/projects.d.ts +19 -0
- package/dist/routes/projects.d.ts.map +1 -0
- package/dist/routes/projects.js +461 -0
- package/dist/routes/projects.js.map +1 -0
- package/dist/routes/provider-credentials.d.ts +81 -0
- package/dist/routes/provider-credentials.d.ts.map +1 -0
- package/dist/routes/provider-credentials.js +86 -0
- package/dist/routes/provider-credentials.js.map +1 -0
- package/dist/routes/route.d.ts +48 -0
- package/dist/routes/route.d.ts.map +1 -0
- package/dist/routes/route.js +15 -0
- package/dist/routes/route.js.map +1 -0
- package/dist/routes/skills.d.ts +30 -0
- package/dist/routes/skills.d.ts.map +1 -0
- package/dist/routes/skills.js +168 -0
- package/dist/routes/skills.js.map +1 -0
- package/dist/routes/surface.d.ts +107 -0
- package/dist/routes/surface.d.ts.map +1 -0
- package/dist/routes/surface.js +297 -0
- package/dist/routes/surface.js.map +1 -0
- package/dist/routes/tenant-credentials.d.ts +74 -0
- package/dist/routes/tenant-credentials.d.ts.map +1 -0
- package/dist/routes/tenant-credentials.js +156 -0
- package/dist/routes/tenant-credentials.js.map +1 -0
- package/dist/routes/work-items.d.ts +40 -0
- package/dist/routes/work-items.d.ts.map +1 -0
- package/dist/routes/work-items.js +695 -0
- package/dist/routes/work-items.js.map +1 -0
- package/dist/rules/binding-context.d.ts +5 -0
- package/dist/rules/binding-context.d.ts.map +1 -0
- package/dist/rules/binding-context.js +28 -0
- package/dist/rules/binding-context.js.map +1 -0
- package/dist/rules/defaults.d.ts +9 -0
- package/dist/rules/defaults.d.ts.map +1 -0
- package/dist/rules/defaults.js +322 -0
- package/dist/rules/defaults.js.map +1 -0
- package/dist/rules/dispatcher.d.ts +43 -0
- package/dist/rules/dispatcher.d.ts.map +1 -0
- package/dist/rules/dispatcher.js +547 -0
- package/dist/rules/dispatcher.js.map +1 -0
- package/dist/rules/index.d.ts +7 -0
- package/dist/rules/index.d.ts.map +1 -0
- package/dist/rules/index.js +5 -0
- package/dist/rules/processor.d.ts +54 -0
- package/dist/rules/processor.d.ts.map +1 -0
- package/dist/rules/processor.js +378 -0
- package/dist/rules/processor.js.map +1 -0
- package/dist/rules/resolve.d.ts +17 -0
- package/dist/rules/resolve.d.ts.map +1 -0
- package/dist/rules/resolve.js +30 -0
- package/dist/rules/resolve.js.map +1 -0
- package/dist/rules/start-coordinator.d.ts +60 -0
- package/dist/rules/start-coordinator.d.ts.map +1 -0
- package/dist/rules/start-coordinator.js +175 -0
- package/dist/rules/start-coordinator.js.map +1 -0
- package/dist/rules/tools.d.ts +10 -0
- package/dist/rules/tools.d.ts.map +1 -0
- package/dist/rules/tools.js +59 -0
- package/dist/rules/tools.js.map +1 -0
- package/dist/rules/transition-service.d.ts +49 -0
- package/dist/rules/transition-service.d.ts.map +1 -0
- package/dist/rules/transition-service.js +232 -0
- package/dist/rules/transition-service.js.map +1 -0
- package/dist/rules/types.d.ts +261 -0
- package/dist/rules/types.d.ts.map +1 -0
- package/dist/rules/types.js +43 -0
- package/dist/rules/types.js.map +1 -0
- package/dist/rules/validation.d.ts +11 -0
- package/dist/rules/validation.d.ts.map +1 -0
- package/dist/rules/validation.js +308 -0
- package/dist/rules/validation.js.map +1 -0
- package/dist/sandbox/fleet.d.ts +222 -0
- package/dist/sandbox/fleet.d.ts.map +1 -0
- package/dist/sandbox/fleet.js +322 -0
- package/dist/sandbox/fleet.js.map +1 -0
- package/dist/sandbox/reattach.d.ts +3 -0
- package/dist/sandbox/reattach.d.ts.map +1 -0
- package/dist/sandbox/reattach.js +16 -0
- package/dist/sandbox/reattach.js.map +1 -0
- package/dist/server-error.d.ts +12 -0
- package/dist/server-error.d.ts.map +1 -0
- package/dist/server-error.js +16 -0
- package/dist/server-error.js.map +1 -0
- package/dist/session/factory-session.d.ts +116 -0
- package/dist/session/factory-session.d.ts.map +1 -0
- package/dist/session/factory-session.js +162 -0
- package/dist/session/factory-session.js.map +1 -0
- package/dist/skills/service.d.ts +48 -0
- package/dist/skills/service.d.ts.map +1 -0
- package/dist/skills/service.js +40 -0
- package/dist/skills/service.js.map +1 -0
- package/dist/spa-static.d.ts +33 -0
- package/dist/spa-static.d.ts.map +1 -0
- package/dist/spa-static.js +104 -0
- package/dist/spa-static.js.map +1 -0
- package/dist/state-signing.d.ts +59 -0
- package/dist/state-signing.d.ts.map +1 -0
- package/dist/state-signing.js +83 -0
- package/dist/state-signing.js.map +1 -0
- package/dist/storage/domains/audit/agent-audit.d.ts +27 -0
- package/dist/storage/domains/audit/agent-audit.d.ts.map +1 -0
- package/dist/storage/domains/audit/agent-audit.js +66 -0
- package/dist/storage/domains/audit/agent-audit.js.map +1 -0
- package/dist/storage/domains/audit/base.d.ts +140 -0
- package/dist/storage/domains/audit/base.d.ts.map +1 -0
- package/dist/storage/domains/audit/base.js +184 -0
- package/dist/storage/domains/audit/base.js.map +1 -0
- package/dist/storage/domains/audit/domain.d.ts +76 -0
- package/dist/storage/domains/audit/domain.d.ts.map +1 -0
- package/dist/storage/domains/audit/domain.js +269 -0
- package/dist/storage/domains/audit/domain.js.map +1 -0
- package/dist/storage/domains/channel-identity/base.d.ts +96 -0
- package/dist/storage/domains/channel-identity/base.d.ts.map +1 -0
- package/dist/storage/domains/channel-identity/base.js +149 -0
- package/dist/storage/domains/channel-identity/base.js.map +1 -0
- package/dist/storage/domains/credentials/base.d.ts +134 -0
- package/dist/storage/domains/credentials/base.d.ts.map +1 -0
- package/dist/storage/domains/credentials/base.js +273 -0
- package/dist/storage/domains/credentials/base.js.map +1 -0
- package/dist/storage/domains/custom-providers/base.d.ts +57 -0
- package/dist/storage/domains/custom-providers/base.d.ts.map +1 -0
- package/dist/storage/domains/custom-providers/base.js +145 -0
- package/dist/storage/domains/custom-providers/base.js.map +1 -0
- package/dist/storage/domains/intake/base.d.ts +38 -0
- package/dist/storage/domains/intake/base.d.ts.map +1 -0
- package/dist/storage/domains/intake/base.js +80 -0
- package/dist/storage/domains/intake/base.js.map +1 -0
- package/dist/storage/domains/integrations/base.d.ts +126 -0
- package/dist/storage/domains/integrations/base.d.ts.map +1 -0
- package/dist/storage/domains/integrations/base.js +317 -0
- package/dist/storage/domains/integrations/base.js.map +1 -0
- package/dist/storage/domains/memory-settings/base.d.ts +62 -0
- package/dist/storage/domains/memory-settings/base.d.ts.map +1 -0
- package/dist/storage/domains/memory-settings/base.js +126 -0
- package/dist/storage/domains/memory-settings/base.js.map +1 -0
- package/dist/storage/domains/model-packs/base.d.ts +49 -0
- package/dist/storage/domains/model-packs/base.d.ts.map +1 -0
- package/dist/storage/domains/model-packs/base.js +96 -0
- package/dist/storage/domains/model-packs/base.js.map +1 -0
- package/dist/storage/domains/projects/base.d.ts +58 -0
- package/dist/storage/domains/projects/base.d.ts.map +1 -0
- package/dist/storage/domains/projects/base.js +111 -0
- package/dist/storage/domains/projects/base.js.map +1 -0
- package/dist/storage/domains/queue-health/base.d.ts +52 -0
- package/dist/storage/domains/queue-health/base.d.ts.map +1 -0
- package/dist/storage/domains/queue-health/base.js +122 -0
- package/dist/storage/domains/queue-health/base.js.map +1 -0
- package/dist/storage/domains/source-control/base.d.ts +360 -0
- package/dist/storage/domains/source-control/base.d.ts.map +1 -0
- package/dist/storage/domains/source-control/base.js +914 -0
- package/dist/storage/domains/source-control/base.js.map +1 -0
- package/dist/storage/domains/source-control/inmemory.d.ts +175 -0
- package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -0
- package/dist/storage/domains/source-control/inmemory.js +380 -0
- package/dist/storage/domains/source-control/inmemory.js.map +1 -0
- package/dist/storage/domains/work-items/base.d.ts +412 -0
- package/dist/storage/domains/work-items/base.d.ts.map +1 -0
- package/dist/storage/domains/work-items/base.js +1320 -0
- package/dist/storage/domains/work-items/base.js.map +1 -0
- package/dist/storage/domains/work-items/metrics.d.ts +101 -0
- package/dist/storage/domains/work-items/metrics.d.ts.map +1 -0
- package/dist/storage/domains/work-items/metrics.js +221 -0
- package/dist/storage/domains/work-items/metrics.js.map +1 -0
- package/dist/timing.d.ts +15 -0
- package/dist/timing.d.ts.map +1 -0
- package/dist/timing.js +34 -0
- package/dist/timing.js.map +1 -0
- package/dist/workspace.d.ts +24 -0
- package/dist/workspace.d.ts.map +1 -0
- package/dist/workspace.js +287 -0
- package/dist/workspace.js.map +1 -0
- package/factory-skills/configure-factory-rules/SKILL.md +67 -0
- package/factory-skills/factory-plan/SKILL.md +61 -0
- package/factory-skills/factory-review/SKILL.md +157 -0
- package/factory-skills/factory-triage/SKILL.md +99 -0
- package/package.json +83 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rules.js","names":["#ingestProject","#relatedItem"],"sources":["../../../src/integrations/github/rules.ts"],"sourcesContent":["import { resolveFactoryGithubRule } from '../../rules/resolve.js';\nimport type {\n FactoryGithubEventName,\n FactoryGithubRuleContext,\n FactoryRuleActor,\n FactoryRuleDecision,\n FactoryRules,\n} from '../../rules/types.js';\nimport { validateFactoryRuleDecisions } from '../../rules/validation.js';\nimport type { IntegrationStorageHandle } from '../../storage/domains/integrations/base.js';\nimport type { FactoryProjectsStorage } from '../../storage/domains/projects/base.js';\nimport type {\n ExternalRepositoryProjectTarget,\n SourceControlStorageHandle,\n} from '../../storage/domains/source-control/base.js';\nimport type { WorkItemRow, WorkItemsStorage } from '../../storage/domains/work-items/base.js';\nimport type { IntegrationContext } from '../base.js';\nimport type { GithubRepositoryPermission } from './integration.js';\nimport { changeRequestTargetKey } from './subscriptions.js';\nimport type { ParsedGithubWebhook } from './webhook.js';\n\nconst TRUSTED_PERMISSIONS = new Set(['write', 'admin']);\nconst RULE_TIMEOUT_MS = 5_000;\nconst FACTORY_TRIAGE_COMMENT_MARKER = '<!-- mastra-factory-triage -->';\n\nasync function withRuleTimeout<T>(promise: Promise<T>): Promise<T> {\n let timeout: ReturnType<typeof setTimeout> | undefined;\n try {\n return await Promise.race([\n promise,\n new Promise<never>((_, reject) => {\n timeout = setTimeout(() => reject(new Error('FACTORY_RULE_TIMEOUT')), RULE_TIMEOUT_MS);\n }),\n ]);\n } finally {\n if (timeout) clearTimeout(timeout);\n }\n}\n\nfunction object(value: unknown): Record<string, unknown> | undefined {\n return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record<string, unknown>) : undefined;\n}\n\nfunction string(value: unknown): string | undefined {\n return typeof value === 'string' && value.length > 0 ? value : undefined;\n}\n\nfunction number(value: unknown): number | undefined {\n return typeof value === 'number' && Number.isFinite(value) ? value : undefined;\n}\n\nfunction boolean(value: unknown): boolean | undefined {\n return typeof value === 'boolean' ? value : undefined;\n}\n\nfunction actorLogins(value: unknown): string[] {\n if (!Array.isArray(value)) return [];\n return value.flatMap(actor => {\n const login = string(object(actor)?.login);\n return login ? [login] : [];\n });\n}\n\nfunction eventName(parsed: ParsedGithubWebhook): FactoryGithubEventName | undefined {\n const action = string(parsed.payload.action);\n if (parsed.event === 'issues' && action === 'opened') return 'issueOpened';\n if (parsed.event === 'issues' && action === 'edited') {\n const changes = object(parsed.payload.changes);\n return object(changes?.title) || object(changes?.body) ? 'issueEdited' : undefined;\n }\n if (parsed.event === 'issues' && action === 'closed') return 'issueClosed';\n if (parsed.event === 'issue_comment') {\n const issue = object(parsed.payload.issue);\n if (object(issue?.pull_request)) return undefined;\n if (action === 'created') return 'issueCommentCreated';\n if (action === 'edited') return 'issueCommentEdited';\n if (action === 'deleted') return 'issueCommentDeleted';\n }\n if (parsed.event === 'pull_request' && action === 'opened') return 'pullRequestOpened';\n if (parsed.event === 'pull_request' && action === 'synchronize') return 'pullRequestUpdated';\n if (parsed.event === 'pull_request' && action === 'closed') {\n return boolean(object(parsed.payload.pull_request)?.merged) ? 'pullRequestMerged' : 'pullRequestClosed';\n }\n if (parsed.event === 'pull_request' && action === 'review_requested') return 'pullRequestReviewRequested';\n return undefined;\n}\n\n/**\n * Canonical source keys (`github-issue:N`, `github-pr:N`) do not identify a\n * repository, so a project linked to several repositories could bind repo A's\n * event to repo B's same-numbered card. The card's intake-stamped URL is\n * authoritative; the intake-stamped `githubRepositoryId` covers URL-less\n * cards. A card with neither signal cannot be attributed by number alone.\n */\nfunction cardBelongsToRepository(item: WorkItemRow, repositoryId: number, repositoryFullName: string): boolean {\n const url = item.externalSource?.url;\n if (url) {\n const match = /^https?:\\/\\/[^/]+\\/(.+)\\/(?:issues|pull)\\/\\d+(?:[/?#]|$)/.exec(url);\n if (match && match[1] === repositoryFullName) return true;\n }\n // A renamed repository leaves the old owner/name in the card URL, so a URL\n // mismatch still defers to the stable intake-stamped repository id.\n return item.metadata?.githubRepositoryId === repositoryId;\n}\n\nfunction canonicalSourceKey(kind: 'issue' | 'pull-request', itemNumber: number): string {\n return kind === 'issue' ? `github-issue:${itemNumber}` : `github-pr:${itemNumber}`;\n}\n\nfunction legacySourceKey(repositoryId: number, kind: 'issue' | 'pull-request', itemNumber: number): string {\n return `github:${repositoryId}:${kind}:${itemNumber}`;\n}\n\nfunction provenanceTarget(repositoryId: number, pullRequestNumber: number): string {\n return `factory-pr-provenance:${repositoryId}:${pullRequestNumber}`;\n}\n\nfunction workItemSource(item: WorkItemRow) {\n if (!item.externalSource) return 'manual' as const;\n return item.externalSource.type === 'pull-request' ? ('github-pr' as const) : ('github-issue' as const);\n}\n\nfunction workItemSourceKey(item: WorkItemRow): string | null {\n return item.externalSource?.externalId ?? null;\n}\n\nasync function githubActor(\n github: GithubRulesIntegration,\n input: { installationId: number; repository: string; login: string; factoryAuthored: boolean },\n): Promise<FactoryRuleActor> {\n let trusted = false;\n try {\n const permission = await github.getRepositoryCollaboratorPermission(\n input.installationId,\n input.repository,\n input.login,\n );\n trusted = permission !== undefined && TRUSTED_PERMISSIONS.has(permission);\n } catch {\n trusted = false;\n }\n return { type: 'github', login: input.login, trusted, factoryAuthored: input.factoryAuthored };\n}\n\ninterface FactoryPullRequestProvenanceData {\n kind: 'factory-pr-provenance';\n workItemId: string;\n}\n\nfunction pullRequestProvenance(data: Record<string, unknown> | undefined): FactoryPullRequestProvenanceData | null {\n if (!data || data.kind !== 'factory-pr-provenance' || typeof data.workItemId !== 'string') return null;\n return { kind: 'factory-pr-provenance', workItemId: data.workItemId };\n}\n\nexport interface GithubRulesIntegration {\n readonly slug?: string;\n getRepositoryCollaboratorPermission(\n installationId: number,\n repoFullName: string,\n username: string,\n ): Promise<GithubRepositoryPermission | undefined>;\n}\n\nexport interface GithubRulesOptions {\n github: GithubRulesIntegration;\n sourceControl: SourceControlStorageHandle;\n /** Integration-scoped storage; provenance rows are validated at read. */\n integrationStorage: IntegrationStorageHandle;\n projects: FactoryProjectsStorage;\n storage: WorkItemsStorage;\n rules: FactoryRules;\n}\n\nexport class GithubRules {\n constructor(private readonly options: GithubRulesOptions) {}\n\n async ingest(parsed: ParsedGithubWebhook): Promise<{ status: 'ignored' | 'committed' | 'replayed' | 'missing' }> {\n const event = eventName(parsed);\n const repository = object(parsed.payload.repository);\n const installationId = number(object(parsed.payload.installation)?.id);\n const repositoryId = number(repository?.id);\n const repositoryName = string(repository?.full_name);\n const login = string(object(parsed.payload.sender)?.login);\n if (!event || !installationId || !repositoryId || !repositoryName || !login) return { status: 'ignored' };\n\n const projects = await this.options.sourceControl.projectRepositories.listByExternalRepository({\n installationExternalId: String(installationId),\n repositoryExternalId: String(repositoryId),\n });\n if (projects.length === 0) return { status: 'ignored' };\n const results = [];\n for (const project of projects) {\n results.push(\n await this.#ingestProject(parsed, event, installationId, repositoryId, repositoryName, login, project),\n );\n }\n if (results.some(result => result.status === 'committed')) return { status: 'committed' };\n if (results.some(result => result.status === 'replayed')) return { status: 'replayed' };\n return results[0] ?? { status: 'ignored' };\n }\n\n async #ingestProject(\n parsed: ParsedGithubWebhook,\n event: FactoryGithubEventName,\n installationId: number,\n repositoryId: number,\n repositoryName: string,\n login: string,\n project: ExternalRepositoryProjectTarget,\n ): Promise<{ status: 'ignored' | 'committed' | 'replayed' | 'missing' }> {\n const factoryProject = await this.options.projects.get({\n orgId: project.orgId,\n id: project.factoryProjectId,\n });\n if (!factoryProject) return { status: 'missing' };\n const issue = object(parsed.payload.issue);\n const issueComment = object(parsed.payload.comment);\n const changes = object(parsed.payload.changes);\n const pullRequest = object(parsed.payload.pull_request);\n const issueNumber = number(issue?.number);\n const pullRequestNumber = number(pullRequest?.number);\n const provenance = pullRequestNumber\n ? pullRequestProvenance(\n (\n await this.options.integrationStorage.subscriptions.listByTarget(\n provenanceTarget(repositoryId, pullRequestNumber),\n { status: 'active' },\n )\n ).find(subscription => subscription.orgId === project.orgId)?.data,\n )\n : null;\n // A review re-request targets the PR's own Review card, not the Work item\n // that provenance would bind the event to — and the sender is whoever\n // clicked re-request, so a Factory-authored PR must not brand a human\n // requester as factory-authored.\n const reviewRequested = event === 'pullRequestReviewRequested';\n const requestedReviewer = string(object(parsed.payload.requested_reviewer)?.login);\n const relatedItem = await this.#relatedItem(\n project.orgId,\n project.factoryProjectId,\n repositoryId,\n repositoryName,\n issueNumber,\n pullRequestNumber,\n string(object(pullRequest?.head)?.ref),\n reviewRequested ? null : provenance,\n );\n const actor = await githubActor(this.options.github, {\n installationId,\n repository: repositoryName,\n login,\n factoryAuthored: (!reviewRequested && provenance !== null) || login === `${this.options.github.slug}[bot]`,\n });\n if (\n actor.type === 'github' &&\n actor.factoryAuthored &&\n (event === 'issueCommentCreated' || event === 'issueCommentEdited') &&\n string(issueComment?.body)?.includes(FACTORY_TRIAGE_COMMENT_MARKER)\n ) {\n return { status: 'ignored' };\n }\n const context: FactoryGithubRuleContext = {\n tenant: { orgId: project.orgId, projectId: project.factoryProjectId },\n actor,\n ingress: { type: 'github', id: `${installationId}:${parsed.deliveryId}` },\n cause: `github.${event}`,\n causalChain: [],\n ruleSetVersion: this.options.rules.version,\n ...(relatedItem\n ? {\n item: {\n id: relatedItem.id,\n source: workItemSource(relatedItem),\n sourceKey: workItemSourceKey(relatedItem),\n parentWorkItemId: relatedItem.parentWorkItemId,\n title: relatedItem.title,\n url: relatedItem.externalSource?.url ?? null,\n stages: relatedItem.stages,\n },\n board: relatedItem.externalSource?.type === 'pull-request' ? ('review' as const) : ('work' as const),\n itemRevision: relatedItem.revision,\n }\n : {}),\n event,\n deliveryId: parsed.deliveryId,\n factory: { createdAt: factoryProject.createdAt.toISOString() },\n repository: { id: repositoryId, fullName: repositoryName },\n ...(issueNumber && string(issue?.title) && string(issue?.html_url)\n ? {\n issue: {\n number: issueNumber,\n title: string(issue?.title)!,\n url: string(issue?.html_url)!,\n ...(string(issue?.created_at) ? { createdAt: string(issue?.created_at) } : {}),\n ...(string(issue?.updated_at) ? { updatedAt: string(issue?.updated_at) } : {}),\n assignees: actorLogins(issue?.assignees),\n ...(string(issue?.state) === 'closed' || string(issue?.state) === 'open'\n ? { state: string(issue?.state) as 'open' | 'closed' }\n : {}),\n ...(string(issue?.state_reason) ? { stateReason: string(issue?.state_reason) } : {}),\n },\n }\n : {}),\n ...(event === 'issueEdited'\n ? { issueChange: { title: Boolean(object(changes?.title)), body: Boolean(object(changes?.body)) } }\n : {}),\n ...(number(issueComment?.id)\n ? {\n issueComment: {\n id: number(issueComment?.id)!,\n ...(string(issueComment?.body) ? { body: string(issueComment?.body) } : {}),\n ...(string(issueComment?.html_url) ? { url: string(issueComment?.html_url) } : {}),\n ...(string(object(issueComment?.user)?.login) ? { author: string(object(issueComment?.user)?.login) } : {}),\n ...(string(object(issueComment?.user)?.type) ? { authorType: string(object(issueComment?.user)?.type) } : {}),\n ...(string(issueComment?.created_at) ? { createdAt: string(issueComment?.created_at) } : {}),\n ...(string(issueComment?.updated_at) ? { updatedAt: string(issueComment?.updated_at) } : {}),\n },\n }\n : {}),\n ...(pullRequestNumber && string(pullRequest?.title) && string(pullRequest?.html_url)\n ? {\n pullRequest: {\n number: pullRequestNumber,\n title: string(pullRequest?.title)!,\n url: string(pullRequest?.html_url)!,\n ...(string(pullRequest?.created_at) ? { createdAt: string(pullRequest?.created_at) } : {}),\n state: string(pullRequest?.state) === 'closed' ? ('closed' as const) : ('open' as const),\n draft: boolean(pullRequest?.draft) ?? false,\n merged: boolean(pullRequest?.merged) ?? false,\n assignees: actorLogins(pullRequest?.assignees),\n requestedReviewers: actorLogins(pullRequest?.requested_reviewers),\n headBranch: string(object(pullRequest?.head)?.ref) ?? '',\n baseBranch: string(object(pullRequest?.base)?.ref) ?? '',\n },\n }\n : {}),\n ...(reviewRequested && requestedReviewer\n ? {\n reviewRequest: {\n reviewer: requestedReviewer,\n factoryReviewer: requestedReviewer === `${this.options.github.slug}[bot]`,\n },\n }\n : {}),\n ...(object(parsed.payload.review)\n ? {\n review: {\n id: number(object(parsed.payload.review)?.id) ?? 0,\n state: string(object(parsed.payload.review)?.state) ?? 'unknown',\n url: string(object(parsed.payload.review)?.html_url) ?? '',\n },\n }\n : {}),\n };\n\n const rule = resolveFactoryGithubRule(this.options.rules, event);\n let decision: FactoryRuleDecision | void;\n let decisions: Record<string, unknown>[] = [];\n let outcome: { status: 'accepted' | 'rejected'; code?: string; reason?: string } = { status: 'accepted' };\n try {\n decision = rule ? await withRuleTimeout(Promise.resolve(rule(Object.freeze(context)))) : undefined;\n if (decision?.type === 'reject') {\n outcome = { status: 'rejected', code: decision.code, reason: decision.reason };\n } else if (decision) {\n decisions = validateFactoryRuleDecisions([decision]).map(entry => ({ ...entry }));\n }\n } catch (error) {\n const timedOut = error instanceof Error && error.message === 'FACTORY_RULE_TIMEOUT';\n outcome = {\n status: 'rejected',\n code: timedOut ? 'timeout' : 'rule_error',\n reason: timedOut\n ? 'Factory rule evaluation timed out.'\n : error instanceof Error\n ? error.message.slice(0, 2_000)\n : 'Factory GitHub rule failed.',\n };\n }\n\n const committed = await this.options.storage.commitRuleEvaluation({\n orgId: project.orgId,\n factoryProjectId: project.factoryProjectId,\n workItemId: relatedItem?.id ?? null,\n ingress: { identity: `${installationId}:${parsed.deliveryId}`, triggerType: `github.${event}` },\n ruleSetVersion: this.options.rules.version,\n expectedRevision: relatedItem?.revision ?? null,\n actor: { ...actor },\n outcome,\n decisions,\n causalChain: [],\n now: new Date(),\n });\n return { status: committed.status };\n }\n\n async #relatedItem(\n orgId: string,\n projectId: string,\n repositoryId: number,\n repositoryFullName: string,\n issueNumber: number | undefined,\n pullRequestNumber: number | undefined,\n pullRequestHeadBranch: string | undefined,\n provenance: FactoryPullRequestProvenanceData | null,\n ): Promise<WorkItemRow | undefined> {\n const items = await this.options.storage.list({ orgId, factoryProjectId: projectId });\n if (provenance) return items.find(item => item.id === provenance.workItemId);\n if (issueNumber) {\n return (\n items.find(\n item =>\n item.externalSource?.externalId === canonicalSourceKey('issue', issueNumber) &&\n cardBelongsToRepository(item, repositoryId, repositoryFullName),\n ) ?? items.find(item => item.externalSource?.externalId === legacySourceKey(repositoryId, 'issue', issueNumber))\n );\n }\n if (pullRequestNumber) {\n return (\n items.find(\n item =>\n item.externalSource?.externalId === canonicalSourceKey('pull-request', pullRequestNumber) &&\n cardBelongsToRepository(item, repositoryId, repositoryFullName),\n ) ??\n items.find(\n item => item.externalSource?.externalId === legacySourceKey(repositoryId, 'pull-request', pullRequestNumber),\n ) ??\n // Provenance fallback: a PR pushed from a work item's session branch\n // belongs to that item even when no gh-pr-create provenance was\n // recorded (session predating state seeding, or the PR was opened\n // outside the tracked tool call). Session branches are per-item\n // (`factory/issue-N`), so a head-branch match is unambiguous.\n (pullRequestHeadBranch\n ? items.find(\n item =>\n item.externalSource?.type !== 'pull-request' &&\n Object.values(item.sessions).some(session => session.branch === pullRequestHeadBranch),\n )\n : undefined)\n );\n }\n return undefined;\n }\n}\n\nexport interface ReconcilePullRequestState {\n title: string;\n url: string;\n state: 'open' | 'closed';\n draft: boolean;\n merged: boolean;\n assignees?: string[];\n requestedReviewers?: string[];\n headBranch: string;\n baseBranch: string;\n author?: string;\n createdAt?: string;\n mergedBy?: string;\n}\n\nexport type GithubPullRequestFetcher = (input: {\n installationId: number;\n repository: string;\n number: number;\n}) => Promise<ReconcilePullRequestState | undefined>;\n\nexport interface ReconcileIssueState {\n title: string;\n url: string;\n state: 'open' | 'closed';\n /** GitHub close reason: `completed`, `not_planned`, or `duplicate`. */\n stateReason?: string;\n assignees?: string[];\n author?: string;\n createdAt?: string;\n updatedAt?: string;\n}\n\nexport type GithubIssueFetcher = (input: {\n installationId: number;\n repository: string;\n number: number;\n}) => Promise<ReconcileIssueState | undefined>;\n\nexport interface ReconcileRepository {\n id: number;\n fullName: string;\n installationId: number;\n}\n\nexport interface ReconcileSweepSummary {\n /** Factory-configured repositories included in the sweep. */\n repositories: number;\n /** PRs whose live state was fetched from GitHub. */\n checked: number;\n /** Missed merges replayed through the rules ingress. */\n merged: number;\n /** Missed closes-without-merge replayed through the rules ingress. */\n closed: number;\n /** Issue-backed work cards whose live state was fetched from GitHub. */\n issuesChecked: number;\n /** Missed issue closes replayed through the rules ingress. */\n issuesClosed: number;\n /** PRs/issues (or whole repositories) skipped because of an error. */\n failed: number;\n /** Error samples with context, capped at {@link RECONCILE_ERROR_SAMPLE_LIMIT}. */\n errors: Array<{ repository: string; pullRequestNumber?: number; issueNumber?: number; error: string }>;\n}\n\nexport type GithubPullRequestReconciler = (repositories: ReconcileRepository[]) => Promise<ReconcileSweepSummary>;\n\nconst RECONCILE_ERROR_SAMPLE_LIMIT = 5;\n\nfunction sameStrings(left: unknown, right: string[] | undefined): boolean {\n if (right === undefined) return true;\n if (!Array.isArray(left)) return false;\n const leftValues = new Set(left.flatMap(value => (typeof value === 'string' ? [value] : [])));\n const rightValues = new Set(right);\n return leftValues.size === rightValues.size && [...leftValues].every(value => rightValues.has(value));\n}\n\n/**\n * Extracts the PR number a work item tracks, but only when the item belongs\n * to the given repository. Card URLs pin the repository unambiguously; the\n * legacy source key embeds the repository id. Canonical keys (`github-pr:N`)\n * carry no repository, so they are only trusted when the item has no URL —\n * a project mapped to multiple repositories must not reconcile one repo's\n * card against another repo's PR number.\n */\nfunction reconcilablePullRequestNumber(item: WorkItemRow, repository: ReconcileRepository): number | undefined {\n if (item.externalSource?.type !== 'pull-request') return undefined;\n const url = item.externalSource.url;\n if (url) {\n const match = /^https?:\\/\\/[^/]+\\/(.+)\\/pull\\/(\\d+)(?:[/?#]|$)/.exec(url);\n if (!match) return undefined;\n return match[1] === repository.fullName ? Number(match[2]) : undefined;\n }\n const externalId = item.externalSource.externalId;\n const legacy = /^github:(\\d+):pull-request:(\\d+)$/.exec(externalId);\n if (legacy) return Number(legacy[1]) === repository.id ? Number(legacy[2]) : undefined;\n const canonical = /^github-pr:(\\d+)$/.exec(externalId);\n return canonical ? Number(canonical[1]) : undefined;\n}\n\n/**\n * Extracts the issue number a work item tracks, but only when the item\n * belongs to the given repository. Stricter than\n * {@link reconcilablePullRequestNumber}: a canonical key with no URL is only\n * trusted when the intake-stamped `githubRepositoryId` confirms the\n * repository, because the sweep initiates closes on its own.\n */\nfunction reconcilableIssueNumber(item: WorkItemRow, repository: ReconcileRepository): number | undefined {\n if (item.externalSource?.type !== 'issue') return undefined;\n const url = item.externalSource.url;\n if (url) {\n const match = /^https?:\\/\\/[^/]+\\/(.+)\\/issues\\/(\\d+)(?:[/?#]|$)/.exec(url);\n if (!match) return undefined;\n // A renamed repository leaves the old owner/name in the card URL, so a\n // URL mismatch still defers to the stable intake-stamped repository id.\n const belongs = match[1] === repository.fullName || item.metadata?.githubRepositoryId === repository.id;\n return belongs ? Number(match[2]) : undefined;\n }\n const externalId = item.externalSource.externalId;\n const legacy = /^github:(\\d+):issue:(\\d+)$/.exec(externalId);\n if (legacy) return Number(legacy[1]) === repository.id ? Number(legacy[2]) : undefined;\n const canonical = /^github-issue:(\\d+)$/.exec(externalId);\n if (!canonical) return undefined;\n // Canonical keys carry no repository; only the intake-stamped repository id\n // can attribute a URL-less card, and guessing would let a multi-repo\n // project close repo B's card because repo A's same-numbered issue closed.\n return item.metadata?.githubRepositoryId === repository.id ? Number(canonical[1]) : undefined;\n}\n\nexport function reconciledIssueClosedEvent(\n repository: ReconcileRepository,\n issueNumber: number,\n state: ReconcileIssueState,\n): ParsedGithubWebhook {\n return {\n event: 'issues',\n // Stable per (repository, issue): the ingress dedupe makes repeat\n // reconcile cycles replay instead of re-committing decisions.\n deliveryId: `reconcile:${repository.id}:issue:${issueNumber}:closed`,\n payload: {\n action: 'closed',\n installation: { id: repository.installationId },\n repository: { id: repository.id, full_name: repository.fullName },\n sender: { login: state.author ?? 'github' },\n issue: {\n number: issueNumber,\n title: state.title,\n html_url: state.url,\n state: 'closed',\n ...(state.stateReason ? { state_reason: state.stateReason } : {}),\n ...(state.createdAt ? { created_at: state.createdAt } : {}),\n ...(state.updatedAt ? { updated_at: state.updatedAt } : {}),\n assignees: (state.assignees ?? []).map(login => ({ login })),\n },\n },\n };\n}\n\nexport function reconciledClosedEvent(\n repository: ReconcileRepository,\n pullRequestNumber: number,\n state: ReconcilePullRequestState,\n): ParsedGithubWebhook {\n return {\n event: 'pull_request',\n // Stable per (repository, PR, outcome): the ingress dedupe makes repeat\n // reconcile cycles replay instead of re-committing decisions.\n deliveryId: `reconcile:${repository.id}:pull-request:${pullRequestNumber}:${state.merged ? 'merged' : 'closed'}`,\n payload: {\n action: 'closed',\n installation: { id: repository.installationId },\n repository: { id: repository.id, full_name: repository.fullName },\n sender: { login: state.mergedBy ?? 'github' },\n pull_request: {\n number: pullRequestNumber,\n title: state.title,\n html_url: state.url,\n ...(state.createdAt ? { created_at: state.createdAt } : {}),\n state: 'closed',\n draft: state.draft,\n merged: state.merged,\n assignees: (state.assignees ?? []).map(login => ({ login })),\n requested_reviewers: (state.requestedReviewers ?? []).map(login => ({ login })),\n head: { ref: state.headBranch },\n base: { ref: state.baseBranch },\n },\n },\n };\n}\n\n/**\n * The webhook handler retires subscriptions itself; the sweep replays only the\n * rules ingress, so without this the thread's PR chip and the workspace row\n * stay `open` forever on a deployment GitHub cannot reach.\n */\nasync function retireReconciledSubscriptions(\n storage: IntegrationStorageHandle,\n repository: ReconcileRepository,\n pullRequestNumber: number,\n merged: boolean,\n): Promise<void> {\n const target = changeRequestTargetKey({\n installationExternalId: String(repository.installationId),\n repositoryExternalId: String(repository.id),\n changeRequestId: String(pullRequestNumber),\n });\n const rows = await storage.subscriptions.listByTarget(target);\n await Promise.all(\n rows\n .filter(row => row.status === 'open')\n .map(row => storage.subscriptions.updateStatus(row.id, merged ? 'merged' : 'closed')),\n );\n}\n\n/**\n * State-based safety net for merge signals: webhooks and event-log tailing\n * can miss a merge (cursor gaps, downtime, terminally failed decisions), so\n * this sweep compares still-open PR cards against actual GitHub state and\n * replays the merge through the normal rules ingress when they disagree.\n */\nexport function createGithubPullRequestReconciler(\n options: GithubRulesOptions,\n fetchPullRequest: GithubPullRequestFetcher,\n fetchIssue?: GithubIssueFetcher,\n): GithubPullRequestReconciler {\n const rules = new GithubRules(options);\n return async repositories => {\n const summary: ReconcileSweepSummary = {\n repositories: 0,\n checked: 0,\n merged: 0,\n closed: 0,\n issuesChecked: 0,\n issuesClosed: 0,\n failed: 0,\n errors: [],\n };\n const recordFailure = (\n repository: ReconcileRepository,\n error: unknown,\n numbers?: { pullRequestNumber?: number; issueNumber?: number },\n ) => {\n summary.failed += 1;\n if (summary.errors.length < RECONCILE_ERROR_SAMPLE_LIMIT) {\n summary.errors.push({\n repository: repository.fullName,\n ...(numbers?.pullRequestNumber === undefined ? {} : { pullRequestNumber: numbers.pullRequestNumber }),\n ...(numbers?.issueNumber === undefined ? {} : { issueNumber: numbers.issueNumber }),\n error: error instanceof Error ? error.message : String(error),\n });\n }\n };\n // An installation can expose hundreds of repositories; only the ones\n // actually linked to a factory project can have cards to reconcile, so\n // scope the sweep to those up front instead of probing each repository.\n const configured = new Set(\n (await options.sourceControl.projectRepositories.listConfiguredExternalKeys()).map(\n key => `${key.installationExternalId}\\u0000${key.repositoryExternalId}`,\n ),\n );\n const scoped = repositories.filter(repository =>\n configured.has(`${repository.installationId}\\u0000${repository.id}`),\n );\n summary.repositories = scoped.length;\n for (const repository of scoped) {\n // One broken repository (or a failing token exchange for its\n // installation) must not abort the sweep for the others.\n let cardsByNumber: Map<number, WorkItemRow[]>;\n const issueNumbers = new Set<number>();\n try {\n const projects = await options.sourceControl.projectRepositories.listByExternalRepository({\n installationExternalId: String(repository.installationId),\n repositoryExternalId: String(repository.id),\n });\n if (projects.length === 0) continue;\n cardsByNumber = new Map<number, WorkItemRow[]>();\n for (const project of projects) {\n const items = await options.storage.list({\n orgId: project.orgId,\n factoryProjectId: project.factoryProjectId,\n });\n for (const item of items) {\n const stage = item.stages[0];\n if (fetchIssue) {\n const issueNumber = reconcilableIssueNumber(item, repository);\n // Issue cards need no metadata sync: only cards still on the\n // board can have missed a close, so terminal stages are final.\n if (issueNumber && stage !== 'done' && stage !== 'canceled') issueNumbers.add(issueNumber);\n }\n const pullRequestNumber = reconcilablePullRequestNumber(item, repository);\n if (!pullRequestNumber) continue;\n const metadata = item.metadata ?? {};\n const hasReconciledMetadata =\n (metadata.state === 'open' || metadata.state === 'closed') &&\n typeof metadata.draft === 'boolean' &&\n typeof metadata.merged === 'boolean' &&\n typeof metadata.author === 'string' &&\n Array.isArray(metadata.assignees) &&\n Array.isArray(metadata.requestedReviewers);\n if ((stage === 'done' || stage === 'canceled') && hasReconciledMetadata) continue;\n const cards = cardsByNumber.get(pullRequestNumber) ?? [];\n cards.push(item);\n cardsByNumber.set(pullRequestNumber, cards);\n }\n }\n } catch (error) {\n recordFailure(repository, error);\n continue;\n }\n for (const [pullRequestNumber, cards] of cardsByNumber) {\n try {\n const state = await fetchPullRequest({\n installationId: repository.installationId,\n repository: repository.fullName,\n number: pullRequestNumber,\n });\n summary.checked += 1;\n if (!state) continue;\n for (const card of cards) {\n const metadata = card.metadata ?? {};\n const statusChanged =\n metadata.state !== state.state || metadata.draft !== state.draft || metadata.merged !== state.merged;\n const authorChanged = state.author !== undefined && metadata.author !== state.author;\n const assigneesChanged = !sameStrings(metadata.assignees, state.assignees);\n const reviewersChanged = !sameStrings(metadata.requestedReviewers, state.requestedReviewers);\n if (!statusChanged && !authorChanged && !assigneesChanged && !reviewersChanged) continue;\n try {\n await options.storage.update({\n orgId: card.orgId,\n id: card.id,\n userId: 'factory-rule-dispatcher',\n patch: {\n metadata: {\n state: state.state,\n draft: state.draft,\n merged: state.merged,\n ...(state.author ? { author: state.author } : {}),\n ...(state.assignees ? { assignees: state.assignees } : {}),\n ...(state.requestedReviewers ? { requestedReviewers: state.requestedReviewers } : {}),\n },\n },\n });\n } catch (error) {\n recordFailure(repository, error, { pullRequestNumber });\n }\n }\n if (state.state !== 'closed') continue;\n await rules.ingest(reconciledClosedEvent(repository, pullRequestNumber, state));\n await retireReconciledSubscriptions(options.integrationStorage, repository, pullRequestNumber, state.merged);\n if (state.merged) summary.merged += 1;\n else summary.closed += 1;\n } catch (error) {\n recordFailure(repository, error, { pullRequestNumber });\n }\n }\n if (!fetchIssue) continue;\n for (const issueNumber of issueNumbers) {\n try {\n const state = await fetchIssue({\n installationId: repository.installationId,\n repository: repository.fullName,\n number: issueNumber,\n });\n summary.issuesChecked += 1;\n if (!state || state.state !== 'closed') continue;\n await rules.ingest(reconciledIssueClosedEvent(repository, issueNumber, state));\n summary.issuesClosed += 1;\n } catch (error) {\n recordFailure(repository, error, { issueNumber });\n }\n }\n }\n return summary;\n };\n}\n\nfunction githubRulesOptions(\n github: GithubRulesIntegration,\n context: IntegrationContext,\n): GithubRulesOptions | undefined {\n if (!context.rules) return undefined;\n return {\n github,\n sourceControl: context.storage.sourceControl,\n integrationStorage: context.storage.generic,\n projects: context.storage.projects,\n storage: context.rules.workItems,\n rules: context.rules.config,\n };\n}\n\nexport function attachGithubRules(\n github: GithubRulesIntegration,\n context: IntegrationContext,\n): ((event: ParsedGithubWebhook) => Promise<unknown>) | undefined {\n const options = githubRulesOptions(github, context);\n if (!options) return undefined;\n const rules = new GithubRules(options);\n return event => rules.ingest(event);\n}\n\nexport function attachGithubReconciler(\n github: GithubRulesIntegration,\n context: IntegrationContext,\n fetchPullRequest: GithubPullRequestFetcher,\n fetchIssue?: GithubIssueFetcher,\n): GithubPullRequestReconciler | undefined {\n const options = githubRulesOptions(github, context);\n if (!options) return undefined;\n return createGithubPullRequestReconciler(options, fetchPullRequest, fetchIssue);\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { SandboxFleet } from '../../sandbox/fleet.js';
|
|
2
|
+
import type { SourceControlSession, SourceControlStorageHandle } from '../../storage/domains/source-control/base.js';
|
|
3
|
+
import type { WorkItemsStorage } from '../../storage/domains/work-items/base.js';
|
|
4
|
+
/**
|
|
5
|
+
* Scrub a sandbox that is about to enter the reuse pool: force-checkout the
|
|
6
|
+
* default branch and drop the released session's local state, so idle pooled
|
|
7
|
+
* VMs don't sit on stale branches, dirty worktrees, or abandoned work while
|
|
8
|
+
* they wait for the next claim. Best-effort — the VM may already be reaped
|
|
9
|
+
* (the pooled claim then falls back to fresh provisioning) and the claim
|
|
10
|
+
* path recycles the workdir again before reuse, so a failed scrub never
|
|
11
|
+
* blocks the release.
|
|
12
|
+
*/
|
|
13
|
+
export declare function cleanReleasedSandbox(options: {
|
|
14
|
+
fleet: Pick<SandboxFleet, 'reattachSandbox'>;
|
|
15
|
+
sourceControl: Pick<SourceControlStorageHandle, 'projectRepositories' | 'repositories'>;
|
|
16
|
+
orgId: string;
|
|
17
|
+
projectRepositoryId: string;
|
|
18
|
+
sandboxId: string;
|
|
19
|
+
sandboxWorkdir: string;
|
|
20
|
+
}): Promise<void>;
|
|
21
|
+
/**
|
|
22
|
+
* Reclaim the sandbox a just-deleted user session was holding: return a remote
|
|
23
|
+
* VM to the reuse pool (scrubbed first), or tear a local/unpooled one down.
|
|
24
|
+
*
|
|
25
|
+
* Runs after the session row is already gone. Waking the VM and scrubbing a
|
|
26
|
+
* large checkout takes minutes, and none of it is something the caller can act
|
|
27
|
+
* on — the workspace has disappeared from their list either way. Nothing can
|
|
28
|
+
* claim the sandbox until `release` publishes it to the pool, so deferring the
|
|
29
|
+
* whole sequence preserves the scrub-before-reuse guarantee.
|
|
30
|
+
*/
|
|
31
|
+
export declare function reclaimDeletedSessionSandbox(options: {
|
|
32
|
+
fleet: Pick<SandboxFleet, 'provider' | 'reattachSandbox' | 'teardownSandbox'>;
|
|
33
|
+
sourceControl: Pick<SourceControlStorageHandle, 'sandboxPool' | 'projectRepositories' | 'repositories'>;
|
|
34
|
+
session: Pick<SourceControlSession, 'orgId' | 'userId' | 'projectRepositoryId' | 'sandboxId' | 'sandboxWorkdir'>;
|
|
35
|
+
}): Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* Release the sandboxes held by a work item's sessions back to the reuse pool.
|
|
38
|
+
*
|
|
39
|
+
* Called when the item commits into a terminal stage (`done` / `canceled`):
|
|
40
|
+
* its branch sessions stop receiving runs, so their VMs — which would
|
|
41
|
+
* otherwise idle until the provider reaps them — can serve the next session
|
|
42
|
+
* for the same repository instead of a fresh provision. Each session
|
|
43
|
+
* keeps its row (a reopened branch simply claims a pooled VM or provisions
|
|
44
|
+
* fresh on next use); it only loses its sandbox binding.
|
|
45
|
+
*/
|
|
46
|
+
export declare function releaseWorkItemSandboxes(options: {
|
|
47
|
+
workItems: Pick<WorkItemsStorage, 'get'>;
|
|
48
|
+
sourceControl: Pick<SourceControlStorageHandle, 'sessions' | 'sandboxPool' | 'projectRepositories' | 'repositories'>;
|
|
49
|
+
fleet: Pick<SandboxFleet, 'reattachSandbox'>;
|
|
50
|
+
orgId: string;
|
|
51
|
+
workItemId: string;
|
|
52
|
+
}): Promise<void>;
|
|
53
|
+
//# sourceMappingURL=sandbox-release.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sandbox-release.d.ts","sourceRoot":"","sources":["../../../src/integrations/github/sandbox-release.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAA0B,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACnF,OAAO,KAAK,EAAE,oBAAoB,EAAE,0BAA0B,EAAE,MAAM,8CAA8C,CAAC;AACrH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0CAA0C,CAAC;AAGjF;;;;;;;;GAQG;AACH,wBAAsB,oBAAoB,CAAC,OAAO,EAAE;IAClD,KAAK,EAAE,IAAI,CAAC,YAAY,EAAE,iBAAiB,CAAC,CAAC;IAC7C,aAAa,EAAE,IAAI,CAAC,0BAA0B,EAAE,qBAAqB,GAAG,cAAc,CAAC,CAAC;IACxF,KAAK,EAAE,MAAM,CAAC;IACd,mBAAmB,EAAE,MAAM,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;CACxB,GAAG,OAAO,CAAC,IAAI,CAAC,CAkBhB;AAED;;;;;;;;;GASG;AACH,wBAAsB,4BAA4B,CAAC,OAAO,EAAE;IAC1D,KAAK,EAAE,IAAI,CAAC,YAAY,EAAE,UAAU,GAAG,iBAAiB,GAAG,iBAAiB,CAAC,CAAC;IAC9E,aAAa,EAAE,IAAI,CAAC,0BAA0B,EAAE,aAAa,GAAG,qBAAqB,GAAG,cAAc,CAAC,CAAC;IACxG,OAAO,EAAE,IAAI,CAAC,oBAAoB,EAAE,OAAO,GAAG,QAAQ,GAAG,qBAAqB,GAAG,WAAW,GAAG,gBAAgB,CAAC,CAAC;CAClH,GAAG,OAAO,CAAC,IAAI,CAAC,CAqChB;AAED;;;;;;;;;GASG;AACH,wBAAsB,wBAAwB,CAAC,OAAO,EAAE;IACtD,SAAS,EAAE,IAAI,CAAC,gBAAgB,EAAE,KAAK,CAAC,CAAC;IACzC,aAAa,EAAE,IAAI,CAAC,0BAA0B,EAAE,UAAU,GAAG,aAAa,GAAG,qBAAqB,GAAG,cAAc,CAAC,CAAC;IACrH,KAAK,EAAE,IAAI,CAAC,YAAY,EAAE,iBAAiB,CAAC,CAAC;IAC7C,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;CACpB,GAAG,OAAO,CAAC,IAAI,CAAC,CA8BhB"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { recycleClaimedWorkdir } from "./sandbox.js";
|
|
2
|
+
//#region src/integrations/github/sandbox-release.ts
|
|
3
|
+
/**
|
|
4
|
+
* Scrub a sandbox that is about to enter the reuse pool: force-checkout the
|
|
5
|
+
* default branch and drop the released session's local state, so idle pooled
|
|
6
|
+
* VMs don't sit on stale branches, dirty worktrees, or abandoned work while
|
|
7
|
+
* they wait for the next claim. Best-effort — the VM may already be reaped
|
|
8
|
+
* (the pooled claim then falls back to fresh provisioning) and the claim
|
|
9
|
+
* path recycles the workdir again before reuse, so a failed scrub never
|
|
10
|
+
* blocks the release.
|
|
11
|
+
*/
|
|
12
|
+
async function cleanReleasedSandbox(options) {
|
|
13
|
+
try {
|
|
14
|
+
const projectRepository = await options.sourceControl.projectRepositories.get({
|
|
15
|
+
orgId: options.orgId,
|
|
16
|
+
id: options.projectRepositoryId
|
|
17
|
+
});
|
|
18
|
+
if (!projectRepository) return;
|
|
19
|
+
const repository = await options.sourceControl.repositories.get({
|
|
20
|
+
orgId: options.orgId,
|
|
21
|
+
id: projectRepository.repositoryId
|
|
22
|
+
});
|
|
23
|
+
if (!repository) return;
|
|
24
|
+
await recycleClaimedWorkdir(await options.fleet.reattachSandbox(options.sandboxId), options.sandboxWorkdir, repository.defaultBranch);
|
|
25
|
+
} catch {}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Reclaim the sandbox a just-deleted user session was holding: return a remote
|
|
29
|
+
* VM to the reuse pool (scrubbed first), or tear a local/unpooled one down.
|
|
30
|
+
*
|
|
31
|
+
* Runs after the session row is already gone. Waking the VM and scrubbing a
|
|
32
|
+
* large checkout takes minutes, and none of it is something the caller can act
|
|
33
|
+
* on — the workspace has disappeared from their list either way. Nothing can
|
|
34
|
+
* claim the sandbox until `release` publishes it to the pool, so deferring the
|
|
35
|
+
* whole sequence preserves the scrub-before-reuse guarantee.
|
|
36
|
+
*/
|
|
37
|
+
async function reclaimDeletedSessionSandbox(options) {
|
|
38
|
+
const { fleet, sourceControl, session } = options;
|
|
39
|
+
if (!session.sandboxId) return;
|
|
40
|
+
if (fleet.provider !== "local" && session.sandboxWorkdir) {
|
|
41
|
+
await cleanReleasedSandbox({
|
|
42
|
+
fleet,
|
|
43
|
+
sourceControl,
|
|
44
|
+
orgId: session.orgId,
|
|
45
|
+
projectRepositoryId: session.projectRepositoryId,
|
|
46
|
+
sandboxId: session.sandboxId,
|
|
47
|
+
sandboxWorkdir: session.sandboxWorkdir
|
|
48
|
+
});
|
|
49
|
+
await sourceControl.sandboxPool.release({
|
|
50
|
+
orgId: session.orgId,
|
|
51
|
+
projectRepositoryId: session.projectRepositoryId,
|
|
52
|
+
userId: session.userId,
|
|
53
|
+
sandboxId: session.sandboxId,
|
|
54
|
+
sandboxWorkdir: session.sandboxWorkdir
|
|
55
|
+
});
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
let sandbox;
|
|
59
|
+
try {
|
|
60
|
+
sandbox = await fleet.reattachSandbox(session.sandboxId);
|
|
61
|
+
} catch {}
|
|
62
|
+
await fleet.teardownSandbox({
|
|
63
|
+
sandboxId: session.sandboxId,
|
|
64
|
+
setSandboxId: async () => {},
|
|
65
|
+
clear: async () => {}
|
|
66
|
+
}, sandbox);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Release the sandboxes held by a work item's sessions back to the reuse pool.
|
|
70
|
+
*
|
|
71
|
+
* Called when the item commits into a terminal stage (`done` / `canceled`):
|
|
72
|
+
* its branch sessions stop receiving runs, so their VMs — which would
|
|
73
|
+
* otherwise idle until the provider reaps them — can serve the next session
|
|
74
|
+
* for the same repository instead of a fresh provision. Each session
|
|
75
|
+
* keeps its row (a reopened branch simply claims a pooled VM or provisions
|
|
76
|
+
* fresh on next use); it only loses its sandbox binding.
|
|
77
|
+
*/
|
|
78
|
+
async function releaseWorkItemSandboxes(options) {
|
|
79
|
+
const { workItems, sourceControl } = options;
|
|
80
|
+
const item = await workItems.get({
|
|
81
|
+
orgId: options.orgId,
|
|
82
|
+
id: options.workItemId
|
|
83
|
+
});
|
|
84
|
+
if (!item) return;
|
|
85
|
+
const sessionIds = [...new Set(Object.values(item.sessions).map((session) => session.sessionId))];
|
|
86
|
+
for (const sessionId of sessionIds) {
|
|
87
|
+
const session = await sourceControl.sessions.getBySessionId(sessionId);
|
|
88
|
+
if (!session || session.orgId !== options.orgId) continue;
|
|
89
|
+
if (!session.sandboxId || !session.sandboxWorkdir) continue;
|
|
90
|
+
await cleanReleasedSandbox({
|
|
91
|
+
fleet: options.fleet,
|
|
92
|
+
sourceControl,
|
|
93
|
+
orgId: session.orgId,
|
|
94
|
+
projectRepositoryId: session.projectRepositoryId,
|
|
95
|
+
sandboxId: session.sandboxId,
|
|
96
|
+
sandboxWorkdir: session.sandboxWorkdir
|
|
97
|
+
});
|
|
98
|
+
await sourceControl.sandboxPool.release({
|
|
99
|
+
orgId: session.orgId,
|
|
100
|
+
projectRepositoryId: session.projectRepositoryId,
|
|
101
|
+
userId: session.userId,
|
|
102
|
+
sandboxId: session.sandboxId,
|
|
103
|
+
sandboxWorkdir: session.sandboxWorkdir
|
|
104
|
+
});
|
|
105
|
+
await sourceControl.sessions.setSandbox({
|
|
106
|
+
id: session.id,
|
|
107
|
+
sandboxId: null,
|
|
108
|
+
sandboxWorkdir: session.sandboxWorkdir
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
//#endregion
|
|
113
|
+
export { cleanReleasedSandbox, reclaimDeletedSessionSandbox, releaseWorkItemSandboxes };
|
|
114
|
+
|
|
115
|
+
//# sourceMappingURL=sandbox-release.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sandbox-release.js","names":[],"sources":["../../../src/integrations/github/sandbox-release.ts"],"sourcesContent":["import type { MaterializationSandbox, SandboxFleet } from '../../sandbox/fleet.js';\nimport type { SourceControlSession, SourceControlStorageHandle } from '../../storage/domains/source-control/base.js';\nimport type { WorkItemsStorage } from '../../storage/domains/work-items/base.js';\nimport { recycleClaimedWorkdir } from './sandbox.js';\n\n/**\n * Scrub a sandbox that is about to enter the reuse pool: force-checkout the\n * default branch and drop the released session's local state, so idle pooled\n * VMs don't sit on stale branches, dirty worktrees, or abandoned work while\n * they wait for the next claim. Best-effort — the VM may already be reaped\n * (the pooled claim then falls back to fresh provisioning) and the claim\n * path recycles the workdir again before reuse, so a failed scrub never\n * blocks the release.\n */\nexport async function cleanReleasedSandbox(options: {\n fleet: Pick<SandboxFleet, 'reattachSandbox'>;\n sourceControl: Pick<SourceControlStorageHandle, 'projectRepositories' | 'repositories'>;\n orgId: string;\n projectRepositoryId: string;\n sandboxId: string;\n sandboxWorkdir: string;\n}): Promise<void> {\n try {\n const projectRepository = await options.sourceControl.projectRepositories.get({\n orgId: options.orgId,\n id: options.projectRepositoryId,\n });\n if (!projectRepository) return;\n const repository = await options.sourceControl.repositories.get({\n orgId: options.orgId,\n id: projectRepository.repositoryId,\n });\n if (!repository) return;\n const sandbox = await options.fleet.reattachSandbox(options.sandboxId);\n await recycleClaimedWorkdir(sandbox, options.sandboxWorkdir, repository.defaultBranch);\n } catch {\n // Reaped, unreachable, or wedged — the claim-side recycle is the\n // correctness guarantee; this scrub is hygiene for idle VMs.\n }\n}\n\n/**\n * Reclaim the sandbox a just-deleted user session was holding: return a remote\n * VM to the reuse pool (scrubbed first), or tear a local/unpooled one down.\n *\n * Runs after the session row is already gone. Waking the VM and scrubbing a\n * large checkout takes minutes, and none of it is something the caller can act\n * on — the workspace has disappeared from their list either way. Nothing can\n * claim the sandbox until `release` publishes it to the pool, so deferring the\n * whole sequence preserves the scrub-before-reuse guarantee.\n */\nexport async function reclaimDeletedSessionSandbox(options: {\n fleet: Pick<SandboxFleet, 'provider' | 'reattachSandbox' | 'teardownSandbox'>;\n sourceControl: Pick<SourceControlStorageHandle, 'sandboxPool' | 'projectRepositories' | 'repositories'>;\n session: Pick<SourceControlSession, 'orgId' | 'userId' | 'projectRepositoryId' | 'sandboxId' | 'sandboxWorkdir'>;\n}): Promise<void> {\n const { fleet, sourceControl, session } = options;\n if (!session.sandboxId) return;\n if (fleet.provider !== 'local' && session.sandboxWorkdir) {\n // Keep the remote VM alive: return it to the reuse pool so the next\n // session for this repository link and user claims it (repo already\n // cloned) instead of provisioning a fresh sandbox. Scrub the session's\n // work off the VM first so it doesn't idle with stale branches or dirty\n // state, and so nothing claims it mid-scrub.\n await cleanReleasedSandbox({\n fleet,\n sourceControl,\n orgId: session.orgId,\n projectRepositoryId: session.projectRepositoryId,\n sandboxId: session.sandboxId,\n sandboxWorkdir: session.sandboxWorkdir,\n });\n await sourceControl.sandboxPool.release({\n orgId: session.orgId,\n projectRepositoryId: session.projectRepositoryId,\n userId: session.userId,\n sandboxId: session.sandboxId,\n sandboxWorkdir: session.sandboxWorkdir,\n });\n return;\n }\n let sandbox: MaterializationSandbox | undefined;\n try {\n sandbox = await fleet.reattachSandbox(session.sandboxId);\n } catch {\n // The provider may already have reclaimed the sandbox.\n }\n // The session row is deleted, so there is no binding left to clear.\n await fleet.teardownSandbox(\n { sandboxId: session.sandboxId, setSandboxId: async () => {}, clear: async () => {} },\n sandbox,\n );\n}\n\n/**\n * Release the sandboxes held by a work item's sessions back to the reuse pool.\n *\n * Called when the item commits into a terminal stage (`done` / `canceled`):\n * its branch sessions stop receiving runs, so their VMs — which would\n * otherwise idle until the provider reaps them — can serve the next session\n * for the same repository instead of a fresh provision. Each session\n * keeps its row (a reopened branch simply claims a pooled VM or provisions\n * fresh on next use); it only loses its sandbox binding.\n */\nexport async function releaseWorkItemSandboxes(options: {\n workItems: Pick<WorkItemsStorage, 'get'>;\n sourceControl: Pick<SourceControlStorageHandle, 'sessions' | 'sandboxPool' | 'projectRepositories' | 'repositories'>;\n fleet: Pick<SandboxFleet, 'reattachSandbox'>;\n orgId: string;\n workItemId: string;\n}): Promise<void> {\n const { workItems, sourceControl } = options;\n const item = await workItems.get({ orgId: options.orgId, id: options.workItemId });\n if (!item) return;\n const sessionIds = [...new Set(Object.values(item.sessions).map(session => session.sessionId))];\n for (const sessionId of sessionIds) {\n const session = await sourceControl.sessions.getBySessionId(sessionId);\n if (!session || session.orgId !== options.orgId) continue;\n if (!session.sandboxId || !session.sandboxWorkdir) continue;\n await cleanReleasedSandbox({\n fleet: options.fleet,\n sourceControl,\n orgId: session.orgId,\n projectRepositoryId: session.projectRepositoryId,\n sandboxId: session.sandboxId,\n sandboxWorkdir: session.sandboxWorkdir,\n });\n await sourceControl.sandboxPool.release({\n orgId: session.orgId,\n projectRepositoryId: session.projectRepositoryId,\n userId: session.userId,\n sandboxId: session.sandboxId,\n sandboxWorkdir: session.sandboxWorkdir,\n });\n await sourceControl.sessions.setSandbox({\n id: session.id,\n sandboxId: null,\n sandboxWorkdir: session.sandboxWorkdir,\n });\n }\n}\n"],"mappings":";;;;;;;;;;;AAcA,eAAsB,qBAAqB,SAOzB;CAChB,IAAI;EACF,MAAM,oBAAoB,MAAM,QAAQ,cAAc,oBAAoB,IAAI;GAC5E,OAAO,QAAQ;GACf,IAAI,QAAQ;EACd,CAAC;EACD,IAAI,CAAC,mBAAmB;EACxB,MAAM,aAAa,MAAM,QAAQ,cAAc,aAAa,IAAI;GAC9D,OAAO,QAAQ;GACf,IAAI,kBAAkB;EACxB,CAAC;EACD,IAAI,CAAC,YAAY;EAEjB,MAAM,sBAAsB,MADN,QAAQ,MAAM,gBAAgB,QAAQ,SAAS,GAChC,QAAQ,gBAAgB,WAAW,aAAa;CACvF,QAAQ,CAGR;AACF;;;;;;;;;;;AAYA,eAAsB,6BAA6B,SAIjC;CAChB,MAAM,EAAE,OAAO,eAAe,YAAY;CAC1C,IAAI,CAAC,QAAQ,WAAW;CACxB,IAAI,MAAM,aAAa,WAAW,QAAQ,gBAAgB;EAMxD,MAAM,qBAAqB;GACzB;GACA;GACA,OAAO,QAAQ;GACf,qBAAqB,QAAQ;GAC7B,WAAW,QAAQ;GACnB,gBAAgB,QAAQ;EAC1B,CAAC;EACD,MAAM,cAAc,YAAY,QAAQ;GACtC,OAAO,QAAQ;GACf,qBAAqB,QAAQ;GAC7B,QAAQ,QAAQ;GAChB,WAAW,QAAQ;GACnB,gBAAgB,QAAQ;EAC1B,CAAC;EACD;CACF;CACA,IAAI;CACJ,IAAI;EACF,UAAU,MAAM,MAAM,gBAAgB,QAAQ,SAAS;CACzD,QAAQ,CAER;CAEA,MAAM,MAAM,gBACV;EAAE,WAAW,QAAQ;EAAW,cAAc,YAAY,CAAC;EAAG,OAAO,YAAY,CAAC;CAAE,GACpF,OACF;AACF;;;;;;;;;;;AAYA,eAAsB,yBAAyB,SAM7B;CAChB,MAAM,EAAE,WAAW,kBAAkB;CACrC,MAAM,OAAO,MAAM,UAAU,IAAI;EAAE,OAAO,QAAQ;EAAO,IAAI,QAAQ;CAAW,CAAC;CACjF,IAAI,CAAC,MAAM;CACX,MAAM,aAAa,CAAC,GAAG,IAAI,IAAI,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,KAAI,YAAW,QAAQ,SAAS,CAAC,CAAC;CAC9F,KAAK,MAAM,aAAa,YAAY;EAClC,MAAM,UAAU,MAAM,cAAc,SAAS,eAAe,SAAS;EACrE,IAAI,CAAC,WAAW,QAAQ,UAAU,QAAQ,OAAO;EACjD,IAAI,CAAC,QAAQ,aAAa,CAAC,QAAQ,gBAAgB;EACnD,MAAM,qBAAqB;GACzB,OAAO,QAAQ;GACf;GACA,OAAO,QAAQ;GACf,qBAAqB,QAAQ;GAC7B,WAAW,QAAQ;GACnB,gBAAgB,QAAQ;EAC1B,CAAC;EACD,MAAM,cAAc,YAAY,QAAQ;GACtC,OAAO,QAAQ;GACf,qBAAqB,QAAQ;GAC7B,QAAQ,QAAQ;GAChB,WAAW,QAAQ;GACnB,gBAAgB,QAAQ;EAC1B,CAAC;EACD,MAAM,cAAc,SAAS,WAAW;GACtC,IAAI,QAAQ;GACZ,WAAW;GACX,gBAAgB,QAAQ;EAC1B,CAAC;CACH;AACF"}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repo materialization for GitHub-backed repositories.
|
|
3
|
+
*
|
|
4
|
+
* A GitHub repo is never cloned onto the server host. Instead each project gets
|
|
5
|
+
* its own isolated sandbox (provisioned by the fleet in `../sandbox/fleet`) and
|
|
6
|
+
* the repo is cloned *inside* that sandbox. The agent's file tools and command
|
|
7
|
+
* tools then operate entirely against the remote checkout.
|
|
8
|
+
*
|
|
9
|
+
* - `ensureProjectSandbox(row)` / `teardownProjectSandbox(row)` bind the fleet's
|
|
10
|
+
* provision/reattach/teardown lifecycle to the per-(project,user) sandbox row.
|
|
11
|
+
* - `materializeRepo(row, token)` runs `git clone` (first open) or `git pull`
|
|
12
|
+
* (re-open) inside the sandbox, using a short-lived installation token that is
|
|
13
|
+
* scrubbed from the git remote afterwards so it never persists in the VM.
|
|
14
|
+
*
|
|
15
|
+
* This module owns everything git/GitHub: clone/pull, commit/push, worktrees,
|
|
16
|
+
* and `gh pr create`. Sandbox provisioning, budgets, and workdir layout live in
|
|
17
|
+
* the fleet module.
|
|
18
|
+
*/
|
|
19
|
+
import type { MaterializationSandbox, ProgressFn, SandboxFleet } from '../../sandbox/fleet.js';
|
|
20
|
+
import type { ProjectRepositorySandbox, SourceControlStorageHandle } from '../../storage/domains/source-control/base.js';
|
|
21
|
+
type SourceControlSandboxStorage = SourceControlStorageHandle['sandboxes'];
|
|
22
|
+
type MaterializationStore = Pick<SourceControlSandboxStorage, 'markMaterialized'>;
|
|
23
|
+
interface RepoMaterializationBinding {
|
|
24
|
+
id: string;
|
|
25
|
+
sandboxWorkdir: string;
|
|
26
|
+
materializedAt: Date | null;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Provision a new sandbox (persisting its provider id on first open) or
|
|
30
|
+
* reattach to the stored one. Returns a started, live sandbox.
|
|
31
|
+
*/
|
|
32
|
+
export declare function ensureProjectSandbox(options: {
|
|
33
|
+
fleet: SandboxFleet;
|
|
34
|
+
row: ProjectRepositorySandbox;
|
|
35
|
+
storage: SourceControlSandboxStorage;
|
|
36
|
+
token: string;
|
|
37
|
+
onProgress?: ProgressFn;
|
|
38
|
+
}): Promise<MaterializationSandbox>;
|
|
39
|
+
/**
|
|
40
|
+
* Tear down a user's sandbox for a project: stop the live VM (best-effort) and
|
|
41
|
+
* clear the persisted `sandboxId`/`materializedAt` on the per-(project,user)
|
|
42
|
+
* binding row so the next open re-provisions cleanly.
|
|
43
|
+
*
|
|
44
|
+
* @param row the per-(project,user) sandbox binding to tear down
|
|
45
|
+
* @param sandbox an already-reattached live sandbox to stop, when available
|
|
46
|
+
*/
|
|
47
|
+
export declare function teardownProjectSandbox(options: {
|
|
48
|
+
fleet: SandboxFleet;
|
|
49
|
+
row: ProjectRepositorySandbox;
|
|
50
|
+
storage: SourceControlSandboxStorage;
|
|
51
|
+
sandbox?: MaterializationSandbox;
|
|
52
|
+
}): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* Single-quote a string for safe POSIX shell interpolation. Wraps the value in
|
|
55
|
+
* single quotes and escapes any embedded single quote using the canonical
|
|
56
|
+
* close-quote / escaped-quote / reopen-quote sequence (`'\''`). This is the
|
|
57
|
+
* standard POSIX-safe construction and prevents the quoted string from being
|
|
58
|
+
* terminated early.
|
|
59
|
+
*/
|
|
60
|
+
export declare function shellQuote(value: string): string;
|
|
61
|
+
/** Branch checkout only fetches one ref — a much tighter budget applies. */
|
|
62
|
+
export declare const CHECKOUT_COMMAND_TIMEOUT_MS: number;
|
|
63
|
+
/** Error raised when the sandbox cannot materialize the repo (actionable). */
|
|
64
|
+
export declare class MaterializeError extends Error {
|
|
65
|
+
readonly code: 'git-missing' | 'egress-blocked' | 'clone-failed' | 'pull-failed' | 'push-failed' | 'commit-failed' | 'gh-missing' | 'pr-failed';
|
|
66
|
+
constructor(message: string, code: 'git-missing' | 'egress-blocked' | 'clone-failed' | 'pull-failed' | 'push-failed' | 'commit-failed' | 'gh-missing' | 'pr-failed');
|
|
67
|
+
}
|
|
68
|
+
/** Repo metadata needed to materialize, read from the org-owned project row. */
|
|
69
|
+
export interface RepoMaterializeInfo {
|
|
70
|
+
repoFullName: string;
|
|
71
|
+
defaultBranch: string;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Materialize the repo inside the user's sandbox. Clones on first open, pulls on
|
|
75
|
+
* re-open. Always scrubs the install token from the remote afterwards and sets
|
|
76
|
+
* `materialized_at` on the per-user sandbox binding row.
|
|
77
|
+
*
|
|
78
|
+
*/
|
|
79
|
+
export declare function materializeRepo(options: {
|
|
80
|
+
/** The per-(project,user) sandbox binding (provisioned via `ensureProjectSandbox`). */
|
|
81
|
+
row: RepoMaterializationBinding;
|
|
82
|
+
/** Repo metadata from the org-owned project row. */
|
|
83
|
+
repoInfo: RepoMaterializeInfo;
|
|
84
|
+
/** The live sandbox to run git inside. */
|
|
85
|
+
sandbox: MaterializationSandbox;
|
|
86
|
+
/** A freshly minted, short-lived installation access token. */
|
|
87
|
+
token: string;
|
|
88
|
+
storage: MaterializationStore;
|
|
89
|
+
onProgress?: ProgressFn;
|
|
90
|
+
}): Promise<void>;
|
|
91
|
+
/**
|
|
92
|
+
* Reset a pooled workdir that a new session just claimed: the previous
|
|
93
|
+
* session's branch and dirty state must not leak into the new session, so
|
|
94
|
+
* force-checkout the default branch and drop all local modifications. When
|
|
95
|
+
* the claimed VM was reaped and re-provisioned there is no checkout yet and
|
|
96
|
+
* this is a no-op (the clone path handles it). A wedged checkout falls back
|
|
97
|
+
* to wiping the workdir so `materializeRepo` re-clones inside the same VM
|
|
98
|
+
* instead of permanently failing the session.
|
|
99
|
+
*/
|
|
100
|
+
export declare function recycleClaimedWorkdir(sandbox: MaterializationSandbox, workdir: string, defaultBranch: string): Promise<void>;
|
|
101
|
+
/** Check out a session's branch inside its isolated repository clone. */
|
|
102
|
+
export declare function checkoutSessionBranch(sandbox: MaterializationSandbox, workdir: string, { branch, baseBranch, token, repoFullName, }: {
|
|
103
|
+
branch: string;
|
|
104
|
+
baseBranch: string;
|
|
105
|
+
token: string;
|
|
106
|
+
repoFullName: string;
|
|
107
|
+
}): Promise<void>;
|
|
108
|
+
/**
|
|
109
|
+
* Validate a git ref (branch) name. Server-side defense-in-depth: only allow a
|
|
110
|
+
* conservative character set so a branch can never be built into a shell
|
|
111
|
+
* command in a way that escapes quoting. Mirrors the route-layer check.
|
|
112
|
+
*/
|
|
113
|
+
export declare function isValidGitRef(value: unknown): value is string;
|
|
114
|
+
/** Identity used to author commits inside the sandbox. */
|
|
115
|
+
export interface GitIdentity {
|
|
116
|
+
name?: string | null;
|
|
117
|
+
email?: string | null;
|
|
118
|
+
/** GitHub login, used to derive a stable noreply identity when name/email are absent. */
|
|
119
|
+
login?: string | null;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Resolve a concrete `{ name, email }` for git authorship from a possibly-sparse
|
|
123
|
+
* identity. Falls back to a GitHub-style noreply identity so commits are never
|
|
124
|
+
* authored with an empty or host-derived identity.
|
|
125
|
+
*/
|
|
126
|
+
export declare function resolveGitIdentity(identity: GitIdentity): {
|
|
127
|
+
name: string;
|
|
128
|
+
email: string;
|
|
129
|
+
};
|
|
130
|
+
/**
|
|
131
|
+
* Configure `user.name` / `user.email` for the given repo working tree inside
|
|
132
|
+
* the sandbox so commits are authored correctly. Values are shell-quoted.
|
|
133
|
+
*/
|
|
134
|
+
export declare function configureGitIdentity(sandbox: MaterializationSandbox, workdir: string, identity: GitIdentity): Promise<void>;
|
|
135
|
+
/**
|
|
136
|
+
* Temporarily rewrite `origin` to a tokenized URL, run `fn` (e.g. a push), and
|
|
137
|
+
* **always** scrub the remote back to the tokenless URL in a `finally`. The
|
|
138
|
+
* token therefore only ever lives in the remote URL for the duration of the
|
|
139
|
+
* operation and is never left in the VM's git config.
|
|
140
|
+
*
|
|
141
|
+
* On the success path the scrub must succeed (a leaked token is a hard error);
|
|
142
|
+
* if it fails we surface it. On the failure path the scrub is best-effort but
|
|
143
|
+
* still attempted, and the original operation error is rethrown.
|
|
144
|
+
*/
|
|
145
|
+
export declare function withInstallToken<T>(sandbox: MaterializationSandbox, workdir: string, repoFullName: string, token: string, fn: () => Promise<T>): Promise<T>;
|
|
146
|
+
/**
|
|
147
|
+
* Push a branch back to GitHub from inside the sandbox using a short-lived
|
|
148
|
+
* installation token. The branch is ref-validated, the token is injected only
|
|
149
|
+
* into the remote URL via `withInstallToken`, and egress failures are
|
|
150
|
+
* classified into actionable errors.
|
|
151
|
+
*/
|
|
152
|
+
export declare function pushBranch(sandbox: MaterializationSandbox, workdir: string, branch: string, token: string, repoFullName: string): Promise<void>;
|
|
153
|
+
export interface CommitResult {
|
|
154
|
+
/** True when a commit was created; false when there was nothing to commit. */
|
|
155
|
+
committed: boolean;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Stage every change in the working tree and create a commit inside the
|
|
159
|
+
* sandbox. The git identity is configured first so authorship is correct. When
|
|
160
|
+
* there is nothing to commit this is a no-op (`committed: false`) rather than an
|
|
161
|
+
* error, so callers can safely commit-then-push without first diffing.
|
|
162
|
+
*
|
|
163
|
+
* @param sandbox the live sandbox containing the checkout
|
|
164
|
+
* @param workdir the worktree (or repo) path to commit in
|
|
165
|
+
* @param message the commit message (quoted; arbitrary text is safe)
|
|
166
|
+
* @param identity authorship identity for the commit
|
|
167
|
+
*/
|
|
168
|
+
export declare function commitAll(sandbox: MaterializationSandbox, workdir: string, message: string, identity: GitIdentity): Promise<CommitResult>;
|
|
169
|
+
/** Error raised when a worktree cannot be created/reused inside the sandbox. */
|
|
170
|
+
export declare class WorktreeError extends Error {
|
|
171
|
+
readonly code: 'invalid-branch' | 'worktree-failed' | 'setup-failed';
|
|
172
|
+
constructor(message: string, code: 'invalid-branch' | 'worktree-failed' | 'setup-failed');
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Reduce a (already ref-validated) branch name to a filesystem-safe directory
|
|
176
|
+
* segment for the worktree path: slashes/dots/unsafe chars collapsed to `-`.
|
|
177
|
+
* This only affects the *directory name*, never the git branch itself.
|
|
178
|
+
*
|
|
179
|
+
* Sanitization is lossy (e.g. `feat/a` and `feat-a` both reduce to `feat-a`),
|
|
180
|
+
* so an 8-char hash of the original branch is appended whenever the sanitized
|
|
181
|
+
* form differs from the input. That keeps clean names (`main`) readable while
|
|
182
|
+
* guaranteeing distinct branches never share a worktree directory.
|
|
183
|
+
*/
|
|
184
|
+
export declare function safeBranchDir(branch: string): string;
|
|
185
|
+
/**
|
|
186
|
+
* Compute the absolute worktree path for a branch, server-side only. Worktrees
|
|
187
|
+
* live alongside the repo checkout under a sibling `worktrees/` directory so the
|
|
188
|
+
* repo's `.git` is shared. Never derived from client-supplied paths.
|
|
189
|
+
*/
|
|
190
|
+
export declare function computeWorktreePath(repoWorkdir: string, branch: string): string;
|
|
191
|
+
export interface EnsureWorktreeResult {
|
|
192
|
+
worktreePath: string;
|
|
193
|
+
branch: string;
|
|
194
|
+
baseBranch: string;
|
|
195
|
+
/** True when an existing worktree was reused rather than freshly created. */
|
|
196
|
+
reused: boolean;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Create (or reuse) a git worktree + branch inside the sandbox for a unit of
|
|
200
|
+
* work. Idempotent: if a worktree already exists at the computed path it is
|
|
201
|
+
* reused. The branch is created from the freshly fetched `origin/<baseBranch>`
|
|
202
|
+
* — never the sandbox's possibly stale local ref — so new worktrees always
|
|
203
|
+
* start from the latest remote state.
|
|
204
|
+
*
|
|
205
|
+
* @param sandbox live sandbox containing the base checkout
|
|
206
|
+
* @param repoWorkdir the base repo checkout path inside the sandbox
|
|
207
|
+
* @param branch the feature branch (ref-validated server-side)
|
|
208
|
+
* @param baseBranch the branch to fork from (ref-validated; defaults to the repo's default branch)
|
|
209
|
+
* @param token short-lived installation token used only for the base-branch fetch
|
|
210
|
+
* @param repoFullName `owner/repo` used to build the tokenized remote URL
|
|
211
|
+
*/
|
|
212
|
+
export declare function ensureWorktree(sandbox: MaterializationSandbox, repoWorkdir: string, { branch, baseBranch, token, repoFullName, }: {
|
|
213
|
+
branch: string;
|
|
214
|
+
baseBranch: string;
|
|
215
|
+
token: string;
|
|
216
|
+
repoFullName: string;
|
|
217
|
+
}): Promise<EnsureWorktreeResult>;
|
|
218
|
+
/**
|
|
219
|
+
* Run the project's setup command (e.g. `pnpm i && pnpm build`) inside a
|
|
220
|
+
* freshly created worktree. Called before the worktree is handed to any agent
|
|
221
|
+
* run so the checkout is ready to build/test. A non-zero exit is a hard error —
|
|
222
|
+
* starting agent work in a half-set-up tree is worse than failing the request.
|
|
223
|
+
*
|
|
224
|
+
* Security model: the command is intentionally arbitrary shell — that is the
|
|
225
|
+
* feature (install deps, build, seed fixtures). It is only configurable by
|
|
226
|
+
* authenticated org members (the settings route is gated by
|
|
227
|
+
* `resolveOrgTenant` + org-scoped project lookup, with length and
|
|
228
|
+
* control-character validation), and it executes exclusively inside the
|
|
229
|
+
* project's isolated sandbox — the same environment where org members already
|
|
230
|
+
* run arbitrary shell via the agent's command tool. It never runs on the web
|
|
231
|
+
* server host, so it grants no privilege beyond what sandbox access already
|
|
232
|
+
* provides.
|
|
233
|
+
*
|
|
234
|
+
* @param sandbox live sandbox containing the worktree
|
|
235
|
+
* @param worktreePath the server-computed worktree path the command runs in
|
|
236
|
+
* @param command the org-configured setup shell command
|
|
237
|
+
*/
|
|
238
|
+
export declare function runWorktreeSetup(sandbox: MaterializationSandbox, worktreePath: string, command: string): Promise<void>;
|
|
239
|
+
/**
|
|
240
|
+
* Remove a worktree (and its local feature branch) from the sandbox. The
|
|
241
|
+
* checkout is removed with `--force` — the caller owns confirming that any
|
|
242
|
+
* uncommitted work in it can be discarded. Idempotent: a worktree whose
|
|
243
|
+
* directory is already gone only has its metadata pruned.
|
|
244
|
+
*
|
|
245
|
+
* @param sandbox live sandbox containing the base checkout
|
|
246
|
+
* @param repoWorkdir the base repo checkout path inside the sandbox
|
|
247
|
+
* @param branch the worktree's feature branch (ref-validated)
|
|
248
|
+
* @param worktreePath the persisted, server-computed worktree path
|
|
249
|
+
*/
|
|
250
|
+
export declare function removeWorktree(sandbox: MaterializationSandbox, repoWorkdir: string, { branch, worktreePath }: {
|
|
251
|
+
branch: string;
|
|
252
|
+
worktreePath: string;
|
|
253
|
+
}): Promise<void>;
|
|
254
|
+
export interface CreatePullRequestArgs {
|
|
255
|
+
/** Short-lived installation token, injected only into the `gh` process env. */
|
|
256
|
+
token: string;
|
|
257
|
+
/** Base branch the PR merges into. Ref-validated. */
|
|
258
|
+
base: string;
|
|
259
|
+
/** Head branch the PR is opened from. Ref-validated. */
|
|
260
|
+
head: string;
|
|
261
|
+
/** PR title. */
|
|
262
|
+
title: string;
|
|
263
|
+
/** PR body (optional). */
|
|
264
|
+
body?: string;
|
|
265
|
+
}
|
|
266
|
+
export interface CreatePullRequestResult {
|
|
267
|
+
/** The PR URL parsed from `gh pr create` stdout. */
|
|
268
|
+
url: string;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Open a pull request from inside the sandbox via `gh pr create`. The token is
|
|
272
|
+
* passed only through a per-invocation `GH_TOKEN` env scoped to the single `gh`
|
|
273
|
+
* process (never persisted), all arguments are shell-quoted, and the resulting
|
|
274
|
+
* PR URL is parsed from stdout.
|
|
275
|
+
*
|
|
276
|
+
* @param sandbox live sandbox containing the checkout
|
|
277
|
+
* @param workdir the worktree (or repo) path the PR head branch is checked out in
|
|
278
|
+
*/
|
|
279
|
+
export declare function createPullRequest(sandbox: MaterializationSandbox, workdir: string, { token, base, head, title, body }: CreatePullRequestArgs): Promise<CreatePullRequestResult>;
|
|
280
|
+
export {};
|
|
281
|
+
//# sourceMappingURL=sandbox.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sandbox.d.ts","sourceRoot":"","sources":["../../../src/integrations/github/sandbox.ts"],"names":[],"mappings":"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"}
|