@bevel-software/platform-shared 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/dist/auth/types.d.ts +15 -0
- package/dist/auth/types.d.ts.map +1 -0
- package/dist/auth/types.js +2 -0
- package/dist/auth/types.js.map +1 -0
- package/dist/chat/types.d.ts +39 -0
- package/dist/chat/types.d.ts.map +1 -0
- package/dist/chat/types.js +13 -0
- package/dist/chat/types.js.map +1 -0
- package/dist/git/branchAuthor.d.ts +28 -0
- package/dist/git/branchAuthor.d.ts.map +1 -0
- package/dist/git/branchAuthor.js +45 -0
- package/dist/git/branchAuthor.js.map +1 -0
- package/dist/git/pr.types.d.ts +264 -0
- package/dist/git/pr.types.d.ts.map +1 -0
- package/dist/git/pr.types.js +2 -0
- package/dist/git/pr.types.js.map +1 -0
- package/dist/git/protected.d.ts +52 -0
- package/dist/git/protected.d.ts.map +1 -0
- package/dist/git/protected.js +92 -0
- package/dist/git/protected.js.map +1 -0
- package/dist/git/review.types.d.ts +49 -0
- package/dist/git/review.types.d.ts.map +1 -0
- package/dist/git/review.types.js +2 -0
- package/dist/git/review.types.js.map +1 -0
- package/dist/git/types.d.ts +107 -0
- package/dist/git/types.d.ts.map +1 -0
- package/dist/git/types.js +2 -0
- package/dist/git/types.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/workflow/events.d.ts +218 -0
- package/dist/workflow/events.d.ts.map +1 -0
- package/dist/workflow/events.js +48 -0
- package/dist/workflow/events.js.map +1 -0
- package/dist/workflow/interface.d.ts +233 -0
- package/dist/workflow/interface.d.ts.map +1 -0
- package/dist/workflow/interface.js +20 -0
- package/dist/workflow/interface.js.map +1 -0
- package/dist/workflow/types.d.ts +131 -0
- package/dist/workflow/types.d.ts.map +1 -0
- package/dist/workflow/types.js +17 -0
- package/dist/workflow/types.js.map +1 -0
- package/dist/workspace/filename.d.ts +33 -0
- package/dist/workspace/filename.d.ts.map +1 -0
- package/dist/workspace/filename.js +98 -0
- package/dist/workspace/filename.js.map +1 -0
- package/dist/workspace/frontmatter.d.ts +23 -0
- package/dist/workspace/frontmatter.d.ts.map +1 -0
- package/dist/workspace/frontmatter.js +48 -0
- package/dist/workspace/frontmatter.js.map +1 -0
- package/dist/workspace/kb-layout.d.ts +66 -0
- package/dist/workspace/kb-layout.d.ts.map +1 -0
- package/dist/workspace/kb-layout.js +63 -0
- package/dist/workspace/kb-layout.js.map +1 -0
- package/dist/workspace/types.d.ts +57 -0
- package/dist/workspace/types.d.ts.map +1 -0
- package/dist/workspace/types.js +2 -0
- package/dist/workspace/types.js.map +1 -0
- package/package.json +41 -0
- package/src/auth/types.ts +16 -0
- package/src/chat/types.ts +53 -0
- package/src/git/branchAuthor.ts +46 -0
- package/src/git/pr.types.ts +274 -0
- package/src/git/protected.ts +121 -0
- package/src/git/review.types.ts +51 -0
- package/src/git/types.ts +170 -0
- package/src/index.ts +23 -0
- package/src/workflow/events.ts +271 -0
- package/src/workflow/interface.ts +357 -0
- package/src/workflow/types.ts +156 -0
- package/src/workspace/filename.ts +102 -0
- package/src/workspace/frontmatter.ts +45 -0
- package/src/workspace/kb-layout.ts +81 -0
- package/src/workspace/types.ts +59 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow event bus types — what the backend pushes over SSE to keep the
|
|
3
|
+
* frontend in sync with state changes, including those triggered by other
|
|
4
|
+
* users on the same branch or by the agent acting server-side.
|
|
5
|
+
*
|
|
6
|
+
* **Scope model** — events have one of three scopes, set by the variant:
|
|
7
|
+
*
|
|
8
|
+
* - **Workspace-scoped** (`workspaceId` field). Pushed to every connected
|
|
9
|
+
* session whose current focus is `workspaceId`. Examples: a file changed
|
|
10
|
+
* on this branch, someone took the lock on a file. Anyone editing this
|
|
11
|
+
* branch cares.
|
|
12
|
+
*
|
|
13
|
+
* - **User-scoped** (`forUserId` field, no `workspaceId`). Pushed only to
|
|
14
|
+
* sessions opened by `forUserId`, regardless of their current focus.
|
|
15
|
+
* Examples: an agent turn called a tool on your behalf, your view should
|
|
16
|
+
* navigate to a different branch. Other users on the same branch don't
|
|
17
|
+
* care.
|
|
18
|
+
*
|
|
19
|
+
* - **Global** (no `workspaceId`, no `forUserId`). Pushed to every
|
|
20
|
+
* authenticated session. Examples: change requests opening / merging /
|
|
21
|
+
* being rejected — any user with access to the repo might care.
|
|
22
|
+
*
|
|
23
|
+
* **Identity + replay** — every event carries an `id` (monotonic int) and an
|
|
24
|
+
* ISO `ts`. Clients reconnecting with `Last-Event-ID: <id>` get the buffered
|
|
25
|
+
* tail replayed (server's ring buffer is finite — see `WorkflowEventBus`).
|
|
26
|
+
* If `id` is older than what the buffer still has, the server sends a
|
|
27
|
+
* `{ kind: 'resync' }` instead so the client refetches the world cleanly.
|
|
28
|
+
*
|
|
29
|
+
* **Idempotency over exactly-once** — clients tolerate duplicates and
|
|
30
|
+
* out-of-order delivery: every payload carries enough state for the UI to
|
|
31
|
+
* reach the right end state regardless of how many times it sees the event
|
|
32
|
+
* (e.g. `file-changed` includes the new SHA — UI only refreshes if the open
|
|
33
|
+
* tab's SHA differs).
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
export interface WorkflowEventEnvelope {
|
|
37
|
+
/** Monotonic, process-local. Used for `Last-Event-ID` replay. */
|
|
38
|
+
id: number;
|
|
39
|
+
/** ISO timestamp the event was emitted on the backend. */
|
|
40
|
+
ts: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ── Workspace-scoped ─────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A file's content changed on disk — fires for ANY write that lands bytes
|
|
47
|
+
* for `(workspaceId, branch, path)`, whether or not a commit followed.
|
|
48
|
+
*
|
|
49
|
+
* - **`newSha` set** → a commit landed. The disk write came through the
|
|
50
|
+
* full lock-release pipeline (acquire → write → commitFile → push).
|
|
51
|
+
* History views should refetch.
|
|
52
|
+
*
|
|
53
|
+
* - **`newSha === null`** → disk bytes changed but no commit yet, i.e.
|
|
54
|
+
* the writer is mid-edit (lock still held — e.g. the editor's Ctrl+S
|
|
55
|
+
* autosave-to-disk before the final Save click). Watchers should
|
|
56
|
+
* refetch content but NOT history (nothing new in git log).
|
|
57
|
+
*
|
|
58
|
+
* Emitted from both the human PUT/PATCH/DELETE routes and the agent's
|
|
59
|
+
* LockingFilesystem so anyone tailing the file sees changes immediately.
|
|
60
|
+
*/
|
|
61
|
+
export interface FileChangedEvent {
|
|
62
|
+
kind: 'file-changed';
|
|
63
|
+
workspaceId: string;
|
|
64
|
+
branch: string;
|
|
65
|
+
path: string;
|
|
66
|
+
/** Commit sha if a commit landed; null if this was a disk-only write. */
|
|
67
|
+
newSha: string | null;
|
|
68
|
+
/** The user whose write caused the change. */
|
|
69
|
+
byUserId: string;
|
|
70
|
+
byUserName: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Someone (`holderUserId`) just acquired the lock on `(branch, path)`. */
|
|
74
|
+
export interface LockAcquiredEvent {
|
|
75
|
+
kind: 'lock-acquired';
|
|
76
|
+
workspaceId: string;
|
|
77
|
+
branch: string;
|
|
78
|
+
path: string;
|
|
79
|
+
holderUserId: string;
|
|
80
|
+
holderName: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The lock on `(branch, path)` was released — file is free for the next caller. */
|
|
84
|
+
export interface LockReleasedEvent {
|
|
85
|
+
kind: 'lock-released';
|
|
86
|
+
workspaceId: string;
|
|
87
|
+
branch: string;
|
|
88
|
+
path: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The file tree for `(workspaceId, branch)` changed in a way that's not
|
|
93
|
+
* captured by a single `file-changed` event — typically a recursive delete,
|
|
94
|
+
* a move, or an unzip extracting many files. Emitted alongside per-file
|
|
95
|
+
* `file-changed` events, but lets the file explorer refresh once instead of
|
|
96
|
+
* N times.
|
|
97
|
+
*/
|
|
98
|
+
export interface FsTreeChangedEvent {
|
|
99
|
+
kind: 'fs-tree-changed';
|
|
100
|
+
workspaceId: string;
|
|
101
|
+
branch: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ── User-scoped ──────────────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Agent called `switch_branch` mid-turn and the target validated +
|
|
108
|
+
* pre-warmed successfully. Tells the frontend to navigate to the new draft
|
|
109
|
+
* once the in-flight chat turn completes — navigation mid-stream would tear
|
|
110
|
+
* down the chat connection. The chat hook stashes the latest pending
|
|
111
|
+
* destination and consumes it in `onFinish`. The native chat path stamps the
|
|
112
|
+
* `threadId`; the registry `switch_branch` tool (called over the internal
|
|
113
|
+
* loopback, which carries no threadId) omits it, so the frontend keys the
|
|
114
|
+
* pending destination on `forUserId` — a user has one active chat stream.
|
|
115
|
+
*
|
|
116
|
+
* User-scoped (no `workspaceId`) — only the user whose agent ran the tool
|
|
117
|
+
* should follow; other users on the same branch don't care.
|
|
118
|
+
*/
|
|
119
|
+
export interface BranchSwitchedEvent {
|
|
120
|
+
kind: 'branch-switched';
|
|
121
|
+
forUserId: string;
|
|
122
|
+
threadId?: string;
|
|
123
|
+
branch: string;
|
|
124
|
+
workspaceId: string;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Agent (running on `forUserId`'s behalf) is about to call a tool — used by
|
|
129
|
+
* the file explorer to highlight files the agent is touching and by the
|
|
130
|
+
* chat sidebar to show a live activity feed without parsing the chat
|
|
131
|
+
* stream's data parts.
|
|
132
|
+
*
|
|
133
|
+
* Fired BEFORE the tool runs; the resulting `file-changed` /
|
|
134
|
+
* `change-request-opened` / etc. events arrive after.
|
|
135
|
+
*/
|
|
136
|
+
export interface AgentToolCallEvent {
|
|
137
|
+
kind: 'agent-tool-call';
|
|
138
|
+
forUserId: string;
|
|
139
|
+
threadId: string;
|
|
140
|
+
tool: string;
|
|
141
|
+
/**
|
|
142
|
+
* Free-form summary of what the tool is about to do (e.g. `"writing
|
|
143
|
+
* Knowledge/Foo.md"`, `"opening change request Foo → target-company-state"`).
|
|
144
|
+
* Optional — purely informational, never load-bearing.
|
|
145
|
+
*/
|
|
146
|
+
summary?: string;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ── Global ───────────────────────────────────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
export interface ChangeRequestOpenedEvent {
|
|
152
|
+
kind: 'change-request-opened';
|
|
153
|
+
number: number;
|
|
154
|
+
source: string;
|
|
155
|
+
target: string;
|
|
156
|
+
/** Hash of the author's email (matches the marker in the CR body); null when unattributable. */
|
|
157
|
+
authorIdHash: string | null;
|
|
158
|
+
title: string;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export interface ChangeRequestMergedEvent {
|
|
162
|
+
kind: 'change-request-merged';
|
|
163
|
+
number: number;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export interface ChangeRequestRejectedEvent {
|
|
167
|
+
kind: 'change-request-rejected';
|
|
168
|
+
number: number;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* A merge the caller triggered did NOT land — either the gate refused it, the
|
|
173
|
+
* branch needs conflict resolution, or `gh pr merge` failed. The merge route
|
|
174
|
+
* is async (returns 202 immediately so a large PR's merge can't outlive the
|
|
175
|
+
* gateway timeout), so this is how the failure reaches the UI that kicked it
|
|
176
|
+
* off. Success travels on `change-request-merged` instead.
|
|
177
|
+
*
|
|
178
|
+
* User-scoped (`forUserId`, no `workspaceId`) — a failed merge changes no
|
|
179
|
+
* shared state; only the user who clicked needs the reason, so we don't
|
|
180
|
+
* broadcast the error string to every session.
|
|
181
|
+
*/
|
|
182
|
+
export interface ChangeRequestMergeFailedEvent {
|
|
183
|
+
kind: 'change-request-merge-failed';
|
|
184
|
+
forUserId: string;
|
|
185
|
+
number: number;
|
|
186
|
+
/** Human-readable reason for the error banner (tokens already redacted). */
|
|
187
|
+
reason: string;
|
|
188
|
+
/**
|
|
189
|
+
* True when the merge failed because the branch needs conflict resolution.
|
|
190
|
+
* The UI routes to the agent resolution flow instead of showing an error.
|
|
191
|
+
*/
|
|
192
|
+
conflicts: boolean;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Per-file approval on a change request was added or removed (e.g. via the
|
|
197
|
+
* Approve / Withdraw confirmation buttons, or auto-invalidated by a new
|
|
198
|
+
* commit). The UI re-fetches the CR detail to get the updated approval set.
|
|
199
|
+
*/
|
|
200
|
+
export interface ApprovalChangedEvent {
|
|
201
|
+
kind: 'approval-changed';
|
|
202
|
+
number: number;
|
|
203
|
+
path: string;
|
|
204
|
+
approverUserId: string;
|
|
205
|
+
change: 'added' | 'removed';
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ── Control ──────────────────────────────────────────────────────────────────
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Sent every ~25 s on the SSE stream to keep proxies / load balancers from
|
|
212
|
+
* killing idle connections. Carries no payload state — the client ignores it
|
|
213
|
+
* apart from the keepalive effect.
|
|
214
|
+
*/
|
|
215
|
+
export interface HeartbeatEvent {
|
|
216
|
+
kind: 'heartbeat';
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Server told the client to discard its cached state and refetch from
|
|
221
|
+
* scratch — sent when a reconnecting client's `Last-Event-ID` is older than
|
|
222
|
+
* the server's ring buffer still has, so we can't trust an event replay to
|
|
223
|
+
* catch them up.
|
|
224
|
+
*/
|
|
225
|
+
export interface ResyncEvent {
|
|
226
|
+
kind: 'resync';
|
|
227
|
+
/** Human-readable reason for logs / debugging. */
|
|
228
|
+
reason: string;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ── Union + helpers ──────────────────────────────────────────────────────────
|
|
232
|
+
|
|
233
|
+
export type WorkflowEventPayload =
|
|
234
|
+
| FileChangedEvent
|
|
235
|
+
| LockAcquiredEvent
|
|
236
|
+
| LockReleasedEvent
|
|
237
|
+
| FsTreeChangedEvent
|
|
238
|
+
| BranchSwitchedEvent
|
|
239
|
+
| AgentToolCallEvent
|
|
240
|
+
| ChangeRequestOpenedEvent
|
|
241
|
+
| ChangeRequestMergedEvent
|
|
242
|
+
| ChangeRequestRejectedEvent
|
|
243
|
+
| ChangeRequestMergeFailedEvent
|
|
244
|
+
| ApprovalChangedEvent
|
|
245
|
+
| HeartbeatEvent
|
|
246
|
+
| ResyncEvent;
|
|
247
|
+
|
|
248
|
+
export type WorkflowEvent = WorkflowEventEnvelope & WorkflowEventPayload;
|
|
249
|
+
|
|
250
|
+
/** True for events scoped to a workspace — the focus filter applies. */
|
|
251
|
+
export function isWorkspaceScoped(
|
|
252
|
+
e: WorkflowEventPayload,
|
|
253
|
+
): e is WorkflowEventPayload & { workspaceId: string } {
|
|
254
|
+
return (
|
|
255
|
+
e.kind === 'file-changed' ||
|
|
256
|
+
e.kind === 'lock-acquired' ||
|
|
257
|
+
e.kind === 'lock-released' ||
|
|
258
|
+
e.kind === 'fs-tree-changed'
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** True for events scoped to a specific user — the user filter applies. */
|
|
263
|
+
export function isUserScoped(
|
|
264
|
+
e: WorkflowEventPayload,
|
|
265
|
+
): e is WorkflowEventPayload & { forUserId: string } {
|
|
266
|
+
return (
|
|
267
|
+
e.kind === 'agent-tool-call' ||
|
|
268
|
+
e.kind === 'branch-switched' ||
|
|
269
|
+
e.kind === 'change-request-merge-failed'
|
|
270
|
+
);
|
|
271
|
+
}
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `IWorkflowService` — the abstraction the rest of the app talks to instead
|
|
3
|
+
* of raw git/gh. See `bevel-platform/PLAN.md` for the motivation and the spec the
|
|
4
|
+
* methods here encode (protected branches, atomic per-file changes, file
|
|
5
|
+
* locks, change requests as branch-pair reconciliations).
|
|
6
|
+
*
|
|
7
|
+
* Naming convention: methods speak workflow vocabulary (`commitChange`,
|
|
8
|
+
* `openChangeRequest`, `acquireLock`), never git vocabulary (`commit`,
|
|
9
|
+
* `createPr`, no analogue for locks). Implementations may delegate to the
|
|
10
|
+
* existing git/PR/review-workflow services until those are folded into
|
|
11
|
+
* `modules/workflow`; callers should not depend on that fact.
|
|
12
|
+
*
|
|
13
|
+
* Some methods on this interface have no backing service yet — file locks,
|
|
14
|
+
* `openChangeRequest`, `updateFromTarget`. The facade implementation throws
|
|
15
|
+
* `NotImplementedWorkflowError` from those until the underlying support
|
|
16
|
+
* lands. The interface defines them up-front so consumers can plan against
|
|
17
|
+
* the final surface area.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { AuthUser } from '../auth/types.js';
|
|
21
|
+
import type {
|
|
22
|
+
AcquireLockResult,
|
|
23
|
+
Branch,
|
|
24
|
+
BranchWorkspaceStatus,
|
|
25
|
+
CancelChangeRequestResult,
|
|
26
|
+
Change,
|
|
27
|
+
ChangeInput,
|
|
28
|
+
ChangeRequest,
|
|
29
|
+
ChangeRequestComment,
|
|
30
|
+
ChangeRequestDetail,
|
|
31
|
+
ChangeRequestState,
|
|
32
|
+
ChangedFile,
|
|
33
|
+
FileApproval,
|
|
34
|
+
FileLock,
|
|
35
|
+
MergeChangeRequestOutcome,
|
|
36
|
+
OpenChangeRequestInput,
|
|
37
|
+
PostChangeRequestCommentInput,
|
|
38
|
+
} from './types.js';
|
|
39
|
+
|
|
40
|
+
export interface IWorkflowService {
|
|
41
|
+
// ── Branches ──────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
listBranches(workspaceId: string, opts?: { freshFetch?: boolean }): Promise<Branch[]>;
|
|
44
|
+
/**
|
|
45
|
+
* Create a new unprotected branch. Protected branches cannot be created
|
|
46
|
+
* via the workflow — the protected set is bootstrapped from the KB repo's
|
|
47
|
+
* initial state and never grown at runtime.
|
|
48
|
+
*/
|
|
49
|
+
createBranch(workspaceId: string, name: string, fromBase?: string): Promise<Branch>;
|
|
50
|
+
/**
|
|
51
|
+
* Delete a branch. Removes both local and origin refs in one call. The
|
|
52
|
+
* branch's author (per `<email-localpart>/...` naming) OR an admin (per the
|
|
53
|
+
* workspace's `roles.yaml`) can delete it. Admins can additionally remove
|
|
54
|
+
* unprefixed CLI-created branches that have no recognisable author.
|
|
55
|
+
*
|
|
56
|
+
* `onlyIfNoRemote: true` is the legacy orphan-cleanup path: bypasses the
|
|
57
|
+
* author/admin check (callers prune any orphan they encounter) and refuses
|
|
58
|
+
* if origin still has the ref. Protected and current branches always reject.
|
|
59
|
+
*/
|
|
60
|
+
deleteBranch(
|
|
61
|
+
workspaceId: string,
|
|
62
|
+
name: string,
|
|
63
|
+
user: AuthUser,
|
|
64
|
+
opts?: { onlyIfNoRemote?: boolean },
|
|
65
|
+
): Promise<void>;
|
|
66
|
+
// `switchBranch` removed: under the per-branch workspace model the active
|
|
67
|
+
// branch is the workspace's identity. Switching branches is a workspace
|
|
68
|
+
// selection (`WorkspaceService.getOrCreateForBranch`), not an operation
|
|
69
|
+
// on an existing workspace's clone.
|
|
70
|
+
// `forkCurrentToDraft` removed: under the per-branch workspace model each
|
|
71
|
+
// branch is its own workspace; nothing carries uncommitted edits across.
|
|
72
|
+
// `createBranch` + URL navigation covers the "start a new draft" intent.
|
|
73
|
+
/** Current branch's workspace status — currently just the branch name + upstream sync state. */
|
|
74
|
+
branchStatus(workspaceId: string): Promise<BranchWorkspaceStatus>;
|
|
75
|
+
// `discardWorkingChanges` removed: under save=share the working tree is
|
|
76
|
+
// never dirty, so there is nothing for "undo my unsaved edits" to undo.
|
|
77
|
+
/**
|
|
78
|
+
* Push the current branch upstream so other users (and change requests)
|
|
79
|
+
* can see the commits on it. Workflow term: "share current branch". The
|
|
80
|
+
* underlying implementation is `git push -u origin <branch>`.
|
|
81
|
+
*/
|
|
82
|
+
shareCurrentBranch(workspaceId: string, user: AuthUser): Promise<void>;
|
|
83
|
+
/**
|
|
84
|
+
* Number of commits on `branch` that aren't on `baseBranch` — exactly
|
|
85
|
+
* what a CR from `branch` to `baseBranch` would propose for review.
|
|
86
|
+
* Implemented as `git rev-list --count <baseBranch>..<branch>` against
|
|
87
|
+
* the workspace's clone; returns 0 when the branch has no unmerged
|
|
88
|
+
* commits (a fresh fork that nobody wrote to) and when either ref
|
|
89
|
+
* cannot be resolved. Background runs use this to decide whether a
|
|
90
|
+
* routine branch deserves a change request — without it, a run where
|
|
91
|
+
* the agent commits files but forgets to call `open_change_request`
|
|
92
|
+
* would silently lose its work to the no-changes cleanup path.
|
|
93
|
+
*/
|
|
94
|
+
countCommitsAhead(workspaceId: string, branch: string, baseBranch: string): Promise<number>;
|
|
95
|
+
/** Fetch remote refs without modifying the working tree. */
|
|
96
|
+
refreshRemotes(workspaceId: string): Promise<void>;
|
|
97
|
+
/** Pull the latest remote state into the current branch (rebase strategy). */
|
|
98
|
+
updateFromRemote(workspaceId: string): Promise<void>;
|
|
99
|
+
/**
|
|
100
|
+
* Hard-reset the workspace's checked-out branch to `origin/<branch>` (fetch
|
|
101
|
+
* first), discarding any local divergence. Break-glass primitive for
|
|
102
|
+
* roles.yaml recovery so the restoring commit can fast-forward on push.
|
|
103
|
+
*/
|
|
104
|
+
resetToRemote(workspaceId: string, branch: string): Promise<void>;
|
|
105
|
+
/**
|
|
106
|
+
* Best-fit protected branch that `branch` was forked from — picks the
|
|
107
|
+
* protected branch on origin yielding the smallest "ahead" count.
|
|
108
|
+
* Returns null if no protected branch can be reached.
|
|
109
|
+
*/
|
|
110
|
+
resolveForkBase(workspaceId: string, branch: string): Promise<string | null>;
|
|
111
|
+
/**
|
|
112
|
+
* Paths that the next `commitChange` would include — what `git add -A`
|
|
113
|
+
* would stage: modified + deleted + renamed + untracked (honouring
|
|
114
|
+
* `.gitignore`). Returns only paths.
|
|
115
|
+
*/
|
|
116
|
+
listPendingChangePaths(workspaceId: string): Promise<string[]>;
|
|
117
|
+
// `listWorkingChanges` + `diffFileInWorking` removed: under save=share the
|
|
118
|
+
// working tree is never dirty, so both report the empty state by
|
|
119
|
+
// definition. Cross-branch / per-commit diff cases are covered by
|
|
120
|
+
// `compareFile` + `showFileAtChange` below.
|
|
121
|
+
|
|
122
|
+
// ── Changes ───────────────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Commit one atomic change. The current backing implementation packs every
|
|
126
|
+
* dirty path in the working tree into the commit; the workflow spec
|
|
127
|
+
* eventually constrains this to one file per change, enforced at the
|
|
128
|
+
* commit boundary. Until that lands, callers should already commit
|
|
129
|
+
* one-file-at-a-time by convention.
|
|
130
|
+
*/
|
|
131
|
+
commitChange(workspaceId: string, user: AuthUser, input: ChangeInput): Promise<Change>;
|
|
132
|
+
/**
|
|
133
|
+
* Commit whatever the caller has already written to the working tree as ONE
|
|
134
|
+
* atomic change, then push. Unlike `commitChange` (one file at a time, guarded
|
|
135
|
+
* at the commit boundary) this commits the whole dirty set together — for the
|
|
136
|
+
* rare case where a group of files must land together or not at all (e.g. a
|
|
137
|
+
* role rename rewriting `roles.yaml` + every reference). Does NOT write
|
|
138
|
+
* content: the caller writes the files first (e.g. via the lock-aware
|
|
139
|
+
* filesystem) and is responsible for any validation. Returns null on a no-op
|
|
140
|
+
* (clean tree).
|
|
141
|
+
*/
|
|
142
|
+
commitChanges(
|
|
143
|
+
workspaceId: string,
|
|
144
|
+
user: AuthUser,
|
|
145
|
+
summary: string,
|
|
146
|
+
): Promise<Change | null>;
|
|
147
|
+
/** Per-file history. Newest first; clamps to ≤ 100 entries. */
|
|
148
|
+
listChangesForFile(workspaceId: string, path: string, limit?: number): Promise<Change[]>;
|
|
149
|
+
/**
|
|
150
|
+
* Revert a single change by creating a new change that undoes it.
|
|
151
|
+
* Refused on protected branches; refused without write permission on the
|
|
152
|
+
* touched paths.
|
|
153
|
+
*/
|
|
154
|
+
revertChange(workspaceId: string, user: AuthUser, sha: string): Promise<Change>;
|
|
155
|
+
/**
|
|
156
|
+
* Diff of one file between two branches. Both names must resolve to a
|
|
157
|
+
* known branch on this workspace (local head or remote-tracking). Returns
|
|
158
|
+
* the raw unified diff body — empty when identical, includes the
|
|
159
|
+
* "new file" / "deleted file" headers when the file only exists on one
|
|
160
|
+
* side.
|
|
161
|
+
*/
|
|
162
|
+
compareFile(
|
|
163
|
+
workspaceId: string,
|
|
164
|
+
path: string,
|
|
165
|
+
fromBranch: string,
|
|
166
|
+
toBranch: string,
|
|
167
|
+
): Promise<string>;
|
|
168
|
+
/**
|
|
169
|
+
* Diff of one file at a specific change (sha) — i.e. `git show <sha> -- <path>`.
|
|
170
|
+
* Used by the File viewer's History tab.
|
|
171
|
+
*/
|
|
172
|
+
showFileAtChange(workspaceId: string, path: string, sha: string): Promise<string>;
|
|
173
|
+
|
|
174
|
+
// ── File locks (new — currently NotImplementedWorkflowError) ──────────────
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Try to acquire an edit lock on `(branch, path)` for the caller. Returns
|
|
178
|
+
* `{ acquired: true, lock }` on success; `{ acquired: false, lock }` if
|
|
179
|
+
* someone else already holds it (the existing lock is included so the UI
|
|
180
|
+
* can render "Locked by X" without a follow-up call).
|
|
181
|
+
*
|
|
182
|
+
* Pessimistic per the spec. Auto-released when the lock expires past TTL
|
|
183
|
+
* without a heartbeat — stale locks must not block forever.
|
|
184
|
+
*/
|
|
185
|
+
acquireLock(
|
|
186
|
+
workspaceId: string,
|
|
187
|
+
branch: string,
|
|
188
|
+
path: string,
|
|
189
|
+
user: AuthUser,
|
|
190
|
+
): Promise<AcquireLockResult>;
|
|
191
|
+
/** Heartbeat to keep an acquired lock alive past its current TTL. */
|
|
192
|
+
heartbeatLock(workspaceId: string, branch: string, path: string, user: AuthUser): Promise<FileLock>;
|
|
193
|
+
/**
|
|
194
|
+
* Commit the lock-held file's accumulated edits as a change *without*
|
|
195
|
+
* releasing the lock. Used by the client-side autosave checkpoint timer
|
|
196
|
+
* so the lock stays exclusively the caller's across many interim
|
|
197
|
+
* commits — releasing + reacquiring around every checkpoint would let
|
|
198
|
+
* another user grab the lock in the gap. Returns `null` when the file
|
|
199
|
+
* has no pending edits (idempotent). Refused when the lock isn't held
|
|
200
|
+
* by `user`.
|
|
201
|
+
*/
|
|
202
|
+
commitFileWhileLocked(
|
|
203
|
+
workspaceId: string,
|
|
204
|
+
branch: string,
|
|
205
|
+
path: string,
|
|
206
|
+
user: AuthUser,
|
|
207
|
+
summary?: string,
|
|
208
|
+
): Promise<Change | null>;
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Release a held lock. The "I'm done editing this file" terminal signal.
|
|
212
|
+
*
|
|
213
|
+
* **Does not commit or push synchronously.** This method enqueues a row in
|
|
214
|
+
* `pending_commits` and drops the lock, then returns `void`. The background
|
|
215
|
+
* `PendingCommitsWorker` claims the row, synthesises the commit subject
|
|
216
|
+
* (callers do *not* supply one — the worker derives a default from the path
|
|
217
|
+
* and any worker-side metadata), runs `commitFile` against whatever bytes
|
|
218
|
+
* are on disk, and pushes the result. Decoupling the user-visible lock
|
|
219
|
+
* release from git means a transient push failure no longer wedges the
|
|
220
|
+
* editor, and a process crash mid-commit becomes resumable instead of
|
|
221
|
+
* leaving an orphan on disk. See `lock-decoupling-plan.md` for the full
|
|
222
|
+
* design.
|
|
223
|
+
*
|
|
224
|
+
* The autosave-checkpoint timer and explicit `commitChange` calls land
|
|
225
|
+
* commits directly (no queue); `releaseLock` is queue-only.
|
|
226
|
+
*/
|
|
227
|
+
releaseLock(
|
|
228
|
+
workspaceId: string,
|
|
229
|
+
branch: string,
|
|
230
|
+
path: string,
|
|
231
|
+
user: AuthUser,
|
|
232
|
+
): Promise<void>;
|
|
233
|
+
/**
|
|
234
|
+
* Drop the lock **without committing**. Used on the failure path of a
|
|
235
|
+
* lock-aware write — when the underlying op threw, we don't want the
|
|
236
|
+
* normal `releaseLock` to commit + push whatever partial state happens
|
|
237
|
+
* to be on disk. The lock row goes away (so the next caller can edit
|
|
238
|
+
* the file); the working tree is left as-is. Idempotent like
|
|
239
|
+
* `releaseLock`: a no-op when the caller doesn't hold the lock.
|
|
240
|
+
*/
|
|
241
|
+
releaseLockNoCommit(
|
|
242
|
+
workspaceId: string,
|
|
243
|
+
branch: string,
|
|
244
|
+
path: string,
|
|
245
|
+
user: AuthUser,
|
|
246
|
+
): Promise<void>;
|
|
247
|
+
/** Current lock holder for `(branch, path)`, or null when nobody holds it. */
|
|
248
|
+
getLock(workspaceId: string, branch: string, path: string): Promise<FileLock | null>;
|
|
249
|
+
|
|
250
|
+
// ── Change Requests ───────────────────────────────────────────────────────
|
|
251
|
+
|
|
252
|
+
listChangeRequests(opts?: { fresh?: boolean }): Promise<ChangeRequest[]>;
|
|
253
|
+
/** Change requests authored by the caller (hash-matched against the body marker). */
|
|
254
|
+
listChangeRequestsAuthoredBy(emailOrLogin: string): Promise<ChangeRequest[]>;
|
|
255
|
+
/** Change requests touching files the user has write permission on. */
|
|
256
|
+
listChangeRequestsForUser(
|
|
257
|
+
workspaceId: string,
|
|
258
|
+
email: string,
|
|
259
|
+
opts?: { fresh?: boolean },
|
|
260
|
+
): Promise<ChangeRequest[]>;
|
|
261
|
+
getChangeRequest(number: number): Promise<ChangeRequest | null>;
|
|
262
|
+
getChangeRequestDetail(
|
|
263
|
+
number: number,
|
|
264
|
+
opts?: { fresh?: boolean; workspaceId?: string; viewerEmail?: string },
|
|
265
|
+
): Promise<ChangeRequestDetail | null>;
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Open a new change request. Per spec, auto-merges `targetBranch` into
|
|
269
|
+
* `sourceBranch` and surfaces the resulting diff as the changes to review.
|
|
270
|
+
* Throws `NotImplementedWorkflowError` until the backing create-PR path
|
|
271
|
+
* lands in the workflow module (today the agent shells `gh pr create`
|
|
272
|
+
* directly).
|
|
273
|
+
*/
|
|
274
|
+
openChangeRequest(
|
|
275
|
+
workspaceId: string,
|
|
276
|
+
user: AuthUser,
|
|
277
|
+
input: OpenChangeRequestInput,
|
|
278
|
+
): Promise<ChangeRequestDetail>;
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Re-run `targetBranch → sourceBranch` merge on an existing change request.
|
|
282
|
+
* Used when the target has advanced since the change request was opened.
|
|
283
|
+
* Throws `NotImplementedWorkflowError` until the backing merge path lands.
|
|
284
|
+
*/
|
|
285
|
+
updateFromTarget(workspaceId: string, user: AuthUser, number: number): Promise<ChangeRequestDetail>;
|
|
286
|
+
|
|
287
|
+
// Comments
|
|
288
|
+
listComments(number: number): Promise<ChangeRequestComment[]>;
|
|
289
|
+
postComment(
|
|
290
|
+
number: number,
|
|
291
|
+
user: AuthUser,
|
|
292
|
+
input: PostChangeRequestCommentInput,
|
|
293
|
+
headSha: string,
|
|
294
|
+
): Promise<ChangeRequestComment>;
|
|
295
|
+
editComment(commentId: string, number: number, user: AuthUser, body: string): Promise<ChangeRequestComment>;
|
|
296
|
+
deleteComment(commentId: string, number: number, user: AuthUser): Promise<void>;
|
|
297
|
+
|
|
298
|
+
// Approvals — per-file, tied to the file's current change. New changes
|
|
299
|
+
// invalidate approvals on the affected file.
|
|
300
|
+
approveFile(
|
|
301
|
+
number: number,
|
|
302
|
+
path: string,
|
|
303
|
+
user: AuthUser,
|
|
304
|
+
files: ChangedFile[],
|
|
305
|
+
headSha: string,
|
|
306
|
+
baseBranch: string,
|
|
307
|
+
authorIdHash: string | null,
|
|
308
|
+
workspaceId: string,
|
|
309
|
+
): Promise<FileApproval[]>;
|
|
310
|
+
unapproveFile(
|
|
311
|
+
number: number,
|
|
312
|
+
path: string,
|
|
313
|
+
user: AuthUser,
|
|
314
|
+
files: ChangedFile[],
|
|
315
|
+
headSha: string,
|
|
316
|
+
baseBranch: string,
|
|
317
|
+
authorIdHash: string | null,
|
|
318
|
+
workspaceId: string,
|
|
319
|
+
): Promise<FileApproval[]>;
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Reject a change request without merging. Authorized for the author or
|
|
323
|
+
* for users with write permission to every changed file on the target
|
|
324
|
+
* branch.
|
|
325
|
+
*/
|
|
326
|
+
rejectChangeRequest(
|
|
327
|
+
number: number,
|
|
328
|
+
user: AuthUser,
|
|
329
|
+
state: ChangeRequestState,
|
|
330
|
+
authorIdHash: string | null,
|
|
331
|
+
baseBranch: string,
|
|
332
|
+
workspaceId: string,
|
|
333
|
+
): Promise<CancelChangeRequestResult>;
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Merge a change request. The workflow tries a direct merge first; on
|
|
337
|
+
* conflicts it returns a `conflicts-need-resolution` outcome and the
|
|
338
|
+
* caller (typically the agent) is responsible for resolving them as new
|
|
339
|
+
* changes on the source branch. Once the source contains target cleanly,
|
|
340
|
+
* a subsequent `mergeChangeRequest` call lands the merge.
|
|
341
|
+
*
|
|
342
|
+
* The hard merge invariant — only this method may invoke `gh pr merge` —
|
|
343
|
+
* is preserved through the facade by delegating to
|
|
344
|
+
* `IReviewWorkflowService.mergePr`.
|
|
345
|
+
*/
|
|
346
|
+
mergeChangeRequest(
|
|
347
|
+
number: number,
|
|
348
|
+
user: AuthUser,
|
|
349
|
+
headSha: string,
|
|
350
|
+
approvals: FileApproval[],
|
|
351
|
+
state: ChangeRequestState,
|
|
352
|
+
title: string,
|
|
353
|
+
baseBranch: string,
|
|
354
|
+
workspaceId: string,
|
|
355
|
+
opts?: { bypass?: boolean },
|
|
356
|
+
): Promise<MergeChangeRequestOutcome>;
|
|
357
|
+
}
|