@treeseed/sdk 0.8.6 → 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) });
@@ -256,7 +256,7 @@ export declare function validateTreeseedCommandEnvironment({ tenantRoot, scope,
256
256
  }): {
257
257
  registry: import("../../platform/environment.ts").TreeseedResolvedEnvironmentRegistry;
258
258
  values: {};
259
- validation: import("../../platform/environment.ts").TreeseedEnvironmentValidationResult;
259
+ validation: any;
260
260
  };
261
261
  export declare function assertTreeseedCommandEnvironment({ tenantRoot, scope, purpose }: {
262
262
  tenantRoot: any;
@@ -265,7 +265,7 @@ export declare function assertTreeseedCommandEnvironment({ tenantRoot, scope, pu
265
265
  }): {
266
266
  registry: import("../../platform/environment.ts").TreeseedResolvedEnvironmentRegistry;
267
267
  values: {};
268
- validation: import("../../platform/environment.ts").TreeseedEnvironmentValidationResult;
268
+ validation: any;
269
269
  };
270
270
  export declare function ensureTreeseedActVerificationTooling({ tenantRoot, installIfMissing, env, write }?: {
271
271
  tenantRoot?: string | undefined;
@@ -34,7 +34,8 @@ import {
34
34
  } from "./railway-deploy.js";
35
35
  import {
36
36
  normalizeRailwayEnvironmentName,
37
- resolveRailwayWorkspace
37
+ resolveRailwayWorkspace,
38
+ resolveRailwayWorkspaceContext
38
39
  } from "./railway-api.js";
39
40
  import {
40
41
  createGitHubApiClient,
@@ -1585,20 +1586,52 @@ function applyTreeseedEnvironmentToProcess({ tenantRoot, scope, override = false
1585
1586
  function validateTreeseedCommandEnvironment({ tenantRoot, scope, purpose }) {
1586
1587
  const registry = collectTreeseedEnvironmentContext(tenantRoot);
1587
1588
  const values = resolveTreeseedLaunchEnvironment({ tenantRoot, scope });
1588
- const validation = validateTreeseedEnvironmentValues({
1589
+ const validation = filterValidationByWorkflowPlane(validateTreeseedEnvironmentValues({
1589
1590
  values,
1590
1591
  scope,
1591
1592
  purpose,
1592
1593
  deployConfig: registry.context.deployConfig,
1593
1594
  tenantConfig: registry.context.tenantConfig,
1594
1595
  plugins: registry.context.plugins
1595
- });
1596
+ }));
1596
1597
  return {
1597
1598
  registry,
1598
1599
  values,
1599
1600
  validation
1600
1601
  };
1601
1602
  }
1603
+ function filterValidationByWorkflowPlane(validation) {
1604
+ const plane = process.env.TREESEED_WORKFLOW_PLANE;
1605
+ if (plane !== "web" && plane !== "processing") {
1606
+ return validation;
1607
+ }
1608
+ const problemApplies = (problem) => doesEntryApplyToWorkflowPlane(problem.entry, plane);
1609
+ const missing = validation.missing.filter(problemApplies);
1610
+ const invalid = validation.invalid.filter(problemApplies);
1611
+ const entries = validation.entries.filter((entry) => doesEntryApplyToWorkflowPlane(entry, plane));
1612
+ const required = validation.required.filter((entry) => doesEntryApplyToWorkflowPlane(entry, plane));
1613
+ return {
1614
+ ...validation,
1615
+ ok: missing.length === 0 && invalid.length === 0,
1616
+ entries,
1617
+ required,
1618
+ missing,
1619
+ invalid
1620
+ };
1621
+ }
1622
+ function doesEntryApplyToWorkflowPlane(entry, plane) {
1623
+ const targets = new Set(entry.targets ?? []);
1624
+ const hasProcessingTarget = targets.has("railway-secret") || targets.has("railway-var");
1625
+ const hasWebTarget = targets.has("cloudflare-secret") || targets.has("cloudflare-var") || targets.has("local-cloudflare");
1626
+ const hasWorkflowTarget = targets.has("github-secret") || targets.has("github-variable");
1627
+ if (plane === "web") {
1628
+ return !hasProcessingTarget || hasWebTarget || hasWorkflowTarget;
1629
+ }
1630
+ if (plane === "processing") {
1631
+ return !hasWebTarget || hasProcessingTarget || hasWorkflowTarget;
1632
+ }
1633
+ return true;
1634
+ }
1602
1635
  function assertTreeseedCommandEnvironment({ tenantRoot, scope, purpose }) {
1603
1636
  const report = validateTreeseedCommandEnvironment({ tenantRoot, scope, purpose });
1604
1637
  if (report.validation.ok) {
@@ -1818,30 +1851,53 @@ function checkGitHubConnection({ tenantRoot, env }) {
1818
1851
  if (!gh) {
1819
1852
  return providerConnectionResult("github", false, "GitHub CLI `gh` is not installed.");
1820
1853
  }
1821
- const repository = maybeResolveGitHubRepositorySlug(tenantRoot);
1822
- 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 = "";
1823
1878
  for (let attempt = 0; attempt < 3; attempt += 1) {
1824
- const result = spawnSync(gh, args, {
1825
- cwd: tenantRoot,
1826
- stdio: "pipe",
1827
- encoding: "utf8",
1828
- env: createTreeseedManagedToolEnv({ ...process.env, ...env }),
1829
- timeout: CLI_CHECK_TIMEOUT_MS
1830
- });
1831
- if (result.status === 0) {
1832
- const resolved = result.stdout.trim();
1833
- return providerConnectionResult(
1834
- "github",
1835
- true,
1836
- repository ? `GitHub token can access ${resolved || repository}.` : resolved ? `Authenticated as ${resolved}.` : "GitHub API check succeeded."
1837
- );
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;
1838
1895
  }
1839
- const detail = formatCheckOutput(result) || "GitHub API check failed.";
1840
- if (attempt >= 2 || !isTransientProviderConnectionError(detail)) {
1841
- return providerConnectionResult("github", false, detail);
1896
+ if (attempt >= 2 || !isTransientProviderConnectionError(lastDetail)) {
1897
+ return providerConnectionResult("github", false, lastDetail || "GitHub API check failed.");
1842
1898
  }
1843
1899
  }
1844
- return providerConnectionResult("github", false, "GitHub API check failed.");
1900
+ return providerConnectionResult("github", false, lastDetail || "GitHub API check failed.");
1845
1901
  }
1846
1902
  function checkCloudflareConnection({ tenantRoot, env }) {
1847
1903
  if (!env.CLOUDFLARE_API_TOKEN) {
@@ -1894,23 +1950,26 @@ async function checkRailwayConnection({ tenantRoot, env }) {
1894
1950
  const checkPromise = (async () => {
1895
1951
  for (let attempt = 0; attempt < 3; attempt += 1) {
1896
1952
  try {
1897
- const railwayCommand = resolveTreeseedToolCommand("railway", { env });
1898
- const whoami = railwayCommand ? checkCommand(railwayCommand.command, [...railwayCommand.argsPrefix, "whoami"], { cwd: tenantRoot, env }) : { ok: false, stdout: "", detail: "Railway CLI is unavailable." };
1899
- if (!whoami.ok) {
1900
- if (/rate.?limit|too many requests|429/iu.test(whoami.detail || "")) {
1901
- return providerConnectionResult(
1902
- "railway",
1903
- false,
1904
- "Railway connectivity preflight was rate-limited; bootstrap will continue and rely on API-backed reconcile verification.",
1905
- { skipped: true, warning: true, rateLimited: true }
1906
- );
1907
- }
1908
- throw new Error(whoami.detail || "Railway CLI authentication check failed.");
1909
- }
1910
- const identity = whoami.stdout.replace(/^logged in as\s+/iu, "").replace(/\s*👋\s*$/u, "").trim() || "an account";
1911
- return providerConnectionResult("railway", true, `Railway authenticated as ${identity} in workspace ${workspaceName}. Project and service existence will be reconciled during bootstrap.`);
1953
+ const workspace = await resolveRailwayWorkspaceContext({ env, workspace: workspaceName });
1954
+ return providerConnectionResult("railway", true, `Railway API token can access workspace ${workspace.name}. Project and service existence will be reconciled during bootstrap.`);
1912
1955
  } catch (error) {
1913
1956
  const detail = error instanceof Error ? error.message : "Railway API check failed.";
1957
+ if (/rate.?limit|too many requests|429/iu.test(detail || "")) {
1958
+ return providerConnectionResult(
1959
+ "railway",
1960
+ false,
1961
+ "Railway connectivity preflight was rate-limited; bootstrap will continue and rely on API-backed reconcile verification.",
1962
+ { skipped: true, warning: true, rateLimited: true }
1963
+ );
1964
+ }
1965
+ if (attempt >= 2 && isTransientProviderConnectionError(detail)) {
1966
+ return providerConnectionResult(
1967
+ "railway",
1968
+ false,
1969
+ "Railway connectivity preflight hit transient API failures; bootstrap will continue and rely on API-backed reconcile verification.",
1970
+ { skipped: true, warning: true, transient: true }
1971
+ );
1972
+ }
1914
1973
  if (attempt >= 2 || !isTransientProviderConnectionError(detail)) {
1915
1974
  return providerConnectionResult("railway", false, detail);
1916
1975
  }
@@ -1928,8 +1987,22 @@ async function checkRailwayConnection({ tenantRoot, env }) {
1928
1987
  }
1929
1988
  async function checkTreeseedProviderConnections({ tenantRoot, scope = "prod", env = process.env, valuesOverlay = {} } = {}) {
1930
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
+ };
1931
2002
  const rawCommandEnv = {
1932
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"),
1933
2006
  CLOUDFLARE_API_TOKEN: values.CLOUDFLARE_API_TOKEN,
1934
2007
  CLOUDFLARE_ACCOUNT_ID: values.CLOUDFLARE_ACCOUNT_ID,
1935
2008
  RAILWAY_API_TOKEN: values.RAILWAY_API_TOKEN,
@@ -1044,22 +1044,44 @@ function shouldManageCloudflareWebCacheRules(deployConfig, target) {
1044
1044
  function cloudflareApiRequest(path, { method = "GET", body, env, allowFailure = false } = {}) {
1045
1045
  const requestScript = `import { readFileSync } from 'node:fs';
1046
1046
  const input = JSON.parse(readFileSync(0, 'utf8') || '{}');
1047
- const response = await fetch(input.url, {
1048
- method: input.method,
1049
- headers: {
1050
- authorization: 'Bearer ' + input.token,
1051
- 'content-type': 'application/json',
1052
- },
1053
- body: input.body ? JSON.stringify(input.body) : undefined,
1054
- });
1055
- const rawBody = await response.text();
1056
- let payload;
1057
- try {
1058
- payload = rawBody ? JSON.parse(rawBody) : {};
1059
- } catch {
1060
- payload = { success: false, errors: [{ message: rawBody || 'empty response' }] };
1047
+ function errorMessage(error) {
1048
+ const parts = [];
1049
+ if (error && typeof error.message === 'string') parts.push(error.message);
1050
+ const cause = error?.cause;
1051
+ if (cause && typeof cause.message === 'string') parts.push(cause.message);
1052
+ if (cause && typeof cause.code === 'string') parts.push(cause.code);
1053
+ if (Array.isArray(cause?.errors)) {
1054
+ for (const entry of cause.errors) {
1055
+ if (entry && typeof entry.message === 'string') parts.push(entry.message);
1056
+ if (entry && typeof entry.code === 'string') parts.push(entry.code);
1057
+ }
1058
+ }
1059
+ return [...new Set(parts.filter(Boolean))].join('; ') || String(error);
1061
1060
  }
1062
- process.stdout.write(JSON.stringify({ ok: response.ok, payload }));`;
1061
+ try {
1062
+ const response = await fetch(input.url, {
1063
+ method: input.method,
1064
+ headers: {
1065
+ authorization: 'Bearer ' + input.token,
1066
+ 'content-type': 'application/json',
1067
+ },
1068
+ body: input.body ? JSON.stringify(input.body) : undefined,
1069
+ });
1070
+ const rawBody = await response.text();
1071
+ let payload;
1072
+ try {
1073
+ payload = rawBody ? JSON.parse(rawBody) : {};
1074
+ } catch {
1075
+ payload = { success: false, errors: [{ message: rawBody || 'empty response' }] };
1076
+ }
1077
+ process.stdout.write(JSON.stringify({ ok: response.ok, payload }));
1078
+ } catch (error) {
1079
+ process.stdout.write(JSON.stringify({
1080
+ ok: false,
1081
+ transient: true,
1082
+ payload: { success: false, errors: [{ message: errorMessage(error) }] },
1083
+ }));
1084
+ }`;
1063
1085
  const requestInput = JSON.stringify({
1064
1086
  url: `https://api.cloudflare.com/client/v4${path}`,
1065
1087
  method,
@@ -1067,6 +1089,11 @@ process.stdout.write(JSON.stringify({ ok: response.ok, payload }));`;
1067
1089
  token: env?.CLOUDFLARE_API_TOKEN ?? process.env.CLOUDFLARE_API_TOKEN ?? ""
1068
1090
  });
1069
1091
  const isTransient = (text) => /fetch failed|timed out|etimedout|econnreset|enetunreach|temporarily unavailable|aborted/iu.test(text || "");
1092
+ const formatPayloadErrors = (payload) => Array.isArray(payload?.errors) ? payload.errors.map((entry) => entry?.message ?? JSON.stringify(entry)).join("; ") : "";
1093
+ const summarizeChildError = (text) => {
1094
+ const lines = String(text || "").split("\n").map((line) => line.trim()).filter(Boolean);
1095
+ return lines.find((line) => /fetch failed|timed out|etimedout|econnreset|enetunreach|temporarily unavailable|aborted|typeerror|error/iu.test(line)) ?? lines[0] ?? "";
1096
+ };
1070
1097
  let attempt = 0;
1071
1098
  for (; ; ) {
1072
1099
  const response = spawnSync(
@@ -1085,29 +1112,48 @@ process.stdout.write(JSON.stringify({ ok: response.ok, payload }));`;
1085
1112
  }
1086
1113
  );
1087
1114
  if (response.error?.code === "ETIMEDOUT") {
1088
- if (attempt < 2) {
1115
+ if (attempt < 4) {
1089
1116
  attempt += 1;
1117
+ sleepSync(500 * attempt);
1090
1118
  continue;
1091
1119
  }
1092
1120
  if (!allowFailure) {
1093
- throw new Error(`Cloudflare API request timed out: ${method} ${path}`);
1121
+ throw new Error(`Cloudflare API request timed out after ${attempt + 1} attempts: ${method} ${path}`);
1094
1122
  }
1095
1123
  return null;
1096
1124
  }
1097
1125
  const stderr = response.stderr?.trim() || "";
1098
1126
  if (response.status !== 0) {
1099
- if (attempt < 2 && isTransient(stderr)) {
1127
+ if (attempt < 4 && isTransient(stderr)) {
1100
1128
  attempt += 1;
1129
+ sleepSync(500 * attempt);
1101
1130
  continue;
1102
1131
  }
1103
1132
  if (!allowFailure) {
1104
- throw new Error(stderr || `Cloudflare API request failed: ${method} ${path}`);
1133
+ const detail = summarizeChildError(stderr);
1134
+ throw new Error(detail ? `Cloudflare API request failed after ${attempt + 1} attempts: ${method} ${path}: ${detail}` : `Cloudflare API request failed after ${attempt + 1} attempts: ${method} ${path}`);
1105
1135
  }
1106
1136
  }
1107
- const parsed = JSON.parse(response.stdout?.trim() || '{"ok":false,"payload":{"success":false,"errors":[{"message":"empty response"}]}}');
1137
+ let parsed;
1138
+ try {
1139
+ parsed = JSON.parse(response.stdout?.trim() || '{"ok":false,"payload":{"success":false,"errors":[{"message":"empty response"}]}}');
1140
+ } catch {
1141
+ parsed = {
1142
+ ok: false,
1143
+ payload: {
1144
+ success: false,
1145
+ errors: [{ message: response.stdout?.trim() || stderr || "empty response" }]
1146
+ }
1147
+ };
1148
+ }
1149
+ const details = formatPayloadErrors(parsed.payload);
1150
+ if (!parsed.ok && parsed.transient && attempt < 4 && isTransient(details)) {
1151
+ attempt += 1;
1152
+ sleepSync(500 * attempt);
1153
+ continue;
1154
+ }
1108
1155
  if (!parsed.ok && !allowFailure) {
1109
- const details = Array.isArray(parsed.payload?.errors) ? parsed.payload.errors.map((entry) => entry?.message ?? JSON.stringify(entry)).join("; ") : "unknown error";
1110
- throw new Error(details || `Cloudflare API request failed: ${method} ${path}`);
1156
+ throw new Error(details ? `Cloudflare API request failed after ${attempt + 1} attempts: ${method} ${path}: ${details}` : `Cloudflare API request failed after ${attempt + 1} attempts: ${method} ${path}`);
1111
1157
  }
1112
1158
  return parsed.payload;
1113
1159
  }
@@ -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")) {
@@ -506,7 +506,7 @@ function parseFallbackDeployConfig(configPath) {
506
506
  slug: expectString(record.slug, "slug"),
507
507
  siteUrl: expectString(record.siteUrl, "siteUrl"),
508
508
  contactEmail: expectString(record.contactEmail, "contactEmail"),
509
- hosting: parsedHosting && record.hub === void 0 && record.runtime === void 0 ? parsedHosting : normalizeLegacyHostingFromPlanes(hub, runtime),
509
+ hosting: parsedHosting?.kind === "market_control_plane" ? { ...parsedHosting, registration: "none" } : parsedHosting && record.hub === void 0 && record.runtime === void 0 ? parsedHosting : normalizeLegacyHostingFromPlanes(hub, runtime),
510
510
  hub,
511
511
  runtime,
512
512
  cloudflare: {
@@ -541,7 +541,7 @@ function parseDeployConfig(raw) {
541
541
  const turnstile = optionalRecord(parsed.turnstile, "turnstile") ?? {};
542
542
  optionalBoolean(turnstile.enabled, "turnstile.enabled");
543
543
  const normalizedHosting = normalizeLegacyHostingFromPlanes(hub, runtime);
544
- const compatibilityHosting = hosting && !parsed.hub && !parsed.runtime ? hosting : normalizedHosting;
544
+ const compatibilityHosting = hosting?.kind === "market_control_plane" ? { ...hosting, registration: "none" } : hosting && !parsed.hub && !parsed.runtime ? hosting : normalizedHosting;
545
545
  return {
546
546
  name: expectString(parsed.name, "name"),
547
547
  slug: expectString(parsed.slug, "slug"),
@@ -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.6",
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": {