@treeseed/sdk 0.8.7 → 0.8.8

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.
@@ -1,4 +1,4 @@
1
- import type { CapacityEstimateConfidence, CapacityGrant, CapacityPlan, CapacityProviderLane, CapacityReservation, TaskEstimateProfile } from './sdk-types.ts';
1
+ import type { CapacityEstimateConfidence, CapacityGrant, CapacityPlan, CapacityProvider, CapacityProviderLane, CapacityReservation, CreateCapacityReservationRequest, CreateCapacityRoutingDecisionRequest, RecordCapacityUsageRequest, TaskEstimateProfile } from './sdk-types.ts';
2
2
  import type { AgentProviderProfile } from './types/agents.ts';
3
3
  export type ProcessingEnvironment = 'local' | 'staging' | 'prod';
4
4
  export interface CapacityProviderRegistration {
@@ -62,6 +62,111 @@ export interface CapacityLaneScore {
62
62
  costPenalty: number;
63
63
  reasons: string[];
64
64
  }
65
+ export interface CapacityTaskEstimate {
66
+ taskSignature: string;
67
+ confidence: CapacityEstimateConfidence;
68
+ estimatedCreditsP50: number;
69
+ estimatedCreditsP90: number;
70
+ reservedCredits: number;
71
+ }
72
+ export interface TeamCapacitySummary {
73
+ teamId: string;
74
+ monthlyCredits: number | null;
75
+ monthlyUsedCredits: number;
76
+ monthlyRemainingCredits: number | null;
77
+ dailyCredits: number | null;
78
+ dailyUsedCredits: number;
79
+ dailyReservedCredits: number;
80
+ dailyRemainingCredits: number | null;
81
+ providerCount: number;
82
+ activeProviderCount: number;
83
+ degradedProviderCount: number;
84
+ grantCount: number;
85
+ blockedTaskCount: number;
86
+ approvalRequiredCount: number;
87
+ }
88
+ export interface ProjectCapacitySummary extends TeamCapacitySummary {
89
+ projectId: string;
90
+ environment: ProcessingEnvironment;
91
+ readiness: 'ready' | 'waiting_for_budget' | 'waiting_for_provider' | 'paused_by_policy' | 'needs_approval';
92
+ reasons: string[];
93
+ }
94
+ export interface RouteAndReserveInput {
95
+ plan: CapacityPlan;
96
+ estimate: CapacityTaskEstimate;
97
+ taskId?: string | null;
98
+ workDayId?: string | null;
99
+ taskKind?: string | null;
100
+ requiredCapabilities?: string[];
101
+ modelClass?: string | null;
102
+ priorityClass?: string | null;
103
+ allowDegradedProviders?: boolean;
104
+ repositoryMutation?: boolean;
105
+ production?: boolean;
106
+ selectedModel?: string | null;
107
+ source?: string;
108
+ metadata?: Record<string, unknown>;
109
+ }
110
+ export type RouteAndReserveBlockCode = 'no_capacity_provider' | 'no_capacity_grant' | 'no_eligible_lane' | 'insufficient_budget' | 'approval_required';
111
+ export interface RouteAndReserveCandidate {
112
+ providerId: string;
113
+ laneId: string;
114
+ grantId: string;
115
+ remainingCredits: number | null;
116
+ score: CapacityLaneScore;
117
+ eligible: boolean;
118
+ reasons: string[];
119
+ }
120
+ export type RouteAndReserveResult = {
121
+ ok: true;
122
+ provider: CapacityProvider;
123
+ lane: CapacityProviderLane;
124
+ grant: CapacityGrant;
125
+ estimate: CapacityTaskEstimate;
126
+ remainingCreditsBefore: number | null;
127
+ reservation: CreateCapacityReservationRequest;
128
+ routingDecision: CreateCapacityRoutingDecisionRequest;
129
+ ledgerEntry: RecordCapacityUsageRequest;
130
+ capacityMetadata: {
131
+ providerId: string;
132
+ laneId: string;
133
+ grantId: string;
134
+ reservationId: string | null;
135
+ routingDecisionId: string | null;
136
+ estimatedCreditsP50: number;
137
+ estimatedCreditsP90: number;
138
+ reservedCredits: number;
139
+ };
140
+ candidates: RouteAndReserveCandidate[];
141
+ } | {
142
+ ok: false;
143
+ code: RouteAndReserveBlockCode;
144
+ reason: string;
145
+ estimate: CapacityTaskEstimate;
146
+ candidates: RouteAndReserveCandidate[];
147
+ };
148
+ export interface CapacitySettlementInput {
149
+ reservation: CapacityReservation;
150
+ actualCredits: number;
151
+ actualProviderUnits?: number | null;
152
+ actualUsd?: number | null;
153
+ teamId?: string | null;
154
+ projectId?: string | null;
155
+ workDayId?: string | null;
156
+ taskId?: string | null;
157
+ source?: string;
158
+ metadata?: Record<string, unknown>;
159
+ }
160
+ export interface CapacitySettlement {
161
+ reservationId: string;
162
+ state: 'consumed' | 'overran_pending_approval';
163
+ consumeEntry: RecordCapacityUsageRequest;
164
+ releaseEntry: RecordCapacityUsageRequest | null;
165
+ overrunEntry: RecordCapacityUsageRequest | null;
166
+ consumedCredits: number;
167
+ releasedCredits: number;
168
+ overrunCredits: number;
169
+ }
65
170
  export declare function reserveCreditsForEstimate(input: CapacityEstimateInput): {
66
171
  taskSignature: string;
67
172
  confidence: CapacityEstimateConfidence;
@@ -78,9 +183,23 @@ export declare function summarizeCapacityPlan(plan: CapacityPlan): {
78
183
  laneCount: number;
79
184
  grantCount: number;
80
185
  };
186
+ export declare function summarizeTeamCapacityPlan(plan: CapacityPlan): TeamCapacitySummary;
187
+ export declare function summarizeProjectCapacityPlan(plan: CapacityPlan, options?: {
188
+ workPolicyEnabled?: boolean | null;
189
+ approvalRequiredCount?: number;
190
+ blockedTaskCount?: number;
191
+ }): ProjectCapacitySummary;
81
192
  export declare function scoreCapacityLane(input: CapacityLaneCandidate): CapacityLaneScore;
82
193
  export declare function selectBestCapacityLane(candidates: CapacityLaneCandidate[]): {
83
194
  selected: CapacityLaneScore;
84
195
  scores: CapacityLaneScore[];
85
196
  };
86
197
  export declare function reservationHasCapacity(reservation: CapacityReservation): boolean;
198
+ export declare function createReservationReleaseEntry(input: {
199
+ reservation: CapacityReservation;
200
+ credits?: number | null;
201
+ source?: string;
202
+ metadata?: Record<string, unknown>;
203
+ }): RecordCapacityUsageRequest;
204
+ export declare function settleCapacityActuals(input: CapacitySettlementInput): CapacitySettlement;
205
+ export declare function routeAndReserveCapacity(input: RouteAndReserveInput): RouteAndReserveResult;
package/dist/capacity.js CHANGED
@@ -6,6 +6,97 @@ function scarcityPenalty(level) {
6
6
  if (level === "medium") return 15;
7
7
  return 0;
8
8
  }
9
+ function metadataStatus(value) {
10
+ const status = value?.status;
11
+ return typeof status === "string" ? status : null;
12
+ }
13
+ function stringArray(value) {
14
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
15
+ }
16
+ function booleanValue(value) {
17
+ return typeof value === "boolean" ? value : null;
18
+ }
19
+ function numberValue(value) {
20
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
21
+ }
22
+ function reservationDebit(reservation) {
23
+ if (reservation.state === "released" || reservation.state === "expired" || reservation.state === "cancelled") {
24
+ return 0;
25
+ }
26
+ if (reservation.state === "consumed" || reservation.state === "failed") {
27
+ return Math.max(0, reservation.consumedCredits);
28
+ }
29
+ return Math.max(reservation.reservedCredits, reservation.consumedCredits, 0);
30
+ }
31
+ function activeReservationDebit(reservation) {
32
+ if (reservation.state === "reserved" || reservation.state === "consuming") {
33
+ return Math.max(reservation.reservedCredits, reservation.consumedCredits, 0);
34
+ }
35
+ if (reservation.state === "consumed" || reservation.state === "failed") {
36
+ return Math.max(reservation.consumedCredits, 0);
37
+ }
38
+ return 0;
39
+ }
40
+ function grantMatchesReservation(grant, reservation) {
41
+ if (grant.teamId !== reservation.teamId) return false;
42
+ if (grant.capacityProviderId !== reservation.capacityProviderId) return false;
43
+ if (grant.laneId && grant.laneId !== reservation.laneId) return false;
44
+ if (grant.projectId && grant.projectId !== reservation.projectId) return false;
45
+ return true;
46
+ }
47
+ function grantRemainingCredits(plan, grant) {
48
+ const limit = grant.dailyCreditLimit ?? grant.monthlyCreditLimit;
49
+ if (limit === null || limit === void 0) return null;
50
+ const debits = plan.activeReservations.filter((reservation) => grantMatchesReservation(grant, reservation)).reduce((total, reservation) => total + reservationDebit(reservation), 0);
51
+ return Math.max(0, Number(limit) - debits);
52
+ }
53
+ function providerIsEligible(provider, input) {
54
+ if (provider.status === "active") return true;
55
+ if (provider.status === "degraded" && input.allowDegradedProviders) return true;
56
+ return false;
57
+ }
58
+ function grantIsEligible(grant, input) {
59
+ if (grant.state !== "active") return false;
60
+ if (grant.teamId !== input.plan.teamId) return false;
61
+ if (grant.environment && grant.environment !== input.plan.environment) return false;
62
+ if (grant.projectId && grant.projectId !== input.plan.projectId) return false;
63
+ return true;
64
+ }
65
+ function lanePolicyReasons(lane, input) {
66
+ const reasons = [];
67
+ const laneStatus = metadataStatus(lane.metadata);
68
+ if (laneStatus && laneStatus !== "active") reasons.push(`lane_status:${laneStatus}`);
69
+ const policy = lane.routingPolicy ?? {};
70
+ const taskKinds = stringArray(policy.taskKinds);
71
+ const taskKind = input.taskKind ?? input.estimate.taskSignature;
72
+ if (taskKinds.length > 0 && !taskKinds.includes(taskKind)) reasons.push("task_kind_mismatch");
73
+ const requiredCapabilities = stringArray(policy.requiredCapabilities);
74
+ const missingCapabilities = (input.requiredCapabilities ?? []).filter((capability) => !requiredCapabilities.includes(capability));
75
+ if (requiredCapabilities.length > 0 && missingCapabilities.length > 0) {
76
+ reasons.push("capability_mismatch");
77
+ }
78
+ const allowedEnvironments = stringArray(policy.allowedEnvironments);
79
+ if (allowedEnvironments.length > 0 && !allowedEnvironments.includes(input.plan.environment)) {
80
+ reasons.push("environment_mismatch");
81
+ }
82
+ const maxCreditsPerTask = numberValue(policy.maxCreditsPerTask);
83
+ if (maxCreditsPerTask !== null && input.estimate.reservedCredits > maxCreditsPerTask) {
84
+ reasons.push("task_credit_limit_exceeded");
85
+ }
86
+ const approvalThreshold = numberValue(policy.requiresApprovalAboveCredits);
87
+ if (approvalThreshold !== null && input.estimate.reservedCredits > approvalThreshold) {
88
+ reasons.push("approval_required");
89
+ }
90
+ const repositoryMutationAllowed = booleanValue(policy.repositoryMutationAllowed);
91
+ if (input.repositoryMutation && repositoryMutationAllowed === false) {
92
+ reasons.push("repository_mutation_not_allowed");
93
+ }
94
+ const productionAllowed = booleanValue(policy.productionAllowed);
95
+ if (input.production && productionAllowed === false) {
96
+ reasons.push("production_not_allowed");
97
+ }
98
+ return reasons;
99
+ }
9
100
  function reserveCreditsForEstimate(input) {
10
101
  const profileP50 = finiteNumber(input.profile?.creditsP50);
11
102
  const profileP90 = finiteNumber(input.profile?.creditsP90);
@@ -26,7 +117,7 @@ function reserveCreditsForEstimate(input) {
26
117
  };
27
118
  }
28
119
  function summarizeCapacityPlan(plan) {
29
- const reservedCredits = plan.activeReservations.filter((reservation) => reservation.state === "reserved").reduce((total, reservation) => total + reservation.reservedCredits, 0);
120
+ const reservedCredits = plan.activeReservations.filter((reservation) => reservation.state === "reserved" || reservation.state === "consuming").reduce((total, reservation) => total + reservation.reservedCredits, 0);
30
121
  const consumedCredits = plan.activeReservations.reduce((total, reservation) => total + reservation.consumedCredits, 0);
31
122
  const grantedDailyCredits = plan.grants.filter((grant) => grant.state === "active").reduce((total, grant) => total + (grant.dailyCreditLimit ?? 0), 0);
32
123
  return {
@@ -39,6 +130,55 @@ function summarizeCapacityPlan(plan) {
39
130
  grantCount: plan.grants.length
40
131
  };
41
132
  }
133
+ function summarizeTeamCapacityPlan(plan) {
134
+ const dailyCredits = plan.grants.filter((grant) => grant.state === "active").reduce((total, grant) => total + (grant.dailyCreditLimit ?? 0), 0);
135
+ const monthlyCredits = plan.grants.filter((grant) => grant.state === "active").reduce((total, grant) => total + (grant.monthlyCreditLimit ?? 0), 0);
136
+ const dailyReservedCredits = plan.activeReservations.reduce((total, reservation) => total + activeReservationDebit(reservation), 0);
137
+ const dailyUsedCredits = plan.activeReservations.reduce((total, reservation) => total + Math.max(0, reservation.consumedCredits), 0);
138
+ return {
139
+ teamId: plan.teamId,
140
+ monthlyCredits: monthlyCredits > 0 ? monthlyCredits : null,
141
+ monthlyUsedCredits: dailyUsedCredits,
142
+ monthlyRemainingCredits: monthlyCredits > 0 ? Math.max(0, monthlyCredits - dailyUsedCredits) : null,
143
+ dailyCredits: dailyCredits > 0 ? dailyCredits : null,
144
+ dailyUsedCredits,
145
+ dailyReservedCredits,
146
+ dailyRemainingCredits: dailyCredits > 0 ? Math.max(0, dailyCredits - dailyReservedCredits - dailyUsedCredits) : null,
147
+ providerCount: plan.providers.length,
148
+ activeProviderCount: plan.providers.filter((provider) => provider.status === "active").length,
149
+ degradedProviderCount: plan.providers.filter((provider) => provider.status === "degraded").length,
150
+ grantCount: plan.grants.length,
151
+ blockedTaskCount: 0,
152
+ approvalRequiredCount: 0
153
+ };
154
+ }
155
+ function summarizeProjectCapacityPlan(plan, options = {}) {
156
+ const summary = summarizeTeamCapacityPlan(plan);
157
+ const reasons = [];
158
+ let readiness = "ready";
159
+ if (options.workPolicyEnabled === false) {
160
+ readiness = "paused_by_policy";
161
+ reasons.push("work_policy_disabled");
162
+ } else if (summary.activeProviderCount <= 0) {
163
+ readiness = "waiting_for_provider";
164
+ reasons.push("no_active_provider");
165
+ } else if (summary.dailyRemainingCredits !== null && summary.dailyRemainingCredits <= 0) {
166
+ readiness = "waiting_for_budget";
167
+ reasons.push("daily_budget_exhausted");
168
+ } else if ((options.approvalRequiredCount ?? 0) > 0) {
169
+ readiness = "needs_approval";
170
+ reasons.push("approval_required");
171
+ }
172
+ return {
173
+ ...summary,
174
+ projectId: plan.projectId,
175
+ environment: plan.environment,
176
+ readiness,
177
+ reasons,
178
+ blockedTaskCount: options.blockedTaskCount ?? summary.blockedTaskCount,
179
+ approvalRequiredCount: options.approvalRequiredCount ?? summary.approvalRequiredCount
180
+ };
181
+ }
42
182
  function scoreCapacityLane(input) {
43
183
  const reasons = [];
44
184
  let agentFit = 0;
@@ -91,10 +231,239 @@ function selectBestCapacityLane(candidates) {
91
231
  function reservationHasCapacity(reservation) {
92
232
  return reservation.state === "reserved" && reservation.reservedCredits > reservation.consumedCredits;
93
233
  }
234
+ function createReservationReleaseEntry(input) {
235
+ const credits = Math.max(0, Number(input.credits ?? input.reservation.reservedCredits - input.reservation.consumedCredits));
236
+ return {
237
+ capacityProviderId: input.reservation.capacityProviderId,
238
+ laneId: input.reservation.laneId,
239
+ reservationId: input.reservation.id,
240
+ teamId: input.reservation.teamId,
241
+ projectId: input.reservation.projectId,
242
+ workDayId: input.reservation.workDayId,
243
+ taskId: input.reservation.taskId,
244
+ phase: "reservation_released",
245
+ credits: -credits,
246
+ source: input.source ?? "capacity_coordinator",
247
+ metadata: input.metadata ?? {}
248
+ };
249
+ }
250
+ function settleCapacityActuals(input) {
251
+ const consumedCredits = Math.max(0, Number(input.actualCredits ?? 0));
252
+ const releasedCredits = Math.max(0, input.reservation.reservedCredits - consumedCredits);
253
+ const overrunCredits = Math.max(0, consumedCredits - input.reservation.reservedCredits);
254
+ const base = {
255
+ capacityProviderId: input.reservation.capacityProviderId,
256
+ laneId: input.reservation.laneId,
257
+ reservationId: input.reservation.id,
258
+ teamId: input.teamId ?? input.reservation.teamId,
259
+ projectId: input.projectId ?? input.reservation.projectId,
260
+ workDayId: input.workDayId ?? input.reservation.workDayId,
261
+ taskId: input.taskId ?? input.reservation.taskId,
262
+ source: input.source ?? "capacity_coordinator",
263
+ metadata: input.metadata ?? {}
264
+ };
265
+ const consumeEntry = {
266
+ ...base,
267
+ phase: "task_completed_actual_settlement",
268
+ credits: consumedCredits,
269
+ providerUnits: input.actualProviderUnits ?? null,
270
+ usd: input.actualUsd ?? null
271
+ };
272
+ const releaseEntry = releasedCredits > 0 ? {
273
+ ...base,
274
+ phase: "reservation_released",
275
+ credits: -releasedCredits
276
+ } : null;
277
+ const overrunEntry = overrunCredits > 0 ? {
278
+ ...base,
279
+ phase: "overrun_hold",
280
+ credits: overrunCredits
281
+ } : null;
282
+ return {
283
+ reservationId: input.reservation.id,
284
+ state: overrunCredits > 0 ? "overran_pending_approval" : "consumed",
285
+ consumeEntry,
286
+ releaseEntry,
287
+ overrunEntry,
288
+ consumedCredits,
289
+ releasedCredits,
290
+ overrunCredits
291
+ };
292
+ }
293
+ function routeAndReserveCapacity(input) {
294
+ const providers = input.plan.providers.filter((provider2) => providerIsEligible(provider2, input));
295
+ const grants = input.plan.grants.filter((grant2) => grantIsEligible(grant2, input));
296
+ const candidates = [];
297
+ for (const grant2 of grants) {
298
+ const provider2 = providers.find((candidate) => candidate.id === grant2.capacityProviderId);
299
+ if (!provider2) continue;
300
+ const lanes = input.plan.lanes.filter(
301
+ (lane2) => lane2.capacityProviderId === provider2.id && (!grant2.laneId || grant2.laneId === lane2.id)
302
+ );
303
+ for (const lane2 of lanes) {
304
+ const reasons = lanePolicyReasons(lane2, input);
305
+ const remainingCredits = grantRemainingCredits(input.plan, grant2);
306
+ if (remainingCredits !== null && remainingCredits < input.estimate.reservedCredits && (grant2.overflowPolicy === "deny" || grant2.overflowPolicy === "hard_grant")) {
307
+ reasons.push("insufficient_budget");
308
+ }
309
+ if (remainingCredits !== null && remainingCredits < input.estimate.reservedCredits && grant2.overflowPolicy === "approval_required") {
310
+ reasons.push("approval_required");
311
+ }
312
+ const score = scoreCapacityLane({
313
+ lane: lane2,
314
+ grant: grant2,
315
+ remainingCredits,
316
+ taskKind: input.taskKind ?? input.estimate.taskSignature,
317
+ requiredCapabilities: input.requiredCapabilities,
318
+ modelClass: input.modelClass ?? null
319
+ });
320
+ candidates.push({
321
+ providerId: provider2.id,
322
+ laneId: lane2.id,
323
+ grantId: grant2.id,
324
+ remainingCredits,
325
+ score,
326
+ eligible: reasons.length === 0,
327
+ reasons
328
+ });
329
+ }
330
+ }
331
+ if (input.plan.providers.length === 0 || providers.length === 0) {
332
+ return {
333
+ ok: false,
334
+ code: "no_capacity_provider",
335
+ reason: "No active helper capacity provider is available.",
336
+ estimate: input.estimate,
337
+ candidates
338
+ };
339
+ }
340
+ if (grants.length === 0) {
341
+ return {
342
+ ok: false,
343
+ code: "no_capacity_grant",
344
+ reason: "No active capacity grant is available for this team, project, and environment.",
345
+ estimate: input.estimate,
346
+ candidates
347
+ };
348
+ }
349
+ const eligible = candidates.filter((candidate) => candidate.eligible).sort((left, right) => right.score.score - left.score.score || left.laneId.localeCompare(right.laneId));
350
+ const selected = eligible[0] ?? null;
351
+ if (!selected) {
352
+ const hasApprovalBlock = candidates.some((candidate) => candidate.reasons.includes("approval_required"));
353
+ const hasBudgetBlock = candidates.some((candidate) => candidate.reasons.includes("insufficient_budget"));
354
+ return {
355
+ ok: false,
356
+ code: hasApprovalBlock ? "approval_required" : hasBudgetBlock ? "insufficient_budget" : "no_eligible_lane",
357
+ reason: hasApprovalBlock ? "The requested helper task needs approval before capacity can be reserved." : hasBudgetBlock ? "The requested helper task is above the remaining approved budget." : "No provider lane matches the task policy and capability requirements.",
358
+ estimate: input.estimate,
359
+ candidates
360
+ };
361
+ }
362
+ const provider = providers.find((candidate) => candidate.id === selected.providerId);
363
+ const lane = input.plan.lanes.find((candidate) => candidate.id === selected.laneId);
364
+ const grant = grants.find((candidate) => candidate.id === selected.grantId);
365
+ if (!provider || !lane || !grant) {
366
+ return {
367
+ ok: false,
368
+ code: "no_eligible_lane",
369
+ reason: "The selected capacity lane could not be resolved.",
370
+ estimate: input.estimate,
371
+ candidates
372
+ };
373
+ }
374
+ const candidatePayload = candidates.map((candidate) => ({
375
+ providerId: candidate.providerId,
376
+ laneId: candidate.laneId,
377
+ grantId: candidate.grantId,
378
+ remainingCredits: candidate.remainingCredits,
379
+ eligible: candidate.eligible,
380
+ reasons: candidate.reasons,
381
+ score: candidate.score.score
382
+ }));
383
+ const scorePayload = Object.fromEntries(candidates.map((candidate) => [candidate.laneId, candidate.score]));
384
+ const reservation = {
385
+ capacityProviderId: provider.id,
386
+ laneId: lane.id,
387
+ teamId: input.plan.teamId,
388
+ projectId: input.plan.projectId,
389
+ workDayId: input.workDayId ?? null,
390
+ taskId: input.taskId ?? null,
391
+ state: "reserved",
392
+ reservedCredits: input.estimate.reservedCredits,
393
+ metadata: {
394
+ ...input.metadata ?? {},
395
+ grantId: grant.id,
396
+ taskSignature: input.estimate.taskSignature,
397
+ estimatedCreditsP50: input.estimate.estimatedCreditsP50,
398
+ estimatedCreditsP90: input.estimate.estimatedCreditsP90
399
+ }
400
+ };
401
+ const routingDecision = {
402
+ taskId: input.taskId ?? null,
403
+ workDayId: input.workDayId ?? null,
404
+ projectId: input.plan.projectId,
405
+ selectedProviderId: provider.id,
406
+ selectedLaneId: lane.id,
407
+ selectedModel: input.selectedModel ?? null,
408
+ decision: "selected",
409
+ reason: selected.score.reasons.length > 0 ? selected.score.reasons.join(",") : "best_eligible_lane",
410
+ candidates: candidatePayload,
411
+ scores: scorePayload,
412
+ metadata: {
413
+ ...input.metadata ?? {},
414
+ grantId: grant.id,
415
+ remainingCreditsBefore: selected.remainingCredits,
416
+ reservedCredits: input.estimate.reservedCredits
417
+ }
418
+ };
419
+ const ledgerEntry = {
420
+ capacityProviderId: provider.id,
421
+ laneId: lane.id,
422
+ teamId: input.plan.teamId,
423
+ projectId: input.plan.projectId,
424
+ workDayId: input.workDayId ?? null,
425
+ taskId: input.taskId ?? null,
426
+ phase: "reservation_created",
427
+ credits: input.estimate.reservedCredits,
428
+ source: input.source ?? "capacity_coordinator",
429
+ metadata: {
430
+ ...input.metadata ?? {},
431
+ grantId: grant.id,
432
+ taskSignature: input.estimate.taskSignature
433
+ }
434
+ };
435
+ return {
436
+ ok: true,
437
+ provider,
438
+ lane,
439
+ grant,
440
+ estimate: input.estimate,
441
+ remainingCreditsBefore: selected.remainingCredits,
442
+ reservation,
443
+ routingDecision,
444
+ ledgerEntry,
445
+ capacityMetadata: {
446
+ providerId: provider.id,
447
+ laneId: lane.id,
448
+ grantId: grant.id,
449
+ reservationId: reservation.id ?? null,
450
+ routingDecisionId: routingDecision.id ?? null,
451
+ estimatedCreditsP50: input.estimate.estimatedCreditsP50,
452
+ estimatedCreditsP90: input.estimate.estimatedCreditsP90,
453
+ reservedCredits: input.estimate.reservedCredits
454
+ },
455
+ candidates
456
+ };
457
+ }
94
458
  export {
459
+ createReservationReleaseEntry,
95
460
  reservationHasCapacity,
96
461
  reserveCreditsForEstimate,
462
+ routeAndReserveCapacity,
97
463
  scoreCapacityLane,
98
464
  selectBestCapacityLane,
99
- summarizeCapacityPlan
465
+ settleCapacityActuals,
466
+ summarizeCapacityPlan,
467
+ summarizeProjectCapacityPlan,
468
+ summarizeTeamCapacityPlan
100
469
  };
package/dist/index.d.ts CHANGED
@@ -3,7 +3,7 @@ export { ContentGraphRuntime } from './graph.ts';
3
3
  export { projectConnectionModeFromHosting } from './sdk-types.ts';
4
4
  export { createControlPlaneReporter } from './control-plane.ts';
5
5
  export { ControlPlaneClient } from './control-plane-client.ts';
6
- export { reservationHasCapacity, reserveCreditsForEstimate, scoreCapacityLane, selectBestCapacityLane, summarizeCapacityPlan, } from './capacity.ts';
6
+ export { reservationHasCapacity, reserveCreditsForEstimate, routeAndReserveCapacity, scoreCapacityLane, selectBestCapacityLane, settleCapacityActuals, createReservationReleaseEntry, summarizeCapacityPlan, summarizeProjectCapacityPlan, summarizeTeamCapacityPlan, } from './capacity.ts';
7
7
  export { executeKnowledgeHubProviderLaunch, validateKnowledgeHubProviderLaunchPrerequisites, } from './operations/services/hub-provider-launch.ts';
8
8
  export { createKnowledgeHubRepositories, defaultHubContentResolutionPolicy, executeKnowledgeHubLaunch, normalizeKnowledgeHubLaunchIntent, planKnowledgeHubLaunch, planKnowledgeHubRepositories, validateRepositoryHost, type HubContentResolutionPolicy, type KnowledgeHubLaunchIntent, type KnowledgeHubLaunchPhase, type KnowledgeHubLaunchPlan, type KnowledgeHubLaunchResult, type KnowledgeHubRepositoryPlan, type RepositoryHost, } from './operations/services/hub-launch.ts';
9
9
  export { ensureRailwayEnvironment, ensureRailwayProject, ensureRailwayService, getRailwayAuthProfile, listRailwayEnvironments, listRailwayProjects, listRailwayServices, listRailwayVariables, railwayGraphqlRequest, resolveRailwayApiToken, resolveRailwayApiUrl, resolveRailwayWorkspace, resolveRailwayWorkspaceContext, upsertRailwayVariables, } from './operations/services/railway-api.ts';
package/dist/index.js CHANGED
@@ -6,9 +6,14 @@ import { ControlPlaneClient } from "./control-plane-client.js";
6
6
  import {
7
7
  reservationHasCapacity,
8
8
  reserveCreditsForEstimate,
9
+ routeAndReserveCapacity,
9
10
  scoreCapacityLane,
10
11
  selectBestCapacityLane,
11
- summarizeCapacityPlan
12
+ settleCapacityActuals,
13
+ createReservationReleaseEntry,
14
+ summarizeCapacityPlan,
15
+ summarizeProjectCapacityPlan,
16
+ summarizeTeamCapacityPlan
12
17
  } from "./capacity.js";
13
18
  import {
14
19
  executeKnowledgeHubProviderLaunch,
@@ -250,6 +255,7 @@ export {
250
255
  createFilesystemContentSource,
251
256
  createKnowledgeHubRepositories,
252
257
  createPublishedContentPipeline,
258
+ createReservationReleaseEntry,
253
259
  createTeamScopedR2OverlayContentPublishProvider,
254
260
  createTeamScopedR2OverlayContentRuntimeProvider,
255
261
  createTreeseedManagedToolEnv,
@@ -336,14 +342,18 @@ export {
336
342
  resolveTreeseedTenantRoot,
337
343
  resolveTreeseedToolBinary,
338
344
  resolveTreeseedToolCommand,
345
+ routeAndReserveCapacity,
339
346
  runTreeseedCopilotTask,
340
347
  runTreeseedVerifyDriver,
341
348
  scoreCapacityLane,
342
349
  selectBestCapacityLane,
343
350
  setActiveMarketProfile,
344
351
  setMarketSession,
352
+ settleCapacityActuals,
345
353
  signEditorialPreviewToken,
346
354
  summarizeCapacityPlan,
355
+ summarizeProjectCapacityPlan,
356
+ summarizeTeamCapacityPlan,
347
357
  tenantFeatureEnabled,
348
358
  tenantModelRendered,
349
359
  upsertRailwayVariables,
@@ -165,6 +165,38 @@ export declare class MarketClient {
165
165
  environments: ProjectEnvironmentAccess[];
166
166
  };
167
167
  }>;
168
+ teamCapacity(teamId: string): Promise<{
169
+ ok: true;
170
+ payload: Record<string, unknown>;
171
+ }>;
172
+ launchManagedCapacityProvider(teamId: string, body?: Record<string, unknown>): Promise<{
173
+ ok: true;
174
+ payload: Record<string, unknown>;
175
+ }>;
176
+ capacityProvider(providerId: string): Promise<{
177
+ ok: true;
178
+ payload: Record<string, unknown>;
179
+ }>;
180
+ resetCapacityProviderApiKey(providerId: string, body?: Record<string, unknown>): Promise<{
181
+ ok: true;
182
+ payload: Record<string, unknown>;
183
+ }>;
184
+ revokeCapacityProviderApiKey(providerId: string, keyId: string): Promise<{
185
+ ok: true;
186
+ payload: Record<string, unknown>;
187
+ }>;
188
+ capacityGrants(teamId: string): Promise<{
189
+ ok: true;
190
+ payload: unknown[];
191
+ }>;
192
+ createCapacityGrant(teamId: string, body: Record<string, unknown>): Promise<{
193
+ ok: true;
194
+ payload: Record<string, unknown>;
195
+ }>;
196
+ enqueueAgentTask(projectId: string, body: Record<string, unknown>): Promise<{
197
+ ok: true;
198
+ payload: Record<string, unknown>;
199
+ }>;
168
200
  catalog(kind?: string | null): Promise<{
169
201
  ok: true;
170
202
  payload: unknown[];
@@ -315,6 +315,54 @@ class MarketClient {
315
315
  { requireAuth: true }
316
316
  );
317
317
  }
318
+ teamCapacity(teamId) {
319
+ return this.request(
320
+ `/v1/teams/${encodeURIComponent(teamId)}/capacity`,
321
+ { requireAuth: true }
322
+ );
323
+ }
324
+ launchManagedCapacityProvider(teamId, body = {}) {
325
+ return this.request(
326
+ `/v1/teams/${encodeURIComponent(teamId)}/capacity/providers/managed`,
327
+ { method: "POST", body, requireAuth: true }
328
+ );
329
+ }
330
+ capacityProvider(providerId) {
331
+ return this.request(
332
+ `/v1/capacity/providers/${encodeURIComponent(providerId)}`,
333
+ { requireAuth: true }
334
+ );
335
+ }
336
+ resetCapacityProviderApiKey(providerId, body = {}) {
337
+ return this.request(
338
+ `/v1/capacity/providers/${encodeURIComponent(providerId)}/api-keys/reset`,
339
+ { method: "POST", body, requireAuth: true }
340
+ );
341
+ }
342
+ revokeCapacityProviderApiKey(providerId, keyId) {
343
+ return this.request(
344
+ `/v1/capacity/providers/${encodeURIComponent(providerId)}/api-keys/${encodeURIComponent(keyId)}/revoke`,
345
+ { method: "POST", requireAuth: true }
346
+ );
347
+ }
348
+ capacityGrants(teamId) {
349
+ return this.request(
350
+ `/v1/teams/${encodeURIComponent(teamId)}/capacity-grants`,
351
+ { requireAuth: true }
352
+ );
353
+ }
354
+ createCapacityGrant(teamId, body) {
355
+ return this.request(
356
+ `/v1/teams/${encodeURIComponent(teamId)}/capacity-grants`,
357
+ { method: "POST", body, requireAuth: true }
358
+ );
359
+ }
360
+ enqueueAgentTask(projectId, body) {
361
+ return this.request(
362
+ `/v1/projects/${encodeURIComponent(projectId)}/agent-tasks`,
363
+ { method: "POST", body, requireAuth: true }
364
+ );
365
+ }
318
366
  catalog(kind) {
319
367
  const query = kind ? `?kind=${encodeURIComponent(kind)}` : "";
320
368
  return this.request(`/v1/catalog${query}`, { requireAuth: Boolean(this.accessToken) });
@@ -1851,30 +1851,53 @@ function checkGitHubConnection({ tenantRoot, env }) {
1851
1851
  if (!gh) {
1852
1852
  return providerConnectionResult("github", false, "GitHub CLI `gh` is not installed.");
1853
1853
  }
1854
- const repository = maybeResolveGitHubRepositorySlug(tenantRoot);
1855
- const args = repository ? ["repo", "view", repository, "--json", "nameWithOwner", "--jq", ".nameWithOwner"] : ["api", "user", "--jq", ".login"];
1854
+ const identityMode = env.TREESEED_GITHUB_IDENTITY_MODE === "account" ? "account" : "repository";
1855
+ const repository = identityMode === "repository" ? maybeResolveGitHubRepositorySlug(tenantRoot) : null;
1856
+ const owner = typeof env.TREESEED_HOSTED_HUBS_GITHUB_OWNER === "string" ? env.TREESEED_HOSTED_HUBS_GITHUB_OWNER.trim() : "";
1857
+ const commandCandidates = repository ? [{
1858
+ args: ["repo", "view", repository, "--json", "nameWithOwner", "--jq", ".nameWithOwner"],
1859
+ successMessage: (resolved) => `GitHub token can access ${resolved || repository}.`
1860
+ }] : owner ? [
1861
+ {
1862
+ args: ["api", `orgs/${owner}`, "--jq", ".login"],
1863
+ successMessage: (resolved) => `GitHub token can access organization ${resolved || owner}.`,
1864
+ optional: true
1865
+ },
1866
+ {
1867
+ args: ["api", `users/${owner}`, "--jq", ".login"],
1868
+ successMessage: (resolved) => `GitHub token can access user ${resolved || owner}.`,
1869
+ optional: true
1870
+ }
1871
+ ] : [
1872
+ {
1873
+ args: ["api", "user", "--jq", ".login"],
1874
+ successMessage: (resolved) => resolved ? `Authenticated as ${resolved}.` : "GitHub API check succeeded."
1875
+ }
1876
+ ];
1877
+ let lastDetail = "";
1856
1878
  for (let attempt = 0; attempt < 3; attempt += 1) {
1857
- const result = spawnSync(gh, args, {
1858
- cwd: tenantRoot,
1859
- stdio: "pipe",
1860
- encoding: "utf8",
1861
- env: createTreeseedManagedToolEnv({ ...process.env, ...env }),
1862
- timeout: CLI_CHECK_TIMEOUT_MS
1863
- });
1864
- if (result.status === 0) {
1865
- const resolved = result.stdout.trim();
1866
- return providerConnectionResult(
1867
- "github",
1868
- true,
1869
- repository ? `GitHub token can access ${resolved || repository}.` : resolved ? `Authenticated as ${resolved}.` : "GitHub API check succeeded."
1870
- );
1879
+ for (const candidate of commandCandidates) {
1880
+ const result = spawnSync(gh, candidate.args, {
1881
+ cwd: tenantRoot,
1882
+ stdio: "pipe",
1883
+ encoding: "utf8",
1884
+ env: createTreeseedManagedToolEnv({ ...process.env, ...env }),
1885
+ timeout: CLI_CHECK_TIMEOUT_MS
1886
+ });
1887
+ if (result.status === 0) {
1888
+ return providerConnectionResult("github", true, candidate.successMessage(result.stdout.trim()));
1889
+ }
1890
+ lastDetail = formatCheckOutput(result) || "GitHub API check failed.";
1891
+ if (candidate.optional && !isTransientProviderConnectionError(lastDetail)) {
1892
+ continue;
1893
+ }
1894
+ break;
1871
1895
  }
1872
- const detail = formatCheckOutput(result) || "GitHub API check failed.";
1873
- if (attempt >= 2 || !isTransientProviderConnectionError(detail)) {
1874
- return providerConnectionResult("github", false, detail);
1896
+ if (attempt >= 2 || !isTransientProviderConnectionError(lastDetail)) {
1897
+ return providerConnectionResult("github", false, lastDetail || "GitHub API check failed.");
1875
1898
  }
1876
1899
  }
1877
- return providerConnectionResult("github", false, "GitHub API check failed.");
1900
+ return providerConnectionResult("github", false, lastDetail || "GitHub API check failed.");
1878
1901
  }
1879
1902
  function checkCloudflareConnection({ tenantRoot, env }) {
1880
1903
  if (!env.CLOUDFLARE_API_TOKEN) {
@@ -1964,8 +1987,22 @@ async function checkRailwayConnection({ tenantRoot, env }) {
1964
1987
  }
1965
1988
  async function checkTreeseedProviderConnections({ tenantRoot, scope = "prod", env = process.env, valuesOverlay = {} } = {}) {
1966
1989
  const values = collectTreeseedConfigSeedValues(tenantRoot, scope, env, valuesOverlay);
1990
+ const passthroughValue = (key) => {
1991
+ const overlayValue = valuesOverlay?.[key];
1992
+ if (typeof overlayValue === "string" && overlayValue.trim()) {
1993
+ return overlayValue.trim();
1994
+ }
1995
+ const envValue = env?.[key];
1996
+ if (typeof envValue === "string" && envValue.trim()) {
1997
+ return envValue.trim();
1998
+ }
1999
+ const resolvedValue = values?.[key];
2000
+ return typeof resolvedValue === "string" && resolvedValue.trim() ? resolvedValue.trim() : void 0;
2001
+ };
1967
2002
  const rawCommandEnv = {
1968
2003
  GH_TOKEN: values.GH_TOKEN,
2004
+ TREESEED_GITHUB_IDENTITY_MODE: passthroughValue("TREESEED_GITHUB_IDENTITY_MODE"),
2005
+ TREESEED_HOSTED_HUBS_GITHUB_OWNER: passthroughValue("TREESEED_HOSTED_HUBS_GITHUB_OWNER"),
1969
2006
  CLOUDFLARE_API_TOKEN: values.CLOUDFLARE_API_TOKEN,
1970
2007
  CLOUDFLARE_ACCOUNT_ID: values.CLOUDFLARE_ACCOUNT_ID,
1971
2008
  RAILWAY_API_TOKEN: values.RAILWAY_API_TOKEN,
@@ -514,8 +514,14 @@ async function runTreeseedHostingAudit({
514
514
  const connectionReport = await checkTreeseedProviderConnections({
515
515
  tenantRoot,
516
516
  scope: resolved.scope,
517
- env: values,
518
- valuesOverlay: values
517
+ env: {
518
+ ...values,
519
+ TREESEED_GITHUB_IDENTITY_MODE: "account"
520
+ },
521
+ valuesOverlay: {
522
+ ...values,
523
+ TREESEED_GITHUB_IDENTITY_MODE: "account"
524
+ }
519
525
  });
520
526
  checks.push(...providerConnectionChecks(connectionReport, hostKinds));
521
527
  if (hostKinds.includes("email")) {
@@ -48,6 +48,8 @@ import {
48
48
  resolveRailwayWorkspaceContext,
49
49
  upsertRailwayVariables
50
50
  } from "../operations/services/railway-api.js";
51
+ import { loadTreeseedReconcileState } from "./state.js";
52
+ import { createTreeseedReconcileUnitId } from "./units.js";
51
53
  function toDeployTarget(target) {
52
54
  return target.kind === "persistent" ? createPersistentDeployTarget(target.scope) : createBranchPreviewDeployTarget(target.branchName);
53
55
  }
@@ -289,6 +291,28 @@ function storeCustomDomainState(input, provider, domain, value) {
289
291
  function getCustomDomainState(input, provider, domain) {
290
292
  return input.context.session.get(customDomainStateKey(provider, domain));
291
293
  }
294
+ function getPersistedCustomDomainState(input, provider, domain) {
295
+ if (!domain) {
296
+ return null;
297
+ }
298
+ if (provider === "railway") {
299
+ try {
300
+ const state = loadTreeseedReconcileState(input.context.tenantRoot, input.context.target);
301
+ const unitId = createTreeseedReconcileUnitId("custom-domain:api", domain);
302
+ const unit = state.units[unitId];
303
+ const reconciled = unit?.lastReconciledState;
304
+ if (reconciled && typeof reconciled === "object" && reconciled.domain === domain) {
305
+ return reconciled;
306
+ }
307
+ const observed = unit?.lastObservedState;
308
+ if (observed && typeof observed === "object" && observed.domain === domain) {
309
+ return observed;
310
+ }
311
+ } catch {
312
+ }
313
+ }
314
+ return null;
315
+ }
292
316
  function listCloudflareDnsRecords(env, zoneId, recordName) {
293
317
  const query = recordName ? `?name=${encodeURIComponent(recordName)}` : "";
294
318
  const payload = cloudflareApiRequest(`/zones/${encodeURIComponent(zoneId)}/dns_records${query}`, {
@@ -402,22 +426,70 @@ function normalizeRailwayDomainDnsRecord(value) {
402
426
  status: typeof record.status === "string" ? record.status.trim().toUpperCase() : ""
403
427
  };
404
428
  }
429
+ function firstRailwayDomainString(...values) {
430
+ for (const value of values) {
431
+ if (typeof value === "string" && value.trim()) {
432
+ return value.trim();
433
+ }
434
+ }
435
+ return null;
436
+ }
437
+ function firstRailwayDomainArray(...values) {
438
+ for (const value of values) {
439
+ if (Array.isArray(value)) {
440
+ return value;
441
+ }
442
+ }
443
+ return [];
444
+ }
405
445
  function normalizeRailwayDomainPayload(value) {
406
446
  if (!value || typeof value !== "object") {
407
447
  return null;
408
448
  }
409
449
  const record = value;
450
+ const status = record.status && typeof record.status === "object" ? record.status : {};
410
451
  const domain = typeof record.domain === "string" ? record.domain.trim() : typeof record.name === "string" ? record.name.trim() : "";
411
- const dnsRecordCandidates = Array.isArray(record.dnsRecords) ? record.dnsRecords : Array.isArray(record.status?.dnsRecords) ? record.status.dnsRecords : [];
452
+ const serviceDomain = firstRailwayDomainString(
453
+ record.serviceDomain,
454
+ record.target,
455
+ record.targetDomain,
456
+ record.cnameTarget,
457
+ record.cname,
458
+ record.dnsTarget,
459
+ status.serviceDomain,
460
+ status.target,
461
+ status.targetDomain,
462
+ status.cnameTarget,
463
+ status.cname,
464
+ status.dnsTarget
465
+ );
466
+ const dnsRecordCandidates = firstRailwayDomainArray(
467
+ record.dnsRecords,
468
+ record.requiredDnsRecords,
469
+ record.requiredRecords,
470
+ record.records,
471
+ record.dns,
472
+ status.dnsRecords,
473
+ status.requiredDnsRecords,
474
+ status.requiredRecords,
475
+ status.records,
476
+ status.dns
477
+ );
412
478
  const dnsRecords = dnsRecordCandidates.map((entry) => normalizeRailwayDomainDnsRecord(entry)).filter(Boolean);
479
+ const effectiveDnsRecords = dnsRecords.length > 0 || !domain || !serviceDomain || serviceDomain === domain ? dnsRecords : [{
480
+ type: "CNAME",
481
+ name: domain,
482
+ content: serviceDomain,
483
+ status: ""
484
+ }];
413
485
  return {
414
486
  id: typeof record.id === "string" ? record.id.trim() : null,
415
487
  domain,
416
- serviceDomain: typeof record.serviceDomain === "string" ? record.serviceDomain.trim() : typeof record.target === "string" ? record.target.trim() : null,
417
- certificateStatus: typeof record.status?.certificateStatus === "string" ? String(record.status.certificateStatus).trim().toUpperCase() : null,
418
- verificationDnsHost: typeof record.verificationDnsHost === "string" ? record.verificationDnsHost.trim() : typeof record.status?.verificationDnsHost === "string" ? String(record.status.verificationDnsHost).trim() : null,
419
- verificationToken: typeof record.verificationToken === "string" ? record.verificationToken.trim() : typeof record.status?.verificationToken === "string" ? String(record.status.verificationToken).trim() : null,
420
- dnsRecords
488
+ serviceDomain,
489
+ certificateStatus: typeof status.certificateStatus === "string" ? String(status.certificateStatus).trim().toUpperCase() : null,
490
+ verificationDnsHost: typeof record.verificationDnsHost === "string" ? record.verificationDnsHost.trim() : typeof status.verificationDnsHost === "string" ? String(status.verificationDnsHost).trim() : null,
491
+ verificationToken: typeof record.verificationToken === "string" ? record.verificationToken.trim() : typeof status.verificationToken === "string" ? String(status.verificationToken).trim() : null,
492
+ dnsRecords: effectiveDnsRecords
421
493
  };
422
494
  }
423
495
  async function ensureRailwayCustomDomain(input, service, domain, env, identifiers) {
@@ -1581,8 +1653,16 @@ function resolveDesiredDnsRecords(input) {
1581
1653
  if (!domain) {
1582
1654
  return [];
1583
1655
  }
1584
- const railwayState = getCustomDomainState(input, "railway", domain) ?? input.persistedState?.lastObservedState ?? input.persistedState?.lastReconciledState;
1656
+ const railwayState = getCustomDomainState(input, "railway", domain) ?? getPersistedCustomDomainState(input, "railway", domain) ?? input.persistedState?.lastObservedState ?? input.persistedState?.lastReconciledState;
1585
1657
  const records = Array.isArray(railwayState?.dnsRecords) ? railwayState.dnsRecords.map((entry) => normalizeRailwayDomainDnsRecord(entry)).filter(Boolean) : [];
1658
+ if (records.length === 0 && typeof railwayState?.serviceDomain === "string" && railwayState.serviceDomain.trim() && railwayState.serviceDomain.trim() !== domain) {
1659
+ records.push({
1660
+ type: "CNAME",
1661
+ name: domain,
1662
+ content: railwayState.serviceDomain.trim(),
1663
+ status: ""
1664
+ });
1665
+ }
1586
1666
  const desiredRecords = records.map((record) => ({
1587
1667
  ...record,
1588
1668
  proxied: false
@@ -1685,8 +1765,9 @@ function verifyCustomDomainUnit(input) {
1685
1765
  }
1686
1766
  case "custom-domain:api": {
1687
1767
  const domain = String(input.unit.spec.domain ?? "").trim();
1688
- const live = getCustomDomainState(input, "railway", domain) ?? input.persistedState?.lastObservedState ?? input.persistedState?.lastReconciledState ?? null;
1768
+ const live = getCustomDomainState(input, "railway", domain) ?? getPersistedCustomDomainState(input, "railway", domain) ?? input.persistedState?.lastObservedState ?? input.persistedState?.lastReconciledState ?? null;
1689
1769
  const dnsRecords = Array.isArray(live?.dnsRecords) ? live.dnsRecords : [];
1770
+ const hasDnsRequirements = dnsRecords.length > 0 || typeof live?.serviceDomain === "string" && live.serviceDomain.trim().length > 0 || typeof live?.verificationDnsHost === "string" && live.verificationDnsHost.trim().length > 0 && typeof live?.verificationToken === "string" && live.verificationToken.trim().length > 0;
1690
1771
  return summarizeVerification(input.unit.unitId, [
1691
1772
  verificationCheck("custom-domain.exists", "Railway custom domain attachment exists", "cli", {
1692
1773
  exists: Boolean(live?.domain),
@@ -1695,10 +1776,13 @@ function verifyCustomDomainUnit(input) {
1695
1776
  issues: live?.domain ? [] : [`Railway custom domain ${domain || "(unset)"} is missing.`]
1696
1777
  }),
1697
1778
  verificationCheck("custom-domain.dns-requirements", "Railway custom domain exposes DNS requirements", "api", {
1698
- exists: dnsRecords.length > 0,
1779
+ exists: hasDnsRequirements,
1699
1780
  expected: true,
1700
- observed: dnsRecords.length,
1701
- issues: dnsRecords.length > 0 ? [] : [`Railway custom domain ${domain || "(unset)"} did not expose DNS requirements.`]
1781
+ observed: dnsRecords.length > 0 ? dnsRecords.length : {
1782
+ serviceDomain: typeof live?.serviceDomain === "string" ? live.serviceDomain : null,
1783
+ verificationDnsHost: typeof live?.verificationDnsHost === "string" ? live.verificationDnsHost : null
1784
+ },
1785
+ issues: hasDnsRequirements ? [] : [`Railway custom domain ${domain || "(unset)"} did not expose DNS requirements.`]
1702
1786
  })
1703
1787
  ]);
1704
1788
  }
@@ -394,21 +394,21 @@ export interface TaskCreditLedgerEntry {
394
394
  projectId: string;
395
395
  workDayId: string;
396
396
  taskId: string | null;
397
- phase: 'seed' | 'settle' | 'refund' | 'grant' | 'reserve' | 'consume' | 'release' | 'adjustment';
397
+ phase: 'seed' | 'settle' | 'refund' | 'grant' | 'reserve' | 'consume' | 'release' | 'adjustment' | 'grant_created' | 'reservation_created' | 'reservation_released' | 'task_started' | 'task_completed_estimate_settlement' | 'task_completed_actual_settlement' | 'task_failed_refund' | 'manual_adjustment' | 'monthly_rollover' | 'overrun_hold';
398
398
  credits: number;
399
399
  metadata?: Record<string, unknown>;
400
400
  createdAt: string;
401
401
  }
402
402
  export type CapacityProviderKind = 'treeseed_managed' | 'team_owned' | 'external' | 'hybrid';
403
- export type CapacityProviderStatus = 'pending' | 'active' | 'paused' | 'configuration_required' | 'disabled';
403
+ export type CapacityProviderStatus = 'pending' | 'credential_required' | 'registering' | 'active' | 'degraded' | 'draining' | 'paused' | 'configuration_required' | 'disabled' | 'failed';
404
404
  export type CapacityProviderBillingScope = 'treeseed' | 'team' | 'external';
405
405
  export type CapacityBusinessModel = 'subscription_quota' | 'token_metered' | 'hybrid_usage_based' | 'infrastructure_runtime' | 'custom';
406
406
  export type CapacityLaneUnit = 'treeseed_credit' | 'quota_minute' | 'token_usd' | 'github_ai_credit' | 'worker_second' | 'request' | 'custom';
407
407
  export type CapacityScarcityLevel = 'low' | 'medium' | 'high';
408
408
  export type CapacityGrantScope = 'team' | 'project' | 'workday' | 'overflow_pool';
409
409
  export type CapacityGrantState = 'active' | 'paused' | 'expired' | 'disabled';
410
- export type CapacityOverflowPolicy = 'hard_grant' | 'soft_grant' | 'weighted_fair_share' | 'approval_required';
411
- export type CapacityReservationState = 'reserved' | 'consumed' | 'released' | 'expired' | 'cancelled';
410
+ export type CapacityOverflowPolicy = 'deny' | 'hard_grant' | 'soft_grant' | 'weighted_fair_share' | 'approval_required' | 'fallback_lane' | 'platform_subsidy';
411
+ export type CapacityReservationState = 'reserved' | 'consuming' | 'consumed' | 'released' | 'expired' | 'cancelled' | 'failed' | 'overran_pending_approval';
412
412
  export type CapacityEstimatePhase = 'intent' | 'discovery' | 'plan' | 'execution' | 'actual';
413
413
  export type CapacityEstimateConfidence = 'low' | 'medium' | 'high';
414
414
  export type CapacityApprovalState = 'pending' | 'approved' | 'rejected' | 'expired' | 'superseded';
@@ -1667,6 +1667,7 @@ export interface RecordCapacityUsageRequest {
1667
1667
  usd?: number | null;
1668
1668
  source?: string;
1669
1669
  metadata?: Record<string, unknown> | null;
1670
+ usageActual?: Record<string, unknown> | null;
1670
1671
  }
1671
1672
  export interface CreateCapacityRoutingDecisionRequest {
1672
1673
  id?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@treeseed/sdk",
3
- "version": "0.8.7",
3
+ "version": "0.8.8",
4
4
  "description": "Shared Treeseed SDK for content-backed and D1-backed object models.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {