@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
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-neutral issue-tracker surface for native agents. Jira Cloud is the
|
|
3
|
+
* first implementation (./jira.ts). The control plane holds the credential;
|
|
4
|
+
* no method takes a token. See docs/private/2026-09-30-jira-issue-tracker-design.md.
|
|
5
|
+
*/
|
|
6
|
+
export type IssueTrackerProvider = "jira";
|
|
7
|
+
export declare const ISSUE_TRACKER_PROVIDERS: readonly IssueTrackerProvider[];
|
|
8
|
+
/** The product name shown to people (e.g. in a pull request's "Resolves Jira issue …" line). */
|
|
9
|
+
export declare const ISSUE_TRACKER_NAMES: Readonly<Record<IssueTrackerProvider, string>>;
|
|
10
|
+
export interface IssuePerson {
|
|
11
|
+
accountId: string;
|
|
12
|
+
displayName: string;
|
|
13
|
+
}
|
|
14
|
+
export interface IssueCommentView {
|
|
15
|
+
id: string;
|
|
16
|
+
author: IssuePerson | null;
|
|
17
|
+
created: string;
|
|
18
|
+
body: string;
|
|
19
|
+
/** Written by the bot and carrying this agent's footer: editable with jira_edit_own_comment. */
|
|
20
|
+
byThisAgent: boolean;
|
|
21
|
+
}
|
|
22
|
+
export interface IssueView {
|
|
23
|
+
key: string;
|
|
24
|
+
projectKey: string;
|
|
25
|
+
summary: string;
|
|
26
|
+
description: string;
|
|
27
|
+
status: string;
|
|
28
|
+
/** The status's category (Jira statusCategory.key): "done" is what dedupe treats as resolved. */
|
|
29
|
+
statusCategory: IssueStatusCategory;
|
|
30
|
+
issueType: string;
|
|
31
|
+
priority: string | null;
|
|
32
|
+
labels: string[];
|
|
33
|
+
assignee: IssuePerson | null;
|
|
34
|
+
reporter: IssuePerson | null;
|
|
35
|
+
url: string;
|
|
36
|
+
/** Most recent last; capped. */
|
|
37
|
+
comments: IssueCommentView[];
|
|
38
|
+
commentsTruncated: boolean;
|
|
39
|
+
/** Capped list; contents are read with readAttachmentText. Filenames are untrusted. */
|
|
40
|
+
attachments: IssueAttachmentView[];
|
|
41
|
+
/** Issue links (e.g. "relates to PROJ-9"), capped. Summaries are untrusted issue text. */
|
|
42
|
+
links: IssueLinkView[];
|
|
43
|
+
}
|
|
44
|
+
export interface IssueLinkView {
|
|
45
|
+
/** How this issue relates to the other, as Jira words it from this side ("relates to", "is blocked by"). */
|
|
46
|
+
relation: string;
|
|
47
|
+
key: string;
|
|
48
|
+
summary: string;
|
|
49
|
+
status: string;
|
|
50
|
+
}
|
|
51
|
+
export type IssueStatusCategory = "new" | "indeterminate" | "done";
|
|
52
|
+
export interface IssueAttachmentView {
|
|
53
|
+
id: string;
|
|
54
|
+
filename: string;
|
|
55
|
+
mimeType: string;
|
|
56
|
+
size: number;
|
|
57
|
+
}
|
|
58
|
+
export interface CreateIssueInput {
|
|
59
|
+
projectKey: string;
|
|
60
|
+
/** Issue type name (case-insensitive); resolved to an id from the project's create metadata. */
|
|
61
|
+
issueType: string;
|
|
62
|
+
summary: string;
|
|
63
|
+
descriptionMarkdown: string;
|
|
64
|
+
labels?: string[];
|
|
65
|
+
/** Priority name. */
|
|
66
|
+
priority?: string;
|
|
67
|
+
/** Component names. */
|
|
68
|
+
components?: string[];
|
|
69
|
+
parentKey?: string;
|
|
70
|
+
/** customfield_N -> raw JSON value. */
|
|
71
|
+
customFields?: Record<string, unknown>;
|
|
72
|
+
/** Issue properties, set in the create call itself. */
|
|
73
|
+
properties?: Record<string, unknown>;
|
|
74
|
+
/** Projects whose bare issue keys in the description become smart links. */
|
|
75
|
+
issueKeyProjects?: readonly string[];
|
|
76
|
+
}
|
|
77
|
+
export interface CreateMetaIssueType {
|
|
78
|
+
id: string;
|
|
79
|
+
name: string;
|
|
80
|
+
subtask: boolean;
|
|
81
|
+
}
|
|
82
|
+
export interface CreateMetaField {
|
|
83
|
+
fieldId: string;
|
|
84
|
+
name: string;
|
|
85
|
+
required: boolean;
|
|
86
|
+
hasDefault: boolean;
|
|
87
|
+
allowedValues?: string[];
|
|
88
|
+
}
|
|
89
|
+
export interface IssueSearchHit {
|
|
90
|
+
key: string;
|
|
91
|
+
summary: string;
|
|
92
|
+
status: string;
|
|
93
|
+
issueType: string;
|
|
94
|
+
updated: string;
|
|
95
|
+
url: string;
|
|
96
|
+
}
|
|
97
|
+
export interface IssueSearchResult {
|
|
98
|
+
issues: IssueSearchHit[];
|
|
99
|
+
truncated: boolean;
|
|
100
|
+
}
|
|
101
|
+
/** Who the tracker credential acts as (Jira: GET /myself). */
|
|
102
|
+
export interface IssueTrackerIdentity {
|
|
103
|
+
accountId: string;
|
|
104
|
+
displayName: string;
|
|
105
|
+
accountType: string;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Bounds one tracker call for callers on a latency budget (e.g. inside a database transaction).
|
|
109
|
+
* `timeoutMs` caps the WHOLE call, every request it makes included; with `retryOn429: false` a 429 fails
|
|
110
|
+
* at once instead of sleeping for Retry-After, so the call cannot outlive `timeoutMs`.
|
|
111
|
+
*/
|
|
112
|
+
export interface TrackerCallOptions {
|
|
113
|
+
timeoutMs?: number;
|
|
114
|
+
retryOn429?: boolean;
|
|
115
|
+
}
|
|
116
|
+
/** What cost attribution keeps about an issue (src/core/attribution.ts): the hierarchy as of now. */
|
|
117
|
+
export interface IssueSnapshot {
|
|
118
|
+
/** The key that was asked for (WorkItem.key). */
|
|
119
|
+
key: string;
|
|
120
|
+
title?: string;
|
|
121
|
+
type?: string;
|
|
122
|
+
url: string;
|
|
123
|
+
/** The project (Jira) / team (Linear) the issue is in now. */
|
|
124
|
+
scopeKey: string;
|
|
125
|
+
parent?: {
|
|
126
|
+
key: string;
|
|
127
|
+
title?: string;
|
|
128
|
+
kind: string;
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
export interface IssueTracker {
|
|
132
|
+
readonly provider: IssueTrackerProvider;
|
|
133
|
+
/** The bot's own accountId (cached after the first call). */
|
|
134
|
+
botAccountId(): Promise<string>;
|
|
135
|
+
/** The account the credential acts as, from the same cached call as botAccountId. */
|
|
136
|
+
identity(): Promise<IssueTrackerIdentity>;
|
|
137
|
+
getIssue(key: string, opts: {
|
|
138
|
+
maxComments: number;
|
|
139
|
+
agentMarker: string;
|
|
140
|
+
} & TrackerCallOptions): Promise<IssueView>;
|
|
141
|
+
/** The project the issue is in now; an old key (kept as an alias after a move) resolves to its new project. */
|
|
142
|
+
issueProject(key: string): Promise<string>;
|
|
143
|
+
/** Title, type, scope and parent of `key` in one call, for cost attribution. Throws IssueTrackerError. */
|
|
144
|
+
snapshotIssue(key: string, opts?: {
|
|
145
|
+
timeoutMs?: number;
|
|
146
|
+
retryOn429?: boolean;
|
|
147
|
+
}): Promise<IssueSnapshot>;
|
|
148
|
+
search(jql: string, opts: {
|
|
149
|
+
maxResults: number;
|
|
150
|
+
}): Promise<IssueSearchResult>;
|
|
151
|
+
/** Whether issue `key` matches `jql` (used for a link's jqlFilter). Callers on a latency budget pass a short timeout and no 429 retry. */
|
|
152
|
+
matchesJql(key: string, jql: string, opts?: TrackerCallOptions): Promise<boolean>;
|
|
153
|
+
comment(key: string,
|
|
154
|
+
/** issueKeyProjects: projects whose bare issue keys in the text become smart links. */
|
|
155
|
+
input: {
|
|
156
|
+
markdown: string;
|
|
157
|
+
visibilityRole?: string;
|
|
158
|
+
issueKeyProjects?: readonly string[];
|
|
159
|
+
}, opts?: TrackerCallOptions): Promise<{
|
|
160
|
+
id: string;
|
|
161
|
+
url: string;
|
|
162
|
+
}>;
|
|
163
|
+
editComment(key: string, commentId: string, input: {
|
|
164
|
+
markdown: string;
|
|
165
|
+
issueKeyProjects?: readonly string[];
|
|
166
|
+
}): Promise<void>;
|
|
167
|
+
/** The comment's author accountId and plain text (uncapped: only for ownership checks, never shown to a model), or null when it doesn't exist. */
|
|
168
|
+
readComment(key: string, commentId: string): Promise<{
|
|
169
|
+
authorId: string | null;
|
|
170
|
+
body: string;
|
|
171
|
+
} | null>;
|
|
172
|
+
issueUrl(key: string): string;
|
|
173
|
+
/** Transitions the service account can perform now: target status name + category. */
|
|
174
|
+
transitions(key: string): Promise<Array<{
|
|
175
|
+
id: string;
|
|
176
|
+
name: string;
|
|
177
|
+
toStatus: string;
|
|
178
|
+
toCategory: string;
|
|
179
|
+
}>>;
|
|
180
|
+
/** Performs the transition whose target status name matches (case-insensitive). */
|
|
181
|
+
transitionTo(key: string, toStatus: string): Promise<{
|
|
182
|
+
transitionId: string;
|
|
183
|
+
toStatus: string;
|
|
184
|
+
}>;
|
|
185
|
+
/** Field ids the service account may edit on this issue (from editmeta). */
|
|
186
|
+
editableFields(key: string): Promise<string[]>;
|
|
187
|
+
/** Sets fields: labels (string[]), components (names), priority (name), customfield_N (raw JSON value). */
|
|
188
|
+
editFields(key: string, fields: Record<string, unknown>): Promise<void>;
|
|
189
|
+
linkTypes(opts?: TrackerCallOptions): Promise<Array<{
|
|
190
|
+
name: string;
|
|
191
|
+
inward: string;
|
|
192
|
+
outward: string;
|
|
193
|
+
}>>;
|
|
194
|
+
linkIssues(input: {
|
|
195
|
+
type: string;
|
|
196
|
+
inwardKey: string;
|
|
197
|
+
outwardKey: string;
|
|
198
|
+
}, opts?: TrackerCallOptions): Promise<void>;
|
|
199
|
+
/** Adds (or, for an existing globalId, updates) a web link on the issue: Jira's remote link, shown under "Web links". */
|
|
200
|
+
addRemoteLink(key: string, input: {
|
|
201
|
+
globalId: string;
|
|
202
|
+
url: string;
|
|
203
|
+
title: string;
|
|
204
|
+
status?: {
|
|
205
|
+
resolved: boolean;
|
|
206
|
+
};
|
|
207
|
+
}): Promise<void>;
|
|
208
|
+
/** The property's JSON value, or null when it does not exist. */
|
|
209
|
+
getProperty(key: string, property: string): Promise<unknown>;
|
|
210
|
+
setProperty(key: string, property: string, value: unknown): Promise<void>;
|
|
211
|
+
/** Issue types creatable in the project by the service account. */
|
|
212
|
+
createMeta(projectKey: string): Promise<{
|
|
213
|
+
issueTypes: CreateMetaIssueType[];
|
|
214
|
+
}>;
|
|
215
|
+
/** Create-screen fields for one issue type (by id). */
|
|
216
|
+
fieldMeta(projectKey: string, issueTypeId: string): Promise<CreateMetaField[]>;
|
|
217
|
+
/** Creates an issue; missing required fields (without defaults) fail with tracker_invalid_request naming them. */
|
|
218
|
+
createIssue(input: CreateIssueInput, opts?: TrackerCallOptions): Promise<{
|
|
219
|
+
key: string;
|
|
220
|
+
url: string;
|
|
221
|
+
}>;
|
|
222
|
+
/** First `maxBytes` of a text-like attachment, decoded as UTF-8; other types fail with tracker_invalid_request. */
|
|
223
|
+
readAttachmentText(id: string, maxBytes: number): Promise<{
|
|
224
|
+
filename: string;
|
|
225
|
+
mimeType: string;
|
|
226
|
+
text: string;
|
|
227
|
+
truncated: boolean;
|
|
228
|
+
}>;
|
|
229
|
+
}
|
|
230
|
+
export type IssueTrackerErrorCode = "tracker_not_found" | "tracker_permission_denied" | "tracker_rate_limited" | "tracker_invalid_request" | "tracker_api_error" | "tracker_invalid_response";
|
|
231
|
+
export declare class IssueTrackerError extends Error {
|
|
232
|
+
readonly code: IssueTrackerErrorCode;
|
|
233
|
+
constructor(code: IssueTrackerErrorCode, message?: string);
|
|
234
|
+
}
|
|
235
|
+
export type IssueTrackerRegistry = Partial<Record<IssueTrackerProvider, IssueTracker>>;
|
|
236
|
+
export type IssueEventKind = "created" | "transitioned" | "labeled" | "assigned" | "mention";
|
|
237
|
+
/** What a webhook adapter (jira-events.ts) normalises a payload into, for src/core/issue-events.ts. */
|
|
238
|
+
export interface IssueEvent {
|
|
239
|
+
provider: IssueTrackerProvider;
|
|
240
|
+
projectKey: string;
|
|
241
|
+
issueKey: string;
|
|
242
|
+
/** Every kind this one delivery represents (an update can transition AND label). */
|
|
243
|
+
kinds: IssueEventKind[];
|
|
244
|
+
actor: {
|
|
245
|
+
accountId: string;
|
|
246
|
+
displayName: string;
|
|
247
|
+
};
|
|
248
|
+
/** The new status name, with "transitioned". */
|
|
249
|
+
toStatus?: string;
|
|
250
|
+
/** Labels added by this change, with "labeled". */
|
|
251
|
+
addedLabels: string[];
|
|
252
|
+
/** The new assignee's accountId, with "assigned". */
|
|
253
|
+
assigneeAccountId?: string;
|
|
254
|
+
/** The mentioning comment, with "mention". */
|
|
255
|
+
comment?: {
|
|
256
|
+
id: string;
|
|
257
|
+
body: string;
|
|
258
|
+
};
|
|
259
|
+
/** Untrusted: whoever wrote them was not gated. Absent when the payload lacked the issue. */
|
|
260
|
+
subject?: {
|
|
261
|
+
summary: string;
|
|
262
|
+
description: string;
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
/** Shared by the ingress and the tools: a Jira issue key, e.g. PROJ-123. */
|
|
266
|
+
export declare const ISSUE_KEY: RegExp;
|
|
267
|
+
/** A Jira project key. */
|
|
268
|
+
export declare const PROJECT_KEY: RegExp;
|
|
269
|
+
export declare const projectOf: (issueKey: string) => string;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const ISSUE_TRACKER_PROVIDERS = ["jira"];
|
|
2
|
+
/** The product name shown to people (e.g. in a pull request's "Resolves Jira issue …" line). */
|
|
3
|
+
export const ISSUE_TRACKER_NAMES = { jira: "Jira" };
|
|
4
|
+
export class IssueTrackerError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
constructor(code, message = code) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.name = "IssueTrackerError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/** Shared by the ingress and the tools: a Jira issue key, e.g. PROJ-123. */
|
|
13
|
+
export const ISSUE_KEY = /^[A-Z][A-Z0-9_]{0,254}-[1-9]\d{0,9}$/;
|
|
14
|
+
/** A Jira project key. */
|
|
15
|
+
export const PROJECT_KEY = /^[A-Z][A-Z0-9_]{0,254}$/;
|
|
16
|
+
export const projectOf = (issueKey) => issueKey.slice(0, issueKey.lastIndexOf("-"));
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type CollectExclusions } from "../../coding/collect-exclude.js";
|
|
2
2
|
import type { CodingProvider } from "../../coding/provider.js";
|
|
3
|
+
import { type ServiceStateReporter } from "./service-state.js";
|
|
3
4
|
import type { JobHandle, JobResult, JobSpec, JobStatus, WorkspaceJobLauncher } from "./types.js";
|
|
4
5
|
/** How long one service image pull may take before the run fails as service_unready. */
|
|
5
6
|
export declare const SERVICE_IMAGE_PULL_TIMEOUT_MS = 300000;
|
|
@@ -99,6 +100,8 @@ export interface DockerJobLauncherOptions {
|
|
|
99
100
|
* DEFAULT_SERVICE_READY_TIMEOUT_MS). Start-up never runs past the run's own deadline either.
|
|
100
101
|
*/
|
|
101
102
|
serviceReadyTimeoutMs?: number;
|
|
103
|
+
/** Display-only observer of each service's start-up state (the viewer); its errors are ignored. */
|
|
104
|
+
onServiceState?: ServiceStateReporter;
|
|
102
105
|
/** Optional observer invoked after provisioning fails but before resources are cleaned up. */
|
|
103
106
|
onProvisionFailure?: (context: {
|
|
104
107
|
runId: string;
|
|
@@ -148,7 +151,8 @@ export declare class DockerJobLauncher implements WorkspaceJobLauncher {
|
|
|
148
151
|
* attempt bounded by timeoutSeconds, failing after failureThreshold consecutive failures. A
|
|
149
152
|
* service that exited or vanished fails at once, and so does one still unready when the start-up
|
|
150
153
|
* budget runs out (each probe and wait is clipped to what is left of it). Probe output is never
|
|
151
|
-
* logged or kept, so these errors carry no cause.
|
|
154
|
+
* logged or kept, so these errors carry no cause. Returns the number of probes made; records
|
|
155
|
+
* why it failed in `failure` for the display-only state report.
|
|
152
156
|
*/
|
|
153
157
|
private waitForServiceReady;
|
|
154
158
|
private refresh;
|
|
@@ -12,6 +12,7 @@ import { assertDockerHostSupportsIsolation, assertClaudeAgentContainerInspection
|
|
|
12
12
|
import { serviceUnreadyError } from "../../coding/services/wording.js";
|
|
13
13
|
import { assertServiceContainerInspection, buildDockerServicePlan, dockerServiceContainerNames, } from "./docker-services.js";
|
|
14
14
|
import { claudeToolSetup } from "./claude-tool-setup.js";
|
|
15
|
+
import { reportServiceState, } from "./service-state.js";
|
|
15
16
|
import { replaceDirectoryFromStaging } from "./workspace-swap.js";
|
|
16
17
|
const BACKEND = "docker";
|
|
17
18
|
const STATE_SCHEMA_VERSION = 1;
|
|
@@ -773,41 +774,61 @@ export class DockerJobLauncher {
|
|
|
773
774
|
if (!keeperId)
|
|
774
775
|
throw new Error("docker_resource_attestation_failed");
|
|
775
776
|
await this.startContainer(keeperName);
|
|
777
|
+
const report = (name, update) => reportServiceState(this.options.onServiceState, { runId: record.runId, name, ...update });
|
|
778
|
+
for (const planned of services)
|
|
779
|
+
await report(planned.service.name, { state: "pending" });
|
|
776
780
|
for (const planned of services) {
|
|
777
781
|
const name = planned.service.name;
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
await this.ensureServiceImage(planned, budget);
|
|
781
|
-
if (budget.remainingMs() <= 0)
|
|
782
|
-
throw serviceUnready(name);
|
|
783
|
-
let createFailure;
|
|
782
|
+
const failure = { reason: "image_unavailable" };
|
|
783
|
+
let attempts;
|
|
784
784
|
try {
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
785
|
+
if (budget.remainingMs() <= 0)
|
|
786
|
+
throw serviceUnready(name);
|
|
787
|
+
await this.ensureServiceImage(planned, budget);
|
|
788
|
+
if (budget.remainingMs() <= 0)
|
|
789
|
+
throw serviceUnready(name);
|
|
790
|
+
failure.reason = "start_failed";
|
|
791
|
+
let createFailure;
|
|
792
|
+
try {
|
|
793
|
+
await this.createAndAssert(appendLabels(planned.createArgs, labels), ["container", "inspect", planned.container], (value) => {
|
|
794
|
+
const inspected = value;
|
|
795
|
+
assertServiceContainerInspection(inspected, record.spec, planned.service, keeperId);
|
|
796
|
+
if (!labelsMatch(inspected.Config?.Labels, labels))
|
|
797
|
+
throw new Error("docker_resource_attestation_failed");
|
|
798
|
+
}, undefined, (error) => {
|
|
799
|
+
createFailure = error;
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
catch (error) {
|
|
803
|
+
// createAndAssert tolerates a failed create (a crash retry may find it made), so a service
|
|
804
|
+
// Docker refused to create shows up as a failed inspection; the create's own failure is the
|
|
805
|
+
// useful cause, not that generic "not found". Attestation failures rethrow as is.
|
|
806
|
+
if (error instanceof DockerCommandError)
|
|
807
|
+
throw serviceUnready(name, createFailure ?? error);
|
|
808
|
+
throw error;
|
|
809
|
+
}
|
|
810
|
+
if (budget.remainingMs() <= 0)
|
|
811
|
+
throw serviceUnready(name);
|
|
812
|
+
try {
|
|
813
|
+
await this.startContainer(planned.container);
|
|
814
|
+
}
|
|
815
|
+
catch (error) {
|
|
816
|
+
throw serviceUnready(name, error);
|
|
817
|
+
}
|
|
818
|
+
failure.reason = "probe_failed";
|
|
819
|
+
await report(name, { state: "probing", attempts: 0 });
|
|
820
|
+
attempts = await this.waitForServiceReady(planned, budget, failure, (made) => report(name, { state: "probing", attempts: made }));
|
|
793
821
|
}
|
|
794
822
|
catch (error) {
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
823
|
+
const reason = failure.reason !== "exited" && budget.remainingMs() <= 0 ? "timed_out" : failure.reason;
|
|
824
|
+
await report(name, {
|
|
825
|
+
state: "failed",
|
|
826
|
+
reason,
|
|
827
|
+
...(failure.attempts === undefined ? {} : { attempts: failure.attempts }),
|
|
828
|
+
});
|
|
800
829
|
throw error;
|
|
801
830
|
}
|
|
802
|
-
|
|
803
|
-
throw serviceUnready(name);
|
|
804
|
-
try {
|
|
805
|
-
await this.startContainer(planned.container);
|
|
806
|
-
}
|
|
807
|
-
catch (error) {
|
|
808
|
-
throw serviceUnready(name, error);
|
|
809
|
-
}
|
|
810
|
-
await this.waitForServiceReady(planned, budget);
|
|
831
|
+
await report(name, { state: "ready", attempts });
|
|
811
832
|
}
|
|
812
833
|
return keeperId;
|
|
813
834
|
}
|
|
@@ -844,9 +865,10 @@ export class DockerJobLauncher {
|
|
|
844
865
|
* attempt bounded by timeoutSeconds, failing after failureThreshold consecutive failures. A
|
|
845
866
|
* service that exited or vanished fails at once, and so does one still unready when the start-up
|
|
846
867
|
* budget runs out (each probe and wait is clipped to what is left of it). Probe output is never
|
|
847
|
-
* logged or kept, so these errors carry no cause.
|
|
868
|
+
* logged or kept, so these errors carry no cause. Returns the number of probes made; records
|
|
869
|
+
* why it failed in `failure` for the display-only state report.
|
|
848
870
|
*/
|
|
849
|
-
async waitForServiceReady(planned, budget) {
|
|
871
|
+
async waitForServiceReady(planned, budget, failure, onProbeFailed) {
|
|
850
872
|
const { command, periodSeconds, timeoutSeconds, failureThreshold } = planned.service.readiness;
|
|
851
873
|
const name = planned.service.name;
|
|
852
874
|
let failures = 0;
|
|
@@ -858,12 +880,16 @@ export class DockerJobLauncher {
|
|
|
858
880
|
state = await this.inspect(["container", "inspect", planned.container]);
|
|
859
881
|
}
|
|
860
882
|
catch (error) {
|
|
861
|
-
if (error instanceof DockerCommandError)
|
|
883
|
+
if (error instanceof DockerCommandError) {
|
|
884
|
+
failure.reason = "exited";
|
|
862
885
|
throw serviceUnready(name);
|
|
886
|
+
}
|
|
863
887
|
throw error;
|
|
864
888
|
}
|
|
865
|
-
if (state.State?.Running !== true)
|
|
889
|
+
if (state.State?.Running !== true) {
|
|
890
|
+
failure.reason = "exited";
|
|
866
891
|
throw serviceUnready(name);
|
|
892
|
+
}
|
|
867
893
|
const remaining = budget.remainingMs();
|
|
868
894
|
if (remaining <= 0)
|
|
869
895
|
throw serviceUnready(name);
|
|
@@ -872,13 +898,15 @@ export class DockerJobLauncher {
|
|
|
872
898
|
timeoutMs: Math.min(timeoutSeconds * 1_000, remaining),
|
|
873
899
|
maxOutputBytes: MAX_SERVICE_PROBE_OUTPUT_BYTES,
|
|
874
900
|
});
|
|
875
|
-
return;
|
|
901
|
+
return failures + 1;
|
|
876
902
|
}
|
|
877
903
|
catch {
|
|
878
904
|
failures += 1;
|
|
905
|
+
failure.attempts = failures;
|
|
879
906
|
if (failures >= failureThreshold)
|
|
880
907
|
throw serviceUnready(name);
|
|
881
908
|
}
|
|
909
|
+
await onProbeFailed(failures);
|
|
882
910
|
const wait = Math.min(periodSeconds * 1_000, budget.remainingMs());
|
|
883
911
|
if (wait <= 0)
|
|
884
912
|
throw serviceUnready(name);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type Readable } from "node:stream";
|
|
2
2
|
import type { KubernetesJobConfig } from "../../config/providers.js";
|
|
3
3
|
import { type KubernetesApi } from "./kubernetes-api.js";
|
|
4
|
+
import { type ServiceStateReporter } from "./service-state.js";
|
|
4
5
|
import type { CodingProvider } from "../../coding/provider.js";
|
|
5
6
|
import type { JobHandle, JobResult, JobSpec, JobStatus, WorkspaceJobLauncher } from "./types.js";
|
|
6
7
|
export interface KubernetesClusterInfo {
|
|
@@ -38,6 +39,8 @@ export interface KubernetesJobLauncherOptions {
|
|
|
38
39
|
stream: Readable;
|
|
39
40
|
done: Promise<number>;
|
|
40
41
|
};
|
|
42
|
+
/** Display-only observer of each service sidecar's start-up state (the viewer); its errors are ignored. */
|
|
43
|
+
onServiceState?: ServiceStateReporter;
|
|
41
44
|
onWarning?: (message: string) => void;
|
|
42
45
|
}
|
|
43
46
|
/** Archives a trusted local directory with the host's `tar`, under a minimal environment. */
|
|
@@ -37,6 +37,7 @@ import { ENFORCEMENT_PROBE_DENY_REACHABLE, ENFORCEMENT_PROBE_PROVEN, ENFORCEMENT
|
|
|
37
37
|
import { claudeToolSetup } from "./claude-tool-setup.js";
|
|
38
38
|
import { readProxyWitness } from "./kubernetes-witness.js";
|
|
39
39
|
import { safeExtract } from "./safe-extract.js";
|
|
40
|
+
import { reportServiceState, } from "./service-state.js";
|
|
40
41
|
import { serviceUnreadyError } from "../../coding/services/wording.js";
|
|
41
42
|
import { replaceDirectoryFromStaging } from "./workspace-swap.js";
|
|
42
43
|
import { collectExclusions, normalizeCollectExclusions, tarExcludeArgs, } from "../../coding/collect-exclude.js";
|
|
@@ -156,6 +157,29 @@ const FATAL_WAITING_REASONS = new Set([
|
|
|
156
157
|
"CreateContainerError",
|
|
157
158
|
"CrashLoopBackOff",
|
|
158
159
|
]);
|
|
160
|
+
const IMAGE_WAITING_REASONS = new Set(["ErrImagePull", "ImagePullBackOff", "InvalidImageName"]);
|
|
161
|
+
/**
|
|
162
|
+
* A service sidecar's start-up state as the pod status shows it. The kubelet runs the startup
|
|
163
|
+
* probe, so attempt counts are unknown here; checks mirror waitForKeeper's own failure rules.
|
|
164
|
+
*/
|
|
165
|
+
function observedServiceState(status) {
|
|
166
|
+
if (status === undefined)
|
|
167
|
+
return { state: "pending" };
|
|
168
|
+
if ((status.restartCount ?? 0) > 0)
|
|
169
|
+
return { state: "failed", reason: "probe_failed" };
|
|
170
|
+
const waiting = status.state?.waiting?.reason ?? "";
|
|
171
|
+
if (IMAGE_WAITING_REASONS.has(waiting))
|
|
172
|
+
return { state: "failed", reason: "image_unavailable" };
|
|
173
|
+
if (FATAL_WAITING_REASONS.has(waiting))
|
|
174
|
+
return { state: "failed", reason: "start_failed" };
|
|
175
|
+
if (status.state?.terminated !== undefined)
|
|
176
|
+
return { state: "failed", reason: "exited" };
|
|
177
|
+
if (status.started === true)
|
|
178
|
+
return { state: "ready" };
|
|
179
|
+
if (status.state?.running !== undefined)
|
|
180
|
+
return { state: "probing" };
|
|
181
|
+
return { state: "pending" };
|
|
182
|
+
}
|
|
159
183
|
const CAPABILITY = /^rrp_[A-Za-z0-9_-]{16,512}$/;
|
|
160
184
|
const TOKEN = /^[a-f0-9]{20}$/;
|
|
161
185
|
const WORKSPACE_STORAGE = `${STORAGE_ROOT}/workspace`;
|
|
@@ -635,7 +659,7 @@ export class KubernetesJobLauncher {
|
|
|
635
659
|
await this.createIfMissing(() => this.api.createSecret(this.namespace, buildCapabilitySecret(spec, this.namespace, capability, toolSetup)));
|
|
636
660
|
await this.createIfMissing(() => this.api.createNetworkPolicy(this.namespace, policy));
|
|
637
661
|
await this.createIfMissing(() => this.api.createPod(this.namespace, pod));
|
|
638
|
-
await this.waitForKeeper(names, (spec.services ?? []).map((service) => service.name), spec.provider === "claude-code");
|
|
662
|
+
await this.waitForKeeper(names, spec.runId, (spec.services ?? []).map((service) => service.name), spec.provider === "claude-code");
|
|
639
663
|
const [actualPod, actualPolicy] = await Promise.all([
|
|
640
664
|
this.api.readPod(this.namespace, names.pod),
|
|
641
665
|
this.api.readNetworkPolicy(this.namespace, names.policy),
|
|
@@ -679,8 +703,16 @@ export class KubernetesJobLauncher {
|
|
|
679
703
|
throw error;
|
|
680
704
|
}
|
|
681
705
|
}
|
|
682
|
-
async waitForKeeper(names, serviceNames, toolRunner = false) {
|
|
706
|
+
async waitForKeeper(names, runId, serviceNames, toolRunner = false) {
|
|
683
707
|
const started = this.now();
|
|
708
|
+
const reported = new Map();
|
|
709
|
+
const report = async (name, observed) => {
|
|
710
|
+
const key = `${observed.state}:${observed.reason ?? ""}`;
|
|
711
|
+
if (reported.get(name) === key)
|
|
712
|
+
return;
|
|
713
|
+
reported.set(name, key);
|
|
714
|
+
await reportServiceState(this.options.onServiceState, { runId, name, ...observed });
|
|
715
|
+
};
|
|
684
716
|
for (;;) {
|
|
685
717
|
const pod = await this.api.readPod(this.namespace, names.pod);
|
|
686
718
|
if (!pod || pod.status?.phase === "Failed")
|
|
@@ -688,6 +720,8 @@ export class KubernetesJobLauncher {
|
|
|
688
720
|
const statuses = pod.status?.containerStatuses ?? [];
|
|
689
721
|
const initStatuses = pod.status?.initContainerStatuses ?? [];
|
|
690
722
|
const sidecar = (name) => initStatuses.find((status) => status.name === serviceContainerName(name));
|
|
723
|
+
for (const name of serviceNames)
|
|
724
|
+
await report(name, observedServiceState(sidecar(name)));
|
|
691
725
|
// A service sidecar that restarted (its startup probe gave up), can't be pulled or created, or
|
|
692
726
|
// exited will never let the keeper start. Name the service rather than a generic start failure.
|
|
693
727
|
const failedService = serviceNames.find((name) => {
|
|
@@ -711,8 +745,12 @@ export class KubernetesJobLauncher {
|
|
|
711
745
|
keeper?.state?.terminated !== undefined) {
|
|
712
746
|
throw new Error("kubernetes_pod_start_failed");
|
|
713
747
|
}
|
|
714
|
-
if (keeper?.ready === true)
|
|
748
|
+
if (keeper?.ready === true) {
|
|
749
|
+
// Native sidecars start in order before the keeper, so a ready keeper means every service started.
|
|
750
|
+
for (const name of serviceNames)
|
|
751
|
+
await report(name, { state: "ready" });
|
|
715
752
|
return;
|
|
753
|
+
}
|
|
716
754
|
if (this.now() - started >= this.readyTimeoutMs) {
|
|
717
755
|
if (toolRunner && toolStatus?.started !== true) {
|
|
718
756
|
throw new Error("kubernetes_tool_runner_unready");
|
|
@@ -720,8 +758,10 @@ export class KubernetesJobLauncher {
|
|
|
720
758
|
// At the bound, a service that never passed its startup probe (still pulling, still starting)
|
|
721
759
|
// is why the keeper never started.
|
|
722
760
|
const waiting = serviceNames.find((name) => sidecar(name)?.started !== true);
|
|
723
|
-
if (waiting !== undefined)
|
|
761
|
+
if (waiting !== undefined) {
|
|
762
|
+
await report(waiting, { state: "failed", reason: "timed_out" });
|
|
724
763
|
throw serviceUnreadyError(waiting);
|
|
764
|
+
}
|
|
725
765
|
throw new Error("kubernetes_pod_start_timeout");
|
|
726
766
|
}
|
|
727
767
|
await this.sleep(READY_POLL_MS);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Display-only observation of coding-run service start-up (docs/coding-services.md): launchers
|
|
3
|
+
* report each service's state as they see it, for the viewer. Never part of the launch's own
|
|
4
|
+
* control flow: a reporter error is swallowed. The launcher does await each report inline,
|
|
5
|
+
* though, so a reporter must be fast.
|
|
6
|
+
*/
|
|
7
|
+
export type ServiceState = "pending" | "probing" | "ready" | "failed";
|
|
8
|
+
/** Fixed identifiers only: never probe output, container logs, or anything from the repository. */
|
|
9
|
+
export type ServiceFailureReason = "image_unavailable" | "start_failed" | "exited" | "probe_failed" | "timed_out";
|
|
10
|
+
export interface ServiceStateUpdate {
|
|
11
|
+
runId: string;
|
|
12
|
+
/** The catalog service name (CodingService.name). */
|
|
13
|
+
name: string;
|
|
14
|
+
state: ServiceState;
|
|
15
|
+
/** Readiness probes completed so far; only launchers that run the probe themselves (Docker) know it. */
|
|
16
|
+
attempts?: number;
|
|
17
|
+
/** Set only with state "failed". */
|
|
18
|
+
reason?: ServiceFailureReason;
|
|
19
|
+
}
|
|
20
|
+
export type ServiceStateReporter = (update: ServiceStateUpdate) => Promise<void>;
|
|
21
|
+
/** Calls `reporter` if there is one; any error it throws is dropped (status is display-only). */
|
|
22
|
+
export declare function reportServiceState(reporter: ServiceStateReporter | undefined, update: ServiceStateUpdate): Promise<void>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Display-only observation of coding-run service start-up (docs/coding-services.md): launchers
|
|
3
|
+
* report each service's state as they see it, for the viewer. Never part of the launch's own
|
|
4
|
+
* control flow: a reporter error is swallowed. The launcher does await each report inline,
|
|
5
|
+
* though, so a reporter must be fast.
|
|
6
|
+
*/
|
|
7
|
+
/** Calls `reporter` if there is one; any error it throws is dropped (status is display-only). */
|
|
8
|
+
export async function reportServiceState(reporter, update) {
|
|
9
|
+
if (!reporter)
|
|
10
|
+
return;
|
|
11
|
+
try {
|
|
12
|
+
await reporter(update);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
// Display-only: a lost update must never fail or slow a launch.
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
* Direct Anthropic (Claude) adapter for the LlmProvider seam. `@anthropic-ai/sdk`
|
|
3
3
|
* is imported only here; all protocol/pricing-dispatch logic lives in the
|
|
4
4
|
* shared ClaudeLlmProvider base class (claude-provider.ts) — this file's only
|
|
5
|
-
* job is constructing the right client
|
|
5
|
+
* job is constructing the right client.
|
|
6
6
|
*/
|
|
7
7
|
import Anthropic from "@anthropic-ai/sdk";
|
|
8
8
|
import { ClaudeLlmProvider } from "./claude-provider.js";
|
|
9
|
-
|
|
9
|
+
import type { CatalogLookup } from "./catalog-lookup.js";
|
|
10
10
|
export declare function anthropicCredentialsPresent(env?: NodeJS.ProcessEnv): boolean;
|
|
11
11
|
export declare class AnthropicLlmProvider extends ClaudeLlmProvider {
|
|
12
|
-
constructor(apiKey?: string, client?: Anthropic);
|
|
12
|
+
constructor(apiKey?: string, client?: Anthropic, lookup?: CatalogLookup);
|
|
13
13
|
}
|
|
@@ -2,24 +2,18 @@
|
|
|
2
2
|
* Direct Anthropic (Claude) adapter for the LlmProvider seam. `@anthropic-ai/sdk`
|
|
3
3
|
* is imported only here; all protocol/pricing-dispatch logic lives in the
|
|
4
4
|
* shared ClaudeLlmProvider base class (claude-provider.ts) — this file's only
|
|
5
|
-
* job is constructing the right client
|
|
5
|
+
* job is constructing the right client.
|
|
6
6
|
*/
|
|
7
7
|
import Anthropic from "@anthropic-ai/sdk";
|
|
8
8
|
import { ClaudeLlmProvider } from "./claude-provider.js";
|
|
9
|
-
import { anthropicPriceUsd, anthropicSupportedEfforts, getAnthropicPricing } from "./pricing-anthropic.js";
|
|
10
|
-
export { anthropicSupportedModels } from "./pricing-anthropic.js";
|
|
11
9
|
export function anthropicCredentialsPresent(env = process.env) {
|
|
12
10
|
return Boolean(env.ANTHROPIC_API_KEY);
|
|
13
11
|
}
|
|
14
12
|
export class AnthropicLlmProvider extends ClaudeLlmProvider {
|
|
15
|
-
constructor(apiKey = process.env.ANTHROPIC_API_KEY ?? "", client) {
|
|
13
|
+
constructor(apiKey = process.env.ANTHROPIC_API_KEY ?? "", client, lookup) {
|
|
16
14
|
if (!client && !apiKey) {
|
|
17
15
|
throw new Error("ANTHROPIC_API_KEY is not set — required by the Anthropic LlmProvider adapter.");
|
|
18
16
|
}
|
|
19
|
-
super(client ?? new Anthropic({ apiKey }),
|
|
20
|
-
getPricing: getAnthropicPricing,
|
|
21
|
-
priceUsd: anthropicPriceUsd,
|
|
22
|
-
supportedEfforts: anthropicSupportedEfforts,
|
|
23
|
-
});
|
|
17
|
+
super(client ?? new Anthropic({ apiKey }), lookup);
|
|
24
18
|
}
|
|
25
19
|
}
|