goalbuddy 0.2.21 → 0.2.22

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 (41) hide show
  1. package/README.md +10 -18
  2. package/goalbuddy/SKILL.md +40 -10
  3. package/goalbuddy/extend/github-projects/README.md +105 -0
  4. package/goalbuddy/extend/github-projects/examples/goal-board-sync/state.yaml +63 -0
  5. package/goalbuddy/extend/github-projects/extension.yaml +43 -0
  6. package/goalbuddy/extend/github-projects/scripts/lib/github-projects.mjs +728 -0
  7. package/goalbuddy/extend/github-projects/scripts/lib/goal-state.mjs +362 -0
  8. package/goalbuddy/extend/github-projects/scripts/sync-github-project.mjs +193 -0
  9. package/goalbuddy/extend/github-projects/test/github-projects.test.mjs +267 -0
  10. package/goalbuddy/extend/local-goal-board/README.md +75 -0
  11. package/goalbuddy/extend/local-goal-board/assets/goalbuddy-mark.png +0 -0
  12. package/goalbuddy/extend/local-goal-board/examples/sample-goal/notes/T001-scout.md +3 -0
  13. package/goalbuddy/extend/local-goal-board/examples/sample-goal/state.yaml +124 -0
  14. package/goalbuddy/extend/local-goal-board/extension.yaml +37 -0
  15. package/goalbuddy/extend/local-goal-board/scripts/lib/goal-board.mjs +1225 -0
  16. package/goalbuddy/extend/local-goal-board/scripts/local-goal-board.mjs +258 -0
  17. package/goalbuddy/extend/local-goal-board/test/local-goal-board.test.mjs +146 -0
  18. package/goalbuddy/scripts/check-goal-state.mjs +24 -9
  19. package/goalbuddy/templates/state.yaml +18 -3
  20. package/internal/cli/goal-maker.mjs +57 -11
  21. package/package.json +3 -2
  22. package/plugins/goalbuddy/.codex-plugin/plugin.json +3 -3
  23. package/plugins/goalbuddy/README.md +1 -5
  24. package/plugins/goalbuddy/skills/goalbuddy/SKILL.md +40 -10
  25. package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/README.md +105 -0
  26. package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/examples/goal-board-sync/state.yaml +63 -0
  27. package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/extension.yaml +43 -0
  28. package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/scripts/lib/github-projects.mjs +728 -0
  29. package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/scripts/lib/goal-state.mjs +362 -0
  30. package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/scripts/sync-github-project.mjs +193 -0
  31. package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/test/github-projects.test.mjs +267 -0
  32. package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/README.md +75 -0
  33. package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/assets/goalbuddy-mark.png +0 -0
  34. package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/examples/sample-goal/notes/T001-scout.md +3 -0
  35. package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/examples/sample-goal/state.yaml +124 -0
  36. package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/extension.yaml +37 -0
  37. package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/scripts/lib/goal-board.mjs +1225 -0
  38. package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/scripts/local-goal-board.mjs +258 -0
  39. package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/test/local-goal-board.test.mjs +146 -0
  40. package/plugins/goalbuddy/skills/goalbuddy/scripts/check-goal-state.mjs +24 -9
  41. package/plugins/goalbuddy/skills/goalbuddy/templates/state.yaml +18 -3
@@ -0,0 +1,728 @@
1
+ export const GITHUB_PROJECT_FIELDS = {
2
+ taskId: "Task ID",
3
+ status: "Status",
4
+ priority: "Priority",
5
+ workType: "Work Type",
6
+ owner: "Owner",
7
+ goalRole: "Goal Role",
8
+ agentResponsible: "Agent Responsible",
9
+ agentLane: "Agent Lane",
10
+ credentialGate: "Credential Gate",
11
+ parentId: "Parent ID",
12
+ dependsOn: "Depends On",
13
+ receiptSummary: "Receipt Summary",
14
+ verify: "Verify",
15
+ allowedFiles: "Allowed Files",
16
+ updated: "Goal Updated",
17
+ };
18
+
19
+ export const GITHUB_PROJECT_VIEWS = {
20
+ board: {
21
+ name: "Goal Board",
22
+ layout: "board",
23
+ graphqlLayout: "BOARD_LAYOUT",
24
+ fields: [
25
+ "priority",
26
+ "status",
27
+ "workType",
28
+ "owner",
29
+ "goalRole",
30
+ "agentResponsible",
31
+ "agentLane",
32
+ "credentialGate",
33
+ ],
34
+ },
35
+ };
36
+
37
+ const STATUS_OPTIONS = [
38
+ { name: "Blocked", color: "RED", description: "Task is blocked." },
39
+ { name: "In Progress", color: "YELLOW", description: "Task is currently active." },
40
+ { name: "Todo", color: "GRAY", description: "Task is waiting." },
41
+ { name: "Done", color: "GREEN", description: "Task is complete." },
42
+ ];
43
+
44
+ const TYPE_OPTIONS = [
45
+ { name: "Discovery", color: "BLUE", description: "Evidence gathering and mapping." },
46
+ { name: "Decision", color: "PURPLE", description: "Review, decision, or audit." },
47
+ { name: "Execution", color: "ORANGE", description: "Bounded implementation or recovery." },
48
+ { name: "Coordination", color: "GREEN", description: "Board, handoff, or PM work." },
49
+ { name: "Recovery", color: "RED", description: "Unblocking or repairing failed verification." },
50
+ ];
51
+
52
+ const PRIORITY_OPTIONS = [
53
+ { name: "P0", color: "RED", description: "Urgent blocker or safety-critical work." },
54
+ { name: "P1", color: "ORANGE", description: "Important current-tranche work." },
55
+ { name: "P2", color: "YELLOW", description: "Useful but not first-order." },
56
+ { name: "P3", color: "GRAY", description: "Parking lot or follow-up." },
57
+ ];
58
+
59
+ const AGENT_LANE_OPTIONS = [
60
+ { name: "PM", color: "GREEN", description: "GoalBuddy PM coordination work." },
61
+ { name: "Scout", color: "BLUE", description: "GoalBuddy evidence mapping work." },
62
+ { name: "Judge", color: "PURPLE", description: "GoalBuddy decision and audit work." },
63
+ { name: "Worker", color: "ORANGE", description: "GoalBuddy bounded implementation work." },
64
+ { name: "User", color: "GRAY", description: "Owner-gated or human action work." },
65
+ ];
66
+
67
+ const TEXT_FIELD_SPECS = [
68
+ ["taskId", GITHUB_PROJECT_FIELDS.taskId],
69
+ ["owner", GITHUB_PROJECT_FIELDS.owner],
70
+ ["goalRole", GITHUB_PROJECT_FIELDS.goalRole],
71
+ ["agentResponsible", GITHUB_PROJECT_FIELDS.agentResponsible],
72
+ ["credentialGate", GITHUB_PROJECT_FIELDS.credentialGate],
73
+ ["parentId", GITHUB_PROJECT_FIELDS.parentId],
74
+ ["dependsOn", GITHUB_PROJECT_FIELDS.dependsOn],
75
+ ["receiptSummary", GITHUB_PROJECT_FIELDS.receiptSummary],
76
+ ["verify", GITHUB_PROJECT_FIELDS.verify],
77
+ ["allowedFiles", GITHUB_PROJECT_FIELDS.allowedFiles],
78
+ ["updated", GITHUB_PROJECT_FIELDS.updated],
79
+ ];
80
+
81
+ const SINGLE_SELECT_FIELD_SPECS = [
82
+ ["status", GITHUB_PROJECT_FIELDS.status, STATUS_OPTIONS],
83
+ ["priority", GITHUB_PROJECT_FIELDS.priority, PRIORITY_OPTIONS],
84
+ ["workType", GITHUB_PROJECT_FIELDS.workType, TYPE_OPTIONS],
85
+ ["agentLane", GITHUB_PROJECT_FIELDS.agentLane, AGENT_LANE_OPTIONS],
86
+ ];
87
+
88
+ export class GitHubProjectsError extends Error {
89
+ constructor(message) {
90
+ super(message);
91
+ this.name = "GitHubProjectsError";
92
+ }
93
+ }
94
+
95
+ export class GitHubProjectsClient {
96
+ constructor({ token, fetchImpl = globalThis.fetch } = {}) {
97
+ if (!token) {
98
+ throw new GitHubProjectsError("Missing GITHUB_TOKEN or GH_TOKEN.");
99
+ }
100
+ if (!fetchImpl) {
101
+ throw new GitHubProjectsError("This Node runtime does not provide fetch.");
102
+ }
103
+ this.token = token;
104
+ this.fetchImpl = fetchImpl;
105
+ }
106
+
107
+ async graphql(query, variables = {}) {
108
+ const response = await this.fetchImpl("https://api.github.com/graphql", {
109
+ method: "POST",
110
+ headers: {
111
+ authorization: `Bearer ${this.token}`,
112
+ "content-type": "application/json",
113
+ "user-agent": "goal-board-sync",
114
+ },
115
+ body: JSON.stringify({ query, variables }),
116
+ });
117
+
118
+ if (!response.ok) {
119
+ throw new GitHubProjectsError(`GitHub GraphQL failed with HTTP ${response.status}.`);
120
+ }
121
+
122
+ const data = await response.json();
123
+ if (data.errors?.length) {
124
+ throw new GitHubProjectsError(data.errors.map((error) => error.message).join("; "));
125
+ }
126
+ return data.data;
127
+ }
128
+
129
+ async rest(path, { method = "GET", body } = {}) {
130
+ const response = await this.fetchImpl(`https://api.github.com/${path}`, {
131
+ method,
132
+ headers: {
133
+ authorization: `Bearer ${this.token}`,
134
+ accept: "application/vnd.github+json",
135
+ "content-type": "application/json",
136
+ "user-agent": "goal-board-sync",
137
+ "x-github-api-version": "2026-03-10",
138
+ },
139
+ body: body ? JSON.stringify(body) : undefined,
140
+ });
141
+
142
+ const text = await response.text();
143
+ const data = text ? JSON.parse(text) : {};
144
+ if (!response.ok) {
145
+ throw new GitHubProjectsError(data.message || `GitHub REST failed with HTTP ${response.status}.`);
146
+ }
147
+ return data;
148
+ }
149
+
150
+ projectById(projectId, cursor = null) {
151
+ return this.graphql(PROJECT_BY_ID_QUERY, { projectId, cursor });
152
+ }
153
+
154
+ projectByOwnerNumber(owner, number, cursor = null) {
155
+ return this.graphql(PROJECT_BY_OWNER_NUMBER_QUERY, { owner, number, cursor });
156
+ }
157
+
158
+ createTextField(projectId, name) {
159
+ return this.graphql(CREATE_FIELD_MUTATION, {
160
+ input: {
161
+ projectId,
162
+ name,
163
+ dataType: "TEXT",
164
+ },
165
+ });
166
+ }
167
+
168
+ createSingleSelectField(projectId, name, options) {
169
+ return this.graphql(CREATE_FIELD_MUTATION, {
170
+ input: {
171
+ projectId,
172
+ name,
173
+ dataType: "SINGLE_SELECT",
174
+ singleSelectOptions: options,
175
+ },
176
+ });
177
+ }
178
+
179
+ updateSingleSelectField(fieldId, options) {
180
+ return this.graphql(UPDATE_FIELD_MUTATION, {
181
+ input: {
182
+ fieldId,
183
+ singleSelectOptions: options,
184
+ },
185
+ });
186
+ }
187
+
188
+ addDraftIssue(projectId, title, body) {
189
+ return this.graphql(ADD_DRAFT_ISSUE_MUTATION, {
190
+ input: {
191
+ projectId,
192
+ title,
193
+ body,
194
+ },
195
+ });
196
+ }
197
+
198
+ updateDraftIssue(draftIssueId, title, body) {
199
+ return this.graphql(UPDATE_DRAFT_ISSUE_MUTATION, {
200
+ input: {
201
+ draftIssueId,
202
+ title,
203
+ body,
204
+ },
205
+ });
206
+ }
207
+
208
+ updateItemField(projectId, itemId, fieldId, value) {
209
+ return this.graphql(UPDATE_ITEM_FIELD_MUTATION, {
210
+ input: {
211
+ projectId,
212
+ itemId,
213
+ fieldId,
214
+ value,
215
+ },
216
+ });
217
+ }
218
+ }
219
+
220
+ export async function loadProject({ client, projectId, owner, number }) {
221
+ const pages = [];
222
+ let cursor = null;
223
+ let baseProject = null;
224
+
225
+ do {
226
+ const data = projectId
227
+ ? await client.projectById(projectId, cursor)
228
+ : await client.projectByOwnerNumber(owner, number, cursor);
229
+ const project = projectId
230
+ ? data.node
231
+ : data.user?.projectV2 || data.organization?.projectV2;
232
+
233
+ if (!project) {
234
+ throw new GitHubProjectsError("GitHub Project not found. Check project ID or owner/project number.");
235
+ }
236
+ if (!project.id) {
237
+ throw new GitHubProjectsError("The supplied GitHub node is not a ProjectV2.");
238
+ }
239
+
240
+ baseProject ||= project;
241
+ pages.push(...(project.items?.nodes || []));
242
+ cursor = project.items?.pageInfo?.hasNextPage ? project.items.pageInfo.endCursor : null;
243
+ } while (cursor);
244
+
245
+ return {
246
+ ...baseProject,
247
+ items: {
248
+ ...baseProject.items,
249
+ nodes: pages,
250
+ },
251
+ };
252
+ }
253
+
254
+ export async function ensureGoalProjectFields(client, project) {
255
+ const byName = indexFieldsByName(project.fields?.nodes || []);
256
+ const fields = {};
257
+
258
+ for (const [key, name] of TEXT_FIELD_SPECS) {
259
+ let field = byName.get(name);
260
+ if (!field) {
261
+ const response = await client.createTextField(project.id, name);
262
+ field = response.createProjectV2Field.projectV2Field;
263
+ byName.set(name, field);
264
+ } else if (field.dataType !== "TEXT") {
265
+ throw new GitHubProjectsError(`Existing project field "${name}" must be a text field.`);
266
+ }
267
+ fields[key] = field;
268
+ }
269
+
270
+ for (const [key, name, options] of SINGLE_SELECT_FIELD_SPECS) {
271
+ let field = byName.get(name);
272
+ if (!field) {
273
+ const response = await client.createSingleSelectField(project.id, name, options);
274
+ field = response.createProjectV2Field.projectV2Field;
275
+ } else if (field.__typename !== "ProjectV2SingleSelectField" || field.dataType !== "SINGLE_SELECT") {
276
+ throw new GitHubProjectsError(`Existing project field "${name}" must be a single-select field.`);
277
+ } else {
278
+ const missing = missingOptions(field, options);
279
+ if (missing.length) {
280
+ const merged = [
281
+ ...(field.options || []).map((option) => ({
282
+ name: option.name,
283
+ color: option.color || "GRAY",
284
+ description: option.description || option.name,
285
+ })),
286
+ ...missing,
287
+ ];
288
+ const response = await client.updateSingleSelectField(field.id, merged);
289
+ field = response.updateProjectV2Field.projectV2Field;
290
+ }
291
+ }
292
+ fields[key] = field;
293
+ }
294
+
295
+ return fields;
296
+ }
297
+
298
+ export function planGitHubProjectSync(tasks, existingItems) {
299
+ const byTaskId = indexProjectItemsByTaskId(existingItems);
300
+ return tasks.map((task) => {
301
+ const existing = byTaskId.get(task.id);
302
+ if (!existing) {
303
+ return {
304
+ type: "create",
305
+ taskId: task.id,
306
+ task,
307
+ };
308
+ }
309
+
310
+ return {
311
+ type: "update",
312
+ taskId: task.id,
313
+ task,
314
+ itemId: existing.itemId,
315
+ draftIssueId: existing.draftIssueId,
316
+ };
317
+ });
318
+ }
319
+
320
+ export async function executeGitHubProjectSync({ client, project, fields, tasks, board }) {
321
+ const operations = planGitHubProjectSync(tasks, project.items?.nodes || []);
322
+
323
+ for (const operation of operations) {
324
+ const body = buildDraftIssueBody(operation.task, board);
325
+ let itemId = operation.itemId;
326
+
327
+ if (operation.type === "create") {
328
+ const response = await client.addDraftIssue(project.id, operation.task.title, body);
329
+ itemId = response.addProjectV2DraftIssue.projectItem.id;
330
+ } else if (operation.draftIssueId) {
331
+ await client.updateDraftIssue(operation.draftIssueId, operation.task.title, body);
332
+ }
333
+
334
+ for (const update of buildFieldUpdates(operation.task, fields)) {
335
+ await client.updateItemField(project.id, itemId, update.fieldId, update.value);
336
+ }
337
+ }
338
+
339
+ return operations;
340
+ }
341
+
342
+ export async function ensureGoalProjectViews({ client, project, fields }) {
343
+ const owner = project.owner;
344
+ if (!owner?.login || !project.number) {
345
+ throw new GitHubProjectsError("Cannot create GitHub Project views without project owner and number.");
346
+ }
347
+
348
+ const ownerPath = owner.__typename === "Organization"
349
+ ? `orgs/${owner.login}`
350
+ : `users/${owner.login}`;
351
+ const existingViews = project.views?.nodes || [];
352
+ const ensured = {};
353
+
354
+ for (const [key, spec] of Object.entries(GITHUB_PROJECT_VIEWS)) {
355
+ const existing = existingViews.find((view) => view.name === spec.name && view.layout === spec.graphqlLayout);
356
+ if (existing) {
357
+ ensured[key] = existing;
358
+ continue;
359
+ }
360
+
361
+ ensured[key] = await client.rest(`${ownerPath}/projectsV2/${project.number}/views`, {
362
+ method: "POST",
363
+ body: buildViewRequestBody(spec, fields),
364
+ });
365
+ }
366
+
367
+ return ensured;
368
+ }
369
+
370
+ export async function ensureGoalBoardView({ client, project, fields }) {
371
+ const views = await ensureGoalProjectViews({ client, project, fields });
372
+ return views.board;
373
+ }
374
+
375
+ export function buildFieldUpdates(task, fields) {
376
+ return [
377
+ textUpdate(fields.taskId, task.id),
378
+ singleSelectUpdate(fields.status, projectStatusForTask(task.status)),
379
+ singleSelectUpdate(fields.priority, priorityForTask(task)),
380
+ singleSelectUpdate(fields.workType, workTypeForTask(task.type)),
381
+ singleSelectUpdate(fields.agentLane, agentLaneForTask(task)),
382
+ textUpdate(fields.owner, task.assignee),
383
+ textUpdate(fields.goalRole, task.goalRole),
384
+ textUpdate(fields.agentResponsible, task.agentResponsible),
385
+ textUpdate(fields.credentialGate, task.credentialGate),
386
+ textUpdate(fields.parentId, task.parentId),
387
+ textUpdate(fields.dependsOn, task.dependsOn.join(", ")),
388
+ textUpdate(fields.receiptSummary, task.receiptSummary),
389
+ textUpdate(fields.verify, task.verify.join("\n")),
390
+ textUpdate(fields.allowedFiles, task.allowedFiles.join("\n")),
391
+ textUpdate(fields.updated, task.updatedLabel),
392
+ ].filter(Boolean);
393
+ }
394
+
395
+ export function buildDraftIssueBody(task, board) {
396
+ const lines = [
397
+ `Mirrors ${board.sourcePath}.`,
398
+ "",
399
+ "YAML remains the source of truth. Edit the GoalBuddy board, then rerun the sync.",
400
+ "",
401
+ `Task ID: ${task.id}`,
402
+ `Status: ${task.status}`,
403
+ `Priority: ${priorityForTask(task)}`,
404
+ `Work type: ${workTypeForTask(task.type)}`,
405
+ `Owner: ${task.assignee || "unassigned"}`,
406
+ `Goal role: ${task.goalRole || "unassigned"}`,
407
+ `Agent responsible: ${task.agentResponsible || "unassigned"}`,
408
+ `Credential gate: ${task.credentialGate || "None"}`,
409
+ "",
410
+ "Objective:",
411
+ task.objective || "None",
412
+ ];
413
+
414
+ if (task.parentId) {
415
+ lines.push("", `Parent: ${task.parentId}`);
416
+ }
417
+ if (task.dependsOn.length) {
418
+ lines.push("", "Depends on:", ...task.dependsOn.map((id) => `- ${id}`));
419
+ }
420
+ if (task.receiptSummary) {
421
+ lines.push("", "Receipt:", task.receiptSummary);
422
+ }
423
+ if (task.verify.length) {
424
+ lines.push("", "Verify:", ...task.verify.map((command) => `- ${command}`));
425
+ }
426
+ if (task.allowedFiles.length) {
427
+ lines.push("", "Allowed files:", ...task.allowedFiles.map((file) => `- ${file}`));
428
+ }
429
+
430
+ return lines.join("\n").slice(0, 65000);
431
+ }
432
+
433
+ export function dryRunGitHubOperations(board) {
434
+ return board.tasks.map((task) => ({
435
+ type: "upsert",
436
+ taskId: task.id,
437
+ title: task.title,
438
+ status: task.status,
439
+ projectStatus: projectStatusForTask(task.status),
440
+ priority: priorityForTask(task),
441
+ typeLabel: workTypeForTask(task.type),
442
+ goalRole: task.goalRole,
443
+ agentResponsible: task.agentResponsible,
444
+ credentialGate: task.credentialGate,
445
+ agentLane: agentLaneForTask(task),
446
+ }));
447
+ }
448
+
449
+ export function projectStatusForTask(status) {
450
+ if (status === "queued") return "Todo";
451
+ if (status === "active") return "In Progress";
452
+ if (status === "blocked") return "Blocked";
453
+ if (status === "done") return "Done";
454
+ return "Todo";
455
+ }
456
+
457
+ export function priorityForTask(task) {
458
+ if (task.priority) return task.priority;
459
+ if (task.status === "blocked") return "P0";
460
+ if (task.status === "active") return "P1";
461
+ if (task.status === "done") return "P3";
462
+ return "P2";
463
+ }
464
+
465
+ export function workTypeForTask(type) {
466
+ if (type === "scout") return "Discovery";
467
+ if (type === "judge") return "Decision";
468
+ if (type === "worker") return "Execution";
469
+ if (type === "pm") return "Coordination";
470
+ return "Coordination";
471
+ }
472
+
473
+ export function agentLaneForTask(task) {
474
+ const candidates = [
475
+ task.agentResponsible,
476
+ task.goalRole,
477
+ task.assignee,
478
+ ].filter(Boolean);
479
+ for (const candidate of candidates) {
480
+ if (["PM", "Scout", "Judge", "Worker"].includes(candidate)) return candidate;
481
+ if (candidate === "User" || candidate === "Owner") return "User";
482
+ }
483
+ return "User";
484
+ }
485
+
486
+ function buildViewRequestBody(spec, fields) {
487
+ return {
488
+ name: spec.name,
489
+ layout: spec.layout,
490
+ visible_fields: fieldDatabaseIds(spec.fields, fields),
491
+ };
492
+ }
493
+
494
+ function fieldDatabaseIds(fieldKeys = [], fields) {
495
+ return fieldKeys
496
+ .map((fieldKey) => fields[fieldKey]?.databaseId)
497
+ .filter(Boolean);
498
+ }
499
+
500
+ function indexFieldsByName(fields) {
501
+ return new Map((fields || []).filter(Boolean).map((field) => [field.name, field]));
502
+ }
503
+
504
+ function missingOptions(field, requiredOptions) {
505
+ const existing = new Set((field.options || []).map((option) => option.name));
506
+ return requiredOptions.filter((option) => !existing.has(option.name));
507
+ }
508
+
509
+ function indexProjectItemsByTaskId(items) {
510
+ const byTaskId = new Map();
511
+
512
+ for (const item of items || []) {
513
+ const taskId = item.taskId?.text?.trim();
514
+ if (!taskId) continue;
515
+ const draftIssueId = item.content?.__typename === "DraftIssue" ? item.content.id : null;
516
+ byTaskId.set(taskId, {
517
+ itemId: item.id,
518
+ draftIssueId,
519
+ item,
520
+ });
521
+ }
522
+
523
+ return byTaskId;
524
+ }
525
+
526
+ function textUpdate(field, text) {
527
+ if (!field?.id) return null;
528
+ return {
529
+ fieldId: field.id,
530
+ value: {
531
+ text: String(text ?? "").slice(0, 1024),
532
+ },
533
+ };
534
+ }
535
+
536
+ function singleSelectUpdate(field, name) {
537
+ if (!field?.id) return null;
538
+ const option = (field.options || []).find((candidate) => candidate.name === name);
539
+ if (!option) {
540
+ throw new GitHubProjectsError(`Field "${field.name}" is missing option "${name}".`);
541
+ }
542
+ return {
543
+ fieldId: field.id,
544
+ value: {
545
+ singleSelectOptionId: option.id,
546
+ },
547
+ };
548
+ }
549
+
550
+ const PROJECT_FIELDS_FRAGMENT = `
551
+ id
552
+ number
553
+ title
554
+ url
555
+ owner {
556
+ __typename
557
+ ... on User {
558
+ login
559
+ }
560
+ ... on Organization {
561
+ login
562
+ }
563
+ }
564
+ fields(first: 100) {
565
+ nodes {
566
+ __typename
567
+ ... on ProjectV2Field {
568
+ id
569
+ databaseId
570
+ name
571
+ dataType
572
+ }
573
+ ... on ProjectV2SingleSelectField {
574
+ id
575
+ databaseId
576
+ name
577
+ dataType
578
+ options {
579
+ id
580
+ name
581
+ color
582
+ description
583
+ }
584
+ }
585
+ }
586
+ }
587
+ items(first: 100, after: $cursor) {
588
+ pageInfo {
589
+ hasNextPage
590
+ endCursor
591
+ }
592
+ nodes {
593
+ id
594
+ taskId: fieldValueByName(name: "Task ID") {
595
+ ... on ProjectV2ItemFieldTextValue {
596
+ text
597
+ }
598
+ }
599
+ content {
600
+ __typename
601
+ ... on DraftIssue {
602
+ id
603
+ title
604
+ body
605
+ }
606
+ }
607
+ }
608
+ }
609
+ views(first: 50) {
610
+ nodes {
611
+ id
612
+ number
613
+ name
614
+ layout
615
+ }
616
+ }
617
+ `;
618
+
619
+ const PROJECT_BY_ID_QUERY = `
620
+ query GoalBoardProjectById($projectId: ID!, $cursor: String) {
621
+ node(id: $projectId) {
622
+ ... on ProjectV2 {
623
+ ${PROJECT_FIELDS_FRAGMENT}
624
+ }
625
+ }
626
+ }
627
+ `;
628
+
629
+ const PROJECT_BY_OWNER_NUMBER_QUERY = `
630
+ query GoalBoardProjectByOwnerNumber($owner: String!, $number: Int!, $cursor: String) {
631
+ user(login: $owner) {
632
+ projectV2(number: $number) {
633
+ ${PROJECT_FIELDS_FRAGMENT}
634
+ }
635
+ }
636
+ organization(login: $owner) {
637
+ projectV2(number: $number) {
638
+ ${PROJECT_FIELDS_FRAGMENT}
639
+ }
640
+ }
641
+ }
642
+ `;
643
+
644
+ const CREATE_FIELD_MUTATION = `
645
+ mutation GoalBoardCreateField($input: CreateProjectV2FieldInput!) {
646
+ createProjectV2Field(input: $input) {
647
+ projectV2Field {
648
+ __typename
649
+ ... on ProjectV2Field {
650
+ id
651
+ databaseId
652
+ name
653
+ dataType
654
+ }
655
+ ... on ProjectV2SingleSelectField {
656
+ id
657
+ databaseId
658
+ name
659
+ dataType
660
+ options {
661
+ id
662
+ name
663
+ color
664
+ description
665
+ }
666
+ }
667
+ }
668
+ }
669
+ }
670
+ `;
671
+
672
+ const UPDATE_FIELD_MUTATION = `
673
+ mutation GoalBoardUpdateField($input: UpdateProjectV2FieldInput!) {
674
+ updateProjectV2Field(input: $input) {
675
+ projectV2Field {
676
+ __typename
677
+ ... on ProjectV2SingleSelectField {
678
+ id
679
+ databaseId
680
+ name
681
+ dataType
682
+ options {
683
+ id
684
+ name
685
+ color
686
+ description
687
+ }
688
+ }
689
+ }
690
+ }
691
+ }
692
+ `;
693
+
694
+ const ADD_DRAFT_ISSUE_MUTATION = `
695
+ mutation GoalBoardAddDraftIssue($input: AddProjectV2DraftIssueInput!) {
696
+ addProjectV2DraftIssue(input: $input) {
697
+ projectItem {
698
+ id
699
+ content {
700
+ __typename
701
+ ... on DraftIssue {
702
+ id
703
+ }
704
+ }
705
+ }
706
+ }
707
+ }
708
+ `;
709
+
710
+ const UPDATE_DRAFT_ISSUE_MUTATION = `
711
+ mutation GoalBoardUpdateDraftIssue($input: UpdateProjectV2DraftIssueInput!) {
712
+ updateProjectV2DraftIssue(input: $input) {
713
+ draftIssue {
714
+ id
715
+ }
716
+ }
717
+ }
718
+ `;
719
+
720
+ const UPDATE_ITEM_FIELD_MUTATION = `
721
+ mutation GoalBoardUpdateItemField($input: UpdateProjectV2ItemFieldValueInput!) {
722
+ updateProjectV2ItemFieldValue(input: $input) {
723
+ projectV2Item {
724
+ id
725
+ }
726
+ }
727
+ }
728
+ `;