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.
- package/README.md +10 -18
- package/goalbuddy/SKILL.md +40 -10
- package/goalbuddy/extend/github-projects/README.md +105 -0
- package/goalbuddy/extend/github-projects/examples/goal-board-sync/state.yaml +63 -0
- package/goalbuddy/extend/github-projects/extension.yaml +43 -0
- package/goalbuddy/extend/github-projects/scripts/lib/github-projects.mjs +728 -0
- package/goalbuddy/extend/github-projects/scripts/lib/goal-state.mjs +362 -0
- package/goalbuddy/extend/github-projects/scripts/sync-github-project.mjs +193 -0
- package/goalbuddy/extend/github-projects/test/github-projects.test.mjs +267 -0
- package/goalbuddy/extend/local-goal-board/README.md +75 -0
- package/goalbuddy/extend/local-goal-board/assets/goalbuddy-mark.png +0 -0
- package/goalbuddy/extend/local-goal-board/examples/sample-goal/notes/T001-scout.md +3 -0
- package/goalbuddy/extend/local-goal-board/examples/sample-goal/state.yaml +124 -0
- package/goalbuddy/extend/local-goal-board/extension.yaml +37 -0
- package/goalbuddy/extend/local-goal-board/scripts/lib/goal-board.mjs +1225 -0
- package/goalbuddy/extend/local-goal-board/scripts/local-goal-board.mjs +258 -0
- package/goalbuddy/extend/local-goal-board/test/local-goal-board.test.mjs +146 -0
- package/goalbuddy/scripts/check-goal-state.mjs +24 -9
- package/goalbuddy/templates/state.yaml +18 -3
- package/internal/cli/goal-maker.mjs +57 -11
- package/package.json +3 -2
- package/plugins/goalbuddy/.codex-plugin/plugin.json +3 -3
- package/plugins/goalbuddy/README.md +1 -5
- package/plugins/goalbuddy/skills/goalbuddy/SKILL.md +40 -10
- package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/README.md +105 -0
- package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/examples/goal-board-sync/state.yaml +63 -0
- package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/extension.yaml +43 -0
- package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/scripts/lib/github-projects.mjs +728 -0
- package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/scripts/lib/goal-state.mjs +362 -0
- package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/scripts/sync-github-project.mjs +193 -0
- package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/test/github-projects.test.mjs +267 -0
- package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/README.md +75 -0
- package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/assets/goalbuddy-mark.png +0 -0
- package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/examples/sample-goal/notes/T001-scout.md +3 -0
- package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/examples/sample-goal/state.yaml +124 -0
- package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/extension.yaml +37 -0
- package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/scripts/lib/goal-board.mjs +1225 -0
- package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/scripts/local-goal-board.mjs +258 -0
- package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/test/local-goal-board.test.mjs +146 -0
- package/plugins/goalbuddy/skills/goalbuddy/scripts/check-goal-state.mjs +24 -9
- 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
|
+
`;
|