1import * as Effect from "effect/Effect";
2import * as Schema from "effect/Schema";
3import {
4 NonNegativeInt,
5 PositiveInt,
6 ProjectId,
7 ThreadId,
8 TrimmedNonEmptyString,
9} from "./baseSchemas.ts";
10import { SourceControlProviderError, SourceControlProviderInfo } from "./sourceControl.ts";
11import { VcsDriverKind } from "./vcs.ts";
12
13const TrimmedNonEmptyStringSchema = TrimmedNonEmptyString;
14const GitFilePath = Schema.String.check(Schema.isNonEmpty());
15const GIT_LIST_BRANCHES_MAX_LIMIT = 200;
16
17// Domain Types
18
19export const GitStackedAction = Schema.Literals([
20 "commit",
21 "push",
22 "create_pr",
23 "commit_push",
24 "commit_push_pr",
25]);
26export type GitStackedAction = typeof GitStackedAction.Type;
27export const GitActionProgressPhase = Schema.Literals(["branch", "commit", "push", "pr"]);
28export type GitActionProgressPhase = typeof GitActionProgressPhase.Type;
29export const GitActionProgressKind = Schema.Literals([
30 "action_started",
31 "phase_started",
32 "hook_started",
33 "hook_output",
34 "hook_finished",
35 "action_finished",
36 "action_failed",
37]);
38export type GitActionProgressKind = typeof GitActionProgressKind.Type;
39export const GitActionProgressStream = Schema.Literals(["stdout", "stderr"]);
40export type GitActionProgressStream = typeof GitActionProgressStream.Type;
41const GitCommitStepStatus = Schema.Literals([
42 "created",
43 "skipped_no_changes",
44 "skipped_not_requested",
45]);
46const GitPushStepStatus = Schema.Literals([
47 "pushed",
48 "skipped_not_requested",
49 "skipped_up_to_date",
50]);
51const GitBranchStepStatus = Schema.Literals(["created", "skipped_not_requested"]);
52const GitPrStepStatus = Schema.Literals(["created", "opened_existing", "skipped_not_requested"]);
53const VcsStatusChangeRequestState = Schema.Literals(["open", "closed", "merged"]);
54const GitPullRequestReference = TrimmedNonEmptyStringSchema;
55const GitPullRequestState = Schema.Literals(["open", "closed", "merged"]);
56const GitPreparePullRequestThreadMode = Schema.Literals(["local", "worktree"]);
57export const GitRunStackedActionToastRunAction = Schema.Struct({
58 kind: GitStackedAction,
59});
60export type GitRunStackedActionToastRunAction = typeof GitRunStackedActionToastRunAction.Type;
61const GitRunStackedActionToastCta = Schema.Union([
62 Schema.Struct({
63 kind: Schema.Literal("none"),
64 }),
65 Schema.Struct({
66 kind: Schema.Literal("open_pr"),
67 label: TrimmedNonEmptyStringSchema,
68 url: Schema.String,
69 }),
70 Schema.Struct({
71 kind: Schema.Literal("run_action"),
72 label: TrimmedNonEmptyStringSchema,
73 action: GitRunStackedActionToastRunAction,
74 }),
75]);
76export type GitRunStackedActionToastCta = typeof GitRunStackedActionToastCta.Type;
77const GitRunStackedActionToast = Schema.Struct({
78 title: TrimmedNonEmptyStringSchema,
79 description: Schema.optional(TrimmedNonEmptyStringSchema),
80 cta: GitRunStackedActionToastCta,
81});
82export type GitRunStackedActionToast = typeof GitRunStackedActionToast.Type;
83
84export const VcsRef = Schema.Struct({
85 name: TrimmedNonEmptyStringSchema,
86 isRemote: Schema.optional(Schema.Boolean),
87 remoteName: Schema.optional(TrimmedNonEmptyStringSchema),
88 current: Schema.Boolean,
89 isDefault: Schema.Boolean,
90 worktreePath: TrimmedNonEmptyStringSchema.pipe(Schema.NullOr),
91});
92export type VcsRef = typeof VcsRef.Type;
93
94const VcsWorktree = Schema.Struct({
95 path: TrimmedNonEmptyStringSchema,
96 refName: TrimmedNonEmptyStringSchema,
97});
98const GitResolvedPullRequest = Schema.Struct({
99 number: PositiveInt,
100 title: TrimmedNonEmptyStringSchema,
101 url: Schema.String,
102 baseBranch: TrimmedNonEmptyStringSchema,
103 headBranch: TrimmedNonEmptyStringSchema,
104 state: GitPullRequestState,
105});
106export type GitResolvedPullRequest = typeof GitResolvedPullRequest.Type;
107
108// RPC Inputs
109
110export const VcsStatusInput = Schema.Struct({
111 cwd: TrimmedNonEmptyStringSchema,
112});
113export type VcsStatusInput = typeof VcsStatusInput.Type;
114
115export const VcsStatusSubscriptionInput = Schema.Struct({
116 ...VcsStatusInput.fields,
117 /** Passive observers receive cached remote status without retaining its refresh loop. */
118 includeRemote: Schema.optional(Schema.Boolean),
119});
120export type VcsStatusSubscriptionInput = typeof VcsStatusSubscriptionInput.Type;
121
122export const VcsPullInput = Schema.Struct({
123 cwd: TrimmedNonEmptyStringSchema,
124});
125export type VcsPullInput = typeof VcsPullInput.Type;
126
127export const GitRunStackedActionInput = Schema.Struct({
128 actionId: TrimmedNonEmptyStringSchema,
129 cwd: TrimmedNonEmptyStringSchema,
130 action: GitStackedAction,
131 commitMessage: Schema.optional(TrimmedNonEmptyStringSchema.check(Schema.isMaxLength(10_000))),
132 featureBranch: Schema.optional(Schema.Boolean),
133 filePaths: Schema.optional(Schema.Array(GitFilePath).check(Schema.isMinLength(1))),
134 /** The thread the action runs beside; a pull request it creates is linked to it. */
135 threadId: Schema.optional(ThreadId),
136 projectId: Schema.optional(ProjectId),
137});
138export type GitRunStackedActionInput = typeof GitRunStackedActionInput.Type;
139
140export const VcsListRefsInput = Schema.Struct({
141 cwd: TrimmedNonEmptyStringSchema,
142 query: Schema.optional(TrimmedNonEmptyStringSchema.check(Schema.isMaxLength(256))),
143 cursor: Schema.optional(NonNegativeInt),
144 includeMatchingRemoteRefs: Schema.optional(Schema.Boolean),
145 refKind: Schema.optional(Schema.Literals(["all", "local", "remote"])),
146 refresh: Schema.optional(Schema.Boolean),
147 limit: Schema.optional(
148 PositiveInt.check(Schema.isLessThanOrEqualTo(GIT_LIST_BRANCHES_MAX_LIMIT)),
149 ),
150});
151export type VcsListRefsInput = typeof VcsListRefsInput.Type;
152
153export const VcsCreateWorktreeInput = Schema.Struct({
154 cwd: TrimmedNonEmptyStringSchema,
155 refName: TrimmedNonEmptyStringSchema,
156 newRefName: Schema.optional(TrimmedNonEmptyStringSchema),
157 baseRefName: Schema.optional(TrimmedNonEmptyStringSchema),
158 path: Schema.NullOr(TrimmedNonEmptyStringSchema),
159});
160export type VcsCreateWorktreeInput = typeof VcsCreateWorktreeInput.Type;
161
162export const GitPullRequestRefInput = Schema.Struct({
163 cwd: TrimmedNonEmptyStringSchema,
164 reference: GitPullRequestReference,
165});
166export type GitPullRequestRefInput = typeof GitPullRequestRefInput.Type;
167
168export const GitPreparePullRequestThreadInput = Schema.Struct({
169 cwd: TrimmedNonEmptyStringSchema,
170 reference: GitPullRequestReference,
171 mode: GitPreparePullRequestThreadMode,
172 threadId: Schema.optional(ThreadId),
173});
174export type GitPreparePullRequestThreadInput = typeof GitPreparePullRequestThreadInput.Type;
175
176export const VcsRemoveWorktreeInput = Schema.Struct({
177 cwd: TrimmedNonEmptyStringSchema,
178 path: TrimmedNonEmptyStringSchema,
179 force: Schema.optional(Schema.Boolean),
180});
181export type VcsRemoveWorktreeInput = typeof VcsRemoveWorktreeInput.Type;
182
183export const VcsCreateRefInput = Schema.Struct({
184 cwd: TrimmedNonEmptyStringSchema,
185 refName: TrimmedNonEmptyStringSchema,
186 switchRef: Schema.optional(Schema.Boolean),
187});
188export type VcsCreateRefInput = typeof VcsCreateRefInput.Type;
189
190export const VcsCreateRefResult = Schema.Struct({
191 refName: TrimmedNonEmptyStringSchema,
192});
193export type VcsCreateRefResult = typeof VcsCreateRefResult.Type;
194
195export const VcsSwitchRefInput = Schema.Struct({
196 cwd: TrimmedNonEmptyStringSchema,
197 refName: TrimmedNonEmptyStringSchema,
198});
199export type VcsSwitchRefInput = typeof VcsSwitchRefInput.Type;
200
201export const VcsInitInput = Schema.Struct({
202 cwd: TrimmedNonEmptyStringSchema,
203 kind: Schema.optional(VcsDriverKind),
204});
205export type VcsInitInput = typeof VcsInitInput.Type;
206
207// RPC Results
208
209const VcsStatusChangeRequest = Schema.Struct({
210 number: PositiveInt,
211 title: TrimmedNonEmptyStringSchema,
212 url: Schema.String,
213 baseRef: TrimmedNonEmptyStringSchema,
214 headRef: TrimmedNonEmptyStringSchema,
215 state: VcsStatusChangeRequestState,
216 /** Optional for compatibility with older servers and providers. */
217 isDraft: Schema.optional(Schema.Boolean),
218 /**
219 * Last provider-side activity (ISO), including comments and metadata edits.
220 * This is not the time a change request closed or merged. Optional for old
221 * servers and providers whose lookups do not report it.
222 */
223 updatedAt: Schema.optional(Schema.NullOr(Schema.String)),
224});
225
226const VcsStatusLocalShape = {
227 isRepo: Schema.Boolean,
228 isJjWorkspace: Schema.optional(Schema.Boolean),
229 sourceControlProvider: Schema.optional(SourceControlProviderInfo),
230 hasPrimaryRemote: Schema.Boolean,
231 isDefaultRef: Schema.Boolean,
232 refName: Schema.NullOr(TrimmedNonEmptyStringSchema),
233 hasWorkingTreeChanges: Schema.Boolean,
234 workingTree: Schema.Struct({
235 files: Schema.Array(
236 Schema.Struct({
237 path: GitFilePath,
238 insertions: NonNegativeInt,
239 deletions: NonNegativeInt,
240 }),
241 ),
242 insertions: NonNegativeInt,
243 deletions: NonNegativeInt,
244 }),
245 /**
246 * Totals for the diff panel's Changes view: merge-base with the base branch to the
247 * working tree, untracked files included. Absent on older servers.
248 */
249 branchChanges: Schema.optional(
250 Schema.Struct({
251 baseRef: Schema.NullOr(TrimmedNonEmptyStringSchema),
252 insertions: NonNegativeInt,
253 deletions: NonNegativeInt,
254 }),
255 ),
256};
257
258const VcsStatusRemoteShape = {
259 hasUpstream: Schema.Boolean,
260 aheadCount: NonNegativeInt,
261 behindCount: NonNegativeInt,
262 aheadOfDefaultCount: Schema.optional(NonNegativeInt),
263 pr: Schema.NullOr(VcsStatusChangeRequest),
264};
265
266export const VcsStatusLocalResult = Schema.Struct(VcsStatusLocalShape);
267export type VcsStatusLocalResult = typeof VcsStatusLocalResult.Type;
268
269export const VcsStatusRemoteResult = Schema.Struct(VcsStatusRemoteShape);
270export type VcsStatusRemoteResult = typeof VcsStatusRemoteResult.Type;
271
272export const VcsStatusResult = Schema.Struct({
273 ...VcsStatusLocalShape,
274 ...VcsStatusRemoteShape,
275});
276export type VcsStatusResult = typeof VcsStatusResult.Type;
277
278export const VcsStatusStreamEvent = Schema.Union([
279 Schema.TaggedStruct("snapshot", {
280 local: VcsStatusLocalResult,
281 remote: Schema.NullOr(VcsStatusRemoteResult),
282 }),
283 Schema.TaggedStruct("localUpdated", {
284 local: VcsStatusLocalResult,
285 }),
286 Schema.TaggedStruct("remoteUpdated", {
287 remote: Schema.NullOr(VcsStatusRemoteResult),
288 }),
289]);
290export type VcsStatusStreamEvent = typeof VcsStatusStreamEvent.Type;
291
292export const VcsListRefsResult = Schema.Struct({
293 refs: Schema.Array(VcsRef),
294 isRepo: Schema.Boolean,
295 hasPrimaryRemote: Schema.Boolean,
296 nextCursor: NonNegativeInt.pipe(Schema.NullOr),
297 totalCount: NonNegativeInt,
298});
299export type VcsListRefsResult = typeof VcsListRefsResult.Type;
300
301export const VcsCreateWorktreeResult = Schema.Struct({
302 worktree: VcsWorktree,
303});
304export type VcsCreateWorktreeResult = typeof VcsCreateWorktreeResult.Type;
305
306export const GitResolvePullRequestResult = Schema.Struct({
307 pullRequest: GitResolvedPullRequest,
308});
309export type GitResolvePullRequestResult = typeof GitResolvePullRequestResult.Type;
310
311export const GitPreparePullRequestThreadResult = Schema.Struct({
312 pullRequest: GitResolvedPullRequest,
313 branch: TrimmedNonEmptyStringSchema,
314 worktreePath: TrimmedNonEmptyStringSchema.pipe(Schema.NullOr),
315 /**
316 * False when the checkout could not be brought to the pull request head — a reused worktree
317 * holding local commits or uncommitted changes keeps its own state, so the code being handed
318 * over is older than the pull request.
319 */
320 isOnPullRequestHead: Schema.Boolean.pipe(Schema.withDecodingDefaultKey(Effect.succeed(true))),
321});
322export type GitPreparePullRequestThreadResult = typeof GitPreparePullRequestThreadResult.Type;
323
324export const VcsSwitchRefResult = Schema.Struct({
325 refName: Schema.NullOr(TrimmedNonEmptyStringSchema),
326});
327export type VcsSwitchRefResult = typeof VcsSwitchRefResult.Type;
328
329export const GitRunStackedActionResult = Schema.Struct({
330 action: GitStackedAction,
331 branch: Schema.Struct({
332 status: GitBranchStepStatus,
333 name: Schema.optional(TrimmedNonEmptyStringSchema),
334 }),
335 commit: Schema.Struct({
336 status: GitCommitStepStatus,
337 commitSha: Schema.optional(TrimmedNonEmptyStringSchema),
338 subject: Schema.optional(TrimmedNonEmptyStringSchema),
339 }),
340 push: Schema.Struct({
341 status: GitPushStepStatus,
342 branch: Schema.optional(TrimmedNonEmptyStringSchema),
343 upstreamBranch: Schema.optional(TrimmedNonEmptyStringSchema),
344 setUpstream: Schema.optional(Schema.Boolean),
345 }),
346 pr: Schema.Struct({
347 status: GitPrStepStatus,
348 url: Schema.optional(Schema.String),
349 number: Schema.optional(PositiveInt),
350 baseBranch: Schema.optional(TrimmedNonEmptyStringSchema),
351 headBranch: Schema.optional(TrimmedNonEmptyStringSchema),
352 title: Schema.optional(TrimmedNonEmptyStringSchema),
353 }),
354 toast: GitRunStackedActionToast,
355});
356export type GitRunStackedActionResult = typeof GitRunStackedActionResult.Type;
357
358export const VcsPullResult = Schema.Struct({
359 status: Schema.Literals(["pulled", "skipped_up_to_date"]),
360 refName: TrimmedNonEmptyStringSchema,
361 upstreamRef: TrimmedNonEmptyStringSchema.pipe(Schema.NullOr),
362});
363export type VcsPullResult = typeof VcsPullResult.Type;
364
365// RPC / domain errors
366
367// Well-known git failures, recognized from stderr at the driver and carried as
368// a closed set of diagnostic tags. Git's stderr itself stays off the error: it
369// echoes argv and remote URLs, which can hold credentials. The tag names the
370// cause for logs and callers; it does not select a message.
371export const GitCommandFailureReason = Schema.Literals([
372 "authentication_failed",
373 "branch_already_exists",
374 "branch_checked_out_in_worktree",
375 "host_key_unverified",
376 "not_a_repository",
377 "path_already_exists",
378 "remote_unreachable",
379 "tag_would_be_clobbered",
380]);
381export type GitCommandFailureReason = typeof GitCommandFailureReason.Type;
382
383export class GitCommandError extends Schema.TaggedError<GitCommandError>()("GitCommandError", {
384 operation: Schema.String,
385 command: Schema.String,
386 cwd: Schema.String,
387 argumentCount: Schema.optional(Schema.Number),
388 exitCode: Schema.optional(Schema.Number),
389 stdoutLength: Schema.optional(Schema.Number),
390 stderrLength: Schema.optional(Schema.Number),
391 outputLength: Schema.optional(Schema.Number),
392 reason: Schema.optional(GitCommandFailureReason),
393 detail: Schema.String,
394 cause: Schema.optional(Schema.Defect()),
395}) {
396 override get message(): string {
397 const reason = this.reason === undefined ? "" : ` (${this.reason})`;
398 return `Git command failed in ${this.operation} (${this.cwd}): ${this.detail}${reason}`;
399 }
400}
401
402export class TextGenerationError extends Schema.TaggedError<TextGenerationError>()(
403 "TextGenerationError",
404 {
405 operation: Schema.String,
406 detail: Schema.String,
407 cause: Schema.optional(Schema.Defect()),
408 },
409) {
410 override get message(): string {
411 return `Text generation failed in ${this.operation}: ${this.detail}`;
412 }
413}
414
415export class GitManagerError extends Schema.TaggedError<GitManagerError>()("GitManagerError", {
416 operation: Schema.String,
417 cwd: Schema.String,
418 detail: Schema.String,
419 cause: Schema.optional(Schema.Defect()),
420}) {
421 override get message(): string {
422 return `Git manager failed in ${this.operation}: ${this.detail}`;
423 }
424}
425
426export class GitPullRequestMaterializationError extends Schema.TaggedError<GitPullRequestMaterializationError>()(
427 "GitPullRequestMaterializationError",
428 {
429 cwd: TrimmedNonEmptyStringSchema,
430 pullRequestNumber: PositiveInt,
431 headRepository: Schema.NullOr(TrimmedNonEmptyStringSchema),
432 headBranch: TrimmedNonEmptyStringSchema,
433 localBranch: TrimmedNonEmptyStringSchema,
434 cause: Schema.Defect(),
435 },
436) {
437 override get message(): string {
438 return `Failed to materialize pull request #${this.pullRequestNumber} branch ${this.headBranch} as ${this.localBranch}.`;
439 }
440}
441
442export const GitManagerServiceError = Schema.Union([
443 GitManagerError,
444 GitPullRequestMaterializationError,
445 GitCommandError,
446 SourceControlProviderError,
447 TextGenerationError,
448]);
449export type GitManagerServiceError = typeof GitManagerServiceError.Type;
450
451const GitActionProgressBase = Schema.Struct({
452 actionId: TrimmedNonEmptyStringSchema,
453 cwd: TrimmedNonEmptyStringSchema,
454 action: GitStackedAction,
455});
456
457const GitActionStartedEvent = Schema.Struct({
458 ...GitActionProgressBase.fields,
459 kind: Schema.Literal("action_started"),
460 phases: Schema.Array(GitActionProgressPhase),
461});
462const GitActionPhaseStartedEvent = Schema.Struct({
463 ...GitActionProgressBase.fields,
464 kind: Schema.Literal("phase_started"),
465 phase: GitActionProgressPhase,
466 label: TrimmedNonEmptyStringSchema,
467});
468const GitActionHookStartedEvent = Schema.Struct({
469 ...GitActionProgressBase.fields,
470 kind: Schema.Literal("hook_started"),
471 hookName: TrimmedNonEmptyStringSchema,
472});
473const GitActionHookOutputEvent = Schema.Struct({
474 ...GitActionProgressBase.fields,
475 kind: Schema.Literal("hook_output"),
476 hookName: Schema.NullOr(TrimmedNonEmptyStringSchema),
477 stream: GitActionProgressStream,
478 text: TrimmedNonEmptyStringSchema,
479});
480const GitActionHookFinishedEvent = Schema.Struct({
481 ...GitActionProgressBase.fields,
482 kind: Schema.Literal("hook_finished"),
483 hookName: TrimmedNonEmptyStringSchema,
484 exitCode: Schema.NullOr(Schema.Int),
485 durationMs: Schema.NullOr(NonNegativeInt),
486});
487const GitActionFinishedEvent = Schema.Struct({
488 ...GitActionProgressBase.fields,
489 kind: Schema.Literal("action_finished"),
490 result: GitRunStackedActionResult,
491});
492const GitActionFailedEvent = Schema.Struct({
493 ...GitActionProgressBase.fields,
494 kind: Schema.Literal("action_failed"),
495 phase: Schema.NullOr(GitActionProgressPhase),
496 message: TrimmedNonEmptyStringSchema,
497});
498
499export const GitActionProgressEvent = Schema.Union([
500 GitActionStartedEvent,
501 GitActionPhaseStartedEvent,
502 GitActionHookStartedEvent,
503 GitActionHookOutputEvent,
504 GitActionHookFinishedEvent,
505 GitActionFinishedEvent,
506 GitActionFailedEvent,
507]);
508export type GitActionProgressEvent = typeof GitActionProgressEvent.Type;