@mastra/factory 0.24.0-alpha.3 → 0.24.0-alpha.4
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/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +9 -3
- package/dist/factory.js.map +1 -1
- package/dist/integrations/github/default-rules.d.ts.map +1 -1
- package/dist/integrations/github/default-rules.js +1 -16
- package/dist/integrations/github/default-rules.js.map +1 -1
- package/dist/integrations/github/sandbox-release.d.ts +15 -3
- package/dist/integrations/github/sandbox-release.d.ts.map +1 -1
- package/dist/integrations/github/sandbox-release.js +20 -3
- package/dist/integrations/github/sandbox-release.js.map +1 -1
- package/dist/integrations/github/webhook.d.ts +4 -2
- package/dist/integrations/github/webhook.d.ts.map +1 -1
- package/dist/integrations/github/webhook.js +59 -36
- package/dist/integrations/github/webhook.js.map +1 -1
- package/dist/integrations/gitlab/api.d.ts +1 -1
- package/dist/integrations/gitlab/api.d.ts.map +1 -1
- package/dist/integrations/gitlab/api.js.map +1 -1
- package/dist/integrations/incidentio/intake.d.ts +1 -1
- package/dist/integrations/incidentio/intake.d.ts.map +1 -1
- package/dist/integrations/incidentio/intake.js.map +1 -1
- package/dist/integrations/platform/connect/routes.d.ts +1 -1
- package/dist/integrations/platform/connect/routes.d.ts.map +1 -1
- package/dist/integrations/platform/connect/routes.js.map +1 -1
- package/dist/integrations/platform/incidentio/integration.d.ts +1 -1
- package/dist/integrations/platform/incidentio/integration.d.ts.map +1 -1
- package/dist/integrations/platform/incidentio/integration.js.map +1 -1
- package/dist/integrations/platform/jira/integration.d.ts +1 -1
- package/dist/integrations/platform/jira/integration.d.ts.map +1 -1
- package/dist/integrations/platform/jira/integration.js.map +1 -1
- package/dist/integrations/platform/linear/event-worker.d.ts +1 -1
- package/dist/integrations/platform/linear/event-worker.d.ts.map +1 -1
- package/dist/integrations/platform/linear/event-worker.js.map +1 -1
- package/dist/review-verdict.d.ts +11 -0
- package/dist/review-verdict.d.ts.map +1 -1
- package/dist/review-verdict.js +16 -1
- package/dist/review-verdict.js.map +1 -1
- package/dist/routes/source-control-sessions.d.ts +7 -0
- package/dist/routes/source-control-sessions.d.ts.map +1 -1
- package/dist/routes/source-control-sessions.js +121 -3
- package/dist/routes/source-control-sessions.js.map +1 -1
- package/dist/routes/surface.d.ts +2 -2
- package/dist/routes/surface.d.ts.map +1 -1
- package/dist/routes/surface.js +2 -0
- package/dist/routes/surface.js.map +1 -1
- package/dist/rules/dispatcher.d.ts.map +1 -1
- package/dist/rules/dispatcher.js +10 -1
- package/dist/rules/dispatcher.js.map +1 -1
- package/dist/sandbox/session-retirement.d.ts +27 -3
- package/dist/sandbox/session-retirement.d.ts.map +1 -1
- package/dist/sandbox/session-retirement.js +158 -20
- package/dist/sandbox/session-retirement.js.map +1 -1
- package/dist/session/credential-policy.d.ts +1 -1
- package/dist/session/credential-policy.d.ts.map +1 -1
- package/dist/session/credential-policy.js.map +1 -1
- package/dist/session/factory-session.d.ts +1 -1
- package/dist/session/factory-session.d.ts.map +1 -1
- package/dist/session/factory-session.js.map +1 -1
- package/dist/session/work-item-repository.d.ts +1 -1
- package/dist/session/work-item-repository.d.ts.map +1 -1
- package/dist/session/work-item-repository.js.map +1 -1
- package/dist/storage/domains/filesystem/base.d.ts +4 -0
- package/dist/storage/domains/filesystem/base.d.ts.map +1 -1
- package/dist/storage/domains/filesystem/base.js +5 -0
- package/dist/storage/domains/filesystem/base.js.map +1 -1
- package/dist/storage/domains/work-items/base.d.ts +7 -5
- package/dist/storage/domains/work-items/base.d.ts.map +1 -1
- package/dist/storage/domains/work-items/base.js +5 -5
- package/dist/storage/domains/work-items/base.js.map +1 -1
- package/dist/timing.d.ts +23 -0
- package/dist/timing.d.ts.map +1 -1
- package/dist/timing.js +85 -6
- package/dist/timing.js.map +1 -1
- package/dist/tools/reports.d.ts +11 -0
- package/dist/tools/reports.d.ts.map +1 -1
- package/dist/tools/reports.js +20 -1
- package/dist/tools/reports.js.map +1 -1
- package/dist/tools/run-watch.d.ts +1 -1
- package/dist/tools/run-watch.d.ts.map +1 -1
- package/dist/tools/run-watch.js.map +1 -1
- package/dist/tools/session-tools.d.ts +1 -1
- package/dist/tools/session-tools.d.ts.map +1 -1
- package/dist/tools/session-tools.js.map +1 -1
- package/dist/workspace.d.ts +1 -1
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +10 -4
- package/dist/workspace.js.map +1 -1
- package/package.json +12 -14
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { isTerminalFactoryRuleStage } from "../../rules/types.js";
|
|
2
2
|
import { hasRecordedVerdict } from "../../boards/review.js";
|
|
3
3
|
import { recordExplicitRuleOverrides } from "../../rules/event-rules.js";
|
|
4
|
-
import {
|
|
4
|
+
import { requestsChangesVerdict } from "../../review-verdict.js";
|
|
5
5
|
import { isFactoryApproveVerdict } from "./stale-reviews.js";
|
|
6
6
|
//#region src/integrations/github/default-rules.ts
|
|
7
7
|
function trustedGithubActor(context) {
|
|
@@ -189,21 +189,6 @@ function dismissStaleFactoryReviews(context) {
|
|
|
189
189
|
function pullRequestReviewSubmitted(context) {
|
|
190
190
|
return addressReviewFeedback(context) ?? dismissStaleFactoryReviews(context);
|
|
191
191
|
}
|
|
192
|
-
/**
|
|
193
|
-
* Detects the `factory-review` handoff verdict in a comment body.
|
|
194
|
-
*
|
|
195
|
-
* GitHub forbids an app from reviewing a pull request it authored, so on
|
|
196
|
-
* Factory-authored PRs the review skill falls back to posting its verdict as a
|
|
197
|
-
* plain comment. That comment is the only signal the authoring agent gets, so
|
|
198
|
-
* it has to be readable back out. The skill's handoff contract puts the verdict
|
|
199
|
-
* on the first line (`Verdict: request changes`), so only that line is
|
|
200
|
-
* inspected — a verdict quoted later in the findings must not count.
|
|
201
|
-
*/
|
|
202
|
-
function requestsChangesVerdict(body) {
|
|
203
|
-
const normalized = normalizedVerdictLine(body);
|
|
204
|
-
if (!normalized) return false;
|
|
205
|
-
return /^verdict: ?(request changes|changes requested)$/.test(normalized);
|
|
206
|
-
}
|
|
207
192
|
function addressPullRequestComment(context) {
|
|
208
193
|
if (context.reviewCommand) return reReviewRequestedPullRequest(context);
|
|
209
194
|
if (!context.item || !context.pullRequest || !context.issueComment) return;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"default-rules.js","names":[],"sources":["../../../src/integrations/github/default-rules.ts"],"sourcesContent":["import { hasRecordedVerdict } from '../../boards/review.js';\nimport { normalizedVerdictLine } from '../../review-verdict.js';\nimport { recordExplicitRuleOverrides } from '../../rules/event-rules.js';\nimport { isTerminalFactoryRuleStage } from '../../rules/types.js';\nimport type { FactoryGithubEventName, FactoryGithubRuleContext, FactoryRuleHandler } from '../../rules/types.js';\nimport { isFactoryApproveVerdict } from './stale-reviews.js';\n\nexport type GithubRuleOverrides = Partial<\n Record<FactoryGithubEventName, FactoryRuleHandler<FactoryGithubRuleContext> | null | undefined>\n>;\nexport type GithubEventRules = Readonly<\n Record<FactoryGithubEventName, FactoryRuleHandler<FactoryGithubRuleContext> | null>\n>;\n\nfunction trustedGithubActor(context: Pick<FactoryGithubRuleContext, 'actor'>): boolean {\n return context.actor.type === 'github' && context.actor.trusted;\n}\n\nfunction githubActorLogin(context: Pick<FactoryGithubRuleContext, 'actor'>): string | undefined {\n return context.actor.type === 'github' ? context.actor.login : undefined;\n}\n\nfunction retriageGithubIssue(context: FactoryGithubRuleContext) {\n if (!context.item || context.item.source !== 'github-issue' || !context.item.url) return;\n if (context.actor.type === 'github' && context.actor.factoryAuthored) return;\n // The stage checks below use Work lane names; other boards opt in with their own rules.\n if (context.board !== 'work') return;\n // Re-triage would take over a card that is being built or reviewed.\n const stage = context.item.stages?.length === 1 ? context.item.stages[0] : undefined;\n if (stage === 'execute' || stage === 'review') return;\n // A finished card has nothing left to triage; talk on a closed issue must not park a run on it.\n if (isTerminalFactoryRuleStage(context.item.stages)) return;\n\n const reason =\n context.event === 'issueEdited'\n ? context.issueChange?.title && context.issueChange.body\n ? 'issue title and body edited'\n : context.issueChange?.title\n ? 'issue title edited'\n : 'issue body edited'\n : context.event === 'issueCommentDeleted'\n ? 'comment deleted'\n : context.event === 'issueCommentEdited'\n ? 'comment edited'\n : 'comment created';\n return {\n type: 'invokeSkill',\n idempotencyKey: `${context.ingress.id}:factory-triage`,\n role: 'triage',\n skillName: 'factory-triage',\n arguments: `Re-triage GitHub issue (${context.item.url}) after ${reason}.`,\n } as const;\n}\n\nfunction createdAfterFactory(createdAt: string | undefined, factoryCreatedAt: string): boolean {\n if (!createdAt) return false;\n const sourceCreatedAt = Date.parse(createdAt);\n const projectCreatedAt = Date.parse(factoryCreatedAt);\n return Number.isFinite(sourceCreatedAt) && Number.isFinite(projectCreatedAt) && sourceCreatedAt > projectCreatedAt;\n}\n\n/**\n * Issue and pull request numbers repeat across repositories, so new cards are\n * keyed by repository. A card this repository already owns keeps its key —\n * including canonical `github-issue:N` / `github-pr:N` keys from before\n * scoping — but only when it is the same kind of source: pull request intake\n * runs with the authoring issue as `context.item`.\n */\nfunction githubSourceKey(\n context: FactoryGithubRuleContext,\n source: 'github-issue' | 'github-pr',\n itemNumber: number,\n): string {\n if (context.item?.source === source && context.item.sourceKey) return context.item.sourceKey;\n return `github:${context.repository.id}:${source === 'github-issue' ? 'issue' : 'pull-request'}:${itemNumber}`;\n}\n\nfunction issueOpened(context: FactoryGithubRuleContext) {\n if (!context.issue) return;\n // Everything arrives on the routed board's initial phase (Work › Intake when\n // no label route selects another board). Trust/timing stamps remain available\n // to custom board rules.\n return {\n type: 'upsertLinkedWorkItem',\n idempotencyKey: `${context.ingress.id}:issue-intake`,\n board: context.intake?.board ?? 'work',\n source: 'github-issue',\n sourceKey: githubSourceKey(context, 'github-issue', context.issue.number),\n title: context.issue.title,\n url: context.issue.url,\n stage: context.intake?.initialPhase ?? 'intake',\n metadata: {\n githubRepositoryId: context.repository.id,\n githubIssueNumber: context.issue.number,\n ...(context.issue.createdAt ? { sourceCreatedAt: context.issue.createdAt } : {}),\n ...(githubActorLogin(context) ? { author: githubActorLogin(context) } : {}),\n authorTrusted: trustedGithubActor(context),\n autoStartCandidate:\n trustedGithubActor(context) && createdAfterFactory(context.issue.createdAt, context.factory.createdAt),\n assignees: context.issue.assignees ?? [],\n labels: context.issue.labels ?? [],\n },\n } as const;\n}\n\nfunction issueClosed(context: FactoryGithubRuleContext) {\n if (!context.item || context.item.source !== 'github-issue' || !context.issue) return;\n // The card's board decides where a closed source sends it; boards that declare no\n // `sourceClosed` mapping keep the card where it is, as do cards already finished.\n const closure = context.boardClosure;\n if (!context.board || !closure || closure.itemTerminal) return;\n // Issue closure is a repository fact, not third-party input — no actor trust\n // gate. `not_planned` (and `duplicate`) means abandoned, everything else is\n // completed work.\n const canceled = context.issue.stateReason === 'not_planned' || context.issue.stateReason === 'duplicate';\n const target = canceled ? closure.canceled : closure.completed;\n return {\n type: 'transition',\n idempotencyKey: `${context.ingress.id}:issue-closed`,\n board: context.board,\n stage: target.phase,\n message: {\n text:\n `GitHub issue #${context.issue.number} was closed` +\n `${context.issue.stateReason ? ` (${context.issue.stateReason})` : ''}; ` +\n `this ${closure.boardTitle} card was moved to ${target.title}.`,\n },\n } as const;\n}\n\nfunction materializePullRequestIntake(\n context: FactoryGithubRuleContext,\n {\n idempotencyKey,\n autoStartCandidate,\n stage = 'intake',\n }: { idempotencyKey: string; autoStartCandidate: boolean; stage?: 'intake' | 'review' },\n) {\n if (!context.pullRequest) return;\n return {\n type: 'upsertLinkedWorkItem',\n idempotencyKey,\n board: 'review',\n source: 'github-pr',\n sourceKey: githubSourceKey(context, 'github-pr', context.pullRequest.number),\n title: context.pullRequest.title,\n url: context.pullRequest.url,\n stage,\n metadata: {\n githubRepositoryId: context.repository.id,\n githubPullRequestNumber: context.pullRequest.number,\n ...(context.pullRequest.createdAt ? { sourceCreatedAt: context.pullRequest.createdAt } : {}),\n factoryAuthored: context.pullRequest.factoryAuthored,\n authorTrusted: trustedGithubActor(context),\n autoStartCandidate,\n state: context.pullRequest.state,\n draft: context.pullRequest.draft,\n merged: context.pullRequest.merged,\n assignees: context.pullRequest.assignees ?? [],\n requestedReviewers: context.pullRequest.requestedReviewers ?? [],\n labels: context.pullRequest.labels ?? [],\n headBranch: context.pullRequest.headBranch,\n baseBranch: context.pullRequest.baseBranch,\n ...(context.pullRequest.author ? { author: context.pullRequest.author } : {}),\n },\n } as const;\n}\n\nfunction pullRequestOpened(context: FactoryGithubRuleContext) {\n if (!context.pullRequest) return;\n // Opening a pull request is evaluated once per card it concerns. This rule\n // files the pull request's own Review card, which is the arrival — the\n // evaluation carrying `pullRequestIntake` — so the authoring Work item's own\n // evaluation only moves it out for review: Building means \"no pull request\n // yet\", Review means one is open. The card rests there through every round of\n // feedback — the builder is woken in place — until the merge closes it.\n if (context.item && context.pullRequestIntake !== true) {\n if (context.board !== 'work' || context.pullRequest.state !== 'open') return;\n if (!context.item.stages.includes('execute')) return;\n // Resolution can match a Work item by branch alone, so a fork's pull\n // request must not be able to move it: same bar as auto-starting a review.\n if (!trustedGithubActor(context) && !context.pullRequest.factoryAuthored) return;\n return {\n type: 'transition',\n idempotencyKey: `${context.ingress.id}:out-for-review`,\n board: 'work',\n stage: 'review',\n } as const;\n }\n // A GitHub App bot is never a collaborator, so Factory's own PRs score\n // untrusted; their authorship is the trust signal.\n const autoStartCandidate =\n (trustedGithubActor(context) || context.pullRequest.factoryAuthored) &&\n createdAfterFactory(context.pullRequest.createdAt, context.factory.createdAt);\n return materializePullRequestIntake(context, {\n idempotencyKey: `${context.ingress.id}:pull-request-intake`,\n autoStartCandidate,\n });\n}\n\nfunction pullRequestMerged(context: FactoryGithubRuleContext) {\n if (!context.item || !context.pullRequest?.merged) return;\n if (context.board === 'review') {\n // The event is bound to the PR's own Review card: a merged PR is finished\n // review work, so always move the card to Done. The message only reaches\n // an active session (if any) — cards without one just move, instead of\n // failing retries against a binding that never existed.\n return {\n type: 'transition',\n idempotencyKey: `${context.ingress.id}:pull-request-merged`,\n board: 'review',\n stage: 'done',\n message: {\n text:\n `Pull request #${context.pullRequest.number} merged; this Review card was moved to Done. ` +\n 'No further review is needed unless follow-up work was requested.',\n },\n } as const;\n }\n // Custom boards define their own lifecycle and cannot accept a transition\n // targeting the built-in Work board. Let their custom rules handle the merge.\n if (context.board !== 'work') return;\n // Provenance bound the event to the originating Work item instead: the merge\n // is what finishes the work, so it closes the Work card alongside its Review card —\n // unless the card has since opened another pull request, which is still out.\n const openPullRequestNumber = context.item.metadata?.openPullRequestNumber;\n if (typeof openPullRequestNumber === 'number' && openPullRequestNumber !== context.pullRequest.number) {\n return {\n type: 'sendMessage',\n idempotencyKey: `${context.ingress.id}:work-merged`,\n role: 'work',\n message:\n `Pull request #${context.pullRequest.number} merged. Pull request #${openPullRequestNumber} is still open, ` +\n 'so this Work card stays in Review until it merges.',\n } as const;\n }\n return {\n type: 'transition',\n idempotencyKey: `${context.ingress.id}:work-merged`,\n board: 'work',\n stage: 'done',\n message: { text: `Pull request #${context.pullRequest.number} merged; this Work card was moved to Done.` },\n } as const;\n}\n\nfunction addressReviewFeedback(context: FactoryGithubRuleContext) {\n if (!context.item || !context.pullRequest || !context.review) return;\n // Only the Work item that authored the PR can act on the feedback. Provenance\n // binds the event there; a Review card seeing its own posted review must not\n // react to it (that would loop the reviewer against itself).\n if (context.board !== 'work') return;\n // A closed or merged pull request has no branch left to push fixes to.\n if (context.pullRequest.state !== 'open' || context.pullRequest.merged) return;\n // `approved` needs no work, and `commented` (a review body with no verdict)\n // is how a reviewer leaves notes without blocking — only a verdict that asks\n // for changes should pull the author back in.\n if (context.review.state.toLowerCase() !== 'changes_requested') return;\n // The authoring thread is already subscribed to this PR (`gh pr create`\n // subscribes automatically), so it can read the individual line comments\n // from its own notification inbox — the message only has to wake it and\n // point at the review.\n return {\n type: 'sendMessage',\n idempotencyKey: `${context.ingress.id}:address-review-feedback`,\n role: 'work',\n priority: 'high',\n message:\n `Changes were requested on pull request #${context.pullRequest.number} (${context.review.url}). ` +\n 'Read the review comments on this PR, address the ones you agree with, and push the fixes to the PR branch. ' +\n 'Reply on GitHub to anything you are deliberately not changing, explaining why.',\n } as const;\n}\n\n/**\n * A Factory approval does not clear a change request left by a different\n * Factory identity (an earlier reviewer token, say), so GitHub would keep the\n * pull request blocked. Ask the dispatcher to dismiss those superseded reviews.\n */\nfunction dismissStaleFactoryReviews(context: FactoryGithubRuleContext) {\n const { pullRequest, review, repository } = context;\n if (!pullRequest || !review || !review.author || !repository.installationId) return;\n if (review.state.toLowerCase() !== 'approved' || !isFactoryApproveVerdict(review.body)) return;\n if (!pullRequest.factoryAuthored || pullRequest.state !== 'open' || pullRequest.merged) return;\n return {\n type: 'dismissStaleReviews',\n idempotencyKey: `${context.ingress.id}:dismiss-stale-reviews`,\n installationId: repository.installationId,\n repository: repository.fullName,\n pullRequestNumber: pullRequest.number,\n approvingReviewId: String(review.id),\n approvingAuthor: review.author,\n } as const;\n}\n\nfunction pullRequestReviewSubmitted(context: FactoryGithubRuleContext) {\n return addressReviewFeedback(context) ?? dismissStaleFactoryReviews(context);\n}\n\n/**\n * Detects the `factory-review` handoff verdict in a comment body.\n *\n * GitHub forbids an app from reviewing a pull request it authored, so on\n * Factory-authored PRs the review skill falls back to posting its verdict as a\n * plain comment. That comment is the only signal the authoring agent gets, so\n * it has to be readable back out. The skill's handoff contract puts the verdict\n * on the first line (`Verdict: request changes`), so only that line is\n * inspected — a verdict quoted later in the findings must not count.\n */\nfunction requestsChangesVerdict(body: string | undefined): boolean {\n // Tolerate the markdown the skill wraps the line in (`**Verdict: ...**`).\n const normalized = normalizedVerdictLine(body);\n if (!normalized) return false;\n // Match the verdict exactly so negated phrasings (\"Verdict: do not request\n // changes\") cannot wake the author.\n return /^verdict: ?(request changes|changes requested)$/.test(normalized);\n}\n\nfunction addressPullRequestComment(context: FactoryGithubRuleContext) {\n // A validated Factory mention is a review-entry request, not feedback for the\n // authoring Work session. Invalid or unrecognized comments retain the normal\n // feedback route below.\n if (context.reviewCommand) return reReviewRequestedPullRequest(context);\n if (!context.item || !context.pullRequest || !context.issueComment) return;\n // Provenance binds the comment to the Work item that authored the PR — the\n // only session that can act on it. A Review card must not react to comments\n // on the PR it is reviewing.\n if (context.board !== 'work') return;\n if (context.pullRequest.state !== 'open' || context.pullRequest.merged) return;\n // `factoryAuthored` is one bit for the whole Factory, so Factory's own\n // comments are indistinguishable between roles — waking on all of them would\n // let the Work agent's own progress comments wake itself in a loop. The one\n // exception is the review verdict the Review run had to post as a comment\n // because GitHub refused a self-review: that is the handoff, and it only ever\n // asks for changes once per review, so it cannot sustain a loop.\n if (\n context.actor.type === 'github' &&\n context.actor.factoryAuthored &&\n !requestsChangesVerdict(context.issueComment.body)\n ) {\n return;\n }\n return {\n type: 'sendMessage',\n idempotencyKey: `${context.ingress.id}:address-pull-request-comment`,\n role: 'work',\n priority: 'high',\n message:\n `${context.issueComment.author ?? 'Someone'} commented on pull request #${context.pullRequest.number} ` +\n `(${context.issueComment.url ?? context.pullRequest.url}). Read the comment, address it if you agree, and push ` +\n 'the fixes to the PR branch. Reply on GitHub to anything you are deliberately not changing, explaining why.',\n } as const;\n}\n\nfunction pullRequestClosed(context: FactoryGithubRuleContext) {\n if (!context.item || !context.pullRequest || context.pullRequest.merged) return;\n if (context.board !== 'review') return;\n // A PR closed without merging is abandoned review work: clear the card off\n // the board instead of leaving it in Reviewing forever.\n return {\n type: 'transition',\n idempotencyKey: `${context.ingress.id}:pull-request-closed`,\n board: 'review',\n stage: 'canceled',\n message: {\n text:\n `Pull request #${context.pullRequest.number} was closed without merging; ` +\n 'this Review card was moved to Canceled.',\n },\n } as const;\n}\n\nfunction reReviewRequestedPullRequest(context: FactoryGithubRuleContext) {\n // GitHub reviewer requests and Factory's exact mention command are both\n // explicit requests to enter the same Review lifecycle.\n const factoryReviewEntry = context.reviewRequest?.factoryReviewer || context.reviewCommand !== undefined;\n if ((context.item && context.board !== 'review') || !factoryReviewEntry) return;\n if (!context.pullRequest || context.pullRequest.state !== 'open' || context.pullRequest.merged) return;\n // Trusted (write/admin) requesters only: creating or re-entering review checks\n // out and executes PR code, the same bar pullRequestOpened applies to auto-review.\n if (!trustedGithubActor(context)) return;\n if (context.actor.type === 'github' && context.actor.factoryAuthored) return;\n if (!context.item) {\n // On this path the actor is the *requester*, so the materialized\n // `authorTrusted` stamp records their trust (always true past the gate\n // above), not the PR author's: a trusted maintainer requesting a Factory\n // review vouches for the PR.\n return materializePullRequestIntake(context, {\n idempotencyKey: `${context.ingress.id}:pull-request-review-requested-intake`,\n autoStartCandidate: true,\n stage: 'review',\n });\n }\n // Already in Reviewing with no verdict yet: a pass is pending or running.\n // Once a verdict is recorded the card rests in Reviewing, so a fresh request\n // re-enters the stage to start another pass.\n const reviewing = context.item.stages.length === 1 && context.item.stages[0] === 'review';\n if (reviewing && !hasRecordedVerdict(context.item)) return;\n return {\n type: 'transition',\n idempotencyKey: `${context.ingress.id}:re-review-requested`,\n board: 'review',\n stage: 'review',\n ...(reviewing ? { reenter: true } : {}),\n } as const;\n}\n\nfunction reReviewUpdatedPullRequest(context: FactoryGithubRuleContext) {\n if (!context.item || context.board !== 'review') return;\n if (!context.pullRequest || context.pullRequest.state !== 'open' || context.pullRequest.merged) return;\n // Intake has not started a review pass yet, so a push there is just more of\n // the code the first pass will read. A push to a card sitting in Reviewing is\n // different: it invalidates whatever that pass is reading, so re-enter the\n // stage to supersede it. `reviewPullRequest` cancels the stale run and picks\n // the right skill for the entry it sees.\n if (context.item.stages.some(stage => stage === 'intake')) return;\n // A fork's author controls its head branch, so an untrusted push must not be\n // able to start review passes on demand; a maintainer can still request one.\n if (!trustedGithubActor(context) && !context.pullRequest.factoryAuthored) return;\n const alreadyReviewing = context.item.stages.some(stage => stage === 'review');\n return {\n type: 'transition',\n idempotencyKey: `${context.ingress.id}:re-review-updated`,\n board: 'review',\n stage: 'review',\n // Re-entry is the point when the card is already Reviewing: the stage's\n // entry rule is what cancels the superseded pass and starts one on the code\n // that just landed.\n ...(alreadyReviewing ? { reenter: true } : {}),\n } as const;\n}\n\nexport const defaultGithubRules = Object.freeze({\n issueOpened: issueOpened,\n issueEdited: retriageGithubIssue,\n issueClosed: issueClosed,\n issueCommentCreated: retriageGithubIssue,\n issueCommentEdited: retriageGithubIssue,\n issueCommentDeleted: retriageGithubIssue,\n pullRequestOpened: pullRequestOpened,\n pullRequestUpdated: reReviewUpdatedPullRequest,\n pullRequestCommentCreated: addressPullRequestComment,\n pullRequestReviewRequested: reReviewRequestedPullRequest,\n pullRequestReviewSubmitted: pullRequestReviewSubmitted,\n pullRequestMerged: pullRequestMerged,\n pullRequestClosed: pullRequestClosed,\n} satisfies GithubEventRules);\n\nexport function resolveGithubRules(overrides?: GithubRuleOverrides): GithubEventRules {\n if (overrides !== undefined && (overrides === null || typeof overrides !== 'object' || Array.isArray(overrides))) {\n throw new Error('GitHub rules must be an object.');\n }\n const rules: Record<string, FactoryRuleHandler<FactoryGithubRuleContext> | null> = { ...defaultGithubRules };\n for (const key of Reflect.ownKeys(overrides ?? {})) {\n if (typeof key !== 'string' || !Object.hasOwn(defaultGithubRules, key)) {\n throw new Error(`Unknown GitHub rule event: ${String(key)}.`);\n }\n const handler = overrides?.[key as FactoryGithubEventName];\n if (handler !== undefined && handler !== null && typeof handler !== 'function') {\n throw new Error(`GitHub rule ${key} must be a function, null, or undefined.`);\n }\n if (handler !== undefined) rules[key] = handler;\n }\n return recordExplicitRuleOverrides(Object.freeze(rules) as GithubEventRules, overrides);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"default-rules.js","names":[],"sources":["../../../src/integrations/github/default-rules.ts"],"sourcesContent":["import { hasRecordedVerdict } from '../../boards/review.js';\nimport { requestsChangesVerdict } from '../../review-verdict.js';\nimport { recordExplicitRuleOverrides } from '../../rules/event-rules.js';\nimport { isTerminalFactoryRuleStage } from '../../rules/types.js';\nimport type { FactoryGithubEventName, FactoryGithubRuleContext, FactoryRuleHandler } from '../../rules/types.js';\nimport { isFactoryApproveVerdict } from './stale-reviews.js';\n\nexport type GithubRuleOverrides = Partial<\n Record<FactoryGithubEventName, FactoryRuleHandler<FactoryGithubRuleContext> | null | undefined>\n>;\nexport type GithubEventRules = Readonly<\n Record<FactoryGithubEventName, FactoryRuleHandler<FactoryGithubRuleContext> | null>\n>;\n\nfunction trustedGithubActor(context: Pick<FactoryGithubRuleContext, 'actor'>): boolean {\n return context.actor.type === 'github' && context.actor.trusted;\n}\n\nfunction githubActorLogin(context: Pick<FactoryGithubRuleContext, 'actor'>): string | undefined {\n return context.actor.type === 'github' ? context.actor.login : undefined;\n}\n\nfunction retriageGithubIssue(context: FactoryGithubRuleContext) {\n if (!context.item || context.item.source !== 'github-issue' || !context.item.url) return;\n if (context.actor.type === 'github' && context.actor.factoryAuthored) return;\n // The stage checks below use Work lane names; other boards opt in with their own rules.\n if (context.board !== 'work') return;\n // Re-triage would take over a card that is being built or reviewed.\n const stage = context.item.stages?.length === 1 ? context.item.stages[0] : undefined;\n if (stage === 'execute' || stage === 'review') return;\n // A finished card has nothing left to triage; talk on a closed issue must not park a run on it.\n if (isTerminalFactoryRuleStage(context.item.stages)) return;\n\n const reason =\n context.event === 'issueEdited'\n ? context.issueChange?.title && context.issueChange.body\n ? 'issue title and body edited'\n : context.issueChange?.title\n ? 'issue title edited'\n : 'issue body edited'\n : context.event === 'issueCommentDeleted'\n ? 'comment deleted'\n : context.event === 'issueCommentEdited'\n ? 'comment edited'\n : 'comment created';\n return {\n type: 'invokeSkill',\n idempotencyKey: `${context.ingress.id}:factory-triage`,\n role: 'triage',\n skillName: 'factory-triage',\n arguments: `Re-triage GitHub issue (${context.item.url}) after ${reason}.`,\n } as const;\n}\n\nfunction createdAfterFactory(createdAt: string | undefined, factoryCreatedAt: string): boolean {\n if (!createdAt) return false;\n const sourceCreatedAt = Date.parse(createdAt);\n const projectCreatedAt = Date.parse(factoryCreatedAt);\n return Number.isFinite(sourceCreatedAt) && Number.isFinite(projectCreatedAt) && sourceCreatedAt > projectCreatedAt;\n}\n\n/**\n * Issue and pull request numbers repeat across repositories, so new cards are\n * keyed by repository. A card this repository already owns keeps its key —\n * including canonical `github-issue:N` / `github-pr:N` keys from before\n * scoping — but only when it is the same kind of source: pull request intake\n * runs with the authoring issue as `context.item`.\n */\nfunction githubSourceKey(\n context: FactoryGithubRuleContext,\n source: 'github-issue' | 'github-pr',\n itemNumber: number,\n): string {\n if (context.item?.source === source && context.item.sourceKey) return context.item.sourceKey;\n return `github:${context.repository.id}:${source === 'github-issue' ? 'issue' : 'pull-request'}:${itemNumber}`;\n}\n\nfunction issueOpened(context: FactoryGithubRuleContext) {\n if (!context.issue) return;\n // Everything arrives on the routed board's initial phase (Work › Intake when\n // no label route selects another board). Trust/timing stamps remain available\n // to custom board rules.\n return {\n type: 'upsertLinkedWorkItem',\n idempotencyKey: `${context.ingress.id}:issue-intake`,\n board: context.intake?.board ?? 'work',\n source: 'github-issue',\n sourceKey: githubSourceKey(context, 'github-issue', context.issue.number),\n title: context.issue.title,\n url: context.issue.url,\n stage: context.intake?.initialPhase ?? 'intake',\n metadata: {\n githubRepositoryId: context.repository.id,\n githubIssueNumber: context.issue.number,\n ...(context.issue.createdAt ? { sourceCreatedAt: context.issue.createdAt } : {}),\n ...(githubActorLogin(context) ? { author: githubActorLogin(context) } : {}),\n authorTrusted: trustedGithubActor(context),\n autoStartCandidate:\n trustedGithubActor(context) && createdAfterFactory(context.issue.createdAt, context.factory.createdAt),\n assignees: context.issue.assignees ?? [],\n labels: context.issue.labels ?? [],\n },\n } as const;\n}\n\nfunction issueClosed(context: FactoryGithubRuleContext) {\n if (!context.item || context.item.source !== 'github-issue' || !context.issue) return;\n // The card's board decides where a closed source sends it; boards that declare no\n // `sourceClosed` mapping keep the card where it is, as do cards already finished.\n const closure = context.boardClosure;\n if (!context.board || !closure || closure.itemTerminal) return;\n // Issue closure is a repository fact, not third-party input — no actor trust\n // gate. `not_planned` (and `duplicate`) means abandoned, everything else is\n // completed work.\n const canceled = context.issue.stateReason === 'not_planned' || context.issue.stateReason === 'duplicate';\n const target = canceled ? closure.canceled : closure.completed;\n return {\n type: 'transition',\n idempotencyKey: `${context.ingress.id}:issue-closed`,\n board: context.board,\n stage: target.phase,\n message: {\n text:\n `GitHub issue #${context.issue.number} was closed` +\n `${context.issue.stateReason ? ` (${context.issue.stateReason})` : ''}; ` +\n `this ${closure.boardTitle} card was moved to ${target.title}.`,\n },\n } as const;\n}\n\nfunction materializePullRequestIntake(\n context: FactoryGithubRuleContext,\n {\n idempotencyKey,\n autoStartCandidate,\n stage = 'intake',\n }: { idempotencyKey: string; autoStartCandidate: boolean; stage?: 'intake' | 'review' },\n) {\n if (!context.pullRequest) return;\n return {\n type: 'upsertLinkedWorkItem',\n idempotencyKey,\n board: 'review',\n source: 'github-pr',\n sourceKey: githubSourceKey(context, 'github-pr', context.pullRequest.number),\n title: context.pullRequest.title,\n url: context.pullRequest.url,\n stage,\n metadata: {\n githubRepositoryId: context.repository.id,\n githubPullRequestNumber: context.pullRequest.number,\n ...(context.pullRequest.createdAt ? { sourceCreatedAt: context.pullRequest.createdAt } : {}),\n factoryAuthored: context.pullRequest.factoryAuthored,\n authorTrusted: trustedGithubActor(context),\n autoStartCandidate,\n state: context.pullRequest.state,\n draft: context.pullRequest.draft,\n merged: context.pullRequest.merged,\n assignees: context.pullRequest.assignees ?? [],\n requestedReviewers: context.pullRequest.requestedReviewers ?? [],\n labels: context.pullRequest.labels ?? [],\n headBranch: context.pullRequest.headBranch,\n baseBranch: context.pullRequest.baseBranch,\n ...(context.pullRequest.author ? { author: context.pullRequest.author } : {}),\n },\n } as const;\n}\n\nfunction pullRequestOpened(context: FactoryGithubRuleContext) {\n if (!context.pullRequest) return;\n // Opening a pull request is evaluated once per card it concerns. This rule\n // files the pull request's own Review card, which is the arrival — the\n // evaluation carrying `pullRequestIntake` — so the authoring Work item's own\n // evaluation only moves it out for review: Building means \"no pull request\n // yet\", Review means one is open. The card rests there through every round of\n // feedback — the builder is woken in place — until the merge closes it.\n if (context.item && context.pullRequestIntake !== true) {\n if (context.board !== 'work' || context.pullRequest.state !== 'open') return;\n if (!context.item.stages.includes('execute')) return;\n // Resolution can match a Work item by branch alone, so a fork's pull\n // request must not be able to move it: same bar as auto-starting a review.\n if (!trustedGithubActor(context) && !context.pullRequest.factoryAuthored) return;\n return {\n type: 'transition',\n idempotencyKey: `${context.ingress.id}:out-for-review`,\n board: 'work',\n stage: 'review',\n } as const;\n }\n // A GitHub App bot is never a collaborator, so Factory's own PRs score\n // untrusted; their authorship is the trust signal.\n const autoStartCandidate =\n (trustedGithubActor(context) || context.pullRequest.factoryAuthored) &&\n createdAfterFactory(context.pullRequest.createdAt, context.factory.createdAt);\n return materializePullRequestIntake(context, {\n idempotencyKey: `${context.ingress.id}:pull-request-intake`,\n autoStartCandidate,\n });\n}\n\nfunction pullRequestMerged(context: FactoryGithubRuleContext) {\n if (!context.item || !context.pullRequest?.merged) return;\n if (context.board === 'review') {\n // The event is bound to the PR's own Review card: a merged PR is finished\n // review work, so always move the card to Done. The message only reaches\n // an active session (if any) — cards without one just move, instead of\n // failing retries against a binding that never existed.\n return {\n type: 'transition',\n idempotencyKey: `${context.ingress.id}:pull-request-merged`,\n board: 'review',\n stage: 'done',\n message: {\n text:\n `Pull request #${context.pullRequest.number} merged; this Review card was moved to Done. ` +\n 'No further review is needed unless follow-up work was requested.',\n },\n } as const;\n }\n // Custom boards define their own lifecycle and cannot accept a transition\n // targeting the built-in Work board. Let their custom rules handle the merge.\n if (context.board !== 'work') return;\n // Provenance bound the event to the originating Work item instead: the merge\n // is what finishes the work, so it closes the Work card alongside its Review card —\n // unless the card has since opened another pull request, which is still out.\n const openPullRequestNumber = context.item.metadata?.openPullRequestNumber;\n if (typeof openPullRequestNumber === 'number' && openPullRequestNumber !== context.pullRequest.number) {\n return {\n type: 'sendMessage',\n idempotencyKey: `${context.ingress.id}:work-merged`,\n role: 'work',\n message:\n `Pull request #${context.pullRequest.number} merged. Pull request #${openPullRequestNumber} is still open, ` +\n 'so this Work card stays in Review until it merges.',\n } as const;\n }\n return {\n type: 'transition',\n idempotencyKey: `${context.ingress.id}:work-merged`,\n board: 'work',\n stage: 'done',\n message: { text: `Pull request #${context.pullRequest.number} merged; this Work card was moved to Done.` },\n } as const;\n}\n\nfunction addressReviewFeedback(context: FactoryGithubRuleContext) {\n if (!context.item || !context.pullRequest || !context.review) return;\n // Only the Work item that authored the PR can act on the feedback. Provenance\n // binds the event there; a Review card seeing its own posted review must not\n // react to it (that would loop the reviewer against itself).\n if (context.board !== 'work') return;\n // A closed or merged pull request has no branch left to push fixes to.\n if (context.pullRequest.state !== 'open' || context.pullRequest.merged) return;\n // `approved` needs no work, and `commented` (a review body with no verdict)\n // is how a reviewer leaves notes without blocking — only a verdict that asks\n // for changes should pull the author back in.\n if (context.review.state.toLowerCase() !== 'changes_requested') return;\n // The authoring thread is already subscribed to this PR (`gh pr create`\n // subscribes automatically), so it can read the individual line comments\n // from its own notification inbox — the message only has to wake it and\n // point at the review.\n return {\n type: 'sendMessage',\n idempotencyKey: `${context.ingress.id}:address-review-feedback`,\n role: 'work',\n priority: 'high',\n message:\n `Changes were requested on pull request #${context.pullRequest.number} (${context.review.url}). ` +\n 'Read the review comments on this PR, address the ones you agree with, and push the fixes to the PR branch. ' +\n 'Reply on GitHub to anything you are deliberately not changing, explaining why.',\n } as const;\n}\n\n/**\n * A Factory approval does not clear a change request left by a different\n * Factory identity (an earlier reviewer token, say), so GitHub would keep the\n * pull request blocked. Ask the dispatcher to dismiss those superseded reviews.\n */\nfunction dismissStaleFactoryReviews(context: FactoryGithubRuleContext) {\n const { pullRequest, review, repository } = context;\n if (!pullRequest || !review || !review.author || !repository.installationId) return;\n if (review.state.toLowerCase() !== 'approved' || !isFactoryApproveVerdict(review.body)) return;\n if (!pullRequest.factoryAuthored || pullRequest.state !== 'open' || pullRequest.merged) return;\n return {\n type: 'dismissStaleReviews',\n idempotencyKey: `${context.ingress.id}:dismiss-stale-reviews`,\n installationId: repository.installationId,\n repository: repository.fullName,\n pullRequestNumber: pullRequest.number,\n approvingReviewId: String(review.id),\n approvingAuthor: review.author,\n } as const;\n}\n\nfunction pullRequestReviewSubmitted(context: FactoryGithubRuleContext) {\n return addressReviewFeedback(context) ?? dismissStaleFactoryReviews(context);\n}\n\nfunction addressPullRequestComment(context: FactoryGithubRuleContext) {\n // A validated Factory mention is a review-entry request, not feedback for the\n // authoring Work session. Invalid or unrecognized comments retain the normal\n // feedback route below.\n if (context.reviewCommand) return reReviewRequestedPullRequest(context);\n if (!context.item || !context.pullRequest || !context.issueComment) return;\n // Provenance binds the comment to the Work item that authored the PR — the\n // only session that can act on it. A Review card must not react to comments\n // on the PR it is reviewing.\n if (context.board !== 'work') return;\n if (context.pullRequest.state !== 'open' || context.pullRequest.merged) return;\n // `factoryAuthored` is one bit for the whole Factory, so Factory's own\n // comments are indistinguishable between roles — waking on all of them would\n // let the Work agent's own progress comments wake itself in a loop. The one\n // exception is the review verdict the Review run had to post as a comment\n // because GitHub refused a self-review: that is the handoff, and it only ever\n // asks for changes once per review, so it cannot sustain a loop.\n if (\n context.actor.type === 'github' &&\n context.actor.factoryAuthored &&\n !requestsChangesVerdict(context.issueComment.body)\n ) {\n return;\n }\n return {\n type: 'sendMessage',\n idempotencyKey: `${context.ingress.id}:address-pull-request-comment`,\n role: 'work',\n priority: 'high',\n message:\n `${context.issueComment.author ?? 'Someone'} commented on pull request #${context.pullRequest.number} ` +\n `(${context.issueComment.url ?? context.pullRequest.url}). Read the comment, address it if you agree, and push ` +\n 'the fixes to the PR branch. Reply on GitHub to anything you are deliberately not changing, explaining why.',\n } as const;\n}\n\nfunction pullRequestClosed(context: FactoryGithubRuleContext) {\n if (!context.item || !context.pullRequest || context.pullRequest.merged) return;\n if (context.board !== 'review') return;\n // A PR closed without merging is abandoned review work: clear the card off\n // the board instead of leaving it in Reviewing forever.\n return {\n type: 'transition',\n idempotencyKey: `${context.ingress.id}:pull-request-closed`,\n board: 'review',\n stage: 'canceled',\n message: {\n text:\n `Pull request #${context.pullRequest.number} was closed without merging; ` +\n 'this Review card was moved to Canceled.',\n },\n } as const;\n}\n\nfunction reReviewRequestedPullRequest(context: FactoryGithubRuleContext) {\n // GitHub reviewer requests and Factory's exact mention command are both\n // explicit requests to enter the same Review lifecycle.\n const factoryReviewEntry = context.reviewRequest?.factoryReviewer || context.reviewCommand !== undefined;\n if ((context.item && context.board !== 'review') || !factoryReviewEntry) return;\n if (!context.pullRequest || context.pullRequest.state !== 'open' || context.pullRequest.merged) return;\n // Trusted (write/admin) requesters only: creating or re-entering review checks\n // out and executes PR code, the same bar pullRequestOpened applies to auto-review.\n if (!trustedGithubActor(context)) return;\n if (context.actor.type === 'github' && context.actor.factoryAuthored) return;\n if (!context.item) {\n // On this path the actor is the *requester*, so the materialized\n // `authorTrusted` stamp records their trust (always true past the gate\n // above), not the PR author's: a trusted maintainer requesting a Factory\n // review vouches for the PR.\n return materializePullRequestIntake(context, {\n idempotencyKey: `${context.ingress.id}:pull-request-review-requested-intake`,\n autoStartCandidate: true,\n stage: 'review',\n });\n }\n // Already in Reviewing with no verdict yet: a pass is pending or running.\n // Once a verdict is recorded the card rests in Reviewing, so a fresh request\n // re-enters the stage to start another pass.\n const reviewing = context.item.stages.length === 1 && context.item.stages[0] === 'review';\n if (reviewing && !hasRecordedVerdict(context.item)) return;\n return {\n type: 'transition',\n idempotencyKey: `${context.ingress.id}:re-review-requested`,\n board: 'review',\n stage: 'review',\n ...(reviewing ? { reenter: true } : {}),\n } as const;\n}\n\nfunction reReviewUpdatedPullRequest(context: FactoryGithubRuleContext) {\n if (!context.item || context.board !== 'review') return;\n if (!context.pullRequest || context.pullRequest.state !== 'open' || context.pullRequest.merged) return;\n // Intake has not started a review pass yet, so a push there is just more of\n // the code the first pass will read. A push to a card sitting in Reviewing is\n // different: it invalidates whatever that pass is reading, so re-enter the\n // stage to supersede it. `reviewPullRequest` cancels the stale run and picks\n // the right skill for the entry it sees.\n if (context.item.stages.some(stage => stage === 'intake')) return;\n // A fork's author controls its head branch, so an untrusted push must not be\n // able to start review passes on demand; a maintainer can still request one.\n if (!trustedGithubActor(context) && !context.pullRequest.factoryAuthored) return;\n const alreadyReviewing = context.item.stages.some(stage => stage === 'review');\n return {\n type: 'transition',\n idempotencyKey: `${context.ingress.id}:re-review-updated`,\n board: 'review',\n stage: 'review',\n // Re-entry is the point when the card is already Reviewing: the stage's\n // entry rule is what cancels the superseded pass and starts one on the code\n // that just landed.\n ...(alreadyReviewing ? { reenter: true } : {}),\n } as const;\n}\n\nexport const defaultGithubRules = Object.freeze({\n issueOpened: issueOpened,\n issueEdited: retriageGithubIssue,\n issueClosed: issueClosed,\n issueCommentCreated: retriageGithubIssue,\n issueCommentEdited: retriageGithubIssue,\n issueCommentDeleted: retriageGithubIssue,\n pullRequestOpened: pullRequestOpened,\n pullRequestUpdated: reReviewUpdatedPullRequest,\n pullRequestCommentCreated: addressPullRequestComment,\n pullRequestReviewRequested: reReviewRequestedPullRequest,\n pullRequestReviewSubmitted: pullRequestReviewSubmitted,\n pullRequestMerged: pullRequestMerged,\n pullRequestClosed: pullRequestClosed,\n} satisfies GithubEventRules);\n\nexport function resolveGithubRules(overrides?: GithubRuleOverrides): GithubEventRules {\n if (overrides !== undefined && (overrides === null || typeof overrides !== 'object' || Array.isArray(overrides))) {\n throw new Error('GitHub rules must be an object.');\n }\n const rules: Record<string, FactoryRuleHandler<FactoryGithubRuleContext> | null> = { ...defaultGithubRules };\n for (const key of Reflect.ownKeys(overrides ?? {})) {\n if (typeof key !== 'string' || !Object.hasOwn(defaultGithubRules, key)) {\n throw new Error(`Unknown GitHub rule event: ${String(key)}.`);\n }\n const handler = overrides?.[key as FactoryGithubEventName];\n if (handler !== undefined && handler !== null && typeof handler !== 'function') {\n throw new Error(`GitHub rule ${key} must be a function, null, or undefined.`);\n }\n if (handler !== undefined) rules[key] = handler;\n }\n return recordExplicitRuleOverrides(Object.freeze(rules) as GithubEventRules, overrides);\n}\n"],"mappings":"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"}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { FactorySandbox } from '@mastra/core/workspace';
|
|
2
|
+
import type { SourceControlSession } from '../../storage/domains/source-control/base.js';
|
|
1
3
|
/**
|
|
2
4
|
* Stop or destroy the sandbox a session holds in this process, dropping it
|
|
3
5
|
* from the per-process memo first so later opens construct fresh.
|
|
@@ -5,8 +7,8 @@
|
|
|
5
7
|
* Sandbox identity is the session id, so there is no reuse pool to publish
|
|
6
8
|
* into — a reopened session resolves the same id through its provider
|
|
7
9
|
* (reconnect/resume, or a fresh VM whose setup hook re-materializes).
|
|
8
|
-
* Sessions never opened in this replica have no memo entry;
|
|
9
|
-
*
|
|
10
|
+
* Sessions never opened in this replica have no memo entry; destroying those
|
|
11
|
+
* goes through {@link destroyDetachedSessionSandbox}.
|
|
10
12
|
*/
|
|
11
13
|
export declare function releaseSessionSandbox(options: {
|
|
12
14
|
/** The session's storage row id (the sandbox identity). */
|
|
@@ -14,12 +16,22 @@ export declare function releaseSessionSandbox(options: {
|
|
|
14
16
|
/** Destroy instead of stop — for sessions that are gone for good. */
|
|
15
17
|
destroy?: boolean;
|
|
16
18
|
}): Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* Destroy a session's VM by identity from a process that holds no instance of
|
|
21
|
+
* it. Constructs the provider sandbox (cheap, side-effect-free by contract)
|
|
22
|
+
* and calls its plain `destroy()` — never `start()` and never the `_destroy`
|
|
23
|
+
* lifecycle wrapper, which skips sandboxes this instance never started.
|
|
24
|
+
*/
|
|
25
|
+
export declare function destroyDetachedSessionSandbox(options: {
|
|
26
|
+
sandbox: FactorySandbox;
|
|
27
|
+
session: Pick<SourceControlSession, 'id' | 'sandboxId'>;
|
|
28
|
+
}): Promise<void>;
|
|
17
29
|
/**
|
|
18
30
|
* Tear down the sandbox a just-deleted user session was holding. The row is
|
|
19
31
|
* already gone, so the VM is destroyed rather than stopped — nothing will
|
|
20
32
|
* ever resolve this session id again.
|
|
21
33
|
*/
|
|
22
34
|
export declare function reclaimDeletedSessionSandbox(options: {
|
|
23
|
-
session: Pick<
|
|
35
|
+
session: Pick<SourceControlSession, 'id'>;
|
|
24
36
|
}): Promise<void>;
|
|
25
37
|
//# sourceMappingURL=sandbox-release.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sandbox-release.d.ts","sourceRoot":"","sources":["../../../src/integrations/github/sandbox-release.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"sandbox-release.d.ts","sourceRoot":"","sources":["../../../src/integrations/github/sandbox-release.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAG7D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,8CAA8C,CAAC;AAEzF;;;;;;;;;GASG;AACH,wBAAsB,qBAAqB,CAAC,OAAO,EAAE;IACnD,2DAA2D;IAC3D,SAAS,EAAE,MAAM,CAAC;IAClB,qEAAqE;IACrE,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,GAAG,OAAO,CAAC,IAAI,CAAC,CAuBhB;AAED;;;;;GAKG;AACH,wBAAsB,6BAA6B,CAAC,OAAO,EAAE;IAC3D,OAAO,EAAE,cAAc,CAAC;IACxB,OAAO,EAAE,IAAI,CAAC,oBAAoB,EAAE,IAAI,GAAG,WAAW,CAAC,CAAC;CACzD,GAAG,OAAO,CAAC,IAAI,CAAC,CAUhB;AAED;;;;GAIG;AACH,wBAAsB,4BAA4B,CAAC,OAAO,EAAE;IAC1D,OAAO,EAAE,IAAI,CAAC,oBAAoB,EAAE,IAAI,CAAC,CAAC;CAC3C,GAAG,OAAO,CAAC,IAAI,CAAC,CAEhB"}
|
|
@@ -7,8 +7,8 @@ import { evictSessionSandbox, peekSessionSandbox } from "../../sandbox/session-s
|
|
|
7
7
|
* Sandbox identity is the session id, so there is no reuse pool to publish
|
|
8
8
|
* into — a reopened session resolves the same id through its provider
|
|
9
9
|
* (reconnect/resume, or a fresh VM whose setup hook re-materializes).
|
|
10
|
-
* Sessions never opened in this replica have no memo entry;
|
|
11
|
-
*
|
|
10
|
+
* Sessions never opened in this replica have no memo entry; destroying those
|
|
11
|
+
* goes through {@link destroyDetachedSessionSandbox}.
|
|
12
12
|
*/
|
|
13
13
|
async function releaseSessionSandbox(options) {
|
|
14
14
|
const entry = peekSessionSandbox(options.sessionId);
|
|
@@ -16,12 +16,29 @@ async function releaseSessionSandbox(options) {
|
|
|
16
16
|
evictSessionSandbox(options.sessionId);
|
|
17
17
|
const sandbox = entry.sandbox;
|
|
18
18
|
if (options.destroy) {
|
|
19
|
+
if (sandbox.status === "pending" && sandbox.destroy) {
|
|
20
|
+
await sandbox.destroy();
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
19
23
|
await (sandbox._destroy ? sandbox._destroy() : sandbox.destroy?.());
|
|
20
24
|
return;
|
|
21
25
|
}
|
|
22
26
|
await (sandbox._stop ? sandbox._stop() : sandbox.stop?.());
|
|
23
27
|
}
|
|
24
28
|
/**
|
|
29
|
+
* Destroy a session's VM by identity from a process that holds no instance of
|
|
30
|
+
* it. Constructs the provider sandbox (cheap, side-effect-free by contract)
|
|
31
|
+
* and calls its plain `destroy()` — never `start()` and never the `_destroy`
|
|
32
|
+
* lifecycle wrapper, which skips sandboxes this instance never started.
|
|
33
|
+
*/
|
|
34
|
+
async function destroyDetachedSessionSandbox(options) {
|
|
35
|
+
await options.sandbox.create({
|
|
36
|
+
sessionId: options.session.id,
|
|
37
|
+
sandboxId: options.session.sandboxId ?? void 0,
|
|
38
|
+
getRepositoryAccess: void 0
|
|
39
|
+
}, {}).destroy();
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
25
42
|
* Tear down the sandbox a just-deleted user session was holding. The row is
|
|
26
43
|
* already gone, so the VM is destroyed rather than stopped — nothing will
|
|
27
44
|
* ever resolve this session id again.
|
|
@@ -33,6 +50,6 @@ async function reclaimDeletedSessionSandbox(options) {
|
|
|
33
50
|
});
|
|
34
51
|
}
|
|
35
52
|
//#endregion
|
|
36
|
-
export { reclaimDeletedSessionSandbox, releaseSessionSandbox };
|
|
53
|
+
export { destroyDetachedSessionSandbox, reclaimDeletedSessionSandbox, releaseSessionSandbox };
|
|
37
54
|
|
|
38
55
|
//# sourceMappingURL=sandbox-release.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sandbox-release.js","names":[],"sources":["../../../src/integrations/github/sandbox-release.ts"],"sourcesContent":["import { evictSessionSandbox, peekSessionSandbox } from '../../sandbox/session-sandbox.js';\n\n/**\n * Stop or destroy the sandbox a session holds in this process, dropping it\n * from the per-process memo first so later opens construct fresh.\n *\n * Sandbox identity is the session id, so there is no reuse pool to publish\n * into — a reopened session resolves the same id through its provider\n * (reconnect/resume, or a fresh VM whose setup hook re-materializes).\n * Sessions never opened in this replica have no memo entry;
|
|
1
|
+
{"version":3,"file":"sandbox-release.js","names":[],"sources":["../../../src/integrations/github/sandbox-release.ts"],"sourcesContent":["import type { FactorySandbox } from '@mastra/core/workspace';\n\nimport { evictSessionSandbox, peekSessionSandbox } from '../../sandbox/session-sandbox.js';\nimport type { SourceControlSession } from '../../storage/domains/source-control/base.js';\n\n/**\n * Stop or destroy the sandbox a session holds in this process, dropping it\n * from the per-process memo first so later opens construct fresh.\n *\n * Sandbox identity is the session id, so there is no reuse pool to publish\n * into — a reopened session resolves the same id through its provider\n * (reconnect/resume, or a fresh VM whose setup hook re-materializes).\n * Sessions never opened in this replica have no memo entry; destroying those\n * goes through {@link destroyDetachedSessionSandbox}.\n */\nexport async function releaseSessionSandbox(options: {\n /** The session's storage row id (the sandbox identity). */\n sessionId: string;\n /** Destroy instead of stop — for sessions that are gone for good. */\n destroy?: boolean;\n}): Promise<void> {\n const entry = peekSessionSandbox(options.sessionId);\n if (!entry) return;\n evictSessionSandbox(options.sessionId);\n const sandbox = entry.sandbox as unknown as {\n _stop?: () => Promise<void>;\n _destroy?: () => Promise<void>;\n stop?: () => Promise<void>;\n destroy?: () => Promise<void>;\n status?: string;\n };\n if (options.destroy) {\n // Constructed here but never started: the lifecycle wrapper treats that as\n // nothing to clean up, yet the VM may exist from an earlier process. The\n // provider's own destroy kills it by identity without resuming.\n if (sandbox.status === 'pending' && sandbox.destroy) {\n await sandbox.destroy();\n return;\n }\n await (sandbox._destroy ? sandbox._destroy() : sandbox.destroy?.());\n return;\n }\n await (sandbox._stop ? sandbox._stop() : sandbox.stop?.());\n}\n\n/**\n * Destroy a session's VM by identity from a process that holds no instance of\n * it. Constructs the provider sandbox (cheap, side-effect-free by contract)\n * and calls its plain `destroy()` — never `start()` and never the `_destroy`\n * lifecycle wrapper, which skips sandboxes this instance never started.\n */\nexport async function destroyDetachedSessionSandbox(options: {\n sandbox: FactorySandbox;\n session: Pick<SourceControlSession, 'id' | 'sandboxId'>;\n}): Promise<void> {\n const sandbox = options.sandbox.create(\n {\n sessionId: options.session.id,\n sandboxId: options.session.sandboxId ?? undefined,\n getRepositoryAccess: undefined,\n },\n {},\n );\n await sandbox.destroy();\n}\n\n/**\n * Tear down the sandbox a just-deleted user session was holding. The row is\n * already gone, so the VM is destroyed rather than stopped — nothing will\n * ever resolve this session id again.\n */\nexport async function reclaimDeletedSessionSandbox(options: {\n session: Pick<SourceControlSession, 'id'>;\n}): Promise<void> {\n await releaseSessionSandbox({ sessionId: options.session.id, destroy: true });\n}\n"],"mappings":";;;;;;;;;;;;AAeA,eAAsB,sBAAsB,SAK1B;CAChB,MAAM,QAAQ,mBAAmB,QAAQ,SAAS;CAClD,IAAI,CAAC,OAAO;CACZ,oBAAoB,QAAQ,SAAS;CACrC,MAAM,UAAU,MAAM;CAOtB,IAAI,QAAQ,SAAS;EAInB,IAAI,QAAQ,WAAW,aAAa,QAAQ,SAAS;GACnD,MAAM,QAAQ,QAAQ;GACtB;EACF;EACA,OAAO,QAAQ,WAAW,QAAQ,SAAS,IAAI,QAAQ,UAAU;EACjE;CACF;CACA,OAAO,QAAQ,QAAQ,QAAQ,MAAM,IAAI,QAAQ,OAAO;AAC1D;;;;;;;AAQA,eAAsB,8BAA8B,SAGlC;CAShB,MARgB,QAAQ,QAAQ,OAC9B;EACE,WAAW,QAAQ,QAAQ;EAC3B,WAAW,QAAQ,QAAQ,aAAa,KAAA;EACxC,qBAAqB,KAAA;CACvB,GACA,CAAC,CAES,CAAC,CAAC,QAAQ;AACxB;;;;;;AAOA,eAAsB,6BAA6B,SAEjC;CAChB,MAAM,sBAAsB;EAAE,WAAW,QAAQ,QAAQ;EAAI,SAAS;CAAK,CAAC;AAC9E"}
|
|
@@ -2,7 +2,7 @@ import type { MountedMastraCode } from '@mastra/code-sdk';
|
|
|
2
2
|
import type { NotificationPriority } from '@mastra/core/notifications';
|
|
3
3
|
import type { Context } from 'hono';
|
|
4
4
|
import type { FactorySessionOwner } from '../subscription-session.js';
|
|
5
|
-
import { GithubAppIdentity } from './app-identity.js';
|
|
5
|
+
import type { GithubAppIdentity } from './app-identity.js';
|
|
6
6
|
import type { GithubIntegration, GithubRepositoryPermission } from './integration.js';
|
|
7
7
|
import type { GithubSignalSubscriptionRow, GithubSubscriptionStorage, GithubWebhookPullRequestTarget } from './subscriptions.js';
|
|
8
8
|
export interface GithubWebhookHandlerOptions {
|
|
@@ -126,7 +126,9 @@ export declare function resolveAuthorizedBots(extra?: readonly string[]): Set<st
|
|
|
126
126
|
* reviewing a pull request it authored, so `factory-review` falls back to
|
|
127
127
|
* posting its verdict as a comment under this login. Those comments have to
|
|
128
128
|
* clear the author gate for the review handoff to reach the authoring agent;
|
|
129
|
-
* the rules layer still decides which of them are worth acting on
|
|
129
|
+
* the rules layer still decides which of them are worth acting on, and
|
|
130
|
+
* managed authoring subscriptions drop the ones that are not a request-changes
|
|
131
|
+
* verdict (see `isIgnoredFactoryAppComment`).
|
|
130
132
|
*/
|
|
131
133
|
export declare function isFactoryAppSender(sender: string | undefined, slug: string | undefined): boolean;
|
|
132
134
|
export declare function dispatchGithubWebhook(parsed: ParsedGithubWebhook, dependencies: GithubWebhookDispatchDependencies): Promise<{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webhook.d.ts","sourceRoot":"","sources":["../../../src/integrations/github/webhook.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAC1D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AACvE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"webhook.d.ts","sourceRoot":"","sources":["../../../src/integrations/github/webhook.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { listPullRequestSubscriptionsForWebhook, retirePullRequestSubscription } from "./subscriptions.js";
|
|
2
|
+
import { requestsChangesVerdict } from "../../review-verdict.js";
|
|
2
3
|
import { resolveSubscriptionSession, subscriptionRunContext } from "../subscription-session.js";
|
|
3
4
|
import { createHmac, timingSafeEqual } from "crypto";
|
|
4
5
|
//#region src/integrations/github/webhook.ts
|
|
@@ -276,12 +277,28 @@ const AUTHOR_GATED_KINDS = /* @__PURE__ */ new Set([
|
|
|
276
277
|
* reviewing a pull request it authored, so `factory-review` falls back to
|
|
277
278
|
* posting its verdict as a comment under this login. Those comments have to
|
|
278
279
|
* clear the author gate for the review handoff to reach the authoring agent;
|
|
279
|
-
* the rules layer still decides which of them are worth acting on
|
|
280
|
+
* the rules layer still decides which of them are worth acting on, and
|
|
281
|
+
* managed authoring subscriptions drop the ones that are not a request-changes
|
|
282
|
+
* verdict (see `isIgnoredFactoryAppComment`).
|
|
280
283
|
*/
|
|
281
284
|
function isFactoryAppSender(sender, slug) {
|
|
282
285
|
if (!sender || !slug) return false;
|
|
283
286
|
return sender.toLowerCase() === `${slug.toLowerCase()}[bot]`;
|
|
284
287
|
}
|
|
288
|
+
/**
|
|
289
|
+
* Factory App comments on a PR the builder authored (approvals, demo links,
|
|
290
|
+
* the builder's own progress notes) are ignored by the default
|
|
291
|
+
* `pullRequestCommentCreated` rule unless they open with a request-changes
|
|
292
|
+
* verdict. Managed authoring subscriptions apply the same filter so those
|
|
293
|
+
* comments cannot wake the builder through the subscription path either.
|
|
294
|
+
*/
|
|
295
|
+
function isIgnoredFactoryAppComment(notification, subscription, payload, github) {
|
|
296
|
+
if (notification.kind !== "issue-comment-created") return false;
|
|
297
|
+
if (!isFactoryManagedAuthoringSubscription(subscription)) return false;
|
|
298
|
+
const sender = notification.metadata.sender;
|
|
299
|
+
if (!sender || !(github?.identity?.matches(sender) || isFactoryAppSender(sender, github?.slug))) return false;
|
|
300
|
+
return !requestsChangesVerdict(getString(getObject(payload.comment)?.body));
|
|
301
|
+
}
|
|
285
302
|
async function isAuthorizedGithubSender(notification, github) {
|
|
286
303
|
if (!AUTHOR_GATED_KINDS.has(notification.kind)) return true;
|
|
287
304
|
const sender = notification.metadata.sender;
|
|
@@ -342,45 +359,51 @@ async function dispatchGithubWebhook(parsed, dependencies) {
|
|
|
342
359
|
let delivered = 0;
|
|
343
360
|
let failed = 0;
|
|
344
361
|
let skipped = 0;
|
|
345
|
-
for (const subscription of subscriptions)
|
|
346
|
-
|
|
347
|
-
label: "GitHub",
|
|
348
|
-
sourceControl: dependencies.github?.sourceControlStorage
|
|
349
|
-
});
|
|
350
|
-
if (!session) {
|
|
362
|
+
for (const subscription of subscriptions) {
|
|
363
|
+
if (isIgnoredFactoryAppComment(notification, subscription, parsed.payload, dependencies.github)) {
|
|
351
364
|
skipped += 1;
|
|
352
|
-
dependencies.onTargetSkipped?.(subscription);
|
|
353
365
|
continue;
|
|
354
366
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
sourceId: parsed.deliveryId,
|
|
365
|
-
dedupeKey: `${parsed.deliveryId}:${subscription.sessionId}:${subscription.threadId}`,
|
|
366
|
-
coalesceKey: `github:${subscription.data.repositoryExternalId}:pull-request:${subscription.data.changeRequestId}`,
|
|
367
|
-
metadata: {
|
|
368
|
-
event: notification.metadata.event,
|
|
369
|
-
action: notification.action,
|
|
370
|
-
repository: notification.metadata.repository,
|
|
371
|
-
issueNumber: notification.metadata.issueNumber,
|
|
372
|
-
pullRequestNumber: notification.metadata.pullRequestNumber,
|
|
373
|
-
targetUrl: notificationTargetUrl(parsed.event, parsed.payload),
|
|
374
|
-
deliveryId: parsed.deliveryId
|
|
367
|
+
try {
|
|
368
|
+
const session = await resolveSubscriptionSession(dependencies.controller, subscription, {
|
|
369
|
+
label: "GitHub",
|
|
370
|
+
sourceControl: dependencies.github?.sourceControlStorage
|
|
371
|
+
});
|
|
372
|
+
if (!session) {
|
|
373
|
+
skipped += 1;
|
|
374
|
+
dependencies.onTargetSkipped?.(subscription);
|
|
375
|
+
continue;
|
|
375
376
|
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
377
|
+
const overrides = managedInlineReviewOverrides(notification, subscription);
|
|
378
|
+
const runContext = await subscriptionRunContext(subscription, dependencies.github?.sourceControlStorage);
|
|
379
|
+
if (!runContext) throw new Error(`GitHub subscription ${subscription.id} has no resolvable tenant identity; not delivered.`);
|
|
380
|
+
const result = await session.sendNotificationSignal({
|
|
381
|
+
source: "github",
|
|
382
|
+
kind: notification.kind,
|
|
383
|
+
summary: overrides?.summary ?? notification.summary,
|
|
384
|
+
priority: notification.priority,
|
|
385
|
+
payload: overrides?.payload ?? notification.payload,
|
|
386
|
+
sourceId: parsed.deliveryId,
|
|
387
|
+
dedupeKey: `${parsed.deliveryId}:${subscription.sessionId}:${subscription.threadId}`,
|
|
388
|
+
coalesceKey: `github:${subscription.data.repositoryExternalId}:pull-request:${subscription.data.changeRequestId}`,
|
|
389
|
+
metadata: {
|
|
390
|
+
event: notification.metadata.event,
|
|
391
|
+
action: notification.action,
|
|
392
|
+
repository: notification.metadata.repository,
|
|
393
|
+
issueNumber: notification.metadata.issueNumber,
|
|
394
|
+
pullRequestNumber: notification.metadata.pullRequestNumber,
|
|
395
|
+
targetUrl: notificationTargetUrl(parsed.event, parsed.payload),
|
|
396
|
+
deliveryId: parsed.deliveryId
|
|
397
|
+
}
|
|
398
|
+
}, { requestContext: runContext });
|
|
399
|
+
await Promise.all([result.persisted, result.accepted].filter(Boolean));
|
|
400
|
+
if (notification.terminal) await retireSubscription(subscription.id, notification.kind === "pull-request-merged" ? "merged" : "closed");
|
|
401
|
+
else if (notification.action === "reopened") await retireSubscription(subscription.id, "open");
|
|
402
|
+
delivered += 1;
|
|
403
|
+
} catch (error) {
|
|
404
|
+
failed += 1;
|
|
405
|
+
dependencies.onTargetError?.(subscription, error);
|
|
406
|
+
}
|
|
384
407
|
}
|
|
385
408
|
return {
|
|
386
409
|
delivered,
|