@wardby/cli 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +22 -3
- package/README.md +11 -9
- package/dist/cli-help.d.ts +1 -1
- package/dist/cli-help.js +1 -0
- package/dist/cli.js +68 -10
- package/dist/coding/base-commit.d.ts +6 -0
- package/dist/coding/base-commit.js +12 -0
- package/dist/coding/protocol.d.ts +17 -1
- package/dist/coding/protocol.js +17 -6
- package/dist/coding/provider.d.ts +6 -1
- package/dist/coding/provider.js +16 -9
- package/dist/config/providers.d.ts +25 -0
- package/dist/config/providers.js +71 -0
- package/dist/core/attribution.d.ts +101 -0
- package/dist/core/attribution.js +208 -0
- package/dist/core/budget-groups.d.ts +17 -10
- package/dist/core/budget-groups.js +15 -12
- package/dist/core/coding-queue.d.ts +3 -0
- package/dist/core/coding-queue.js +6 -2
- package/dist/core/coding-service-status.d.ts +11 -0
- package/dist/core/coding-service-status.js +17 -0
- package/dist/core/cost-report.d.ts +88 -0
- package/dist/core/cost-report.js +248 -0
- package/dist/core/dispatch.d.ts +42 -2
- package/dist/core/dispatch.js +144 -26
- package/dist/core/engine-native.js +17 -4
- package/dist/core/glob.d.ts +10 -0
- package/dist/core/glob.js +33 -0
- package/dist/core/host-events.d.ts +24 -1
- package/dist/core/host-events.js +341 -1
- package/dist/core/host-status.d.ts +19 -2
- package/dist/core/host-status.js +47 -30
- package/dist/core/issue-bridge.d.ts +60 -0
- package/dist/core/issue-bridge.js +189 -0
- package/dist/core/issue-dedupe.d.ts +70 -0
- package/dist/core/issue-dedupe.js +255 -0
- package/dist/core/issue-events.d.ts +42 -0
- package/dist/core/issue-events.js +155 -0
- package/dist/core/issue-status.d.ts +29 -0
- package/dist/core/issue-status.js +241 -0
- package/dist/core/issue-tracker-tools.d.ts +64 -0
- package/dist/core/issue-tracker-tools.js +850 -0
- package/dist/core/model-usage.d.ts +10 -0
- package/dist/core/model-usage.js +24 -0
- package/dist/core/reconciler.d.ts +8 -4
- package/dist/core/reconciler.js +15 -4
- package/dist/core/review-host-tools.js +10 -3
- package/dist/core/run-pricing.d.ts +61 -0
- package/dist/core/run-pricing.js +56 -0
- package/dist/core/runner.d.ts +5 -2
- package/dist/core/runner.js +178 -27
- package/dist/core/scheduler.d.ts +4 -1
- package/dist/core/scheduler.js +3 -2
- package/dist/core/self-defects.d.ts +80 -0
- package/dist/core/self-defects.js +180 -0
- package/dist/core/tool-names.js +3 -0
- package/dist/core/webhooks.d.ts +9 -1
- package/dist/core/webhooks.js +19 -1
- package/dist/env.js +6 -1
- package/dist/generated/prisma/browser.d.ts +66 -0
- package/dist/generated/prisma/client.d.ts +66 -0
- package/dist/generated/prisma/commonInputTypes.d.ts +122 -52
- package/dist/generated/prisma/enums.d.ts +7 -0
- package/dist/generated/prisma/enums.js +6 -0
- package/dist/generated/prisma/internal/class.d.ts +99 -0
- package/dist/generated/prisma/internal/class.js +4 -4
- package/dist/generated/prisma/internal/prismaNamespace.d.ts +826 -1
- package/dist/generated/prisma/internal/prismaNamespace.js +135 -2
- package/dist/generated/prisma/internal/prismaNamespaceBrowser.d.ts +142 -0
- package/dist/generated/prisma/internal/prismaNamespaceBrowser.js +135 -2
- package/dist/generated/prisma/models/Agent.d.ts +389 -1
- package/dist/generated/prisma/models/AgentIssueProject.d.ts +1838 -0
- package/dist/generated/prisma/models/AgentIssueProject.js +1 -0
- package/dist/generated/prisma/models/AgentRepository.d.ts +1 -1
- package/dist/generated/prisma/models/AuthUser.d.ts +1 -1
- package/dist/generated/prisma/models/CodingProxySession.d.ts +73 -1
- package/dist/generated/prisma/models/CodingRun.d.ts +130 -1
- package/dist/generated/prisma/models/CodingRunServiceStatus.d.ts +1404 -0
- package/dist/generated/prisma/models/CodingRunServiceStatus.js +1 -0
- package/dist/generated/prisma/models/IssueFingerprint.d.ts +1183 -0
- package/dist/generated/prisma/models/IssueFingerprint.js +1 -0
- package/dist/generated/prisma/models/IssuePullRequest.d.ts +1255 -0
- package/dist/generated/prisma/models/IssuePullRequest.js +1 -0
- package/dist/generated/prisma/models/ModelCatalogEntry.d.ts +1322 -0
- package/dist/generated/prisma/models/ModelCatalogEntry.js +1 -0
- package/dist/generated/prisma/models/Run.d.ts +933 -1
- package/dist/generated/prisma/models/RunAttribution.d.ts +1259 -0
- package/dist/generated/prisma/models/RunAttribution.js +1 -0
- package/dist/generated/prisma/models/RunIssueStatus.d.ts +1199 -0
- package/dist/generated/prisma/models/RunIssueStatus.js +1 -0
- package/dist/generated/prisma/models/RunModelUsage.d.ts +1316 -0
- package/dist/generated/prisma/models/RunModelUsage.js +1 -0
- package/dist/generated/prisma/models/WorkItem.d.ts +1408 -0
- package/dist/generated/prisma/models/WorkItem.js +1 -0
- package/dist/generated/prisma/models.d.ts +9 -0
- package/dist/help-index.json +355 -16
- package/dist/import/neutral-schema.d.ts +16 -16
- package/dist/knowledge/check.d.ts +13 -0
- package/dist/knowledge/check.js +69 -0
- package/dist/knowledge/cli.d.ts +14 -0
- package/dist/knowledge/cli.js +67 -0
- package/dist/knowledge/concept.d.ts +54 -0
- package/dist/knowledge/concept.js +78 -0
- package/dist/knowledge/note.d.ts +11 -0
- package/dist/knowledge/note.js +39 -0
- package/dist/knowledge/relevance.d.ts +11 -0
- package/dist/knowledge/relevance.js +14 -0
- package/dist/knowledge/span-hash.d.ts +3 -0
- package/dist/knowledge/span-hash.js +16 -0
- package/dist/mcp/auth/access.d.ts +4 -2
- package/dist/mcp/auth/ownership.d.ts +9 -9
- package/dist/mcp/auth/resource-server.d.ts +3 -1
- package/dist/mcp/auth/resource-server.js +18 -3
- package/dist/mcp/auth/self-hosted/credentials.d.ts +3 -3
- package/dist/mcp/auth/self-hosted/session.d.ts +5 -5
- package/dist/mcp/context.d.ts +3 -0
- package/dist/mcp/host-events/deliveries.d.ts +9 -0
- package/dist/mcp/host-events/deliveries.js +17 -0
- package/dist/mcp/host-events/github-ingress.d.ts +4 -2
- package/dist/mcp/host-events/github-ingress.js +4 -13
- package/dist/mcp/host-events/jira-ingress.d.ts +29 -0
- package/dist/mcp/host-events/jira-ingress.js +92 -0
- package/dist/mcp/index.d.ts +2 -0
- package/dist/mcp/index.js +87 -9
- package/dist/mcp/server.js +5 -2
- package/dist/mcp/tools/agents.js +74 -3
- package/dist/mcp/tools/cost-report.d.ts +8 -0
- package/dist/mcp/tools/cost-report.js +60 -0
- package/dist/mcp/tools/issue-projects.d.ts +2 -0
- package/dist/mcp/tools/issue-projects.js +238 -0
- package/dist/mcp/tools/model-catalog.d.ts +22 -0
- package/dist/mcp/tools/model-catalog.js +423 -0
- package/dist/mcp/tools/repositories.js +2 -1
- package/dist/mcp/tools/tools.d.ts +2 -2
- package/dist/mcp/tools/trigger.js +33 -5
- package/dist/mcp/transport/streamable-http.d.ts +5 -0
- package/dist/mcp/transport/streamable-http.js +23 -1
- package/dist/mcp/webhooks/ingress.d.ts +2 -1
- package/dist/mcp/webhooks/ingress.js +9 -2
- package/dist/providers/auth/self-hosted.d.ts +8 -1
- package/dist/providers/auth/self-hosted.js +39 -2
- package/dist/providers/coding-proxy/memory-ledger.d.ts +1 -1
- package/dist/providers/coding-proxy/memory-ledger.js +10 -1
- package/dist/providers/coding-proxy/metering.d.ts +2 -1
- package/dist/providers/coding-proxy/metering.js +13 -2
- package/dist/providers/coding-proxy/prisma-ledger.js +59 -6
- package/dist/providers/coding-proxy/proxy.d.ts +12 -2
- package/dist/providers/coding-proxy/proxy.js +92 -30
- package/dist/providers/coding-proxy/types.d.ts +18 -1
- package/dist/providers/coding-proxy/types.js +12 -1
- package/dist/providers/engine/types.d.ts +19 -0
- package/dist/providers/executor/composition.js +9 -1
- package/dist/providers/executor/container.d.ts +30 -2
- package/dist/providers/executor/container.js +98 -17
- package/dist/providers/executor/dbos.d.ts +2 -0
- package/dist/providers/executor/dbos.js +7 -5
- package/dist/providers/executor/routing.d.ts +6 -0
- package/dist/providers/executor/routing.js +5 -0
- package/dist/providers/executor/types.d.ts +12 -0
- package/dist/providers/issue-tracker/adf.d.ts +31 -0
- package/dist/providers/issue-tracker/adf.js +181 -0
- package/dist/providers/issue-tracker/index.d.ts +5 -0
- package/dist/providers/issue-tracker/index.js +12 -0
- package/dist/providers/issue-tracker/jira-client.d.ts +41 -0
- package/dist/providers/issue-tracker/jira-client.js +151 -0
- package/dist/providers/issue-tracker/jira-events.d.ts +3 -0
- package/dist/providers/issue-tracker/jira-events.js +98 -0
- package/dist/providers/issue-tracker/jira.d.ts +116 -0
- package/dist/providers/issue-tracker/jira.js +502 -0
- package/dist/providers/issue-tracker/types.d.ts +269 -0
- package/dist/providers/issue-tracker/types.js +16 -0
- package/dist/providers/jobs/docker.d.ts +5 -1
- package/dist/providers/jobs/docker.js +61 -33
- package/dist/providers/jobs/kubernetes.d.ts +3 -0
- package/dist/providers/jobs/kubernetes.js +44 -4
- package/dist/providers/jobs/service-state.d.ts +22 -0
- package/dist/providers/jobs/service-state.js +17 -0
- package/dist/providers/llm/anthropic.d.ts +3 -3
- package/dist/providers/llm/anthropic.js +3 -9
- package/dist/providers/llm/bedrock.d.ts +3 -3
- package/dist/providers/llm/bedrock.js +3 -9
- package/dist/providers/llm/catalog-lookup.d.ts +10 -0
- package/dist/providers/llm/catalog-lookup.js +15 -0
- package/dist/providers/llm/catalog-shipped.d.ts +18 -0
- package/dist/providers/llm/catalog-shipped.js +197 -0
- package/dist/providers/llm/catalog-store.d.ts +58 -0
- package/dist/providers/llm/catalog-store.js +138 -0
- package/dist/providers/llm/catalog-types.d.ts +66 -0
- package/dist/providers/llm/catalog-types.js +64 -0
- package/dist/providers/llm/catalog.d.ts +61 -0
- package/dist/providers/llm/catalog.js +147 -0
- package/dist/providers/llm/claude-provider.d.ts +10 -14
- package/dist/providers/llm/claude-provider.js +11 -6
- package/dist/providers/llm/index.d.ts +9 -6
- package/dist/providers/llm/index.js +8 -5
- package/dist/providers/llm/openai.d.ts +14 -5
- package/dist/providers/llm/openai.js +24 -14
- package/dist/providers/llm/pricing-core.d.ts +5 -3
- package/dist/providers/llm/registration.js +8 -12
- package/dist/providers/llm/routing.d.ts +18 -17
- package/dist/providers/llm/routing.js +40 -24
- package/dist/providers/review-host/github-events.js +47 -1
- package/dist/providers/review-host/github.js +7 -6
- package/dist/providers/review-host/types.d.ts +25 -0
- package/dist/providers/vcs/git.js +2 -22
- package/dist/providers/vcs/github.d.ts +20 -0
- package/dist/providers/vcs/github.js +28 -2
- package/dist/providers/vcs/types.d.ts +6 -0
- package/dist/quickstart/index.d.ts +8 -0
- package/dist/quickstart/index.js +34 -34
- package/dist/serve.js +8 -2
- package/dist/viewer/api-schema.d.ts +2757 -0
- package/dist/viewer/api-schema.js +165 -0
- package/dist/viewer/build-schemas.d.ts +2 -0
- package/dist/viewer/build-schemas.js +18 -0
- package/dist/viewer/event-bus.d.ts +38 -0
- package/dist/viewer/event-bus.js +232 -0
- package/dist/viewer/graph.d.ts +40 -0
- package/dist/viewer/graph.js +243 -0
- package/dist/viewer/http.d.ts +30 -0
- package/dist/viewer/http.js +133 -0
- package/dist/viewer/run-detail.d.ts +4 -0
- package/dist/viewer/run-detail.js +61 -0
- package/dist/wardby-bin.js +5 -0
- package/docs/README.md +10 -0
- package/docs/agent-recipes.md +383 -0
- package/docs/code-review-agents.md +29 -2
- package/docs/coding-agent-setup.md +3 -0
- package/docs/coding-worker-isolation.md +39 -5
- package/docs/getting-started-gke.md +28 -11
- package/docs/getting-started-identity-provider.md +49 -38
- package/docs/getting-started.md +14 -0
- package/docs/jira-agents.md +649 -0
- package/docs/knowledge.md +387 -0
- package/docs/models.md +221 -0
- package/docs/security-deployment.md +19 -9
- package/docs/viewer-api.md +142 -0
- package/help/admin-viewer.md +39 -0
- package/help/agent-recipes.md +173 -0
- package/help/architecture-agent.md +189 -0
- package/help/builder-agent.md +80 -0
- package/help/code-review-agents.md +6 -0
- package/help/cost-attribution.md +67 -0
- package/help/creating-agents.md +22 -0
- package/help/deploy-gke.md +6 -0
- package/help/errors/model-unavailable.md +63 -0
- package/help/getting-started.md +1 -0
- package/help/github.md +18 -0
- package/help/identity-and-access.md +8 -3
- package/help/jira.md +135 -0
- package/help/knowledge.md +47 -0
- package/help/models.md +90 -0
- package/help/operating-agents.md +7 -1
- package/help/troubleshooting/budgets.md +6 -0
- package/package.json +5 -2
- package/prisma/migrations/20260930000000_jira_issue_projects/migration.sql +34 -0
- package/prisma/migrations/20261001000000_jira_phase2_allowlists/migration.sql +3 -0
- package/prisma/migrations/20261001010000_jira_link_types_allowlist/migration.sql +2 -0
- package/prisma/migrations/20261002000000_jira_coding_bridge/migration.sql +28 -0
- package/prisma/migrations/20261002010000_jira_issue_creation/migration.sql +25 -0
- package/prisma/migrations/20261003000000_issue_cost_attribution/migration.sql +56 -0
- package/prisma/migrations/20261003010000_coding_run_service_status/migration.sql +23 -0
- package/prisma/migrations/20261003020000_viewer_notify/migration.sql +54 -0
- package/prisma/migrations/20261003030000_viewer_notify_fixes/migration.sql +47 -0
- package/prisma/migrations/20261003040000_viewer_indexes/migration.sql +12 -0
- package/prisma/migrations/20261004000000_model_catalog/migration.sql +26 -0
- package/prisma/schema.prisma +258 -2
- package/dist/mcp/tools/models.d.ts +0 -8
- package/dist/mcp/tools/models.js +0 -15
- package/dist/providers/llm/pricing-anthropic.d.ts +0 -14
- package/dist/providers/llm/pricing-anthropic.js +0 -48
- package/dist/providers/llm/pricing-bedrock-claude.d.ts +0 -20
- package/dist/providers/llm/pricing-bedrock-claude.js +0 -46
- package/dist/providers/llm/pricing.d.ts +0 -30
- package/dist/providers/llm/pricing.js +0 -74
package/dist/core/host-status.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CODING_CODE_PROVIDER } from "../coding/protocol.js";
|
|
1
2
|
import { storedServiceNames } from "../coding/services/catalog.js";
|
|
2
3
|
import { SERVICE_UNREADY_CATEGORY, serviceRefusalSentence, serviceUnreadySentence, } from "../coding/services/wording.js";
|
|
3
4
|
import { PROTECTED_PATH_CATEGORY, PROTECTED_PATH_HOST_LINE } from "../coding/protected-path-wording.js";
|
|
@@ -8,7 +9,7 @@ const log = logger.child({ module: "host-status" });
|
|
|
8
9
|
/** Keeps the edited comment readable; the full reply stays on the run. */
|
|
9
10
|
const MAX_REPLY_CHARS = 2000;
|
|
10
11
|
const TERMINAL = new Set(["succeeded", "failed", "refused", "lost", "budget_exhausted", "cancelled"]);
|
|
11
|
-
const runLine = (runId) => `<sub>wardby run \`${runId}\`</sub>`;
|
|
12
|
+
export const runLine = (runId) => `<sub>wardby run \`${runId}\`</sub>`;
|
|
12
13
|
export function workingBody(runId) {
|
|
13
14
|
return `👀 Working on it.\n\n${runLine(runId)}`;
|
|
14
15
|
}
|
|
@@ -33,12 +34,18 @@ function pullRequestOutcome(result) {
|
|
|
33
34
|
return null;
|
|
34
35
|
if (!Number.isInteger(r.pullRequestNumber) || r.pullRequestNumber <= 0)
|
|
35
36
|
return null;
|
|
36
|
-
return {
|
|
37
|
+
return {
|
|
38
|
+
outcome: r.outcome,
|
|
39
|
+
repository: r.repository,
|
|
40
|
+
pullRequestNumber: r.pullRequestNumber,
|
|
41
|
+
codeProvider: CODING_CODE_PROVIDER,
|
|
42
|
+
...(typeof r.pullRequestUrl === "string" ? { pullRequestUrl: r.pullRequestUrl } : {}),
|
|
43
|
+
};
|
|
37
44
|
}
|
|
38
45
|
/** The agent's reply as a quote, cut to MAX_REPLY_CHARS, with @-mentions defused so nobody is pinged. */
|
|
46
|
+
const cutReply = (text) => text.length > MAX_REPLY_CHARS ? `${text.slice(0, MAX_REPLY_CHARS)}…` : text;
|
|
39
47
|
function quoteReply(text) {
|
|
40
|
-
|
|
41
|
-
return cut
|
|
48
|
+
return cutReply(text)
|
|
42
49
|
.replace(/@(?=[\w-])/g, "@")
|
|
43
50
|
.split("\n")
|
|
44
51
|
.map((line) => `> ${line}`)
|
|
@@ -106,7 +113,41 @@ export function outcomeBody(run, repository, pullRequests, failedChildren = [],
|
|
|
106
113
|
}
|
|
107
114
|
if (links.length > 0)
|
|
108
115
|
return `✅ ${links.join(", ")}.\n\n${footer}`;
|
|
109
|
-
|
|
116
|
+
if (opts.replyAsAnswer && reply)
|
|
117
|
+
return `✅ ${cutReply(reply)}\n\n${footer}`;
|
|
118
|
+
return `✅ ${opts.noPullRequestText ?? "Finished without opening a pull request."}${quoted}\n\n${footer}`;
|
|
119
|
+
}
|
|
120
|
+
/** The parts of a finished run's outcome a status comment reports; shared by GitHub and Jira status comments. */
|
|
121
|
+
export async function collectRunOutcome(db, run) {
|
|
122
|
+
const children = await db.run.findMany({
|
|
123
|
+
where: { parentRunId: run.id },
|
|
124
|
+
select: {
|
|
125
|
+
id: true,
|
|
126
|
+
status: true,
|
|
127
|
+
error: true,
|
|
128
|
+
codingRun: { select: { result: true, failureCategory: true, services: true } },
|
|
129
|
+
},
|
|
130
|
+
orderBy: { startedAt: "asc" },
|
|
131
|
+
});
|
|
132
|
+
const pullRequests = children
|
|
133
|
+
.map((c) => {
|
|
134
|
+
const pr = pullRequestOutcome(c.codingRun?.result);
|
|
135
|
+
return pr ? { ...pr, runId: c.id } : null;
|
|
136
|
+
})
|
|
137
|
+
.filter((pr) => pr !== null);
|
|
138
|
+
const failedChildren = children
|
|
139
|
+
.filter((c) => TERMINAL.has(c.status) && c.status !== "succeeded")
|
|
140
|
+
.map((c) => ({
|
|
141
|
+
id: c.id,
|
|
142
|
+
status: c.status,
|
|
143
|
+
failureCategory: c.codingRun?.failureCategory ?? null,
|
|
144
|
+
error: c.error ?? null,
|
|
145
|
+
services: storedServiceNames(c.codingRun?.services),
|
|
146
|
+
}));
|
|
147
|
+
const budgetSentence = run.status === "budget_exhausted" || run.status === "refused"
|
|
148
|
+
? await loadBudgetSentence(db, run.id, run.status)
|
|
149
|
+
: undefined;
|
|
150
|
+
return { pullRequests, failedChildren, ...(budgetSentence ? { budgetSentence } : {}) };
|
|
110
151
|
}
|
|
111
152
|
/**
|
|
112
153
|
* Writes a finished run's outcome to its status comment and marks it
|
|
@@ -128,31 +169,7 @@ export async function completeHostStatus(db, run, hosts, opts = {}) {
|
|
|
128
169
|
const host = hosts[status.provider];
|
|
129
170
|
if (!host)
|
|
130
171
|
return;
|
|
131
|
-
const
|
|
132
|
-
where: { parentRunId: run.id },
|
|
133
|
-
select: {
|
|
134
|
-
id: true,
|
|
135
|
-
status: true,
|
|
136
|
-
error: true,
|
|
137
|
-
codingRun: { select: { result: true, failureCategory: true, services: true } },
|
|
138
|
-
},
|
|
139
|
-
orderBy: { startedAt: "asc" },
|
|
140
|
-
});
|
|
141
|
-
const pullRequests = children
|
|
142
|
-
.map((c) => pullRequestOutcome(c.codingRun?.result))
|
|
143
|
-
.filter((pr) => pr !== null);
|
|
144
|
-
const failedChildren = children
|
|
145
|
-
.filter((c) => TERMINAL.has(c.status) && c.status !== "succeeded")
|
|
146
|
-
.map((c) => ({
|
|
147
|
-
id: c.id,
|
|
148
|
-
status: c.status,
|
|
149
|
-
failureCategory: c.codingRun?.failureCategory ?? null,
|
|
150
|
-
error: c.error ?? null,
|
|
151
|
-
services: storedServiceNames(c.codingRun?.services),
|
|
152
|
-
}));
|
|
153
|
-
const budgetSentence = run.status === "budget_exhausted" || run.status === "refused"
|
|
154
|
-
? await loadBudgetSentence(db, run.id, run.status)
|
|
155
|
-
: undefined;
|
|
172
|
+
const { pullRequests, failedChildren, budgetSentence } = await collectRunOutcome(db, run);
|
|
156
173
|
const body = outcomeBody(run, status.repository, pullRequests, failedChildren, { budgetSentence });
|
|
157
174
|
let commentId = status.commentId;
|
|
158
175
|
if (commentId) {
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Links a run's pull requests to the issue that started it: records each
|
|
3
|
+
* issue/PR pair, puts a web link on the issue, and applies the link's
|
|
4
|
+
* configured status move. Provider-neutral: a code host is only ever
|
|
5
|
+
* { provider, repository, number }. The issue key comes from the run tree
|
|
6
|
+
* (RunIssueStatus), never from model output. The status move is a control-plane
|
|
7
|
+
* action (not gated by allowedTransitions). Nothing here throws, except
|
|
8
|
+
* handlePullRequestClosed's lookup before it has claimed anything (so the
|
|
9
|
+
* webhook delivery is rolled back and redelivered).
|
|
10
|
+
*/
|
|
11
|
+
import { type PrismaClient } from "#prisma";
|
|
12
|
+
import { type IssueTracker, type IssueTrackerRegistry } from "../providers/issue-tracker/types.js";
|
|
13
|
+
export type IssueBridgeDb = Pick<PrismaClient, "issuePullRequest">;
|
|
14
|
+
export type PullRequestClosedDb = Pick<PrismaClient, "issuePullRequest" | "agentIssueProject">;
|
|
15
|
+
export interface BridgedPullRequest {
|
|
16
|
+
codeProvider: string;
|
|
17
|
+
repository: string;
|
|
18
|
+
number: number;
|
|
19
|
+
url: string;
|
|
20
|
+
openedByRunId: string;
|
|
21
|
+
outcome: "pull_request_opened" | "pull_request_updated";
|
|
22
|
+
}
|
|
23
|
+
export interface RecordPullRequestsInput {
|
|
24
|
+
issueKey: string;
|
|
25
|
+
issueProvider: string;
|
|
26
|
+
agentId: string;
|
|
27
|
+
/** The status the link moves the issue to when a pull request is opened, if configured. */
|
|
28
|
+
onPullRequestOpened?: string | null;
|
|
29
|
+
pullRequests: BridgedPullRequest[];
|
|
30
|
+
}
|
|
31
|
+
export declare const pullRequestGlobalId: (pr: Pick<BridgedPullRequest, "codeProvider" | "repository" | "number">) => string;
|
|
32
|
+
/**
|
|
33
|
+
* Returns sentences for the outcome comment (a refused status move); empty
|
|
34
|
+
* when there is nothing to say. Safe to repeat (a retried completion): each
|
|
35
|
+
* pair's side effects happen once — the status move only when this call newly
|
|
36
|
+
* recorded an opened pull request, and a merged/closed pair is never re-linked
|
|
37
|
+
* (re-posting its web link would un-resolve it). An unreadable store skips
|
|
38
|
+
* both rather than risk repeating them.
|
|
39
|
+
*/
|
|
40
|
+
export declare function recordPullRequests(db: IssueBridgeDb, tracker: IssueTracker, input: RecordPullRequestsInput): Promise<{
|
|
41
|
+
notes: string[];
|
|
42
|
+
}>;
|
|
43
|
+
export interface ClosedPullRequest {
|
|
44
|
+
codeProvider: string;
|
|
45
|
+
repository: string;
|
|
46
|
+
number: number;
|
|
47
|
+
merged: boolean;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* A pull request wardby recorded against an issue was merged or closed: marks
|
|
51
|
+
* each still-open pair, and, where the agent's link to the issue's project is
|
|
52
|
+
* still write, comments on the issue (and on merge resolves the web link and
|
|
53
|
+
* applies the link's onPullRequestMerged move). Only rows wardby wrote are
|
|
54
|
+
* touched, matched by the host's own identity for the PR — nothing from the
|
|
55
|
+
* PR's text. The row is claimed first (open → merged/closed), so a duplicate
|
|
56
|
+
* or concurrent delivery finds nothing to do. Throws only when the lookup of
|
|
57
|
+
* linked rows fails — before anything is claimed — so the caller's webhook
|
|
58
|
+
* delivery is rolled back and redelivered; never throws after that.
|
|
59
|
+
*/
|
|
60
|
+
export declare function handlePullRequestClosed(db: PullRequestClosedDb, trackers: IssueTrackerRegistry | undefined, pr: ClosedPullRequest): Promise<void>;
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Links a run's pull requests to the issue that started it: records each
|
|
3
|
+
* issue/PR pair, puts a web link on the issue, and applies the link's
|
|
4
|
+
* configured status move. Provider-neutral: a code host is only ever
|
|
5
|
+
* { provider, repository, number }. The issue key comes from the run tree
|
|
6
|
+
* (RunIssueStatus), never from model output. The status move is a control-plane
|
|
7
|
+
* action (not gated by allowedTransitions). Nothing here throws, except
|
|
8
|
+
* handlePullRequestClosed's lookup before it has claimed anything (so the
|
|
9
|
+
* webhook delivery is rolled back and redelivered).
|
|
10
|
+
*/
|
|
11
|
+
import { Prisma } from "#prisma";
|
|
12
|
+
import { projectOf, } from "../providers/issue-tracker/types.js";
|
|
13
|
+
import { logger } from "./logger.js";
|
|
14
|
+
const log = logger.child({ module: "issue-bridge" });
|
|
15
|
+
export const pullRequestGlobalId = (pr) => `wardby:pr:${pr.codeProvider}:${pr.repository}#${pr.number}`;
|
|
16
|
+
/**
|
|
17
|
+
* Makes sure the issue/PR pair has a row, without ever overwriting one: a new
|
|
18
|
+
* row is "created"; an existing open one gets its url refreshed ("open"); a
|
|
19
|
+
* merged or closed one is left alone ("settled"). A lost create race (P2002)
|
|
20
|
+
* reads back the winner's row. "unknown" when the store could not be read.
|
|
21
|
+
*/
|
|
22
|
+
async function ensurePair(db, input, pr) {
|
|
23
|
+
const where = {
|
|
24
|
+
codeProvider_repository_number_issueProvider_issueKey: {
|
|
25
|
+
codeProvider: pr.codeProvider,
|
|
26
|
+
repository: pr.repository,
|
|
27
|
+
number: pr.number,
|
|
28
|
+
issueProvider: input.issueProvider,
|
|
29
|
+
issueKey: input.issueKey,
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
try {
|
|
33
|
+
let existing = await db.issuePullRequest.findUnique({ where, select: { state: true } });
|
|
34
|
+
if (!existing) {
|
|
35
|
+
try {
|
|
36
|
+
await db.issuePullRequest.create({
|
|
37
|
+
data: {
|
|
38
|
+
issueProvider: input.issueProvider,
|
|
39
|
+
issueKey: input.issueKey,
|
|
40
|
+
codeProvider: pr.codeProvider,
|
|
41
|
+
repository: pr.repository,
|
|
42
|
+
number: pr.number,
|
|
43
|
+
url: pr.url,
|
|
44
|
+
agentId: input.agentId,
|
|
45
|
+
openedByRunId: pr.openedByRunId,
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
return "created";
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
if (!(err instanceof Prisma.PrismaClientKnownRequestError && err.code === "P2002"))
|
|
52
|
+
throw err;
|
|
53
|
+
existing = await db.issuePullRequest.findUnique({ where, select: { state: true } });
|
|
54
|
+
if (!existing)
|
|
55
|
+
return "unknown";
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (existing.state !== "open")
|
|
59
|
+
return "settled";
|
|
60
|
+
await db.issuePullRequest.update({ where, data: { url: pr.url } });
|
|
61
|
+
return "open";
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
log.warn({ err, issueKey: input.issueKey, repository: pr.repository, number: pr.number }, "could not record the issue/PR pair");
|
|
65
|
+
return "unknown";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Returns sentences for the outcome comment (a refused status move); empty
|
|
70
|
+
* when there is nothing to say. Safe to repeat (a retried completion): each
|
|
71
|
+
* pair's side effects happen once — the status move only when this call newly
|
|
72
|
+
* recorded an opened pull request, and a merged/closed pair is never re-linked
|
|
73
|
+
* (re-posting its web link would un-resolve it). An unreadable store skips
|
|
74
|
+
* both rather than risk repeating them.
|
|
75
|
+
*/
|
|
76
|
+
export async function recordPullRequests(db, tracker, input) {
|
|
77
|
+
const notes = [];
|
|
78
|
+
try {
|
|
79
|
+
let newlyOpened = false;
|
|
80
|
+
for (const pr of input.pullRequests) {
|
|
81
|
+
const state = await ensurePair(db, input, pr);
|
|
82
|
+
if (state === "created" && pr.outcome === "pull_request_opened")
|
|
83
|
+
newlyOpened = true;
|
|
84
|
+
if (state !== "created" && state !== "open")
|
|
85
|
+
continue;
|
|
86
|
+
try {
|
|
87
|
+
await tracker.addRemoteLink(input.issueKey, {
|
|
88
|
+
globalId: pullRequestGlobalId(pr),
|
|
89
|
+
url: pr.url,
|
|
90
|
+
title: `${pr.repository}#${pr.number}`,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
log.warn({ err, issueKey: input.issueKey, repository: pr.repository, number: pr.number }, "could not link the pull request on the issue");
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const target = input.onPullRequestOpened;
|
|
98
|
+
if (target && newlyOpened) {
|
|
99
|
+
try {
|
|
100
|
+
await tracker.transitionTo(input.issueKey, target);
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
log.warn({ err, issueKey: input.issueKey, target }, "could not move the issue after a pull request opened");
|
|
104
|
+
notes.push(`Could not move ${input.issueKey} to "${target}"; it may need a manual move.`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
log.warn({ err, issueKey: input.issueKey }, "could not bridge the pull requests to the issue");
|
|
110
|
+
}
|
|
111
|
+
return { notes };
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* A pull request wardby recorded against an issue was merged or closed: marks
|
|
115
|
+
* each still-open pair, and, where the agent's link to the issue's project is
|
|
116
|
+
* still write, comments on the issue (and on merge resolves the web link and
|
|
117
|
+
* applies the link's onPullRequestMerged move). Only rows wardby wrote are
|
|
118
|
+
* touched, matched by the host's own identity for the PR — nothing from the
|
|
119
|
+
* PR's text. The row is claimed first (open → merged/closed), so a duplicate
|
|
120
|
+
* or concurrent delivery finds nothing to do. Throws only when the lookup of
|
|
121
|
+
* linked rows fails — before anything is claimed — so the caller's webhook
|
|
122
|
+
* delivery is rolled back and redelivered; never throws after that.
|
|
123
|
+
*/
|
|
124
|
+
export async function handlePullRequestClosed(db, trackers, pr) {
|
|
125
|
+
if (!trackers)
|
|
126
|
+
return;
|
|
127
|
+
const where = { codeProvider: pr.codeProvider, repository: pr.repository, number: pr.number };
|
|
128
|
+
// Not caught: nothing is claimed yet, and a throw rolls back the delivery so it is redelivered.
|
|
129
|
+
const rows = await db.issuePullRequest.findMany({ where: { ...where, state: "open" } });
|
|
130
|
+
for (const row of rows) {
|
|
131
|
+
try {
|
|
132
|
+
const claimed = await db.issuePullRequest.updateMany({
|
|
133
|
+
where: { id: row.id, state: "open" },
|
|
134
|
+
data: { state: pr.merged ? "merged" : "closed" },
|
|
135
|
+
});
|
|
136
|
+
if (claimed.count === 0)
|
|
137
|
+
continue;
|
|
138
|
+
const tracker = trackers[row.issueProvider];
|
|
139
|
+
if (!tracker)
|
|
140
|
+
continue;
|
|
141
|
+
const link = await db.agentIssueProject.findUnique({
|
|
142
|
+
where: {
|
|
143
|
+
agentId_provider_projectKey: {
|
|
144
|
+
agentId: row.agentId,
|
|
145
|
+
provider: row.issueProvider,
|
|
146
|
+
projectKey: projectOf(row.issueKey),
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
select: { access: true, commentVisibilityRole: true, onPullRequestMerged: true },
|
|
150
|
+
});
|
|
151
|
+
if (link?.access !== "write")
|
|
152
|
+
continue;
|
|
153
|
+
const ref = `[${row.repository}#${row.number}](${row.url})`;
|
|
154
|
+
let markdown;
|
|
155
|
+
if (pr.merged) {
|
|
156
|
+
await tracker
|
|
157
|
+
.addRemoteLink(row.issueKey, {
|
|
158
|
+
globalId: pullRequestGlobalId(row),
|
|
159
|
+
url: row.url,
|
|
160
|
+
title: `${row.repository}#${row.number}`,
|
|
161
|
+
status: { resolved: true },
|
|
162
|
+
})
|
|
163
|
+
.catch((err) => log.warn({ err, issueKey: row.issueKey, ...where }, "could not resolve the pull request's web link"));
|
|
164
|
+
markdown = `✅ Pull request ${ref} was merged.`;
|
|
165
|
+
const target = link.onPullRequestMerged;
|
|
166
|
+
if (target) {
|
|
167
|
+
try {
|
|
168
|
+
const moved = await tracker.transitionTo(row.issueKey, target);
|
|
169
|
+
markdown += ` Moved to ${moved.toStatus}.`;
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
log.warn({ err, issueKey: row.issueKey, target }, "could not move the issue after its pull request merged");
|
|
173
|
+
markdown += ` Could not move ${row.issueKey} to "${target}"; it may need a manual move.`;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
markdown = `Pull request ${ref} was closed without merging.`;
|
|
179
|
+
}
|
|
180
|
+
await tracker.comment(row.issueKey, {
|
|
181
|
+
markdown,
|
|
182
|
+
...(link.commentVisibilityRole ? { visibilityRole: link.commentVisibilityRole } : {}),
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
log.warn({ err, issueKey: row.issueKey, ...where }, "could not bridge the closed pull request to the issue");
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { PrismaClient } from "#prisma";
|
|
2
|
+
import { type CreateIssueInput, type IssueTracker, type TrackerCallOptions } from "../providers/issue-tracker/types.js";
|
|
3
|
+
export declare const FINGERPRINT_MAX_LENGTH = 200;
|
|
4
|
+
/**
|
|
5
|
+
* Each tracker call inside the lock: a whole-call cap, and a 429 fails at once rather than sleeping. Jira
|
|
6
|
+
* Cloud calls through the api.atlassian.com gateway routinely take several seconds each, and one tracker
|
|
7
|
+
* call can be a few requests (a create resolves the issue type and its fields first), so the cap is generous.
|
|
8
|
+
*/
|
|
9
|
+
export declare const TRACKER_CALL_OPTIONS: Readonly<TrackerCallOptions>;
|
|
10
|
+
/** How long a sighting waits for another sighting of the same fingerprint before answering "busy". */
|
|
11
|
+
export declare const DEDUPE_LOCK_TIMEOUT_MS = 5000;
|
|
12
|
+
/**
|
|
13
|
+
* The critical section is the lock wait (5 s) plus at most two bounded tracker calls (getIssue, then
|
|
14
|
+
* createIssue or comment: 2 x 30 s) plus a few row reads/writes; 90 s leaves ample margin so a create is
|
|
15
|
+
* never in flight when the transaction (and with it the advisory lock) is torn down. The cost: each
|
|
16
|
+
* sighting in progress holds one pool connection for up to this long, and waiters hold one for up to
|
|
17
|
+
* the lock timeout (callers waiting for a free connection give up after maxWait).
|
|
18
|
+
*/
|
|
19
|
+
export declare const DEDUPE_TRANSACTION_TIMEOUT_MS = 90000;
|
|
20
|
+
export type IssueDedupeDb = Pick<PrismaClient, "$transaction">;
|
|
21
|
+
export interface IssueDedupeLink {
|
|
22
|
+
provider: string;
|
|
23
|
+
projectKey: string;
|
|
24
|
+
access: string;
|
|
25
|
+
commentVisibilityRole?: string | null;
|
|
26
|
+
}
|
|
27
|
+
export interface FileIssueInput {
|
|
28
|
+
agentId: string;
|
|
29
|
+
runId?: string | null;
|
|
30
|
+
link: IssueDedupeLink;
|
|
31
|
+
tracker: IssueTracker;
|
|
32
|
+
fingerprint?: string | null;
|
|
33
|
+
/** Everything but the project, which comes from the link. */
|
|
34
|
+
create: Omit<CreateIssueInput, "projectKey">;
|
|
35
|
+
/** Appended after the description (and after any "Regression of" line), so it stays the last line. */
|
|
36
|
+
footerMarkdown?: string;
|
|
37
|
+
/** Posted on the existing issue when the fingerprint is seen again. */
|
|
38
|
+
seenAgainMarkdown: string;
|
|
39
|
+
/** false: only a seen-again update may happen; a create or regression returns issue_cap_reached. Default true. */
|
|
40
|
+
createAllowed?: boolean;
|
|
41
|
+
}
|
|
42
|
+
export type FileIssueOutcome = "created" | "seen_again" | "regression";
|
|
43
|
+
export type FileIssueSuccess = {
|
|
44
|
+
outcome: FileIssueOutcome;
|
|
45
|
+
issueKey: string;
|
|
46
|
+
url: string;
|
|
47
|
+
seenCount: number;
|
|
48
|
+
};
|
|
49
|
+
export type FileIssueResult = FileIssueSuccess | {
|
|
50
|
+
error: string;
|
|
51
|
+
message: string;
|
|
52
|
+
};
|
|
53
|
+
export declare const BUSY_RESULT: {
|
|
54
|
+
readonly error: "busy";
|
|
55
|
+
readonly message: "another sighting of this fingerprint is being filed; try again";
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* The part of a fingerprint that identifies it: letters and digits only, lower-cased (Unicode-normalised),
|
|
59
|
+
* so a model's small slips in case, punctuation or spacing ("checkout-api" vs "checkoutapi") still match.
|
|
60
|
+
*/
|
|
61
|
+
export declare function normalizeFingerprint(fingerprint: string): string;
|
|
62
|
+
export declare function fingerprintHash(fingerprint: string): string;
|
|
63
|
+
/** A stable signed 64-bit advisory-lock key for one (provider, project, hash). */
|
|
64
|
+
export declare function dedupeLockKey(provider: string, projectKey: string, hash: string): bigint;
|
|
65
|
+
export declare function fileIssue(deps: {
|
|
66
|
+
db: IssueDedupeDb;
|
|
67
|
+
lockTimeoutMs?: number;
|
|
68
|
+
}, input: FileIssueInput): Promise<FileIssueResult>;
|
|
69
|
+
/** Postgres lock_not_available (55P03), as raised when lock_timeout expires, however the driver wraps it. */
|
|
70
|
+
export declare function isLockTimeout(err: unknown): boolean;
|