@gh-symphony/cli 0.0.14 → 0.0.15
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/dist/chunk-5NV3LSAJ.js +11 -0
- package/dist/chunk-6HBZC3BE.js +468 -0
- package/dist/chunk-76QPITKI.js +109 -0
- package/dist/chunk-IWR4UQEJ.js +2250 -0
- package/dist/chunk-JO3AXHQI.js +130 -0
- package/dist/chunk-M7OSMUTN.js +874 -0
- package/dist/chunk-MVRF7BES.js +68 -0
- package/dist/chunk-RNWX7DQU.js +4617 -0
- package/dist/chunk-ROGRTUFI.js +108 -0
- package/dist/chunk-TH5QPO3Y.js +67 -0
- package/dist/config-cmd-AZ7POMAA.js +110 -0
- package/dist/index.d.ts +5 -4
- package/dist/index.js +568 -356
- package/dist/init-EZXQAXZM.js +17 -0
- package/dist/logs-6LNGT2GF.js +188 -0
- package/dist/project-3ELXQ35D.js +678 -0
- package/dist/recover-T6ME6C56.js +130 -0
- package/dist/repo-R3XBIVAX.js +121 -0
- package/dist/run-DYINRZHK.js +107 -0
- package/dist/start-PIFQMIC2.js +15 -0
- package/dist/status-3WK5BWRZ.js +11 -0
- package/dist/stop-AA3AP5M6.js +9 -0
- package/dist/version-VBB62JWI.js +30 -0
- package/package.json +9 -4
- package/dist/ansi.d.ts +0 -15
- package/dist/ansi.js +0 -53
- package/dist/commands/config-cmd.d.ts +0 -3
- package/dist/commands/config-cmd.js +0 -90
- package/dist/commands/help.d.ts +0 -3
- package/dist/commands/help.js +0 -55
- package/dist/commands/init.d.ts +0 -34
- package/dist/commands/init.js +0 -477
- package/dist/commands/logs.d.ts +0 -3
- package/dist/commands/logs.js +0 -184
- package/dist/commands/project.d.ts +0 -3
- package/dist/commands/project.js +0 -649
- package/dist/commands/recover.d.ts +0 -3
- package/dist/commands/recover.js +0 -119
- package/dist/commands/repo.d.ts +0 -3
- package/dist/commands/repo.js +0 -103
- package/dist/commands/run.d.ts +0 -3
- package/dist/commands/run.js +0 -95
- package/dist/commands/start.d.ts +0 -20
- package/dist/commands/start.js +0 -344
- package/dist/commands/status-refresh.d.ts +0 -9
- package/dist/commands/status-refresh.js +0 -27
- package/dist/commands/status.d.ts +0 -3
- package/dist/commands/status.js +0 -237
- package/dist/commands/stop.d.ts +0 -3
- package/dist/commands/stop.js +0 -92
- package/dist/commands/version.d.ts +0 -3
- package/dist/commands/version.js +0 -21
- package/dist/completion.d.ts +0 -1
- package/dist/completion.js +0 -204
- package/dist/config.d.ts +0 -38
- package/dist/config.js +0 -82
- package/dist/context/context-types.d.ts +0 -36
- package/dist/context/context-types.js +0 -1
- package/dist/context/generate-context-yaml.d.ts +0 -15
- package/dist/context/generate-context-yaml.js +0 -129
- package/dist/dashboard/renderer.d.ts +0 -9
- package/dist/dashboard/renderer.js +0 -220
- package/dist/detection/environment-detector.d.ts +0 -11
- package/dist/detection/environment-detector.js +0 -140
- package/dist/github/client.d.ts +0 -71
- package/dist/github/client.js +0 -348
- package/dist/github/gh-auth.d.ts +0 -34
- package/dist/github/gh-auth.js +0 -110
- package/dist/mapping/smart-defaults.d.ts +0 -17
- package/dist/mapping/smart-defaults.js +0 -86
- package/dist/orchestrator-runtime.d.ts +0 -1
- package/dist/orchestrator-runtime.js +0 -4
- package/dist/orchestrator-status-endpoint.d.ts +0 -5
- package/dist/orchestrator-status-endpoint.js +0 -27
- package/dist/project-selection.d.ts +0 -8
- package/dist/project-selection.js +0 -56
- package/dist/skills/skill-writer.d.ts +0 -14
- package/dist/skills/skill-writer.js +0 -62
- package/dist/skills/templates/commit.d.ts +0 -2
- package/dist/skills/templates/commit.js +0 -45
- package/dist/skills/templates/document.d.ts +0 -7
- package/dist/skills/templates/document.js +0 -16
- package/dist/skills/templates/gh-project.d.ts +0 -2
- package/dist/skills/templates/gh-project.js +0 -88
- package/dist/skills/templates/gh-symphony.d.ts +0 -2
- package/dist/skills/templates/gh-symphony.js +0 -125
- package/dist/skills/templates/index.d.ts +0 -8
- package/dist/skills/templates/index.js +0 -28
- package/dist/skills/templates/land.d.ts +0 -2
- package/dist/skills/templates/land.js +0 -59
- package/dist/skills/templates/pull.d.ts +0 -2
- package/dist/skills/templates/pull.js +0 -41
- package/dist/skills/templates/push.d.ts +0 -2
- package/dist/skills/templates/push.js +0 -36
- package/dist/skills/types.d.ts +0 -23
- package/dist/skills/types.js +0 -1
- package/dist/workflow/generate-reference-workflow.d.ts +0 -9
- package/dist/workflow/generate-reference-workflow.js +0 -261
- package/dist/workflow/generate-workflow-md.d.ts +0 -12
- package/dist/workflow/generate-workflow-md.js +0 -134
|
@@ -0,0 +1,2250 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
GhAuthError,
|
|
4
|
+
ensureGhAuth,
|
|
5
|
+
getGhToken
|
|
6
|
+
} from "./chunk-JO3AXHQI.js";
|
|
7
|
+
import {
|
|
8
|
+
loadGlobalConfig,
|
|
9
|
+
saveGlobalConfig,
|
|
10
|
+
saveProjectConfig
|
|
11
|
+
} from "./chunk-ROGRTUFI.js";
|
|
12
|
+
|
|
13
|
+
// src/commands/init.ts
|
|
14
|
+
import * as p from "@clack/prompts";
|
|
15
|
+
import { createHash } from "crypto";
|
|
16
|
+
import { mkdir as mkdir3, rename, writeFile as writeFile3 } from "fs/promises";
|
|
17
|
+
import { basename, dirname as dirname2, join as join3, relative, resolve } from "path";
|
|
18
|
+
|
|
19
|
+
// src/github/client.ts
|
|
20
|
+
var DEFAULT_API_URL = "https://api.github.com/graphql";
|
|
21
|
+
var REST_API_URL = "https://api.github.com";
|
|
22
|
+
var GitHubApiError = class extends Error {
|
|
23
|
+
constructor(message, status) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.status = status;
|
|
26
|
+
this.name = "GitHubApiError";
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
var GitHubScopeError = class extends GitHubApiError {
|
|
30
|
+
constructor(message, requiredScopes, currentScopes) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.requiredScopes = requiredScopes;
|
|
33
|
+
this.currentScopes = currentScopes;
|
|
34
|
+
this.name = "GitHubScopeError";
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
function createClient(token, options) {
|
|
38
|
+
return {
|
|
39
|
+
token,
|
|
40
|
+
apiUrl: options?.apiUrl ?? DEFAULT_API_URL,
|
|
41
|
+
fetchImpl: options?.fetchImpl ?? fetch
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
async function validateToken(client) {
|
|
45
|
+
const restUrl = client.apiUrl.replace("/graphql", "");
|
|
46
|
+
const baseUrl = restUrl === client.apiUrl ? REST_API_URL : restUrl;
|
|
47
|
+
const response = await client.fetchImpl(`${baseUrl}/user`, {
|
|
48
|
+
headers: {
|
|
49
|
+
authorization: `Bearer ${client.token}`,
|
|
50
|
+
accept: "application/vnd.github+json"
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
if (!response.ok) {
|
|
54
|
+
if (response.status === 401) {
|
|
55
|
+
throw new GitHubApiError("Invalid token: authentication failed.", 401);
|
|
56
|
+
}
|
|
57
|
+
throw new GitHubApiError(
|
|
58
|
+
`GitHub API error: ${response.status} ${response.statusText}`,
|
|
59
|
+
response.status
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
const scopes = response.headers.get("x-oauth-scopes")?.split(",").map((s) => s.trim()).filter(Boolean) ?? [];
|
|
63
|
+
const user = await response.json();
|
|
64
|
+
return {
|
|
65
|
+
login: user.login,
|
|
66
|
+
name: user.name,
|
|
67
|
+
scopes
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function checkRequiredScopes(scopes) {
|
|
71
|
+
const required = ["repo", "read:org", "project"];
|
|
72
|
+
const normalizedScopes = scopes.map((s) => s.toLowerCase());
|
|
73
|
+
const missing = required.filter((r) => !normalizedScopes.includes(r));
|
|
74
|
+
return { valid: missing.length === 0, missing };
|
|
75
|
+
}
|
|
76
|
+
async function listUserProjects(client) {
|
|
77
|
+
const data = await graphql(
|
|
78
|
+
client,
|
|
79
|
+
VIEWER_PROJECTS_QUERY
|
|
80
|
+
);
|
|
81
|
+
const projects = [];
|
|
82
|
+
for (const node of data.viewer.projectsV2?.nodes ?? []) {
|
|
83
|
+
if (!node) continue;
|
|
84
|
+
projects.push(
|
|
85
|
+
normalizeProjectSummary(node, {
|
|
86
|
+
login: data.viewer.login,
|
|
87
|
+
type: "User"
|
|
88
|
+
})
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
for (const orgNode of data.viewer.organizations?.nodes ?? []) {
|
|
92
|
+
if (!orgNode) continue;
|
|
93
|
+
for (const projNode of orgNode.projectsV2?.nodes ?? []) {
|
|
94
|
+
if (!projNode) continue;
|
|
95
|
+
projects.push(
|
|
96
|
+
normalizeProjectSummary(projNode, {
|
|
97
|
+
login: orgNode.login,
|
|
98
|
+
type: "Organization"
|
|
99
|
+
})
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return projects;
|
|
104
|
+
}
|
|
105
|
+
function normalizeProjectSummary(node, owner) {
|
|
106
|
+
return {
|
|
107
|
+
id: node.id,
|
|
108
|
+
title: node.title,
|
|
109
|
+
shortDescription: node.shortDescription ?? "",
|
|
110
|
+
url: node.url,
|
|
111
|
+
openItemCount: node.items?.totalCount ?? 0,
|
|
112
|
+
owner
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
async function getProjectDetail(client, projectId) {
|
|
116
|
+
const data = await graphql(
|
|
117
|
+
client,
|
|
118
|
+
PROJECT_DETAIL_QUERY,
|
|
119
|
+
{ projectId }
|
|
120
|
+
);
|
|
121
|
+
const project = data.node;
|
|
122
|
+
if (!project || project.__typename !== "ProjectV2") {
|
|
123
|
+
throw new GitHubApiError(`Project not found: ${projectId}`);
|
|
124
|
+
}
|
|
125
|
+
const statusFields = [];
|
|
126
|
+
const textFields = [];
|
|
127
|
+
for (const field of project.fields?.nodes ?? []) {
|
|
128
|
+
if (!field) continue;
|
|
129
|
+
if (field.__typename === "ProjectV2SingleSelectField") {
|
|
130
|
+
statusFields.push({
|
|
131
|
+
id: field.id,
|
|
132
|
+
name: field.name,
|
|
133
|
+
options: (field.options ?? []).map((opt) => ({
|
|
134
|
+
id: opt.id,
|
|
135
|
+
name: opt.name,
|
|
136
|
+
description: opt.description ?? null,
|
|
137
|
+
color: opt.color ?? null
|
|
138
|
+
}))
|
|
139
|
+
});
|
|
140
|
+
} else if (field.__typename === "ProjectV2Field" && field.dataType) {
|
|
141
|
+
textFields.push({
|
|
142
|
+
id: field.id,
|
|
143
|
+
name: field.name,
|
|
144
|
+
dataType: field.dataType
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const repoMap = /* @__PURE__ */ new Map();
|
|
149
|
+
let cursor = null;
|
|
150
|
+
let hasMore = true;
|
|
151
|
+
for (const item of project.items?.nodes ?? []) {
|
|
152
|
+
const repo = item?.content?.repository;
|
|
153
|
+
if (!repo) continue;
|
|
154
|
+
const key = `${repo.owner.login}/${repo.name}`;
|
|
155
|
+
if (!repoMap.has(key)) {
|
|
156
|
+
repoMap.set(key, {
|
|
157
|
+
owner: repo.owner.login,
|
|
158
|
+
name: repo.name,
|
|
159
|
+
url: repo.url,
|
|
160
|
+
cloneUrl: repo.url.endsWith(".git") ? repo.url : `${repo.url}.git`
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
hasMore = project.items?.pageInfo?.hasNextPage ?? false;
|
|
165
|
+
cursor = project.items?.pageInfo?.endCursor ?? null;
|
|
166
|
+
while (hasMore && cursor) {
|
|
167
|
+
const pageData = await graphql(
|
|
168
|
+
client,
|
|
169
|
+
PROJECT_ITEMS_PAGE_QUERY,
|
|
170
|
+
{ projectId, cursor }
|
|
171
|
+
);
|
|
172
|
+
const items = pageData.node?.items;
|
|
173
|
+
if (!items) break;
|
|
174
|
+
for (const item of items.nodes ?? []) {
|
|
175
|
+
const repo = item?.content?.repository;
|
|
176
|
+
if (!repo) continue;
|
|
177
|
+
const key = `${repo.owner.login}/${repo.name}`;
|
|
178
|
+
if (!repoMap.has(key)) {
|
|
179
|
+
repoMap.set(key, {
|
|
180
|
+
owner: repo.owner.login,
|
|
181
|
+
name: repo.name,
|
|
182
|
+
url: repo.url,
|
|
183
|
+
cloneUrl: repo.url.endsWith(".git") ? repo.url : `${repo.url}.git`
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
hasMore = items.pageInfo?.hasNextPage ?? false;
|
|
188
|
+
cursor = items.pageInfo?.endCursor ?? null;
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
id: project.id,
|
|
192
|
+
title: project.title,
|
|
193
|
+
url: project.url,
|
|
194
|
+
statusFields,
|
|
195
|
+
textFields,
|
|
196
|
+
linkedRepositories: [...repoMap.values()]
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
async function graphql(client, query, variables) {
|
|
200
|
+
const response = await client.fetchImpl(client.apiUrl, {
|
|
201
|
+
method: "POST",
|
|
202
|
+
headers: {
|
|
203
|
+
"content-type": "application/json",
|
|
204
|
+
authorization: `Bearer ${client.token}`
|
|
205
|
+
},
|
|
206
|
+
body: JSON.stringify({ query, variables })
|
|
207
|
+
});
|
|
208
|
+
if (!response.ok) {
|
|
209
|
+
const text = await response.text().catch(() => "");
|
|
210
|
+
throw new GitHubApiError(
|
|
211
|
+
`GitHub GraphQL request failed: ${response.status} ${response.statusText}. ${text}`,
|
|
212
|
+
response.status
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
const payload = await response.json();
|
|
216
|
+
if (payload.errors?.length) {
|
|
217
|
+
const scopeMessages = payload.errors.map((e) => e.message).filter((m) => m.includes("has not been granted the required scopes"));
|
|
218
|
+
if (scopeMessages.length > 0) {
|
|
219
|
+
const requiredScopes = /* @__PURE__ */ new Set();
|
|
220
|
+
let currentScopes = [];
|
|
221
|
+
for (const msg of scopeMessages) {
|
|
222
|
+
for (const match of msg.matchAll(
|
|
223
|
+
/requires one of the following scopes: \['([^']+)'\]/g
|
|
224
|
+
)) {
|
|
225
|
+
requiredScopes.add(match[1]);
|
|
226
|
+
}
|
|
227
|
+
if (currentScopes.length === 0) {
|
|
228
|
+
const currMatch = /has only been granted the: \[([^\]]+)\]/.exec(msg);
|
|
229
|
+
if (currMatch) {
|
|
230
|
+
currentScopes = currMatch[1].split(",").map((s) => s.trim().replace(/'/g, "")).filter(Boolean);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
throw new GitHubScopeError(
|
|
235
|
+
"Token is missing required GitHub scopes.",
|
|
236
|
+
[...requiredScopes],
|
|
237
|
+
currentScopes
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
throw new GitHubApiError(
|
|
241
|
+
`GraphQL errors: ${payload.errors.map((e) => e.message).join("; ")}`
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
if (!payload.data) {
|
|
245
|
+
throw new GitHubApiError("GraphQL response missing data.");
|
|
246
|
+
}
|
|
247
|
+
return payload.data;
|
|
248
|
+
}
|
|
249
|
+
var VIEWER_PROJECTS_QUERY = `
|
|
250
|
+
query ViewerProjects {
|
|
251
|
+
viewer {
|
|
252
|
+
login
|
|
253
|
+
projectsV2(first: 50) {
|
|
254
|
+
nodes {
|
|
255
|
+
id
|
|
256
|
+
title
|
|
257
|
+
shortDescription
|
|
258
|
+
url
|
|
259
|
+
items { totalCount }
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
organizations(first: 20) {
|
|
263
|
+
nodes {
|
|
264
|
+
login
|
|
265
|
+
projectsV2(first: 50) {
|
|
266
|
+
nodes {
|
|
267
|
+
id
|
|
268
|
+
title
|
|
269
|
+
shortDescription
|
|
270
|
+
url
|
|
271
|
+
items { totalCount }
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
`;
|
|
279
|
+
var PROJECT_DETAIL_QUERY = `
|
|
280
|
+
query ProjectDetail($projectId: ID!) {
|
|
281
|
+
node(id: $projectId) {
|
|
282
|
+
__typename
|
|
283
|
+
... on ProjectV2 {
|
|
284
|
+
id
|
|
285
|
+
title
|
|
286
|
+
url
|
|
287
|
+
fields(first: 50) {
|
|
288
|
+
nodes {
|
|
289
|
+
__typename
|
|
290
|
+
... on ProjectV2SingleSelectField {
|
|
291
|
+
id
|
|
292
|
+
name
|
|
293
|
+
options {
|
|
294
|
+
id
|
|
295
|
+
name
|
|
296
|
+
description
|
|
297
|
+
color
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
... on ProjectV2Field {
|
|
301
|
+
id
|
|
302
|
+
name
|
|
303
|
+
dataType
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
items(first: 100) {
|
|
308
|
+
nodes {
|
|
309
|
+
content {
|
|
310
|
+
__typename
|
|
311
|
+
... on Issue {
|
|
312
|
+
repository {
|
|
313
|
+
name
|
|
314
|
+
url
|
|
315
|
+
owner { login }
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
... on PullRequest {
|
|
319
|
+
repository {
|
|
320
|
+
name
|
|
321
|
+
url
|
|
322
|
+
owner { login }
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
pageInfo {
|
|
328
|
+
endCursor
|
|
329
|
+
hasNextPage
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
`;
|
|
336
|
+
var PROJECT_ITEMS_PAGE_QUERY = `
|
|
337
|
+
query ProjectItemsPage($projectId: ID!, $cursor: String) {
|
|
338
|
+
node(id: $projectId) {
|
|
339
|
+
... on ProjectV2 {
|
|
340
|
+
items(first: 100, after: $cursor) {
|
|
341
|
+
nodes {
|
|
342
|
+
content {
|
|
343
|
+
__typename
|
|
344
|
+
... on Issue {
|
|
345
|
+
repository {
|
|
346
|
+
name
|
|
347
|
+
url
|
|
348
|
+
owner { login }
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
... on PullRequest {
|
|
352
|
+
repository {
|
|
353
|
+
name
|
|
354
|
+
url
|
|
355
|
+
owner { login }
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
pageInfo {
|
|
361
|
+
endCursor
|
|
362
|
+
hasNextPage
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
`;
|
|
369
|
+
|
|
370
|
+
// src/mapping/smart-defaults.ts
|
|
371
|
+
var ROLE_PATTERNS = [
|
|
372
|
+
{
|
|
373
|
+
role: "active",
|
|
374
|
+
pattern: /^(todo|to.do|to-do|ready|queued|open|new|triage|in.progress|working|active|doing|in.development|developing|wip)$/i
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
role: "wait",
|
|
378
|
+
pattern: /^(review|in.review|pr.review|needs.review|plan.review|awaiting.review|code.review|icebox|someday|later|blocked|on.hold|paused|deferred|draft|backlog)$/i
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
role: "terminal",
|
|
382
|
+
pattern: /^(done|completed?|closed|merged|shipped|resolved|finished|won.?t.do|cancelled)$/i
|
|
383
|
+
}
|
|
384
|
+
];
|
|
385
|
+
function inferStateRole(columnName) {
|
|
386
|
+
const normalized = columnName.trim();
|
|
387
|
+
for (const { role, pattern } of ROLE_PATTERNS) {
|
|
388
|
+
if (pattern.test(normalized)) {
|
|
389
|
+
return { columnName: normalized, role, confidence: "high" };
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return { columnName: normalized, role: null, confidence: "low" };
|
|
393
|
+
}
|
|
394
|
+
function inferAllStateRoles(columnNames) {
|
|
395
|
+
return columnNames.map(inferStateRole);
|
|
396
|
+
}
|
|
397
|
+
function toWorkflowLifecycleConfig(stateFieldName, mappings) {
|
|
398
|
+
const activeStates = [];
|
|
399
|
+
const terminalStates = [];
|
|
400
|
+
const blockerCheckStates = [];
|
|
401
|
+
for (const [columnName, mapping] of Object.entries(mappings)) {
|
|
402
|
+
switch (mapping.role) {
|
|
403
|
+
case "active":
|
|
404
|
+
activeStates.push(columnName);
|
|
405
|
+
break;
|
|
406
|
+
case "terminal":
|
|
407
|
+
terminalStates.push(columnName);
|
|
408
|
+
break;
|
|
409
|
+
case "wait":
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
if (activeStates.length > 0) {
|
|
414
|
+
blockerCheckStates.push(activeStates[0]);
|
|
415
|
+
}
|
|
416
|
+
return {
|
|
417
|
+
stateFieldName,
|
|
418
|
+
activeStates,
|
|
419
|
+
terminalStates,
|
|
420
|
+
blockerCheckStates
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
function validateStateMapping(mappings) {
|
|
424
|
+
const errors = [];
|
|
425
|
+
const warnings = [];
|
|
426
|
+
const entries = Object.entries(mappings);
|
|
427
|
+
const activeEntries = entries.filter(([, m]) => m.role === "active");
|
|
428
|
+
const terminalEntries = entries.filter(([, m]) => m.role === "terminal");
|
|
429
|
+
if (activeEntries.length === 0) {
|
|
430
|
+
errors.push(
|
|
431
|
+
"Missing required role: 'active' \u2014 at least one state must be active."
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
if (terminalEntries.length === 0) {
|
|
435
|
+
errors.push(
|
|
436
|
+
"Missing required role: 'terminal' \u2014 at least one state must be terminal."
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
if (terminalEntries.length > 1) {
|
|
440
|
+
warnings.push(
|
|
441
|
+
`Multiple terminal states: ${terminalEntries.map(([n]) => n).join(", ")}. All will be treated as terminal states.`
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// src/workflow/generate-workflow-md.ts
|
|
448
|
+
function generateWorkflowMarkdown(input) {
|
|
449
|
+
const frontMatter = buildFrontMatter(input);
|
|
450
|
+
const promptBody = buildPromptBody(input.mappings);
|
|
451
|
+
return `---
|
|
452
|
+
${frontMatter}---
|
|
453
|
+
${promptBody}
|
|
454
|
+
`;
|
|
455
|
+
}
|
|
456
|
+
function buildFrontMatter(input) {
|
|
457
|
+
const lines = [];
|
|
458
|
+
lines.push("tracker:");
|
|
459
|
+
lines.push(" kind: github-project");
|
|
460
|
+
lines.push(` project_id: ${input.projectId}`);
|
|
461
|
+
lines.push(` state_field: ${input.stateFieldName}`);
|
|
462
|
+
if (input.lifecycle.activeStates.length > 0) {
|
|
463
|
+
lines.push(" active_states:");
|
|
464
|
+
for (const state of input.lifecycle.activeStates) {
|
|
465
|
+
lines.push(` - ${state}`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
if (input.lifecycle.terminalStates.length > 0) {
|
|
469
|
+
lines.push(" terminal_states:");
|
|
470
|
+
for (const state of input.lifecycle.terminalStates) {
|
|
471
|
+
lines.push(` - ${state}`);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
if (input.lifecycle.blockerCheckStates.length > 0) {
|
|
475
|
+
lines.push(" blocker_check_states:");
|
|
476
|
+
for (const state of input.lifecycle.blockerCheckStates) {
|
|
477
|
+
lines.push(` - ${state}`);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
const agentCommand = resolveAgentCommand(input.runtime);
|
|
481
|
+
lines.push("polling:");
|
|
482
|
+
lines.push(` interval_ms: ${input.pollIntervalMs ?? 3e4}`);
|
|
483
|
+
lines.push("workspace:");
|
|
484
|
+
lines.push(" root: .runtime/symphony-workspaces");
|
|
485
|
+
lines.push("hooks:");
|
|
486
|
+
lines.push(" after_create: hooks/after_create.sh");
|
|
487
|
+
lines.push("agent:");
|
|
488
|
+
lines.push(" max_concurrent_agents: 10");
|
|
489
|
+
lines.push(" max_retry_backoff_ms: 30000");
|
|
490
|
+
lines.push(" retry_base_delay_ms: 10000");
|
|
491
|
+
lines.push("codex:");
|
|
492
|
+
lines.push(` command: ${agentCommand}`);
|
|
493
|
+
lines.push(" read_timeout_ms: 5000");
|
|
494
|
+
lines.push(" turn_timeout_ms: 3600000");
|
|
495
|
+
return lines.join("\n") + "\n";
|
|
496
|
+
}
|
|
497
|
+
function resolveAgentCommand(runtime) {
|
|
498
|
+
switch (runtime) {
|
|
499
|
+
case "codex":
|
|
500
|
+
return "codex app-server";
|
|
501
|
+
case "claude-code":
|
|
502
|
+
return "claude-code";
|
|
503
|
+
default:
|
|
504
|
+
return runtime;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
function buildPromptBody(mappings) {
|
|
508
|
+
const statusMap = generateStatusMapWithDescriptions(mappings);
|
|
509
|
+
const template = `${statusMap}
|
|
510
|
+
|
|
511
|
+
## Agent Instructions
|
|
512
|
+
|
|
513
|
+
You are an AI coding agent working on issue {{issue.identifier}}: "{{issue.title}}".
|
|
514
|
+
|
|
515
|
+
**Repository:** {{issue.repository}}
|
|
516
|
+
**Current state:** {{issue.state}}
|
|
517
|
+
|
|
518
|
+
### Task
|
|
519
|
+
|
|
520
|
+
{{issue.description}}
|
|
521
|
+
|
|
522
|
+
### Default Posture
|
|
523
|
+
|
|
524
|
+
1. This is an unattended orchestration session. Do not ask humans for follow-up actions.
|
|
525
|
+
2. Only abort early if there is a genuine blocker (missing required credentials or secrets).
|
|
526
|
+
3. In your final message, report only what was completed and any blockers. Do not include "next steps".
|
|
527
|
+
|
|
528
|
+
### Workflow
|
|
529
|
+
|
|
530
|
+
1. Read the issue description and understand the requirements.
|
|
531
|
+
2. Explore the codebase to understand the relevant code structure.
|
|
532
|
+
3. Implement the changes following the project's coding conventions.
|
|
533
|
+
4. Write or update tests to cover the changes.
|
|
534
|
+
5. Verify that all existing tests pass.
|
|
535
|
+
6. Create a PR with a clear description of the changes.
|
|
536
|
+
|
|
537
|
+
### Guardrails
|
|
538
|
+
|
|
539
|
+
- Do not edit the issue body for planning or progress tracking.
|
|
540
|
+
- If the issue is in a terminal state, do nothing and exit.
|
|
541
|
+
- If you find out-of-scope improvements, open a separate issue rather than expanding the current scope.
|
|
542
|
+
|
|
543
|
+
### Workpad Template
|
|
544
|
+
|
|
545
|
+
Create a workpad comment on the issue with the following structure to track progress:
|
|
546
|
+
|
|
547
|
+
\`\`\`md
|
|
548
|
+
## Workpad
|
|
549
|
+
|
|
550
|
+
### Plan
|
|
551
|
+
|
|
552
|
+
- [ ] 1. Task item
|
|
553
|
+
|
|
554
|
+
### Acceptance Criteria
|
|
555
|
+
|
|
556
|
+
- [ ] Criterion 1
|
|
557
|
+
|
|
558
|
+
### Validation
|
|
559
|
+
|
|
560
|
+
- [ ] Test: \`command\`
|
|
561
|
+
|
|
562
|
+
### Notes
|
|
563
|
+
|
|
564
|
+
- Progress notes
|
|
565
|
+
\`\`\``;
|
|
566
|
+
return template;
|
|
567
|
+
}
|
|
568
|
+
function generateStatusMapWithDescriptions(mappings) {
|
|
569
|
+
const roleDescriptions = {
|
|
570
|
+
active: "Agent starts work immediately",
|
|
571
|
+
wait: "PR created, awaiting human review",
|
|
572
|
+
terminal: "Completed, agent exits"
|
|
573
|
+
};
|
|
574
|
+
const lines = ["## Status Map", ""];
|
|
575
|
+
for (const [columnName, mapping] of Object.entries(mappings)) {
|
|
576
|
+
const rolePart = `[${mapping.role}]`;
|
|
577
|
+
const goalPart = mapping.goal ? ` \u2014 ${mapping.goal}` : "";
|
|
578
|
+
const descPart = roleDescriptions[mapping.role] ? ` *(${roleDescriptions[mapping.role]})*` : "";
|
|
579
|
+
lines.push(`- **${columnName}** ${rolePart}${goalPart}${descPart}`);
|
|
580
|
+
}
|
|
581
|
+
return lines.join("\n");
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// src/detection/environment-detector.ts
|
|
585
|
+
import { access, readFile } from "fs/promises";
|
|
586
|
+
import { join } from "path";
|
|
587
|
+
function isFileMissing(error) {
|
|
588
|
+
return Boolean(
|
|
589
|
+
error && typeof error === "object" && "code" in error && error.code === "ENOENT"
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
async function fileExists(path) {
|
|
593
|
+
try {
|
|
594
|
+
await access(path);
|
|
595
|
+
return true;
|
|
596
|
+
} catch (error) {
|
|
597
|
+
if (isFileMissing(error)) {
|
|
598
|
+
return false;
|
|
599
|
+
}
|
|
600
|
+
throw error;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
async function readJsonFile(path) {
|
|
604
|
+
try {
|
|
605
|
+
const raw = await readFile(path, "utf8");
|
|
606
|
+
return JSON.parse(raw);
|
|
607
|
+
} catch (error) {
|
|
608
|
+
if (isFileMissing(error)) {
|
|
609
|
+
return null;
|
|
610
|
+
}
|
|
611
|
+
if (error instanceof SyntaxError) {
|
|
612
|
+
return null;
|
|
613
|
+
}
|
|
614
|
+
throw error;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
async function detectPackageManager(cwd) {
|
|
618
|
+
const lockfiles = [
|
|
619
|
+
{ name: "pnpm-lock.yaml", manager: "pnpm" },
|
|
620
|
+
{ name: "bun.lock", manager: "bun" },
|
|
621
|
+
{ name: "bun.lockb", manager: "bun" },
|
|
622
|
+
{ name: "yarn.lock", manager: "yarn" },
|
|
623
|
+
{ name: "package-lock.json", manager: "npm" }
|
|
624
|
+
];
|
|
625
|
+
for (const { name, manager } of lockfiles) {
|
|
626
|
+
const exists = await fileExists(join(cwd, name));
|
|
627
|
+
if (exists) {
|
|
628
|
+
return { packageManager: manager, lockfile: name };
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
return { packageManager: null, lockfile: null };
|
|
632
|
+
}
|
|
633
|
+
async function detectScripts(cwd) {
|
|
634
|
+
const packageJson = await readJsonFile(join(cwd, "package.json"));
|
|
635
|
+
if (!packageJson?.scripts) {
|
|
636
|
+
return { testCommand: null, buildCommand: null, lintCommand: null };
|
|
637
|
+
}
|
|
638
|
+
return {
|
|
639
|
+
testCommand: packageJson.scripts.test ?? null,
|
|
640
|
+
buildCommand: packageJson.scripts.build ?? null,
|
|
641
|
+
lintCommand: packageJson.scripts.lint ?? null
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
async function detectCiPlatform(cwd) {
|
|
645
|
+
const workflowsDir = join(cwd, ".github", "workflows");
|
|
646
|
+
const exists = await fileExists(workflowsDir);
|
|
647
|
+
return exists ? "github-actions" : null;
|
|
648
|
+
}
|
|
649
|
+
async function detectMonorepo(cwd) {
|
|
650
|
+
const hasPnpmWorkspace = await fileExists(join(cwd, "pnpm-workspace.yaml"));
|
|
651
|
+
if (hasPnpmWorkspace) {
|
|
652
|
+
return true;
|
|
653
|
+
}
|
|
654
|
+
const hasLerna = await fileExists(join(cwd, "lerna.json"));
|
|
655
|
+
if (hasLerna) {
|
|
656
|
+
return true;
|
|
657
|
+
}
|
|
658
|
+
const packageJson = await readJsonFile(join(cwd, "package.json"));
|
|
659
|
+
if (packageJson?.workspaces) {
|
|
660
|
+
return true;
|
|
661
|
+
}
|
|
662
|
+
return false;
|
|
663
|
+
}
|
|
664
|
+
async function detectExistingSkills(cwd) {
|
|
665
|
+
const skills = [];
|
|
666
|
+
const claudeSkillsDir = join(cwd, ".claude", "skills");
|
|
667
|
+
try {
|
|
668
|
+
const { readdirSync } = await import("fs");
|
|
669
|
+
const entries = readdirSync(claudeSkillsDir, { withFileTypes: true });
|
|
670
|
+
for (const entry of entries) {
|
|
671
|
+
if (entry.isDirectory()) {
|
|
672
|
+
skills.push(entry.name);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
} catch (error) {
|
|
676
|
+
if (!isFileMissing(error)) {
|
|
677
|
+
throw error;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
const codexSkillsDir = join(cwd, ".codex", "skills");
|
|
681
|
+
try {
|
|
682
|
+
const { readdirSync } = await import("fs");
|
|
683
|
+
const entries = readdirSync(codexSkillsDir, { withFileTypes: true });
|
|
684
|
+
for (const entry of entries) {
|
|
685
|
+
if (entry.isDirectory()) {
|
|
686
|
+
skills.push(entry.name);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
} catch (error) {
|
|
690
|
+
if (!isFileMissing(error)) {
|
|
691
|
+
throw error;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
return skills;
|
|
695
|
+
}
|
|
696
|
+
async function detectEnvironment(cwd) {
|
|
697
|
+
const [
|
|
698
|
+
{ packageManager, lockfile },
|
|
699
|
+
{ testCommand, buildCommand, lintCommand },
|
|
700
|
+
ciPlatform,
|
|
701
|
+
monorepo,
|
|
702
|
+
existingSkills
|
|
703
|
+
] = await Promise.all([
|
|
704
|
+
detectPackageManager(cwd),
|
|
705
|
+
detectScripts(cwd),
|
|
706
|
+
detectCiPlatform(cwd),
|
|
707
|
+
detectMonorepo(cwd),
|
|
708
|
+
detectExistingSkills(cwd)
|
|
709
|
+
]);
|
|
710
|
+
return {
|
|
711
|
+
packageManager,
|
|
712
|
+
lockfile,
|
|
713
|
+
testCommand,
|
|
714
|
+
buildCommand,
|
|
715
|
+
lintCommand,
|
|
716
|
+
ciPlatform,
|
|
717
|
+
monorepo,
|
|
718
|
+
existingSkills
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// src/context/generate-context-yaml.ts
|
|
723
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
724
|
+
import { dirname } from "path";
|
|
725
|
+
function yamlQuote(value) {
|
|
726
|
+
const specialChars = /[:#{'"\]{}()\\[]|\n/;
|
|
727
|
+
if (specialChars.test(value)) {
|
|
728
|
+
return `"${value.replace(/"/g, '\\"')}"`;
|
|
729
|
+
}
|
|
730
|
+
return value;
|
|
731
|
+
}
|
|
732
|
+
function generateContextYamlString(context) {
|
|
733
|
+
const lines = [];
|
|
734
|
+
lines.push("schema_version: 1");
|
|
735
|
+
lines.push(`collected_at: ${context.collected_at}`);
|
|
736
|
+
lines.push("");
|
|
737
|
+
lines.push("project:");
|
|
738
|
+
lines.push(` id: ${context.project.id}`);
|
|
739
|
+
lines.push(` title: ${yamlQuote(context.project.title)}`);
|
|
740
|
+
lines.push(` url: ${context.project.url}`);
|
|
741
|
+
lines.push("");
|
|
742
|
+
lines.push("status_field:");
|
|
743
|
+
lines.push(` id: ${context.status_field.id}`);
|
|
744
|
+
lines.push(` name: ${yamlQuote(context.status_field.name)}`);
|
|
745
|
+
lines.push(" columns:");
|
|
746
|
+
for (const column of context.status_field.columns) {
|
|
747
|
+
lines.push(` - id: ${column.id}`);
|
|
748
|
+
lines.push(` name: ${yamlQuote(column.name)}`);
|
|
749
|
+
lines.push(
|
|
750
|
+
` color: ${column.color === null ? "null" : yamlQuote(column.color)}`
|
|
751
|
+
);
|
|
752
|
+
lines.push(
|
|
753
|
+
` inferred_role: ${column.inferred_role === null ? "null" : column.inferred_role}`
|
|
754
|
+
);
|
|
755
|
+
lines.push(` confidence: ${column.confidence}`);
|
|
756
|
+
}
|
|
757
|
+
lines.push("");
|
|
758
|
+
lines.push("text_fields:");
|
|
759
|
+
if (context.text_fields.length === 0) {
|
|
760
|
+
lines.push(" []");
|
|
761
|
+
} else {
|
|
762
|
+
for (const field of context.text_fields) {
|
|
763
|
+
lines.push(` - id: ${field.id}`);
|
|
764
|
+
lines.push(` name: ${yamlQuote(field.name)}`);
|
|
765
|
+
lines.push(` data_type: ${field.data_type}`);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
lines.push("");
|
|
769
|
+
lines.push("repositories:");
|
|
770
|
+
if (context.repositories.length === 0) {
|
|
771
|
+
lines.push(" []");
|
|
772
|
+
} else {
|
|
773
|
+
for (const repo of context.repositories) {
|
|
774
|
+
lines.push(` - owner: ${repo.owner}`);
|
|
775
|
+
lines.push(` name: ${repo.name}`);
|
|
776
|
+
lines.push(` clone_url: ${repo.clone_url}`);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
lines.push("");
|
|
780
|
+
lines.push("detected_environment:");
|
|
781
|
+
lines.push(
|
|
782
|
+
` packageManager: ${context.detected_environment.packageManager === null ? "null" : yamlQuote(context.detected_environment.packageManager)}`
|
|
783
|
+
);
|
|
784
|
+
lines.push(
|
|
785
|
+
` lockfile: ${context.detected_environment.lockfile === null ? "null" : yamlQuote(context.detected_environment.lockfile)}`
|
|
786
|
+
);
|
|
787
|
+
lines.push(
|
|
788
|
+
` testCommand: ${context.detected_environment.testCommand === null ? "null" : yamlQuote(context.detected_environment.testCommand)}`
|
|
789
|
+
);
|
|
790
|
+
lines.push(
|
|
791
|
+
` buildCommand: ${context.detected_environment.buildCommand === null ? "null" : yamlQuote(context.detected_environment.buildCommand)}`
|
|
792
|
+
);
|
|
793
|
+
lines.push(
|
|
794
|
+
` lintCommand: ${context.detected_environment.lintCommand === null ? "null" : yamlQuote(context.detected_environment.lintCommand)}`
|
|
795
|
+
);
|
|
796
|
+
lines.push(
|
|
797
|
+
` ciPlatform: ${context.detected_environment.ciPlatform === null ? "null" : yamlQuote(context.detected_environment.ciPlatform)}`
|
|
798
|
+
);
|
|
799
|
+
lines.push(` monorepo: ${context.detected_environment.monorepo}`);
|
|
800
|
+
lines.push(" existingSkills:");
|
|
801
|
+
if (context.detected_environment.existingSkills.length === 0) {
|
|
802
|
+
lines.push(" []");
|
|
803
|
+
} else {
|
|
804
|
+
for (const skill of context.detected_environment.existingSkills) {
|
|
805
|
+
lines.push(` - ${yamlQuote(skill)}`);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
lines.push("");
|
|
809
|
+
lines.push("runtime:");
|
|
810
|
+
lines.push(` agent: ${yamlQuote(context.runtime.agent)}`);
|
|
811
|
+
lines.push(` agent_command: ${yamlQuote(context.runtime.agent_command)}`);
|
|
812
|
+
return lines.join("\n") + "\n";
|
|
813
|
+
}
|
|
814
|
+
async function writeContextYaml(outputDir, context) {
|
|
815
|
+
await mkdir(outputDir, { recursive: true });
|
|
816
|
+
const contextPath = `${outputDir}/.gh-symphony/context.yaml`;
|
|
817
|
+
await mkdir(dirname(contextPath), { recursive: true });
|
|
818
|
+
const temporaryPath = `${contextPath}.tmp`;
|
|
819
|
+
const yamlContent = generateContextYamlString(context);
|
|
820
|
+
await writeFile(temporaryPath, yamlContent, "utf8");
|
|
821
|
+
const { rename: rename2 } = await import("fs/promises");
|
|
822
|
+
await rename2(temporaryPath, contextPath);
|
|
823
|
+
}
|
|
824
|
+
function buildContextYaml(params) {
|
|
825
|
+
const columns = params.statusField.options.map((option) => {
|
|
826
|
+
const roleMapping = inferStateRole(option.name);
|
|
827
|
+
return {
|
|
828
|
+
id: option.id,
|
|
829
|
+
name: option.name,
|
|
830
|
+
color: option.color,
|
|
831
|
+
inferred_role: roleMapping.role,
|
|
832
|
+
confidence: roleMapping.confidence
|
|
833
|
+
};
|
|
834
|
+
});
|
|
835
|
+
const textFields = params.projectDetail.textFields.map((field) => ({
|
|
836
|
+
id: field.id,
|
|
837
|
+
name: field.name,
|
|
838
|
+
data_type: field.dataType
|
|
839
|
+
}));
|
|
840
|
+
const repositories = params.projectDetail.linkedRepositories.map((repo) => ({
|
|
841
|
+
owner: repo.owner,
|
|
842
|
+
name: repo.name,
|
|
843
|
+
clone_url: repo.cloneUrl
|
|
844
|
+
}));
|
|
845
|
+
return {
|
|
846
|
+
schema_version: 1,
|
|
847
|
+
collected_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
848
|
+
project: {
|
|
849
|
+
id: params.projectDetail.id,
|
|
850
|
+
title: params.projectDetail.title,
|
|
851
|
+
url: params.projectDetail.url
|
|
852
|
+
},
|
|
853
|
+
status_field: {
|
|
854
|
+
id: params.statusField.id,
|
|
855
|
+
name: params.statusField.name,
|
|
856
|
+
columns
|
|
857
|
+
},
|
|
858
|
+
text_fields: textFields,
|
|
859
|
+
repositories,
|
|
860
|
+
detected_environment: params.detectedEnvironment,
|
|
861
|
+
runtime: params.runtime
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// src/workflow/generate-reference-workflow.ts
|
|
866
|
+
function generateReferenceWorkflow(input) {
|
|
867
|
+
const lines = [];
|
|
868
|
+
lines.push("# Reference WORKFLOW.md \u2014 gh-symphony");
|
|
869
|
+
lines.push("# This file is a reference template for authoring WORKFLOW.md.");
|
|
870
|
+
lines.push(
|
|
871
|
+
"# AI agents reference this file (via the /gh-symphony skill) when designing WORKFLOW.md."
|
|
872
|
+
);
|
|
873
|
+
lines.push("# Do not edit this file directly.");
|
|
874
|
+
lines.push("");
|
|
875
|
+
lines.push("---");
|
|
876
|
+
lines.push("");
|
|
877
|
+
lines.push("# \u2550\u2550\u2550 FRONT MATTER FIELD REFERENCE \u2550\u2550\u2550");
|
|
878
|
+
lines.push(
|
|
879
|
+
"# All front matter fields supported by the gh-symphony parser are listed below."
|
|
880
|
+
);
|
|
881
|
+
lines.push("");
|
|
882
|
+
lines.push("tracker:");
|
|
883
|
+
lines.push(" kind: github-project");
|
|
884
|
+
lines.push(` project_id: ${input.projectId}`);
|
|
885
|
+
lines.push(" state_field: Status");
|
|
886
|
+
lines.push("");
|
|
887
|
+
const activeColumns = input.statusColumns.filter((c) => c.role === "active");
|
|
888
|
+
const waitColumns = input.statusColumns.filter((c) => c.role === "wait");
|
|
889
|
+
const terminalColumns = input.statusColumns.filter(
|
|
890
|
+
(c) => c.role === "terminal"
|
|
891
|
+
);
|
|
892
|
+
const firstActive = activeColumns[0];
|
|
893
|
+
if (activeColumns.length > 0) {
|
|
894
|
+
lines.push(" active_states:");
|
|
895
|
+
for (const col of activeColumns) {
|
|
896
|
+
lines.push(` - ${col.name}`);
|
|
897
|
+
}
|
|
898
|
+
} else {
|
|
899
|
+
lines.push(" active_states: [{active column names}]");
|
|
900
|
+
}
|
|
901
|
+
if (terminalColumns.length > 0) {
|
|
902
|
+
lines.push(" terminal_states:");
|
|
903
|
+
for (const col of terminalColumns) {
|
|
904
|
+
lines.push(` - ${col.name}`);
|
|
905
|
+
}
|
|
906
|
+
} else {
|
|
907
|
+
lines.push(" terminal_states: [{terminal column names}]");
|
|
908
|
+
}
|
|
909
|
+
if (firstActive) {
|
|
910
|
+
lines.push(" blocker_check_states:");
|
|
911
|
+
lines.push(` - ${firstActive.name}`);
|
|
912
|
+
} else {
|
|
913
|
+
lines.push(" blocker_check_states: [{first active state}]");
|
|
914
|
+
}
|
|
915
|
+
lines.push("");
|
|
916
|
+
const agentCommand = resolveAgentCommand2(input.runtime);
|
|
917
|
+
const hookComment = resolveHookComment(input.runtime);
|
|
918
|
+
lines.push("polling:");
|
|
919
|
+
lines.push(" interval_ms: 30000");
|
|
920
|
+
lines.push("");
|
|
921
|
+
lines.push("workspace:");
|
|
922
|
+
lines.push(" root: .runtime/symphony-workspaces");
|
|
923
|
+
lines.push("");
|
|
924
|
+
lines.push("hooks:");
|
|
925
|
+
lines.push(` after_create: hooks/after_create.sh # ${hookComment}`);
|
|
926
|
+
lines.push(" before_run: null");
|
|
927
|
+
lines.push(" after_run: null");
|
|
928
|
+
lines.push(" before_remove: null");
|
|
929
|
+
lines.push(" timeout_ms: 60000");
|
|
930
|
+
lines.push("");
|
|
931
|
+
lines.push("agent:");
|
|
932
|
+
lines.push(" max_concurrent_agents: 10");
|
|
933
|
+
lines.push(" max_retry_backoff_ms: 30000");
|
|
934
|
+
lines.push(" retry_base_delay_ms: 10000");
|
|
935
|
+
lines.push(" max_turns: 20");
|
|
936
|
+
lines.push("");
|
|
937
|
+
lines.push("codex:");
|
|
938
|
+
lines.push(` command: ${agentCommand}`);
|
|
939
|
+
lines.push(" read_timeout_ms: 5000");
|
|
940
|
+
lines.push(" turn_timeout_ms: 3600000");
|
|
941
|
+
lines.push(" stall_timeout_ms: 300000");
|
|
942
|
+
lines.push("");
|
|
943
|
+
lines.push("---");
|
|
944
|
+
lines.push("");
|
|
945
|
+
lines.push("# \u2550\u2550\u2550 PROMPT BODY REFERENCE \u2550\u2550\u2550");
|
|
946
|
+
lines.push(
|
|
947
|
+
"# GitHub Project adaptation of the Elixir Symphony reference prompt."
|
|
948
|
+
);
|
|
949
|
+
lines.push("");
|
|
950
|
+
lines.push("## Status Map");
|
|
951
|
+
lines.push("");
|
|
952
|
+
lines.push("| Status | Role | Agent Action |");
|
|
953
|
+
lines.push("| ------ | ---- | ------------ |");
|
|
954
|
+
for (const col of input.statusColumns) {
|
|
955
|
+
const roleLabel = col.role ?? "unset";
|
|
956
|
+
const action = resolveRoleAction(col.role);
|
|
957
|
+
lines.push(`| ${col.name} | ${roleLabel} | ${action} |`);
|
|
958
|
+
}
|
|
959
|
+
if (waitColumns.length > 0) {
|
|
960
|
+
lines.push("");
|
|
961
|
+
lines.push("**Wait States (awaiting PR review):**");
|
|
962
|
+
for (const col of waitColumns) {
|
|
963
|
+
lines.push(
|
|
964
|
+
`- **${col.name}**: PR created. Awaiting human review. Agent is idle.`
|
|
965
|
+
);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
lines.push("");
|
|
969
|
+
lines.push("## Default Posture");
|
|
970
|
+
lines.push("");
|
|
971
|
+
lines.push(
|
|
972
|
+
"1. This is an unattended orchestration session. Do not ask humans for follow-up tasks."
|
|
973
|
+
);
|
|
974
|
+
lines.push(
|
|
975
|
+
"2. Exit early only for genuine blockers (missing required credentials or secrets)."
|
|
976
|
+
);
|
|
977
|
+
lines.push(
|
|
978
|
+
'3. In your final message, report only completed work and blockers. Do not include "next steps".'
|
|
979
|
+
);
|
|
980
|
+
lines.push(
|
|
981
|
+
"4. Do not modify the issue body for planning or progress-tracking purposes."
|
|
982
|
+
);
|
|
983
|
+
lines.push(
|
|
984
|
+
"5. If the issue is in a terminal state, do nothing and exit immediately."
|
|
985
|
+
);
|
|
986
|
+
lines.push(
|
|
987
|
+
"6. If you discover out-of-scope improvements, open a separate issue rather than expanding the current scope."
|
|
988
|
+
);
|
|
989
|
+
lines.push(
|
|
990
|
+
"7. Keep all commits as logical units and follow conventional commit format."
|
|
991
|
+
);
|
|
992
|
+
lines.push("8. Do not make commits that break existing tests.");
|
|
993
|
+
lines.push("9. Verify all existing tests pass before creating a PR.");
|
|
994
|
+
lines.push("10. Create a workpad as an issue comment to track progress.");
|
|
995
|
+
lines.push("11. Use the gh-project skill to manage issue status.");
|
|
996
|
+
lines.push(
|
|
997
|
+
"12. When a blocker is found, record it in an issue comment and transition the status appropriately."
|
|
998
|
+
);
|
|
999
|
+
lines.push(
|
|
1000
|
+
"13. Once your work is complete and the PR is merged, transition the issue to the Done state."
|
|
1001
|
+
);
|
|
1002
|
+
lines.push("");
|
|
1003
|
+
lines.push("## Related Skills");
|
|
1004
|
+
lines.push("");
|
|
1005
|
+
lines.push(
|
|
1006
|
+
"- **gh-project**: Manage GitHub Project v2 issue status and update fields"
|
|
1007
|
+
);
|
|
1008
|
+
lines.push(
|
|
1009
|
+
"- **commit**: Create logical-unit commits (conventional commit format)"
|
|
1010
|
+
);
|
|
1011
|
+
lines.push("- **push**: Push branch and sync with the remote repository");
|
|
1012
|
+
lines.push("- **pull**: Fetch latest changes and resolve conflicts");
|
|
1013
|
+
lines.push("- **land**: Create PR, request review, and handle merge");
|
|
1014
|
+
lines.push("");
|
|
1015
|
+
lines.push("## Step 0: Determine current state and route");
|
|
1016
|
+
lines.push("");
|
|
1017
|
+
lines.push(
|
|
1018
|
+
"Check the current issue state and route to the appropriate step:"
|
|
1019
|
+
);
|
|
1020
|
+
lines.push("");
|
|
1021
|
+
if (terminalColumns.length > 0) {
|
|
1022
|
+
const terminalNames = terminalColumns.map((c) => c.name).join(", ");
|
|
1023
|
+
lines.push(`- **${terminalNames}** \u2192 Exit immediately. Do nothing.`);
|
|
1024
|
+
}
|
|
1025
|
+
if (waitColumns.length > 0) {
|
|
1026
|
+
const waitNames = waitColumns.map((c) => c.name).join(", ");
|
|
1027
|
+
lines.push(`- **${waitNames}** \u2192 Go to Step 3 (handle awaiting review).`);
|
|
1028
|
+
}
|
|
1029
|
+
if (activeColumns.length > 0) {
|
|
1030
|
+
const activeNames = activeColumns.map((c) => c.name).join(", ");
|
|
1031
|
+
lines.push(
|
|
1032
|
+
`- **${activeNames}** \u2192 Go to Step 1 (start or continue execution).`
|
|
1033
|
+
);
|
|
1034
|
+
}
|
|
1035
|
+
lines.push(
|
|
1036
|
+
"- **Other states** \u2192 Log the unclear state in an issue comment and exit."
|
|
1037
|
+
);
|
|
1038
|
+
lines.push("");
|
|
1039
|
+
lines.push("## Step 1: Start/continue execution");
|
|
1040
|
+
lines.push("");
|
|
1041
|
+
lines.push(
|
|
1042
|
+
"1. Read the issue body and comments to understand current progress."
|
|
1043
|
+
);
|
|
1044
|
+
lines.push(
|
|
1045
|
+
"2. If an existing workpad comment is found, continue from it; otherwise create a new workpad."
|
|
1046
|
+
);
|
|
1047
|
+
lines.push(
|
|
1048
|
+
"3. See the 'Workpad Template' section below for the workpad format."
|
|
1049
|
+
);
|
|
1050
|
+
lines.push(
|
|
1051
|
+
"4. If no branch exists, create a feature branch based on `{issue.repository}`."
|
|
1052
|
+
);
|
|
1053
|
+
lines.push("5. Proceed to Step 2.");
|
|
1054
|
+
lines.push("");
|
|
1055
|
+
lines.push("## Step 2: Execution phase");
|
|
1056
|
+
lines.push("");
|
|
1057
|
+
lines.push("1. Implement according to the issue description.");
|
|
1058
|
+
lines.push(
|
|
1059
|
+
"2. Commit changes in logical units (conventional commit format)."
|
|
1060
|
+
);
|
|
1061
|
+
lines.push("3. Verify all existing tests pass.");
|
|
1062
|
+
lines.push("4. Write tests for new functionality.");
|
|
1063
|
+
lines.push("5. Once all Completion Bar criteria are met, create a PR.");
|
|
1064
|
+
lines.push(
|
|
1065
|
+
"6. After creating the PR, transition the issue status to the Human Review state."
|
|
1066
|
+
);
|
|
1067
|
+
lines.push("7. Proceed to Step 3.");
|
|
1068
|
+
lines.push("");
|
|
1069
|
+
lines.push("## Step 3: Human Review and merge handling");
|
|
1070
|
+
lines.push("");
|
|
1071
|
+
lines.push("1. If a PR already exists, check for review comments.");
|
|
1072
|
+
lines.push(
|
|
1073
|
+
"2. If no review comments are present, remain in the waiting state."
|
|
1074
|
+
);
|
|
1075
|
+
lines.push(
|
|
1076
|
+
"3. If the PR is merged, transition the issue to a terminal state."
|
|
1077
|
+
);
|
|
1078
|
+
lines.push("4. If review changes are requested, proceed to Step 4.");
|
|
1079
|
+
lines.push("");
|
|
1080
|
+
lines.push("## Step 4: Rework handling");
|
|
1081
|
+
lines.push("");
|
|
1082
|
+
lines.push(
|
|
1083
|
+
"1. Read all PR review comments and identify the requested changes."
|
|
1084
|
+
);
|
|
1085
|
+
lines.push(
|
|
1086
|
+
"2. Process the changes following the PR Feedback Sweep Protocol."
|
|
1087
|
+
);
|
|
1088
|
+
lines.push("3. After implementing changes, commit and update the PR.");
|
|
1089
|
+
lines.push("4. Transition the issue status back to the Human Review state.");
|
|
1090
|
+
lines.push("5. Return to Step 3.");
|
|
1091
|
+
lines.push("");
|
|
1092
|
+
lines.push("## PR Feedback Sweep Protocol");
|
|
1093
|
+
lines.push("");
|
|
1094
|
+
lines.push("Order for processing PR review feedback:");
|
|
1095
|
+
lines.push("");
|
|
1096
|
+
lines.push(
|
|
1097
|
+
"1. **Collect all comments**: List all unresolved review comments."
|
|
1098
|
+
);
|
|
1099
|
+
lines.push(
|
|
1100
|
+
"2. **Triage by priority**: Handle blocking comments before non-blocking ones."
|
|
1101
|
+
);
|
|
1102
|
+
lines.push(
|
|
1103
|
+
"3. **Implement changes**: Make the code changes corresponding to each comment."
|
|
1104
|
+
);
|
|
1105
|
+
lines.push(
|
|
1106
|
+
"4. **Reply to comments**: Respond to each review comment with a description of what was done."
|
|
1107
|
+
);
|
|
1108
|
+
lines.push(
|
|
1109
|
+
"5. **Commit**: Commit changes in `fix: address PR review feedback` format."
|
|
1110
|
+
);
|
|
1111
|
+
lines.push("6. **Request re-review**: Ask the reviewer for a re-review.");
|
|
1112
|
+
lines.push("");
|
|
1113
|
+
lines.push("## Completion Bar");
|
|
1114
|
+
lines.push("");
|
|
1115
|
+
lines.push("All of the following must be satisfied before creating a PR:");
|
|
1116
|
+
lines.push("");
|
|
1117
|
+
lines.push(
|
|
1118
|
+
"- [ ] All requirements from the issue description are implemented."
|
|
1119
|
+
);
|
|
1120
|
+
lines.push("- [ ] All existing tests pass.");
|
|
1121
|
+
lines.push("- [ ] Tests are written for new functionality.");
|
|
1122
|
+
lines.push("- [ ] Code style follows project conventions.");
|
|
1123
|
+
lines.push("- [ ] The PR description clearly explains the changes.");
|
|
1124
|
+
lines.push("- [ ] Related documentation is updated (if applicable).");
|
|
1125
|
+
lines.push("");
|
|
1126
|
+
lines.push("## Guardrails");
|
|
1127
|
+
lines.push("");
|
|
1128
|
+
lines.push("- **Scope**: Never make changes outside the scope of the issue.");
|
|
1129
|
+
lines.push(
|
|
1130
|
+
"- **Secrets**: Never hardcode tokens, passwords, or API keys in code."
|
|
1131
|
+
);
|
|
1132
|
+
lines.push(
|
|
1133
|
+
"- **Breaking changes**: Do not modify existing APIs or interfaces without explicit authorization."
|
|
1134
|
+
);
|
|
1135
|
+
lines.push("- **Force push**: Do not force-push to the main/master branch.");
|
|
1136
|
+
lines.push(
|
|
1137
|
+
"- **Issue body**: Do not modify the issue body for progress tracking."
|
|
1138
|
+
);
|
|
1139
|
+
lines.push(
|
|
1140
|
+
"- **Infinite loops**: If the same task fails 3 or more consecutive times, log it as a blocker and exit."
|
|
1141
|
+
);
|
|
1142
|
+
lines.push("");
|
|
1143
|
+
lines.push("## Workpad Template");
|
|
1144
|
+
lines.push("");
|
|
1145
|
+
lines.push("Workpad format to create as an issue comment:");
|
|
1146
|
+
lines.push("");
|
|
1147
|
+
lines.push("```markdown");
|
|
1148
|
+
lines.push("## Workpad \u2014 {issue.identifier}");
|
|
1149
|
+
lines.push("");
|
|
1150
|
+
lines.push("**Status**: {current phase}");
|
|
1151
|
+
lines.push("**Branch**: {branch name}");
|
|
1152
|
+
lines.push("**PR**: {PR URL or not created}");
|
|
1153
|
+
lines.push("");
|
|
1154
|
+
lines.push("### Plan");
|
|
1155
|
+
lines.push("");
|
|
1156
|
+
lines.push("- [ ] {task 1}");
|
|
1157
|
+
lines.push("- [ ] {task 2}");
|
|
1158
|
+
lines.push("");
|
|
1159
|
+
lines.push("### Progress Log");
|
|
1160
|
+
lines.push("");
|
|
1161
|
+
lines.push("- {timestamp}: {action taken}");
|
|
1162
|
+
lines.push("");
|
|
1163
|
+
lines.push("### Blockers");
|
|
1164
|
+
lines.push("");
|
|
1165
|
+
lines.push("None");
|
|
1166
|
+
lines.push("```");
|
|
1167
|
+
lines.push("");
|
|
1168
|
+
return lines.join("\n");
|
|
1169
|
+
}
|
|
1170
|
+
function resolveAgentCommand2(runtime) {
|
|
1171
|
+
switch (runtime) {
|
|
1172
|
+
case "codex":
|
|
1173
|
+
return "codex app-server";
|
|
1174
|
+
case "claude-code":
|
|
1175
|
+
return "claude-code";
|
|
1176
|
+
default:
|
|
1177
|
+
return runtime;
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
function resolveHookComment(runtime) {
|
|
1181
|
+
switch (runtime) {
|
|
1182
|
+
case "codex":
|
|
1183
|
+
return "npm/yarn/pnpm install script";
|
|
1184
|
+
case "claude-code":
|
|
1185
|
+
return "npm/yarn/pnpm install script";
|
|
1186
|
+
default:
|
|
1187
|
+
return "package-manager-specific script";
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
function resolveRoleAction(role) {
|
|
1191
|
+
switch (role) {
|
|
1192
|
+
case "active":
|
|
1193
|
+
return "Agent starts work immediately. Creates workpad and proceeds with implementation.";
|
|
1194
|
+
case "wait":
|
|
1195
|
+
return "PR created. Awaiting human review. Agent is idle.";
|
|
1196
|
+
case "terminal":
|
|
1197
|
+
return "Completed state. Agent exits.";
|
|
1198
|
+
case null:
|
|
1199
|
+
return "Role unset. Must be explicitly configured in WORKFLOW.md.";
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// src/skills/skill-writer.ts
|
|
1204
|
+
import { mkdir as mkdir2, writeFile as writeFile2, readFile as readFile2 } from "fs/promises";
|
|
1205
|
+
import { join as join2 } from "path";
|
|
1206
|
+
function normalizeRuntimeForSkills(runtime) {
|
|
1207
|
+
if (runtime === "claude-code" || runtime.includes("claude-code")) {
|
|
1208
|
+
return "claude-code";
|
|
1209
|
+
}
|
|
1210
|
+
if (runtime === "codex" || runtime.includes("codex")) {
|
|
1211
|
+
return "codex";
|
|
1212
|
+
}
|
|
1213
|
+
return null;
|
|
1214
|
+
}
|
|
1215
|
+
function resolveSkillsDir(repoRoot, runtime) {
|
|
1216
|
+
const normalizedRuntime = normalizeRuntimeForSkills(runtime);
|
|
1217
|
+
if (normalizedRuntime === "claude-code") {
|
|
1218
|
+
return join2(repoRoot, ".claude", "skills");
|
|
1219
|
+
}
|
|
1220
|
+
if (normalizedRuntime === "codex") {
|
|
1221
|
+
return join2(repoRoot, ".codex", "skills");
|
|
1222
|
+
}
|
|
1223
|
+
return null;
|
|
1224
|
+
}
|
|
1225
|
+
async function writeSkillFile(skillsDir, template, context, options) {
|
|
1226
|
+
const skillDir = join2(skillsDir, template.name);
|
|
1227
|
+
const filePath = join2(skillDir, template.fileName);
|
|
1228
|
+
if (!options?.overwrite) {
|
|
1229
|
+
try {
|
|
1230
|
+
await readFile2(filePath, "utf8");
|
|
1231
|
+
return { written: false, path: filePath };
|
|
1232
|
+
} catch (error) {
|
|
1233
|
+
const err = error;
|
|
1234
|
+
if (err.code !== "ENOENT") {
|
|
1235
|
+
throw error;
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
await mkdir2(skillDir, { recursive: true });
|
|
1240
|
+
const content = template.generate(context);
|
|
1241
|
+
const temporaryPath = `${filePath}.tmp`;
|
|
1242
|
+
await writeFile2(temporaryPath, content, "utf8");
|
|
1243
|
+
const { rename: rename2 } = await import("fs/promises");
|
|
1244
|
+
await rename2(temporaryPath, filePath);
|
|
1245
|
+
return { written: true, path: filePath };
|
|
1246
|
+
}
|
|
1247
|
+
async function writeAllSkills(repoRoot, runtime, templates, context, options) {
|
|
1248
|
+
const skillsDir = resolveSkillsDir(repoRoot, runtime);
|
|
1249
|
+
if (!skillsDir) {
|
|
1250
|
+
return { written: [], skipped: [] };
|
|
1251
|
+
}
|
|
1252
|
+
const written = [];
|
|
1253
|
+
const skipped = [];
|
|
1254
|
+
for (const template of templates) {
|
|
1255
|
+
const result = await writeSkillFile(skillsDir, template, context, options);
|
|
1256
|
+
if (result.written) {
|
|
1257
|
+
written.push(result.path);
|
|
1258
|
+
} else {
|
|
1259
|
+
skipped.push(result.path);
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
return { written, skipped };
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
// src/skills/templates/document.ts
|
|
1266
|
+
function renderSkillDocument(options) {
|
|
1267
|
+
const { name, description, bodyLines } = options;
|
|
1268
|
+
return [
|
|
1269
|
+
"---",
|
|
1270
|
+
`name: ${name}`,
|
|
1271
|
+
`description: ${description}`,
|
|
1272
|
+
"license: MIT",
|
|
1273
|
+
"metadata:",
|
|
1274
|
+
" author: gh-symphony",
|
|
1275
|
+
' version: "1.0"',
|
|
1276
|
+
' generatedBy: "gh-symphony"',
|
|
1277
|
+
"---",
|
|
1278
|
+
"",
|
|
1279
|
+
...bodyLines
|
|
1280
|
+
].join("\n");
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
// src/skills/templates/gh-symphony.ts
|
|
1284
|
+
function generateGhSymphonySkill(ctx) {
|
|
1285
|
+
const lines = [];
|
|
1286
|
+
lines.push("# /gh-symphony \u2014 WORKFLOW.md Design & Refinement");
|
|
1287
|
+
lines.push("");
|
|
1288
|
+
lines.push("## Trigger");
|
|
1289
|
+
lines.push("");
|
|
1290
|
+
lines.push("Use this skill when you want to:");
|
|
1291
|
+
lines.push("- Create a new WORKFLOW.md for a GitHub Symphony project");
|
|
1292
|
+
lines.push("- Refine or improve an existing WORKFLOW.md");
|
|
1293
|
+
lines.push("- Validate that a WORKFLOW.md is correctly structured");
|
|
1294
|
+
lines.push("");
|
|
1295
|
+
lines.push("## Prerequisites");
|
|
1296
|
+
lines.push("");
|
|
1297
|
+
lines.push(
|
|
1298
|
+
`- \`${ctx.contextYamlPath}\` must exist (contains GitHub Project metadata)`
|
|
1299
|
+
);
|
|
1300
|
+
lines.push(
|
|
1301
|
+
`- \`${ctx.referenceWorkflowPath}\` must exist (annotated reference template)`
|
|
1302
|
+
);
|
|
1303
|
+
lines.push("- `gh` CLI must be authenticated");
|
|
1304
|
+
lines.push("");
|
|
1305
|
+
lines.push("## Mode Detection");
|
|
1306
|
+
lines.push("");
|
|
1307
|
+
lines.push("Check if `WORKFLOW.md` exists in the current directory:");
|
|
1308
|
+
lines.push("- **Not found** \u2192 enter **Design Mode** (create from scratch)");
|
|
1309
|
+
lines.push("- **Found** \u2192 ask user: refine existing or validate only?");
|
|
1310
|
+
lines.push(" - Refine \u2192 enter **Refine Mode**");
|
|
1311
|
+
lines.push(" - Validate \u2192 enter **Validate Mode**");
|
|
1312
|
+
lines.push("");
|
|
1313
|
+
lines.push("## Design Mode");
|
|
1314
|
+
lines.push("");
|
|
1315
|
+
lines.push(
|
|
1316
|
+
`1. Read \`${ctx.contextYamlPath}\` to understand the project structure`
|
|
1317
|
+
);
|
|
1318
|
+
lines.push(
|
|
1319
|
+
`2. Read \`${ctx.referenceWorkflowPath}\` as the annotated reference`
|
|
1320
|
+
);
|
|
1321
|
+
lines.push("3. Ask the user these key questions:");
|
|
1322
|
+
lines.push(" - Which status columns should be **active** (agent works)?");
|
|
1323
|
+
lines.push(" - Which should be **wait** (agent pauses for human)?");
|
|
1324
|
+
lines.push(" - Which should be **terminal** (agent stops)?");
|
|
1325
|
+
lines.push(" - What runtime is being used? (codex / claude-code / custom)");
|
|
1326
|
+
lines.push(" - Any custom hooks needed? (after_create, before_run, etc.)");
|
|
1327
|
+
lines.push(
|
|
1328
|
+
"4. Generate WORKFLOW.md using the reference as a structural guide"
|
|
1329
|
+
);
|
|
1330
|
+
lines.push("5. Validate the generated file (see Validate Mode)");
|
|
1331
|
+
lines.push("");
|
|
1332
|
+
lines.push("## Refine Mode");
|
|
1333
|
+
lines.push("");
|
|
1334
|
+
lines.push("1. Read the current `WORKFLOW.md`");
|
|
1335
|
+
lines.push(`2. Read \`${ctx.referenceWorkflowPath}\` for comparison`);
|
|
1336
|
+
lines.push("3. Identify missing or incomplete sections:");
|
|
1337
|
+
lines.push(" - Status Map with role annotations");
|
|
1338
|
+
lines.push(" - Default Posture / Agent Instructions");
|
|
1339
|
+
lines.push(" - Guardrails section");
|
|
1340
|
+
lines.push(" - Workpad Template");
|
|
1341
|
+
lines.push(" - Step 0 routing logic");
|
|
1342
|
+
lines.push("4. Propose improvements and apply with user confirmation");
|
|
1343
|
+
lines.push("5. Validate the refined file");
|
|
1344
|
+
lines.push("");
|
|
1345
|
+
lines.push("## Validate Mode");
|
|
1346
|
+
lines.push("");
|
|
1347
|
+
lines.push("Check the WORKFLOW.md for:");
|
|
1348
|
+
lines.push("- Front matter is valid YAML");
|
|
1349
|
+
lines.push(
|
|
1350
|
+
"- Required fields are present (see Supported Front Matter Fields)"
|
|
1351
|
+
);
|
|
1352
|
+
lines.push(
|
|
1353
|
+
"- Template variables use only supported names (see Supported Template Variables)"
|
|
1354
|
+
);
|
|
1355
|
+
lines.push("- Status Map matches the lifecycle configuration");
|
|
1356
|
+
lines.push(
|
|
1357
|
+
"- No unsupported double-brace variable patterns (only the 8 listed below are valid)"
|
|
1358
|
+
);
|
|
1359
|
+
lines.push("");
|
|
1360
|
+
lines.push("## Supported Front Matter Fields");
|
|
1361
|
+
lines.push("");
|
|
1362
|
+
lines.push("```yaml");
|
|
1363
|
+
lines.push("tracker:");
|
|
1364
|
+
lines.push(" kind: github-project");
|
|
1365
|
+
lines.push(" project_id: PVT_xxx");
|
|
1366
|
+
lines.push(" state_field: Status");
|
|
1367
|
+
lines.push(" active_states: [Todo, In Progress]");
|
|
1368
|
+
lines.push(" terminal_states: [Done, Cancelled]");
|
|
1369
|
+
lines.push(" blocker_check_states: [Blocked]");
|
|
1370
|
+
lines.push("polling:");
|
|
1371
|
+
lines.push(" interval_ms: 30000");
|
|
1372
|
+
lines.push("workspace:");
|
|
1373
|
+
lines.push(" root: .runtime/symphony-workspaces");
|
|
1374
|
+
lines.push("hooks:");
|
|
1375
|
+
lines.push(" after_create: |");
|
|
1376
|
+
lines.push(" git clone --depth 1 https://github.com/owner/repo .");
|
|
1377
|
+
lines.push(" before_run: null");
|
|
1378
|
+
lines.push(" after_run: null");
|
|
1379
|
+
lines.push(" before_remove: null");
|
|
1380
|
+
lines.push(" timeout_ms: 60000");
|
|
1381
|
+
lines.push("agent:");
|
|
1382
|
+
lines.push(" max_concurrent_agents: 10");
|
|
1383
|
+
lines.push(" max_retry_backoff_ms: 30000");
|
|
1384
|
+
lines.push(" retry_base_delay_ms: 10000");
|
|
1385
|
+
lines.push(" max_turns: 20");
|
|
1386
|
+
lines.push("codex:");
|
|
1387
|
+
lines.push(" command: codex app-server");
|
|
1388
|
+
lines.push(" read_timeout_ms: 5000");
|
|
1389
|
+
lines.push(" turn_timeout_ms: 3600000");
|
|
1390
|
+
lines.push(" stall_timeout_ms: 300000");
|
|
1391
|
+
lines.push("```");
|
|
1392
|
+
lines.push("");
|
|
1393
|
+
lines.push("## Supported Template Variables");
|
|
1394
|
+
lines.push("");
|
|
1395
|
+
lines.push("Use these in the WORKFLOW.md prompt body (double-brace syntax):");
|
|
1396
|
+
lines.push("");
|
|
1397
|
+
lines.push("| Variable | Description |");
|
|
1398
|
+
lines.push("|----------|-------------|");
|
|
1399
|
+
lines.push("| `issue.identifier` | e.g. `acme/platform#42` |");
|
|
1400
|
+
lines.push("| `issue.title` | Issue title |");
|
|
1401
|
+
lines.push("| `issue.state` | Current tracker state |");
|
|
1402
|
+
lines.push("| `issue.description` | Issue body |");
|
|
1403
|
+
lines.push("| `issue.url` | Issue URL |");
|
|
1404
|
+
lines.push("| `issue.repository` | `owner/name` |");
|
|
1405
|
+
lines.push("| `issue.number` | Issue number |");
|
|
1406
|
+
lines.push("| `attempt` | Retry attempt number (null on first run) |");
|
|
1407
|
+
lines.push("");
|
|
1408
|
+
lines.push(
|
|
1409
|
+
"**Important**: Only these 8 variables are supported. Using any other variable"
|
|
1410
|
+
);
|
|
1411
|
+
lines.push("will cause a runtime error (strict mode validation).");
|
|
1412
|
+
lines.push("");
|
|
1413
|
+
lines.push("## Related Skills");
|
|
1414
|
+
lines.push("");
|
|
1415
|
+
lines.push(
|
|
1416
|
+
"- `/gh-project` \u2014 interact with GitHub Project v2 board (status transitions, workpad comments)"
|
|
1417
|
+
);
|
|
1418
|
+
lines.push(
|
|
1419
|
+
"- `/commit` \u2014 produce clean, logical commits during implementation"
|
|
1420
|
+
);
|
|
1421
|
+
lines.push("- `/push` \u2014 keep remote branch current and publish updates");
|
|
1422
|
+
lines.push("- `/pull` \u2014 sync branch with latest origin/main before handoff");
|
|
1423
|
+
lines.push("- `/land` \u2014 merge approved PR and transition issue to Done");
|
|
1424
|
+
return renderSkillDocument({
|
|
1425
|
+
name: "gh-symphony",
|
|
1426
|
+
description: "Design, refine, and validate repository WORKFLOW.md files for GitHub Symphony projects.",
|
|
1427
|
+
bodyLines: lines
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
// src/skills/templates/gh-project.ts
|
|
1432
|
+
function generateGhProjectSkill(ctx) {
|
|
1433
|
+
const lines = [];
|
|
1434
|
+
lines.push("# /gh-project \u2014 GitHub Project v2 Status Management");
|
|
1435
|
+
lines.push("");
|
|
1436
|
+
lines.push("## Purpose");
|
|
1437
|
+
lines.push("");
|
|
1438
|
+
lines.push(
|
|
1439
|
+
"Interact with the GitHub Project v2 board to manage issue status,"
|
|
1440
|
+
);
|
|
1441
|
+
lines.push("create workpad comments, and handle follow-up issues.");
|
|
1442
|
+
lines.push("");
|
|
1443
|
+
lines.push("## Prerequisites");
|
|
1444
|
+
lines.push("");
|
|
1445
|
+
lines.push("- `gh` CLI is authenticated (`gh auth status`)");
|
|
1446
|
+
lines.push(
|
|
1447
|
+
`- \`${ctx.contextYamlPath}\` exists with field IDs and option IDs`
|
|
1448
|
+
);
|
|
1449
|
+
lines.push("");
|
|
1450
|
+
lines.push("## Column ID Quick Reference");
|
|
1451
|
+
lines.push("");
|
|
1452
|
+
lines.push(`Status Field ID: \`${ctx.statusFieldId}\``);
|
|
1453
|
+
lines.push("");
|
|
1454
|
+
lines.push("| Column Name | Role | Option ID |");
|
|
1455
|
+
lines.push("|-------------|------|-----------|");
|
|
1456
|
+
for (const col of ctx.statusColumns) {
|
|
1457
|
+
const role = col.role ?? "unknown";
|
|
1458
|
+
lines.push(`| ${col.name} | ${role} | \`${col.id}\` |`);
|
|
1459
|
+
}
|
|
1460
|
+
lines.push("");
|
|
1461
|
+
lines.push("## Operations");
|
|
1462
|
+
lines.push("");
|
|
1463
|
+
lines.push("### Change Issue Status");
|
|
1464
|
+
lines.push("");
|
|
1465
|
+
lines.push(
|
|
1466
|
+
"Use `gh project item-edit` with the field ID and option ID from the table above:"
|
|
1467
|
+
);
|
|
1468
|
+
lines.push("");
|
|
1469
|
+
lines.push("```bash");
|
|
1470
|
+
lines.push("# Get the project item ID for an issue");
|
|
1471
|
+
lines.push(
|
|
1472
|
+
"gh project item-list <project-number> --owner <owner> --format json \\"
|
|
1473
|
+
);
|
|
1474
|
+
lines.push(
|
|
1475
|
+
" | jq '.items[] | select(.content.number == <issue-number>) | .id'"
|
|
1476
|
+
);
|
|
1477
|
+
lines.push("");
|
|
1478
|
+
lines.push("# Update the status field");
|
|
1479
|
+
lines.push(`gh project item-edit \\`);
|
|
1480
|
+
lines.push(` --project-id ${ctx.projectId} \\`);
|
|
1481
|
+
lines.push(` --id <item-id> \\`);
|
|
1482
|
+
lines.push(` --field-id ${ctx.statusFieldId} \\`);
|
|
1483
|
+
lines.push(` --single-select-option-id <option-id-from-table-above>`);
|
|
1484
|
+
lines.push("```");
|
|
1485
|
+
lines.push("");
|
|
1486
|
+
lines.push("### Create Workpad Comment");
|
|
1487
|
+
lines.push("");
|
|
1488
|
+
lines.push("```bash");
|
|
1489
|
+
lines.push(
|
|
1490
|
+
'gh issue comment <issue-number> --repo <owner>/<repo> --body "## Workpad\\n\\n### Plan\\n- [ ] Task 1"'
|
|
1491
|
+
);
|
|
1492
|
+
lines.push("```");
|
|
1493
|
+
lines.push("");
|
|
1494
|
+
lines.push("### Update Existing Comment");
|
|
1495
|
+
lines.push("");
|
|
1496
|
+
lines.push("```bash");
|
|
1497
|
+
lines.push(
|
|
1498
|
+
"gh api -X PATCH /repos/<owner>/<repo>/issues/comments/<comment-id> \\"
|
|
1499
|
+
);
|
|
1500
|
+
lines.push(' -f body="## Workpad\\n\\n### Plan\\n- [x] Task 1 (done)"');
|
|
1501
|
+
lines.push("```");
|
|
1502
|
+
lines.push("");
|
|
1503
|
+
lines.push("### Create Follow-up Issue");
|
|
1504
|
+
lines.push("");
|
|
1505
|
+
lines.push("```bash");
|
|
1506
|
+
lines.push("gh issue create --repo <owner>/<repo> \\");
|
|
1507
|
+
lines.push(' --title "Follow-up: <title>" \\');
|
|
1508
|
+
lines.push(' --body "<description>" \\');
|
|
1509
|
+
lines.push(' --label "backlog"');
|
|
1510
|
+
lines.push("```");
|
|
1511
|
+
lines.push("");
|
|
1512
|
+
lines.push("### Add Label");
|
|
1513
|
+
lines.push("");
|
|
1514
|
+
lines.push("```bash");
|
|
1515
|
+
lines.push(
|
|
1516
|
+
'gh issue edit <issue-number> --repo <owner>/<repo> --add-label "<label>"'
|
|
1517
|
+
);
|
|
1518
|
+
lines.push("```");
|
|
1519
|
+
lines.push("");
|
|
1520
|
+
lines.push("## Rules");
|
|
1521
|
+
lines.push("");
|
|
1522
|
+
lines.push(
|
|
1523
|
+
"- Always follow the WORKFLOW.md status map flow for state transitions"
|
|
1524
|
+
);
|
|
1525
|
+
lines.push(
|
|
1526
|
+
"- Before transitioning to a terminal state, verify the Completion Bar is satisfied:"
|
|
1527
|
+
);
|
|
1528
|
+
lines.push(" - All acceptance criteria checked");
|
|
1529
|
+
lines.push(" - All tests passing");
|
|
1530
|
+
lines.push(" - PR merged (if applicable)");
|
|
1531
|
+
lines.push(
|
|
1532
|
+
"- Use the Column ID Quick Reference table above for all status transitions"
|
|
1533
|
+
);
|
|
1534
|
+
lines.push(
|
|
1535
|
+
"- Do not transition issues to terminal states without explicit completion verification"
|
|
1536
|
+
);
|
|
1537
|
+
return renderSkillDocument({
|
|
1538
|
+
name: "gh-project",
|
|
1539
|
+
description: "Manage GitHub Project v2 issue states, workpad comments, and related follow-up actions.",
|
|
1540
|
+
bodyLines: lines
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
// src/skills/templates/commit.ts
|
|
1545
|
+
function generateCommitSkill(_ctx) {
|
|
1546
|
+
const lines = [];
|
|
1547
|
+
lines.push("# /commit \u2014 Clean Commit Workflow");
|
|
1548
|
+
lines.push("");
|
|
1549
|
+
lines.push("## Trigger");
|
|
1550
|
+
lines.push("");
|
|
1551
|
+
lines.push("Use this skill when creating commits during implementation.");
|
|
1552
|
+
lines.push("");
|
|
1553
|
+
lines.push("## Rules");
|
|
1554
|
+
lines.push("");
|
|
1555
|
+
lines.push("- Commit in logical units \u2014 one concern per commit");
|
|
1556
|
+
lines.push("- Never commit a broken intermediate state (tests must pass)");
|
|
1557
|
+
lines.push("- Never commit temporary debug code or commented-out blocks");
|
|
1558
|
+
lines.push("- Run tests before every commit");
|
|
1559
|
+
lines.push("");
|
|
1560
|
+
lines.push("## Format");
|
|
1561
|
+
lines.push("");
|
|
1562
|
+
lines.push("Use Conventional Commit format:");
|
|
1563
|
+
lines.push("");
|
|
1564
|
+
lines.push("```");
|
|
1565
|
+
lines.push("<type>(<scope>): <description>");
|
|
1566
|
+
lines.push("");
|
|
1567
|
+
lines.push("[optional body \u2014 explain WHY, not WHAT, 72 chars/line]");
|
|
1568
|
+
lines.push("");
|
|
1569
|
+
lines.push("[optional footer: Closes #N]");
|
|
1570
|
+
lines.push("```");
|
|
1571
|
+
lines.push("");
|
|
1572
|
+
lines.push("**Types**: `feat`, `fix`, `refactor`, `test`, `docs`, `chore`");
|
|
1573
|
+
lines.push("");
|
|
1574
|
+
lines.push(
|
|
1575
|
+
"**Description**: imperative mood, 50 chars max, no period at end"
|
|
1576
|
+
);
|
|
1577
|
+
lines.push("");
|
|
1578
|
+
lines.push("## Examples");
|
|
1579
|
+
lines.push("");
|
|
1580
|
+
lines.push("```");
|
|
1581
|
+
lines.push("feat(auth): add OAuth2 token refresh");
|
|
1582
|
+
lines.push("fix(api): handle null response from upstream");
|
|
1583
|
+
lines.push("test(worker): add retry exhaustion coverage");
|
|
1584
|
+
lines.push("```");
|
|
1585
|
+
return renderSkillDocument({
|
|
1586
|
+
name: "commit",
|
|
1587
|
+
description: "Create clean, logically scoped commits that keep the repository in a shippable state.",
|
|
1588
|
+
bodyLines: lines
|
|
1589
|
+
});
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
// src/skills/templates/push.ts
|
|
1593
|
+
function generatePushSkill(_ctx) {
|
|
1594
|
+
const lines = [];
|
|
1595
|
+
lines.push("# /push \u2014 Git Push Workflow");
|
|
1596
|
+
lines.push("");
|
|
1597
|
+
lines.push("## Trigger");
|
|
1598
|
+
lines.push("");
|
|
1599
|
+
lines.push(
|
|
1600
|
+
"Use this skill when publishing local commits to the remote branch."
|
|
1601
|
+
);
|
|
1602
|
+
lines.push("");
|
|
1603
|
+
lines.push("## Flow");
|
|
1604
|
+
lines.push("");
|
|
1605
|
+
lines.push("1. Run local tests and lint \u2014 ensure they pass before pushing");
|
|
1606
|
+
lines.push("2. Push to remote:");
|
|
1607
|
+
lines.push(" ```bash");
|
|
1608
|
+
lines.push(" git push origin <branch> # subsequent pushes");
|
|
1609
|
+
lines.push(" git push -u origin <branch> # first push (sets upstream)");
|
|
1610
|
+
lines.push(" ```");
|
|
1611
|
+
lines.push("3. If push is rejected (non-fast-forward):");
|
|
1612
|
+
lines.push(" - Run `git fetch origin && git merge origin/main`");
|
|
1613
|
+
lines.push(" - Resolve any conflicts");
|
|
1614
|
+
lines.push(" - Re-run tests");
|
|
1615
|
+
lines.push(" - Push again");
|
|
1616
|
+
lines.push("4. Record push result in workpad Notes");
|
|
1617
|
+
lines.push("");
|
|
1618
|
+
lines.push("## Rules");
|
|
1619
|
+
lines.push("");
|
|
1620
|
+
lines.push("- Never use `--force` (destructive)");
|
|
1621
|
+
lines.push(
|
|
1622
|
+
"- Only use `--force-with-lease` if absolutely necessary \u2014 record the reason in workpad"
|
|
1623
|
+
);
|
|
1624
|
+
lines.push("- Verify CI starts after push (check GitHub Actions tab)");
|
|
1625
|
+
lines.push("- Do not push directly to `main` or `master`");
|
|
1626
|
+
return renderSkillDocument({
|
|
1627
|
+
name: "push",
|
|
1628
|
+
description: "Publish verified local commits to the remote branch without unsafe force pushes.",
|
|
1629
|
+
bodyLines: lines
|
|
1630
|
+
});
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
// src/skills/templates/pull.ts
|
|
1634
|
+
function generatePullSkill(_ctx) {
|
|
1635
|
+
const lines = [];
|
|
1636
|
+
lines.push("# /pull \u2014 Git Pull / Sync Workflow");
|
|
1637
|
+
lines.push("");
|
|
1638
|
+
lines.push("## Trigger");
|
|
1639
|
+
lines.push("");
|
|
1640
|
+
lines.push(
|
|
1641
|
+
"Use this skill to sync the current branch with the latest `origin/main`"
|
|
1642
|
+
);
|
|
1643
|
+
lines.push("before starting work or before creating a PR.");
|
|
1644
|
+
lines.push("");
|
|
1645
|
+
lines.push("## Flow");
|
|
1646
|
+
lines.push("");
|
|
1647
|
+
lines.push("1. Fetch latest from remote:");
|
|
1648
|
+
lines.push(" ```bash");
|
|
1649
|
+
lines.push(" git fetch origin");
|
|
1650
|
+
lines.push(" ```");
|
|
1651
|
+
lines.push("2. Merge into current branch:");
|
|
1652
|
+
lines.push(" ```bash");
|
|
1653
|
+
lines.push(" git merge origin/main");
|
|
1654
|
+
lines.push(" ```");
|
|
1655
|
+
lines.push("3. If conflicts arise:");
|
|
1656
|
+
lines.push(" - Resolve each conflict file");
|
|
1657
|
+
lines.push(" - Run tests to confirm nothing broke");
|
|
1658
|
+
lines.push(
|
|
1659
|
+
" - Commit the merge: `git commit` (merge commit message is auto-generated)"
|
|
1660
|
+
);
|
|
1661
|
+
lines.push(
|
|
1662
|
+
"4. Re-run tests after merge to confirm the integrated state is clean"
|
|
1663
|
+
);
|
|
1664
|
+
lines.push("5. Record pull skill evidence in workpad Notes:");
|
|
1665
|
+
lines.push(" - merge source (e.g. `origin/main`)");
|
|
1666
|
+
lines.push(" - result: `clean` or `conflicts resolved`");
|
|
1667
|
+
lines.push(" - resulting HEAD short SHA: `git rev-parse --short HEAD`");
|
|
1668
|
+
lines.push("");
|
|
1669
|
+
lines.push("## Rules");
|
|
1670
|
+
lines.push("");
|
|
1671
|
+
lines.push("- Always pull before creating a PR");
|
|
1672
|
+
lines.push("- Always pull at the start of a new work session");
|
|
1673
|
+
lines.push("- Record the pull evidence in the workpad before proceeding");
|
|
1674
|
+
return renderSkillDocument({
|
|
1675
|
+
name: "pull",
|
|
1676
|
+
description: "Sync the current branch with the latest remote base before implementation or review handoff.",
|
|
1677
|
+
bodyLines: lines
|
|
1678
|
+
});
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
// src/skills/templates/land.ts
|
|
1682
|
+
function generateLandSkill(_ctx) {
|
|
1683
|
+
const lines = [];
|
|
1684
|
+
lines.push("# /land \u2014 PR Merge Workflow");
|
|
1685
|
+
lines.push("");
|
|
1686
|
+
lines.push("## Trigger");
|
|
1687
|
+
lines.push("");
|
|
1688
|
+
lines.push(
|
|
1689
|
+
"Use this skill when the issue is in the Merging state (PR approved by human)."
|
|
1690
|
+
);
|
|
1691
|
+
lines.push(
|
|
1692
|
+
"Do NOT call `gh pr merge` directly \u2014 always go through this flow."
|
|
1693
|
+
);
|
|
1694
|
+
lines.push("");
|
|
1695
|
+
lines.push("## Pre-flight Checks");
|
|
1696
|
+
lines.push("");
|
|
1697
|
+
lines.push("Before merging, verify ALL of the following:");
|
|
1698
|
+
lines.push("");
|
|
1699
|
+
lines.push("1. **PR is approved**:");
|
|
1700
|
+
lines.push(" ```bash");
|
|
1701
|
+
lines.push(
|
|
1702
|
+
` gh pr view --json reviews --jq '.reviews[] | select(.state == "APPROVED")'`
|
|
1703
|
+
);
|
|
1704
|
+
lines.push(" ```");
|
|
1705
|
+
lines.push("2. **All CI checks are green**:");
|
|
1706
|
+
lines.push(" ```bash");
|
|
1707
|
+
lines.push(" gh pr checks");
|
|
1708
|
+
lines.push(" ```");
|
|
1709
|
+
lines.push("3. **Branch is up-to-date with base**:");
|
|
1710
|
+
lines.push(" ```bash");
|
|
1711
|
+
lines.push(
|
|
1712
|
+
" git fetch origin && git merge-base --is-ancestor origin/main HEAD"
|
|
1713
|
+
);
|
|
1714
|
+
lines.push(" ```");
|
|
1715
|
+
lines.push(" If not up-to-date, run the `/pull` skill first.");
|
|
1716
|
+
lines.push("");
|
|
1717
|
+
lines.push("## Flow");
|
|
1718
|
+
lines.push("");
|
|
1719
|
+
lines.push("1. Run all pre-flight checks above");
|
|
1720
|
+
lines.push("2. If all checks pass, merge the PR:");
|
|
1721
|
+
lines.push(" ```bash");
|
|
1722
|
+
lines.push(" gh pr merge --squash # squash merge (default)");
|
|
1723
|
+
lines.push(" # or: gh pr merge --merge # merge commit");
|
|
1724
|
+
lines.push(" # or: gh pr merge --rebase # rebase merge");
|
|
1725
|
+
lines.push(" ```");
|
|
1726
|
+
lines.push(" Choose the merge strategy per project policy.");
|
|
1727
|
+
lines.push("3. On merge success:");
|
|
1728
|
+
lines.push(
|
|
1729
|
+
" - Use the **gh-project skill** to transition the issue status to Done"
|
|
1730
|
+
);
|
|
1731
|
+
lines.push(" - Do NOT call status APIs directly \u2014 delegate to gh-project");
|
|
1732
|
+
lines.push("4. On merge failure:");
|
|
1733
|
+
lines.push(" - Record the failure reason in workpad Notes");
|
|
1734
|
+
lines.push(" - Resolve the blocking issue (re-run pre-flight checks)");
|
|
1735
|
+
lines.push(" - Retry the merge");
|
|
1736
|
+
lines.push("5. Loop until merged or blocked by an unresolvable issue");
|
|
1737
|
+
lines.push("");
|
|
1738
|
+
lines.push("## Rules");
|
|
1739
|
+
lines.push("");
|
|
1740
|
+
lines.push("- Never call `gh pr merge` without completing pre-flight checks");
|
|
1741
|
+
lines.push(
|
|
1742
|
+
"- Status transition to Done MUST go through the gh-project skill"
|
|
1743
|
+
);
|
|
1744
|
+
lines.push(
|
|
1745
|
+
"- If any pre-flight check fails, do not merge \u2014 fix the issue first"
|
|
1746
|
+
);
|
|
1747
|
+
lines.push("- Record all merge attempts and outcomes in the workpad");
|
|
1748
|
+
return renderSkillDocument({
|
|
1749
|
+
name: "land",
|
|
1750
|
+
description: "Merge approved pull requests safely after verifying approvals, CI, and branch freshness.",
|
|
1751
|
+
bodyLines: lines
|
|
1752
|
+
});
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
// src/skills/templates/index.ts
|
|
1756
|
+
var ALL_SKILL_TEMPLATES = [
|
|
1757
|
+
{
|
|
1758
|
+
name: "gh-symphony",
|
|
1759
|
+
fileName: "SKILL.md",
|
|
1760
|
+
generate: generateGhSymphonySkill
|
|
1761
|
+
},
|
|
1762
|
+
{
|
|
1763
|
+
name: "gh-project",
|
|
1764
|
+
fileName: "SKILL.md",
|
|
1765
|
+
generate: generateGhProjectSkill
|
|
1766
|
+
},
|
|
1767
|
+
{ name: "commit", fileName: "SKILL.md", generate: generateCommitSkill },
|
|
1768
|
+
{ name: "push", fileName: "SKILL.md", generate: generatePushSkill },
|
|
1769
|
+
{ name: "pull", fileName: "SKILL.md", generate: generatePullSkill },
|
|
1770
|
+
{ name: "land", fileName: "SKILL.md", generate: generateLandSkill }
|
|
1771
|
+
];
|
|
1772
|
+
|
|
1773
|
+
// src/commands/init.ts
|
|
1774
|
+
var KNOWN_REQUIRED_SCOPES = ["repo", "read:org", "project"];
|
|
1775
|
+
function displayScopeError(error, retryCommand) {
|
|
1776
|
+
const plural = error.requiredScopes.length === 1 ? "" : "s";
|
|
1777
|
+
p.log.error(
|
|
1778
|
+
`Token is missing required scope${plural}: ${error.requiredScopes.join(", ")}`
|
|
1779
|
+
);
|
|
1780
|
+
const currentSet = new Set(error.currentScopes.map((s) => s.toLowerCase()));
|
|
1781
|
+
const scopesToAdd = KNOWN_REQUIRED_SCOPES.filter((s) => !currentSet.has(s));
|
|
1782
|
+
const scopeArg = scopesToAdd.length > 0 ? scopesToAdd.join(",") : error.requiredScopes.join(",");
|
|
1783
|
+
p.note(
|
|
1784
|
+
`gh auth refresh --scopes ${scopeArg}
|
|
1785
|
+
|
|
1786
|
+
Then re-run: ${retryCommand}`,
|
|
1787
|
+
"Fix missing scope"
|
|
1788
|
+
);
|
|
1789
|
+
}
|
|
1790
|
+
async function abortIfCancelled(input) {
|
|
1791
|
+
const result = await input;
|
|
1792
|
+
if (p.isCancel(result)) {
|
|
1793
|
+
p.cancel("Setup cancelled.");
|
|
1794
|
+
process.exit(130);
|
|
1795
|
+
}
|
|
1796
|
+
return result;
|
|
1797
|
+
}
|
|
1798
|
+
function parseInitFlags(args) {
|
|
1799
|
+
const flags = {
|
|
1800
|
+
nonInteractive: false,
|
|
1801
|
+
skipSkills: false,
|
|
1802
|
+
skipContext: false
|
|
1803
|
+
};
|
|
1804
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
1805
|
+
const arg = args[i];
|
|
1806
|
+
const next = args[i + 1];
|
|
1807
|
+
switch (arg) {
|
|
1808
|
+
case "--non-interactive":
|
|
1809
|
+
flags.nonInteractive = true;
|
|
1810
|
+
break;
|
|
1811
|
+
case "--project":
|
|
1812
|
+
flags.project = next;
|
|
1813
|
+
i += 1;
|
|
1814
|
+
break;
|
|
1815
|
+
case "--output":
|
|
1816
|
+
flags.output = next;
|
|
1817
|
+
i += 1;
|
|
1818
|
+
break;
|
|
1819
|
+
case "--skip-skills":
|
|
1820
|
+
flags.skipSkills = true;
|
|
1821
|
+
break;
|
|
1822
|
+
case "--skip-context":
|
|
1823
|
+
flags.skipContext = true;
|
|
1824
|
+
break;
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
return flags;
|
|
1828
|
+
}
|
|
1829
|
+
var handler = async (args, options) => {
|
|
1830
|
+
const flags = parseInitFlags(args);
|
|
1831
|
+
if (flags.nonInteractive) {
|
|
1832
|
+
await runNonInteractive(flags, options);
|
|
1833
|
+
return;
|
|
1834
|
+
}
|
|
1835
|
+
await runInteractive(options);
|
|
1836
|
+
};
|
|
1837
|
+
var init_default = handler;
|
|
1838
|
+
async function writeEcosystem(opts) {
|
|
1839
|
+
const { cwd, projectDetail, statusField, runtime, skipSkills, skipContext } = opts;
|
|
1840
|
+
const ghSymphonyDir = join3(cwd, ".gh-symphony");
|
|
1841
|
+
await mkdir3(ghSymphonyDir, { recursive: true });
|
|
1842
|
+
const env = await detectEnvironment(cwd);
|
|
1843
|
+
let contextYamlWritten = false;
|
|
1844
|
+
if (!skipContext) {
|
|
1845
|
+
const contextYaml = buildContextYaml({
|
|
1846
|
+
projectDetail,
|
|
1847
|
+
statusField,
|
|
1848
|
+
detectedEnvironment: env,
|
|
1849
|
+
runtime: {
|
|
1850
|
+
agent: runtime,
|
|
1851
|
+
agent_command: runtime === "codex" ? "bash -lc codex app-server" : runtime === "claude-code" ? "bash -lc claude-code" : runtime
|
|
1852
|
+
}
|
|
1853
|
+
});
|
|
1854
|
+
await writeContextYaml(cwd, contextYaml);
|
|
1855
|
+
contextYamlWritten = true;
|
|
1856
|
+
}
|
|
1857
|
+
const refWorkflow = generateReferenceWorkflow({
|
|
1858
|
+
runtime,
|
|
1859
|
+
statusColumns: statusField.options.map((o) => ({
|
|
1860
|
+
name: o.name,
|
|
1861
|
+
role: null
|
|
1862
|
+
})),
|
|
1863
|
+
projectId: projectDetail.id
|
|
1864
|
+
});
|
|
1865
|
+
const refPath = join3(ghSymphonyDir, "reference-workflow.md");
|
|
1866
|
+
const tmpRef = refPath + ".tmp";
|
|
1867
|
+
await writeFile3(tmpRef, refWorkflow, "utf8");
|
|
1868
|
+
await rename(tmpRef, refPath);
|
|
1869
|
+
const skillsDir = resolveSkillsDir(cwd, runtime);
|
|
1870
|
+
let skillsWritten = [];
|
|
1871
|
+
let skillsSkipped = [];
|
|
1872
|
+
if (!skipSkills && skillsDir) {
|
|
1873
|
+
const result = await writeAllSkills(cwd, runtime, ALL_SKILL_TEMPLATES, {
|
|
1874
|
+
runtime,
|
|
1875
|
+
projectId: projectDetail.id,
|
|
1876
|
+
githubProjectTitle: projectDetail.title,
|
|
1877
|
+
repositories: projectDetail.linkedRepositories.map((r) => ({
|
|
1878
|
+
owner: r.owner,
|
|
1879
|
+
name: r.name
|
|
1880
|
+
})),
|
|
1881
|
+
statusColumns: statusField.options.map((o) => ({
|
|
1882
|
+
id: o.id,
|
|
1883
|
+
name: o.name,
|
|
1884
|
+
role: null
|
|
1885
|
+
})),
|
|
1886
|
+
statusFieldId: statusField.id,
|
|
1887
|
+
contextYamlPath: ".gh-symphony/context.yaml",
|
|
1888
|
+
referenceWorkflowPath: ".gh-symphony/reference-workflow.md"
|
|
1889
|
+
});
|
|
1890
|
+
skillsWritten = result.written.map((p2) => basename(dirname2(p2)));
|
|
1891
|
+
skillsSkipped = result.skipped.map((p2) => basename(dirname2(p2)));
|
|
1892
|
+
}
|
|
1893
|
+
return {
|
|
1894
|
+
projectId: projectDetail.id,
|
|
1895
|
+
githubProjectTitle: projectDetail.title,
|
|
1896
|
+
runtime,
|
|
1897
|
+
skillsDir,
|
|
1898
|
+
contextYamlWritten,
|
|
1899
|
+
referenceWorkflowWritten: true,
|
|
1900
|
+
skillsWritten,
|
|
1901
|
+
skillsSkipped
|
|
1902
|
+
};
|
|
1903
|
+
}
|
|
1904
|
+
function printEcosystemSummary(result, workflowPath, opts) {
|
|
1905
|
+
const cwd = process.cwd();
|
|
1906
|
+
const relWorkflow = relative(cwd, workflowPath) || "WORKFLOW.md";
|
|
1907
|
+
const lines = [];
|
|
1908
|
+
lines.push(`GitHub Project ${result.githubProjectTitle} (${result.projectId})`);
|
|
1909
|
+
lines.push(`Runtime ${result.runtime}`);
|
|
1910
|
+
lines.push("");
|
|
1911
|
+
lines.push("Generated files");
|
|
1912
|
+
lines.push(` \u2713 WORKFLOW.md ${relWorkflow}`);
|
|
1913
|
+
if (result.contextYamlWritten) {
|
|
1914
|
+
lines.push(
|
|
1915
|
+
" \u2713 Context metadata .gh-symphony/context.yaml"
|
|
1916
|
+
);
|
|
1917
|
+
}
|
|
1918
|
+
if (result.referenceWorkflowWritten) {
|
|
1919
|
+
lines.push(
|
|
1920
|
+
" \u2713 Reference workflow .gh-symphony/reference-workflow.md"
|
|
1921
|
+
);
|
|
1922
|
+
}
|
|
1923
|
+
if (result.skillsDir) {
|
|
1924
|
+
const relSkillsDir = relative(cwd, result.skillsDir);
|
|
1925
|
+
lines.push("");
|
|
1926
|
+
lines.push(`Skills \u2192 ${relSkillsDir}/`);
|
|
1927
|
+
for (const name of result.skillsWritten) {
|
|
1928
|
+
lines.push(` \u2713 ${name}`);
|
|
1929
|
+
}
|
|
1930
|
+
for (const name of result.skillsSkipped) {
|
|
1931
|
+
lines.push(` \u2013 ${name} (already exists, skipped)`);
|
|
1932
|
+
}
|
|
1933
|
+
} else if (result.runtime !== "codex" && result.runtime !== "claude-code") {
|
|
1934
|
+
lines.push("");
|
|
1935
|
+
lines.push("Skills \u2192 (skipped \u2014 custom runtime)");
|
|
1936
|
+
}
|
|
1937
|
+
if (opts.interactive) {
|
|
1938
|
+
p.note(lines.join("\n"), "Setup complete");
|
|
1939
|
+
p.outro(opts.nextSteps ?? "Ready.");
|
|
1940
|
+
} else {
|
|
1941
|
+
process.stdout.write(lines.map((l) => ` ${l}`).join("\n") + "\n");
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
async function runNonInteractive(flags, options) {
|
|
1945
|
+
let token;
|
|
1946
|
+
try {
|
|
1947
|
+
token = getGhToken();
|
|
1948
|
+
} catch {
|
|
1949
|
+
process.stderr.write(
|
|
1950
|
+
"Error: GitHub token not found. Run 'gh auth login --scopes repo,read:org,project' or set GITHUB_GRAPHQL_TOKEN.\n"
|
|
1951
|
+
);
|
|
1952
|
+
process.exitCode = 1;
|
|
1953
|
+
return;
|
|
1954
|
+
}
|
|
1955
|
+
const client = createClient(token);
|
|
1956
|
+
let viewer;
|
|
1957
|
+
try {
|
|
1958
|
+
viewer = await validateToken(client);
|
|
1959
|
+
} catch {
|
|
1960
|
+
process.stderr.write("Error: Invalid GitHub token.\n");
|
|
1961
|
+
process.exitCode = 1;
|
|
1962
|
+
return;
|
|
1963
|
+
}
|
|
1964
|
+
const scopeCheck = checkRequiredScopes(viewer.scopes);
|
|
1965
|
+
if (!scopeCheck.valid) {
|
|
1966
|
+
process.stderr.write(
|
|
1967
|
+
`Error: Missing required PAT scopes: ${scopeCheck.missing.join(", ")}
|
|
1968
|
+
`
|
|
1969
|
+
);
|
|
1970
|
+
process.exitCode = 1;
|
|
1971
|
+
return;
|
|
1972
|
+
}
|
|
1973
|
+
const projects = await listUserProjects(client);
|
|
1974
|
+
let githubProject;
|
|
1975
|
+
if (flags.project) {
|
|
1976
|
+
const match = projects.find(
|
|
1977
|
+
(proj) => proj.id === flags.project || proj.url === flags.project
|
|
1978
|
+
);
|
|
1979
|
+
if (!match) {
|
|
1980
|
+
process.stderr.write(`Error: Project not found: ${flags.project}
|
|
1981
|
+
`);
|
|
1982
|
+
process.exitCode = 1;
|
|
1983
|
+
return;
|
|
1984
|
+
}
|
|
1985
|
+
githubProject = await getProjectDetail(client, match.id);
|
|
1986
|
+
} else if (projects.length === 1) {
|
|
1987
|
+
githubProject = await getProjectDetail(client, projects[0].id);
|
|
1988
|
+
} else {
|
|
1989
|
+
process.stderr.write(
|
|
1990
|
+
"Error: --project is required when multiple projects exist.\n"
|
|
1991
|
+
);
|
|
1992
|
+
process.exitCode = 1;
|
|
1993
|
+
return;
|
|
1994
|
+
}
|
|
1995
|
+
const statusField = githubProject.statusFields.find((f) => f.name.toLowerCase() === "status") ?? githubProject.statusFields[0];
|
|
1996
|
+
if (!statusField) {
|
|
1997
|
+
process.stderr.write("Error: No status field found on the project.\n");
|
|
1998
|
+
process.exitCode = 1;
|
|
1999
|
+
return;
|
|
2000
|
+
}
|
|
2001
|
+
const columnNames = statusField.options.map((o) => o.name);
|
|
2002
|
+
const inferred = inferAllStateRoles(columnNames);
|
|
2003
|
+
const mappings = {};
|
|
2004
|
+
for (const mapping of inferred) {
|
|
2005
|
+
if (mapping.role) {
|
|
2006
|
+
mappings[mapping.columnName] = { role: mapping.role };
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
const validation = validateStateMapping(mappings);
|
|
2010
|
+
if (!validation.valid) {
|
|
2011
|
+
process.stderr.write(
|
|
2012
|
+
`Error: Cannot auto-map columns. ${validation.errors.join("; ")}
|
|
2013
|
+
Run without --non-interactive for manual mapping.
|
|
2014
|
+
`
|
|
2015
|
+
);
|
|
2016
|
+
process.exitCode = 1;
|
|
2017
|
+
return;
|
|
2018
|
+
}
|
|
2019
|
+
const lifecycleConfig = toWorkflowLifecycleConfig(statusField.name, mappings);
|
|
2020
|
+
const outputPath = resolve(flags.output ?? "WORKFLOW.md");
|
|
2021
|
+
const workflowMd = generateWorkflowMarkdown({
|
|
2022
|
+
projectId: githubProject.id,
|
|
2023
|
+
stateFieldName: statusField.name,
|
|
2024
|
+
mappings,
|
|
2025
|
+
lifecycle: lifecycleConfig,
|
|
2026
|
+
runtime: "codex"
|
|
2027
|
+
});
|
|
2028
|
+
await writeFile3(outputPath, workflowMd, "utf8");
|
|
2029
|
+
const ecosystemResult = await writeEcosystem({
|
|
2030
|
+
cwd: process.cwd(),
|
|
2031
|
+
projectDetail: githubProject,
|
|
2032
|
+
statusField,
|
|
2033
|
+
runtime: "codex",
|
|
2034
|
+
skipSkills: flags.skipSkills,
|
|
2035
|
+
skipContext: flags.skipContext
|
|
2036
|
+
});
|
|
2037
|
+
if (options.json) {
|
|
2038
|
+
process.stdout.write(
|
|
2039
|
+
JSON.stringify({ output: outputPath, status: "created" }) + "\n"
|
|
2040
|
+
);
|
|
2041
|
+
} else {
|
|
2042
|
+
printEcosystemSummary(ecosystemResult, outputPath, {
|
|
2043
|
+
interactive: false,
|
|
2044
|
+
nextSteps: "Run 'gh-symphony project add' to register a project."
|
|
2045
|
+
});
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
async function runInteractive(options) {
|
|
2049
|
+
p.intro("gh-symphony \u2014 WORKFLOW.md Setup");
|
|
2050
|
+
await runInteractiveStandalone(options);
|
|
2051
|
+
}
|
|
2052
|
+
async function runInteractiveStandalone(_options) {
|
|
2053
|
+
const s1 = p.spinner();
|
|
2054
|
+
s1.start("Checking gh CLI authentication...");
|
|
2055
|
+
let client;
|
|
2056
|
+
try {
|
|
2057
|
+
const { token } = ensureGhAuth();
|
|
2058
|
+
client = createClient(token);
|
|
2059
|
+
s1.stop("Authenticated via gh CLI");
|
|
2060
|
+
} catch (error) {
|
|
2061
|
+
s1.stop("Authentication failed.");
|
|
2062
|
+
if (error instanceof GhAuthError) {
|
|
2063
|
+
if (error.code === "not_installed") {
|
|
2064
|
+
p.log.error(
|
|
2065
|
+
"gh CLI\uAC00 \uC124\uCE58\uB418\uC5B4 \uC788\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. https://cli.github.com \uC5D0\uC11C \uC124\uCE58\uD558\uC138\uC694."
|
|
2066
|
+
);
|
|
2067
|
+
} else if (error.code === "not_authenticated") {
|
|
2068
|
+
p.log.error(
|
|
2069
|
+
"gh auth login --scopes repo,read:org,project \uB97C \uC2E4\uD589\uD558\uC138\uC694."
|
|
2070
|
+
);
|
|
2071
|
+
} else if (error.code === "missing_scopes") {
|
|
2072
|
+
p.log.error(
|
|
2073
|
+
"gh auth refresh --scopes repo,read:org,project \uB97C \uC2E4\uD589\uD558\uC138\uC694."
|
|
2074
|
+
);
|
|
2075
|
+
} else {
|
|
2076
|
+
p.log.error(error.message);
|
|
2077
|
+
}
|
|
2078
|
+
} else {
|
|
2079
|
+
p.log.error(error instanceof Error ? error.message : "Unknown error");
|
|
2080
|
+
}
|
|
2081
|
+
process.exitCode = 1;
|
|
2082
|
+
return;
|
|
2083
|
+
}
|
|
2084
|
+
const s2 = p.spinner();
|
|
2085
|
+
s2.start("Loading projects...");
|
|
2086
|
+
let projects;
|
|
2087
|
+
try {
|
|
2088
|
+
projects = await listUserProjects(client);
|
|
2089
|
+
s2.stop(
|
|
2090
|
+
`Found ${projects.length} project${projects.length === 1 ? "" : "s"}`
|
|
2091
|
+
);
|
|
2092
|
+
} catch (error) {
|
|
2093
|
+
s2.stop("Failed to load projects.");
|
|
2094
|
+
if (error instanceof GitHubScopeError) {
|
|
2095
|
+
displayScopeError(error, "gh-symphony init");
|
|
2096
|
+
} else {
|
|
2097
|
+
p.log.error(error instanceof Error ? error.message : "Unknown error");
|
|
2098
|
+
}
|
|
2099
|
+
process.exitCode = 1;
|
|
2100
|
+
return;
|
|
2101
|
+
}
|
|
2102
|
+
if (projects.length === 0) {
|
|
2103
|
+
p.log.error(
|
|
2104
|
+
"No GitHub Projects found. Create a project at https://github.com/orgs/YOUR_ORG/projects and re-run."
|
|
2105
|
+
);
|
|
2106
|
+
process.exitCode = 1;
|
|
2107
|
+
return;
|
|
2108
|
+
}
|
|
2109
|
+
const selectedGithubProjectId = await abortIfCancelled(
|
|
2110
|
+
p.select({
|
|
2111
|
+
message: "Step 1/2 \u2014 Select a GitHub Project board:",
|
|
2112
|
+
options: projects.map((proj) => ({
|
|
2113
|
+
value: proj.id,
|
|
2114
|
+
label: `${proj.owner.login}/${proj.title}`,
|
|
2115
|
+
hint: `${proj.openItemCount} items`
|
|
2116
|
+
})),
|
|
2117
|
+
maxItems: 15
|
|
2118
|
+
})
|
|
2119
|
+
);
|
|
2120
|
+
const s2d = p.spinner();
|
|
2121
|
+
s2d.start("Loading project details...");
|
|
2122
|
+
let projectDetail;
|
|
2123
|
+
try {
|
|
2124
|
+
projectDetail = await getProjectDetail(client, selectedGithubProjectId);
|
|
2125
|
+
s2d.stop(`Loaded: ${projectDetail.title}`);
|
|
2126
|
+
} catch (error) {
|
|
2127
|
+
s2d.stop("Failed to load project details.");
|
|
2128
|
+
p.log.error(error instanceof Error ? error.message : "Unknown error");
|
|
2129
|
+
process.exitCode = 1;
|
|
2130
|
+
return;
|
|
2131
|
+
}
|
|
2132
|
+
const statusField = projectDetail.statusFields.find((f) => f.name.toLowerCase() === "status") ?? projectDetail.statusFields[0];
|
|
2133
|
+
if (!statusField) {
|
|
2134
|
+
p.log.error(
|
|
2135
|
+
"No status field found on the project. The project needs a single-select 'Status' field."
|
|
2136
|
+
);
|
|
2137
|
+
process.exitCode = 1;
|
|
2138
|
+
return;
|
|
2139
|
+
}
|
|
2140
|
+
const columnNames = statusField.options.map((o) => o.name);
|
|
2141
|
+
const inferred = inferAllStateRoles(columnNames);
|
|
2142
|
+
p.log.info(
|
|
2143
|
+
`Found ${columnNames.length} status columns on field "${statusField.name}".`
|
|
2144
|
+
);
|
|
2145
|
+
const mappings = {};
|
|
2146
|
+
for (const mapping of inferred) {
|
|
2147
|
+
const roleOptions = [
|
|
2148
|
+
{ value: "active", label: "Active (agent works on this)" },
|
|
2149
|
+
{ value: "wait", label: "Wait (human review / hold)" },
|
|
2150
|
+
{ value: "terminal", label: "Terminal (completed)" }
|
|
2151
|
+
];
|
|
2152
|
+
const defaultRole = mapping.role ?? "wait";
|
|
2153
|
+
const sortedOptions = [
|
|
2154
|
+
roleOptions.find((o) => o.value === defaultRole),
|
|
2155
|
+
...roleOptions.filter((o) => o.value !== defaultRole)
|
|
2156
|
+
];
|
|
2157
|
+
const selectedRole = await abortIfCancelled(
|
|
2158
|
+
p.select({
|
|
2159
|
+
message: `Step 2/2 \u2014 Map column "${mapping.columnName}":${mapping.confidence === "high" ? " (auto-detected)" : ""}`,
|
|
2160
|
+
options: sortedOptions
|
|
2161
|
+
})
|
|
2162
|
+
);
|
|
2163
|
+
if (selectedRole !== "skip") {
|
|
2164
|
+
mappings[mapping.columnName] = { role: selectedRole };
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
const validation = validateStateMapping(mappings);
|
|
2168
|
+
if (!validation.valid) {
|
|
2169
|
+
p.log.error("Mapping validation failed:");
|
|
2170
|
+
for (const err of validation.errors) {
|
|
2171
|
+
p.log.error(` \u2022 ${err}`);
|
|
2172
|
+
}
|
|
2173
|
+
process.exitCode = 1;
|
|
2174
|
+
return;
|
|
2175
|
+
}
|
|
2176
|
+
for (const warn of validation.warnings) {
|
|
2177
|
+
p.log.warn(` \u26A0 ${warn}`);
|
|
2178
|
+
}
|
|
2179
|
+
const lifecycleConfig = toWorkflowLifecycleConfig(statusField.name, mappings);
|
|
2180
|
+
const workflowMd = generateWorkflowMarkdown({
|
|
2181
|
+
projectId: projectDetail.id,
|
|
2182
|
+
stateFieldName: statusField.name,
|
|
2183
|
+
mappings,
|
|
2184
|
+
lifecycle: lifecycleConfig,
|
|
2185
|
+
runtime: "codex"
|
|
2186
|
+
});
|
|
2187
|
+
const outputPath = resolve("WORKFLOW.md");
|
|
2188
|
+
await writeFile3(outputPath, workflowMd, "utf8");
|
|
2189
|
+
const ecosystemResult = await writeEcosystem({
|
|
2190
|
+
cwd: process.cwd(),
|
|
2191
|
+
projectDetail,
|
|
2192
|
+
statusField,
|
|
2193
|
+
runtime: "codex",
|
|
2194
|
+
skipSkills: false,
|
|
2195
|
+
skipContext: false
|
|
2196
|
+
});
|
|
2197
|
+
printEcosystemSummary(ecosystemResult, outputPath, {
|
|
2198
|
+
interactive: true,
|
|
2199
|
+
nextSteps: "Run 'gh-symphony project add' to register a project."
|
|
2200
|
+
});
|
|
2201
|
+
}
|
|
2202
|
+
async function writeConfig(configDir, input) {
|
|
2203
|
+
await saveProjectConfig(configDir, input.projectId, {
|
|
2204
|
+
projectId: input.projectId,
|
|
2205
|
+
slug: input.projectId,
|
|
2206
|
+
displayName: input.project.title,
|
|
2207
|
+
workspaceDir: input.workspaceDir,
|
|
2208
|
+
repositories: input.repos.map((r) => ({
|
|
2209
|
+
owner: r.owner,
|
|
2210
|
+
name: r.name,
|
|
2211
|
+
cloneUrl: r.cloneUrl
|
|
2212
|
+
})),
|
|
2213
|
+
tracker: {
|
|
2214
|
+
adapter: "github-project",
|
|
2215
|
+
bindingId: input.project.id,
|
|
2216
|
+
settings: {
|
|
2217
|
+
projectId: input.project.id,
|
|
2218
|
+
...input.assignedOnly ? { assignedOnly: true } : {}
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
});
|
|
2222
|
+
const existing = await loadGlobalConfig(configDir);
|
|
2223
|
+
const globalConfig = {
|
|
2224
|
+
activeProject: input.projectId,
|
|
2225
|
+
projects: [
|
|
2226
|
+
...(existing?.projects ?? []).filter((t) => t !== input.projectId),
|
|
2227
|
+
input.projectId
|
|
2228
|
+
]
|
|
2229
|
+
};
|
|
2230
|
+
await saveGlobalConfig(configDir, globalConfig);
|
|
2231
|
+
}
|
|
2232
|
+
function generateProjectId(githubProjectTitle, uniqueKey) {
|
|
2233
|
+
const slug = githubProjectTitle.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 32);
|
|
2234
|
+
const suffix = createHash("sha1").update(uniqueKey).digest("hex").slice(0, 8);
|
|
2235
|
+
return [slug || "project", suffix].join("-");
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
export {
|
|
2239
|
+
GitHubScopeError,
|
|
2240
|
+
createClient,
|
|
2241
|
+
validateToken,
|
|
2242
|
+
checkRequiredScopes,
|
|
2243
|
+
listUserProjects,
|
|
2244
|
+
getProjectDetail,
|
|
2245
|
+
abortIfCancelled,
|
|
2246
|
+
init_default,
|
|
2247
|
+
writeEcosystem,
|
|
2248
|
+
writeConfig,
|
|
2249
|
+
generateProjectId
|
|
2250
|
+
};
|