beflow 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.
Files changed (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +121 -0
  3. package/config.example.json +68 -0
  4. package/config.schema.json +413 -0
  5. package/package.json +72 -0
  6. package/src/agent/acpx.ts +197 -0
  7. package/src/agent/driver.ts +38 -0
  8. package/src/agent/events.ts +228 -0
  9. package/src/agent/issuefence.ts +42 -0
  10. package/src/agent/report.ts +44 -0
  11. package/src/cli.ts +910 -0
  12. package/src/config/load.ts +45 -0
  13. package/src/config/persist.ts +58 -0
  14. package/src/config/schema.ts +181 -0
  15. package/src/config/store.ts +119 -0
  16. package/src/core/accept.ts +25 -0
  17. package/src/core/continuation.ts +57 -0
  18. package/src/core/deadletter.ts +55 -0
  19. package/src/core/decision.ts +8 -0
  20. package/src/core/doctor.ts +223 -0
  21. package/src/core/drift.ts +59 -0
  22. package/src/core/gc.ts +223 -0
  23. package/src/core/inputquality.ts +30 -0
  24. package/src/core/issuetemplate.ts +175 -0
  25. package/src/core/mcp.ts +191 -0
  26. package/src/core/newissue.ts +343 -0
  27. package/src/core/notify.ts +151 -0
  28. package/src/core/prompts.ts +165 -0
  29. package/src/core/qualitygate.ts +70 -0
  30. package/src/core/queue.ts +40 -0
  31. package/src/core/review.ts +266 -0
  32. package/src/core/run.ts +1075 -0
  33. package/src/core/runstore.ts +144 -0
  34. package/src/core/runsview.ts +111 -0
  35. package/src/core/setup.ts +203 -0
  36. package/src/core/sla.ts +39 -0
  37. package/src/core/template.ts +65 -0
  38. package/src/core/watch.ts +825 -0
  39. package/src/core/worktree.ts +74 -0
  40. package/src/core/writeback.ts +88 -0
  41. package/src/index.ts +154 -0
  42. package/src/model/types.ts +35 -0
  43. package/src/prompts/defaults/continuation.md +9 -0
  44. package/src/prompts/defaults/implement.md +13 -0
  45. package/src/prompts/defaults/issue-enrich.md +30 -0
  46. package/src/prompts/defaults/issues/bug.md +35 -0
  47. package/src/prompts/defaults/issues/feature.md +24 -0
  48. package/src/prompts/defaults/issues/generic.md +16 -0
  49. package/src/prompts/defaults/issues/spike.md +24 -0
  50. package/src/prompts/defaults/report.md +20 -0
  51. package/src/prompts/defaults/review.md +34 -0
  52. package/src/prompts/defaults/spec.md +11 -0
  53. package/src/prompts/defaults/task.md +6 -0
  54. package/src/prompts/defaults/triage.md +11 -0
  55. package/src/prompts/text-modules.d.ts +4 -0
  56. package/src/resolve/jobkind.ts +11 -0
  57. package/src/resolve/metadata.ts +103 -0
  58. package/src/resolve/precedence.ts +104 -0
  59. package/src/trackers/factory.ts +17 -0
  60. package/src/trackers/linear/adapter.ts +416 -0
  61. package/src/trackers/linear/client.ts +264 -0
  62. package/src/trackers/linear/map.ts +113 -0
  63. package/src/trackers/linear/types.ts +44 -0
  64. package/src/trackers/marker.ts +20 -0
  65. package/src/trackers/plane/adapter.ts +754 -0
  66. package/src/trackers/plane/client.ts +302 -0
  67. package/src/trackers/plane/map.ts +168 -0
  68. package/src/trackers/plane/types.ts +134 -0
  69. package/src/trackers/tracker.ts +135 -0
@@ -0,0 +1,302 @@
1
+ import type {
2
+ Paginated,
3
+ RawAttachment,
4
+ RawComment,
5
+ RawCycle,
6
+ RawCycleWorkItem,
7
+ RawIntakeIssue,
8
+ RawLabel,
9
+ RawLink,
10
+ RawModule,
11
+ RawProject,
12
+ RawState,
13
+ RawWorkItem,
14
+ RawWorkItemRelations,
15
+ RawWorkItemType,
16
+ } from "./types.ts";
17
+
18
+ export type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
19
+
20
+ export class PlaneHttpError extends Error {
21
+ public constructor(
22
+ public readonly status: number,
23
+ message: string,
24
+ ) {
25
+ super(message);
26
+ this.name = "PlaneHttpError";
27
+ }
28
+ }
29
+
30
+ export interface PlaneClientOptions {
31
+ baseUrl?: string;
32
+ workspaceSlug: string;
33
+ apiKey: string;
34
+ fetch?: FetchLike;
35
+ sleep?: (ms: number) => Promise<void>;
36
+ }
37
+
38
+ interface ListWorkItemsOptions {
39
+ expand?: string;
40
+ order_by?: string;
41
+ }
42
+
43
+ const MAX_RETRIES = 2;
44
+ const DEFAULT_BASE_URL = "https://api.plane.so";
45
+
46
+ async function realSleep(ms: number): Promise<void> {
47
+ return new Promise((resolve) => {
48
+ setTimeout(resolve, ms);
49
+ });
50
+ }
51
+
52
+ // Deserialization trust boundary: the caller declares the response shape T.
53
+ function hasShape<T>(_value: unknown): _value is T {
54
+ return true;
55
+ }
56
+
57
+ function decodeBody<T>(value: unknown): T {
58
+ if (!hasShape<T>(value)) {
59
+ throw new Error("plane: response body did not match expected shape");
60
+ }
61
+ return value;
62
+ }
63
+
64
+ export class PlaneClient {
65
+ private readonly baseUrl: string;
66
+ private readonly slug: string;
67
+ private readonly apiKey: string;
68
+ private readonly fetch: FetchLike;
69
+ private readonly sleep: (ms: number) => Promise<void>;
70
+
71
+ public constructor(options: PlaneClientOptions) {
72
+ this.baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
73
+ this.slug = options.workspaceSlug;
74
+ this.apiKey = options.apiKey;
75
+ this.fetch = options.fetch ?? (globalThis.fetch as FetchLike);
76
+ this.sleep = options.sleep ?? realSleep;
77
+ }
78
+
79
+ private headers(): Record<string, string> {
80
+ return {
81
+ "Accept": "application/json",
82
+ "Content-Type": "application/json",
83
+ "X-API-Key": this.apiKey,
84
+ };
85
+ }
86
+
87
+ private async request<T>(method: string, path: string, body?: unknown): Promise<T> {
88
+ const url = `${this.baseUrl}${path}`;
89
+ const init: RequestInit = { headers: this.headers(), method };
90
+ if (body !== undefined) {
91
+ init.body = JSON.stringify(body);
92
+ }
93
+
94
+ let attempt = 0;
95
+ for (;;) {
96
+ const response = await this.fetch(url, init);
97
+
98
+ if (response.status === 429 && attempt < MAX_RETRIES) {
99
+ const retryAfter = Number(response.headers.get("Retry-After") ?? "1");
100
+ const seconds = Number.isFinite(retryAfter) ? retryAfter : 1;
101
+ await this.sleep(seconds * 1000);
102
+ attempt += 1;
103
+ continue;
104
+ }
105
+
106
+ if (!response.ok) {
107
+ const snippet = (await response.text()).slice(0, 500);
108
+ throw new PlaneHttpError(
109
+ response.status,
110
+ `plane: ${method} ${path} failed with ${String(response.status)}: ${snippet}`,
111
+ );
112
+ }
113
+
114
+ if (response.status === 204) {
115
+ return decodeBody<T>(undefined);
116
+ }
117
+ const text = await response.text();
118
+ if (text === "") {
119
+ return decodeBody<T>(undefined);
120
+ }
121
+ const parsed: unknown = JSON.parse(text);
122
+ return decodeBody<T>(parsed);
123
+ }
124
+ }
125
+
126
+ private projectBase(projectId: string): string {
127
+ return `/api/v1/workspaces/${this.slug}/projects/${projectId}`;
128
+ }
129
+
130
+ private async paginate<T>(basePath: string): Promise<T[]> {
131
+ const results: T[] = [];
132
+ let cursor: string | undefined;
133
+ for (;;) {
134
+ const sep = basePath.includes("?") ? "&" : "?";
135
+ const path =
136
+ cursor === undefined
137
+ ? `${basePath}${sep}per_page=100`
138
+ : `${basePath}${sep}per_page=100&cursor=${encodeURIComponent(cursor)}`;
139
+ const page = await this.request<Paginated<T>>("GET", path);
140
+ results.push(...page.results);
141
+ if (!page.next_page_results) {
142
+ break;
143
+ }
144
+ cursor = page.next_cursor;
145
+ }
146
+ return results;
147
+ }
148
+
149
+ public async createProject(body: { identifier: string; name: string }): Promise<RawProject> {
150
+ return this.request<RawProject>("POST", `/api/v1/workspaces/${this.slug}/projects/`, body);
151
+ }
152
+
153
+ public async getWorkItemByIdentifier(key: string): Promise<RawWorkItem> {
154
+ const path = `/api/v1/workspaces/${this.slug}/work-items/${key}/?expand=state,labels`;
155
+ return this.request<RawWorkItem>("GET", path);
156
+ }
157
+
158
+ public async listWorkItems(projectId: string, options: ListWorkItemsOptions = {}): Promise<RawWorkItem[]> {
159
+ const expand = options.expand ?? "state,labels";
160
+ const params = new URLSearchParams({ expand });
161
+ if (options.order_by !== undefined && options.order_by !== "") {
162
+ params.set("order_by", options.order_by);
163
+ }
164
+ const path = `${this.projectBase(projectId)}/work-items/?${params.toString()}`;
165
+ return this.paginate<RawWorkItem>(path);
166
+ }
167
+
168
+ public async getWorkItem(projectId: string, id: string): Promise<RawWorkItem> {
169
+ const path = `${this.projectBase(projectId)}/work-items/${id}/?expand=state,labels`;
170
+ return this.request<RawWorkItem>("GET", path);
171
+ }
172
+
173
+ public async createWorkItem(projectId: string, body: unknown): Promise<RawWorkItem> {
174
+ return this.request<RawWorkItem>("POST", `${this.projectBase(projectId)}/work-items/`, body);
175
+ }
176
+
177
+ public async patchWorkItem(projectId: string, id: string, body: unknown): Promise<RawWorkItem> {
178
+ return this.request<RawWorkItem>("PATCH", `${this.projectBase(projectId)}/work-items/${id}/`, body);
179
+ }
180
+
181
+ // Relations are returned as a single grouped object (blocking, blocked_by, …),
182
+ // each group a bare array of work-item UUIDs — NOT the paginated {results}
183
+ // Envelope, so this must not paginate.
184
+ public async listRelations(projectId: string, id: string): Promise<RawWorkItemRelations> {
185
+ return this.request<RawWorkItemRelations>("GET", `${this.projectBase(projectId)}/work-items/${id}/relations/`);
186
+ }
187
+
188
+ public async createComment(projectId: string, id: string, body: { comment_html: string }): Promise<RawComment> {
189
+ return this.request<RawComment>("POST", `${this.projectBase(projectId)}/work-items/${id}/comments/`, body);
190
+ }
191
+
192
+ public async listComments(projectId: string, id: string): Promise<RawComment[]> {
193
+ return this.paginate<RawComment>(`${this.projectBase(projectId)}/work-items/${id}/comments/`);
194
+ }
195
+
196
+ public async createLink(projectId: string, id: string, body: { url: string; title?: string }): Promise<RawLink> {
197
+ return this.request<RawLink>("POST", `${this.projectBase(projectId)}/work-items/${id}/links/`, body);
198
+ }
199
+
200
+ public async listLinks(projectId: string, id: string): Promise<RawLink[]> {
201
+ return this.paginate<RawLink>(`${this.projectBase(projectId)}/work-items/${id}/links/`);
202
+ }
203
+
204
+ // UNVERIFIED endpoint shape — confirm during live dogfood. Returns the raw
205
+ // Response as-is (NOT paginated); the adapter catches any failure and fails closed.
206
+ public async listAttachments(projectId: string, id: string): Promise<RawAttachment[]> {
207
+ return this.request<RawAttachment[]>("GET", `${this.projectBase(projectId)}/work-items/${id}/attachments/`);
208
+ }
209
+
210
+ // UNVERIFIED endpoint shape — confirm during live dogfood. The adapter fails
211
+ // Closed on any error (→ null → unfiltered dispatch), so a wrong shape can't halt.
212
+ public async listCycles(projectId: string): Promise<RawCycle[]> {
213
+ return this.paginate<RawCycle>(`${this.projectBase(projectId)}/cycles/`);
214
+ }
215
+
216
+ // VERIFIED live (2026-06-18): the membership endpoint is `cycle-issues/` (not
217
+ // `cycle-work-items/`, which 404s); each result IS the full work item, so its `id` is
218
+ // the work-item uuid (RawCycleWorkItem reads `work_item ?? id`). Fails closed via the adapter.
219
+ public async listCycleWorkItems(projectId: string, cycleId: string): Promise<RawCycleWorkItem[]> {
220
+ return this.paginate<RawCycleWorkItem>(`${this.projectBase(projectId)}/cycles/${cycleId}/cycle-issues/`);
221
+ }
222
+
223
+ public async listStates(projectId: string): Promise<RawState[]> {
224
+ return this.paginate<RawState>(`${this.projectBase(projectId)}/states/`);
225
+ }
226
+
227
+ public async listLabels(projectId: string): Promise<RawLabel[]> {
228
+ return this.paginate<RawLabel>(`${this.projectBase(projectId)}/labels/`);
229
+ }
230
+
231
+ public async listModules(projectId: string): Promise<RawModule[]> {
232
+ return this.paginate<RawModule>(`${this.projectBase(projectId)}/modules/`);
233
+ }
234
+
235
+ public async listTypes(projectId: string): Promise<RawWorkItemType[]> {
236
+ // The work-item-types endpoint returns a bare array, NOT the cursor-paginated
237
+ // {results} envelope the other list endpoints use — so it must not paginate.
238
+ return this.request<RawWorkItemType[]>("GET", `${this.projectBase(projectId)}/work-item-types/`);
239
+ }
240
+
241
+ public async createState(projectId: string, body: unknown): Promise<RawState> {
242
+ return this.request<RawState>("POST", `${this.projectBase(projectId)}/states/`, body);
243
+ }
244
+
245
+ public async updateState(projectId: string, id: string, body: unknown): Promise<RawState> {
246
+ return this.request<RawState>("PATCH", `${this.projectBase(projectId)}/states/${id}/`, body);
247
+ }
248
+
249
+ public async createLabel(projectId: string, body: unknown): Promise<RawLabel> {
250
+ return this.request<RawLabel>("POST", `${this.projectBase(projectId)}/labels/`, body);
251
+ }
252
+
253
+ public async updateLabel(projectId: string, id: string, body: unknown): Promise<RawLabel> {
254
+ return this.request<RawLabel>("PATCH", `${this.projectBase(projectId)}/labels/${id}/`, body);
255
+ }
256
+
257
+ public async createModule(projectId: string, body: unknown): Promise<RawModule> {
258
+ return this.request<RawModule>("POST", `${this.projectBase(projectId)}/modules/`, body);
259
+ }
260
+
261
+ public async updateModule(projectId: string, id: string, body: unknown): Promise<RawModule> {
262
+ return this.request<RawModule>("PATCH", `${this.projectBase(projectId)}/modules/${id}/`, body);
263
+ }
264
+
265
+ public async deleteModule(projectId: string, id: string): Promise<void> {
266
+ return this.request<undefined>("DELETE", `${this.projectBase(projectId)}/modules/${id}/`);
267
+ }
268
+
269
+ public async deleteLabel(projectId: string, id: string): Promise<void> {
270
+ return this.request<undefined>("DELETE", `${this.projectBase(projectId)}/labels/${id}/`);
271
+ }
272
+
273
+ public async createType(projectId: string, body: unknown): Promise<RawWorkItemType> {
274
+ return this.request<RawWorkItemType>("POST", `${this.projectBase(projectId)}/work-item-types/`, body);
275
+ }
276
+
277
+ public async updateType(projectId: string, id: string, body: unknown): Promise<RawWorkItemType> {
278
+ return this.request<RawWorkItemType>("PATCH", `${this.projectBase(projectId)}/work-item-types/${id}/`, body);
279
+ }
280
+
281
+ public async updateProjectFeatures(
282
+ projectId: string,
283
+ features: { is_issue_type_enabled: boolean; intake_view: boolean; module_view: boolean },
284
+ ): Promise<void> {
285
+ return this.request<undefined>("PATCH", `${this.projectBase(projectId)}/`, features);
286
+ }
287
+
288
+ public async listIntake(projectId: string): Promise<RawIntakeIssue[]> {
289
+ return this.paginate<RawIntakeIssue>(`${this.projectBase(projectId)}/intake-issues/`);
290
+ }
291
+
292
+ // VERIFIED live (2026-06-18): status updates require the /status/ suffix and
293
+ // must use the ISSUE id (item.issueId), NOT the intake record id (item.id).
294
+ // Plain PATCH intake-issues/<id>/ rejects status with HTTP 400.
295
+ public async updateIntakeStatus(projectId: string, issueId: string, status: number): Promise<RawIntakeIssue> {
296
+ return this.request<RawIntakeIssue>(
297
+ "PATCH",
298
+ `${this.projectBase(projectId)}/intake-issues/${issueId}/status/`,
299
+ { status },
300
+ );
301
+ }
302
+ }
@@ -0,0 +1,168 @@
1
+ import type { Issue, StateGroup } from "../../model/types.ts";
2
+ import { parseIssueMeta } from "../../resolve/metadata.ts";
3
+ import type { IntakeItem } from "../tracker.ts";
4
+ import type { RawCycle, RawIntakeIssue, RawLabel, RawModule, RawState, RawWorkItem, RawWorkItemType } from "./types.ts";
5
+
6
+ const PRIORITY_RANK: Record<string, number> = {
7
+ high: 1,
8
+ low: 3,
9
+ medium: 2,
10
+ none: 4,
11
+ urgent: 0,
12
+ };
13
+
14
+ export function priorityRank(priority?: string): number {
15
+ if (priority === undefined) {
16
+ return 4;
17
+ }
18
+ const rank = PRIORITY_RANK[priority];
19
+ return rank ?? 4;
20
+ }
21
+
22
+ const VALID_STATE_GROUPS: ReadonlySet<StateGroup> = new Set<StateGroup>([
23
+ "backlog",
24
+ "unstarted",
25
+ "started",
26
+ "completed",
27
+ "cancelled",
28
+ ]);
29
+
30
+ function isStateGroup(group: string): group is StateGroup {
31
+ return (VALID_STATE_GROUPS as ReadonlySet<string>).has(group);
32
+ }
33
+
34
+ export function mapStateGroup(group: string): StateGroup {
35
+ if (!isStateGroup(group)) {
36
+ throw new Error(
37
+ `plane: unsupported state group "${group}" (expected one of backlog|unstarted|started|completed|cancelled; our board never uses triage states)`,
38
+ );
39
+ }
40
+ return group;
41
+ }
42
+
43
+ function escapeHtml(text: string): string {
44
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
45
+ }
46
+
47
+ export function toCommentHtml(text: string): string {
48
+ return text
49
+ .split(/\n{2,}/)
50
+ .map((paragraph) => escapeHtml(paragraph).replace(/\n/g, "<br>"))
51
+ .map((paragraph) => `<p>${paragraph}</p>`)
52
+ .join("");
53
+ }
54
+
55
+ // Returns the first cycle currently in range. `todayIso` and the cycle dates are
56
+ // `YYYY-MM-DD` strings, for which lexicographic comparison is also chronological.
57
+ // A cycle missing either bound can't be confirmed active and is skipped.
58
+ export function pickActiveCycle(cycles: RawCycle[], todayIso: string): RawCycle | null {
59
+ for (const cycle of cycles) {
60
+ const { start_date, end_date } = cycle;
61
+ if (start_date === undefined || start_date === null || end_date === undefined || end_date === null) {
62
+ continue;
63
+ }
64
+ if (start_date <= todayIso && todayIso <= end_date) {
65
+ return cycle;
66
+ }
67
+ }
68
+ return null;
69
+ }
70
+
71
+ export function keyOf(identifier: string, sequenceId: number): string {
72
+ return `${identifier}-${String(sequenceId)}`;
73
+ }
74
+
75
+ export interface MapContext {
76
+ identifier: string;
77
+ statesById: Map<string, RawState>;
78
+ labelsById: Map<string, RawLabel>;
79
+ modulesById: Map<string, RawModule>;
80
+ typesById: Map<string, RawWorkItemType>;
81
+ }
82
+
83
+ function resolveState(raw: RawWorkItem, ctx: MapContext): { name: string; group: StateGroup } {
84
+ const { state } = raw;
85
+ if (state === undefined) {
86
+ throw new Error(`plane: work item ${raw.id} has no state`);
87
+ }
88
+ if (typeof state === "object") {
89
+ return { group: mapStateGroup(state.group), name: state.name };
90
+ }
91
+ const resolved = ctx.statesById.get(state);
92
+ if (resolved === undefined) {
93
+ throw new Error(`plane: cannot resolve state uuid "${state}" for work item ${raw.id}`);
94
+ }
95
+ return { group: mapStateGroup(resolved.group), name: resolved.name };
96
+ }
97
+
98
+ function resolveLabels(raw: RawWorkItem, ctx: MapContext): string[] {
99
+ const labels = raw.labels ?? [];
100
+ return labels.map((label) => {
101
+ if (typeof label === "object") {
102
+ return label.name;
103
+ }
104
+ const resolved = ctx.labelsById.get(label);
105
+ if (resolved === undefined) {
106
+ throw new Error(`plane: cannot resolve label uuid "${label}" for work item ${raw.id}`);
107
+ }
108
+ return resolved.name;
109
+ });
110
+ }
111
+
112
+ function resolveAreas(raw: RawWorkItem, ctx: MapContext): string[] {
113
+ // Plane's work-item serializer may omit module_ids; an empty/absent value
114
+ // Yields []. Confirmed best-effort pending live verification.
115
+ const moduleIds = raw.module_ids ?? [];
116
+ return moduleIds.map((id) => {
117
+ const resolved = ctx.modulesById.get(id);
118
+ if (resolved === undefined) {
119
+ throw new Error(`plane: cannot resolve module uuid "${id}" for work item ${raw.id}`);
120
+ }
121
+ return resolved.name;
122
+ });
123
+ }
124
+
125
+ function resolveType(raw: RawWorkItem, ctx: MapContext): string | undefined {
126
+ // Plane returns the type as a UUID (in `type`/`type_id`), not its name, and
127
+ // Expand=state,labels does not expand it — so resolve the UUID → name via the
128
+ // Types cache. A bare UUID would silently break mode auto-detect (Spike→triage).
129
+ const type = raw.type ?? raw.type_id;
130
+ if (type === undefined || type === null) {
131
+ return undefined;
132
+ }
133
+ if (typeof type === "object") {
134
+ return type.name;
135
+ }
136
+ return ctx.typesById.get(type)?.name;
137
+ }
138
+
139
+ export function mapWorkItem(raw: RawWorkItem, ctx: MapContext): Issue {
140
+ const labels = resolveLabels(raw, ctx);
141
+ const body = raw.description_stripped ?? "";
142
+ return {
143
+ archived: raw.archived_at !== null && raw.archived_at !== undefined,
144
+ areas: resolveAreas(raw, ctx),
145
+ body,
146
+ id: raw.id,
147
+ key: keyOf(ctx.identifier, raw.sequence_id),
148
+ labels,
149
+ meta: parseIssueMeta(body, labels),
150
+ ...(raw.parent !== null && raw.parent !== undefined ? { parentId: raw.parent } : {}),
151
+ priority: raw.priority,
152
+ state: resolveState(raw, ctx),
153
+ title: raw.name,
154
+ type: resolveType(raw, ctx),
155
+ };
156
+ }
157
+
158
+ export function mapIntakeItem(raw: RawIntakeIssue): IntakeItem {
159
+ const detail = raw.issue_detail;
160
+ return {
161
+ body: detail.description_stripped ?? detail.description ?? "",
162
+ id: raw.id,
163
+ issueId: raw.issue,
164
+ priority: detail.priority,
165
+ status: raw.status,
166
+ title: detail.name,
167
+ };
168
+ }
@@ -0,0 +1,134 @@
1
+ export interface RawProject {
2
+ id: string;
3
+ }
4
+
5
+ export interface Paginated<T> {
6
+ next_cursor: string;
7
+ prev_cursor: string;
8
+ next_page_results: boolean;
9
+ prev_page_results: boolean;
10
+ count: number;
11
+ total_pages: number;
12
+ total_results: number;
13
+ results: T[];
14
+ }
15
+
16
+ export interface RawState {
17
+ id: string;
18
+ name: string;
19
+ group: string;
20
+ color: string;
21
+ sequence?: number;
22
+ is_triage?: boolean;
23
+ default?: boolean;
24
+ }
25
+
26
+ export interface RawLabel {
27
+ id: string;
28
+ name: string;
29
+ color?: string;
30
+ description?: string;
31
+ }
32
+
33
+ export interface RawModule {
34
+ id: string;
35
+ name: string;
36
+ description?: string;
37
+ status?: string;
38
+ }
39
+
40
+ export interface RawWorkItemType {
41
+ id: string;
42
+ name: string;
43
+ description?: string;
44
+ }
45
+
46
+ export interface RawWorkItem {
47
+ id: string;
48
+ name: string;
49
+ sequence_id: number;
50
+ description_html?: string;
51
+ description_stripped?: string;
52
+ priority?: string;
53
+ // State is a uuid string, or an expanded object when ?expand=state.
54
+ state?: string | RawState;
55
+ // Labels are uuid strings, or expanded objects when ?expand=labels.
56
+ labels?: (string | RawLabel)[];
57
+ module_ids?: string[];
58
+ // Parent work item uuid (epic or grouping item); null/absent when top-level.
59
+ parent?: string | null;
60
+ // `null` on a project with issue-types disabled (live shape); the mapper guards it.
61
+ type_id?: string | null;
62
+ // Type may be expanded to an object carrying its name; `null` when types are disabled.
63
+ type?: string | { id: string; name: string } | null;
64
+ // Set when the work item has been archived; null/absent means active.
65
+ archived_at?: string | null;
66
+ }
67
+
68
+ // Plane attachment shape, reconciled against the live MCP surface (2026-06-17):
69
+ // the list exposes a top-level `name` (older builds nest it under `attributes`);
70
+ // the download URL is a SEPARATE call, so `asset_url` is frequently absent. Every
71
+ // field is optional; the adapter keeps any named attachment (URL or not).
72
+ export interface RawAttachment {
73
+ id: string;
74
+ asset_url?: string;
75
+ name?: string;
76
+ attributes?: { name?: string };
77
+ }
78
+
79
+ export interface RawIntakeIssue {
80
+ id: string;
81
+ issue: string;
82
+ issue_detail: {
83
+ id: string;
84
+ name: string;
85
+ description?: string;
86
+ description_stripped?: string;
87
+ priority?: string;
88
+ sequence_id?: number;
89
+ };
90
+ source?: string;
91
+ status: number;
92
+ }
93
+
94
+ // The work-item relations endpoint groups related work items by relation type,
95
+ // each group being a bare array of work-item UUIDs (NOT expanded objects). We
96
+ // only consume blocked_by; the other groups are declared for shape fidelity.
97
+ export interface RawWorkItemRelations {
98
+ blocking?: string[];
99
+ blocked_by?: string[];
100
+ duplicate?: string[];
101
+ relates_to?: string[];
102
+ start_after?: string[];
103
+ start_before?: string[];
104
+ finish_after?: string[];
105
+ finish_before?: string[];
106
+ }
107
+
108
+ // Plane's cycles endpoints are UNVERIFIED against our live API notes, so every
109
+ // Field is optional/tolerant and the adapter fails closed on any fetch error.
110
+ export interface RawCycle {
111
+ id: string;
112
+ name?: string;
113
+ start_date?: string | null;
114
+ end_date?: string | null;
115
+ }
116
+
117
+ export interface RawCycleWorkItem {
118
+ id?: string;
119
+ work_item?: string;
120
+ }
121
+
122
+ export interface RawComment {
123
+ id: string;
124
+ comment_html?: string;
125
+ created_at?: string;
126
+ created_by?: string;
127
+ comment_stripped?: string;
128
+ }
129
+
130
+ export interface RawLink {
131
+ id: string;
132
+ url: string;
133
+ title?: string;
134
+ }