agile-context-engineering 0.3.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/.claude-plugin/marketplace.json +18 -0
  2. package/.claude-plugin/plugin.json +10 -0
  3. package/CHANGELOG.md +7 -1
  4. package/LICENSE +51 -51
  5. package/README.md +330 -318
  6. package/agents/ace-code-discovery-analyst.md +245 -245
  7. package/agents/ace-code-integration-analyst.md +248 -248
  8. package/agents/ace-code-reviewer.md +375 -375
  9. package/agents/ace-product-owner.md +365 -361
  10. package/agents/ace-project-researcher.md +606 -606
  11. package/agents/ace-research-synthesizer.md +228 -228
  12. package/agents/ace-technical-application-architect.md +315 -315
  13. package/agents/ace-wiki-mapper.md +449 -445
  14. package/bin/install.js +605 -195
  15. package/hooks/ace-check-update.js +71 -62
  16. package/hooks/ace-statusline.js +107 -89
  17. package/hooks/hooks.json +14 -0
  18. package/package.json +7 -5
  19. package/shared/lib/ace-core.js +361 -0
  20. package/shared/lib/ace-core.test.js +308 -0
  21. package/shared/lib/ace-github.js +753 -0
  22. package/shared/lib/ace-story.js +400 -0
  23. package/shared/lib/ace-story.test.js +250 -0
  24. package/{agile-context-engineering → shared}/utils/questioning.xml +110 -110
  25. package/{agile-context-engineering → shared}/utils/ui-formatting.md +299 -299
  26. package/{commands/ace/execute-story.md → skills/execute-story/SKILL.md} +116 -138
  27. package/skills/execute-story/script.js +291 -0
  28. package/skills/execute-story/script.test.js +261 -0
  29. package/{agile-context-engineering/templates/product/story.xml → skills/execute-story/story-template.xml} +451 -451
  30. package/skills/execute-story/walkthrough-template.xml +255 -0
  31. package/{agile-context-engineering/workflows/execute-story.xml → skills/execute-story/workflow.xml} +1221 -1219
  32. package/skills/help/SKILL.md +71 -0
  33. package/skills/help/script.js +315 -0
  34. package/skills/help/script.test.js +183 -0
  35. package/{agile-context-engineering/workflows/help.xml → skills/help/workflow.xml} +544 -533
  36. package/{commands/ace/init-coding-standards.md → skills/init-coding-standards/SKILL.md} +91 -83
  37. package/{agile-context-engineering/templates/wiki/coding-standards.xml → skills/init-coding-standards/coding-standards-template.xml} +531 -531
  38. package/skills/init-coding-standards/script.js +50 -0
  39. package/skills/init-coding-standards/script.test.js +70 -0
  40. package/{agile-context-engineering/workflows/init-coding-standards.xml → skills/init-coding-standards/workflow.xml} +381 -386
  41. package/skills/map-cross-cutting/SKILL.md +126 -0
  42. package/{agile-context-engineering/templates/wiki → skills/map-cross-cutting}/system-cross-cutting.xml +197 -197
  43. package/skills/map-cross-cutting/workflow.xml +330 -0
  44. package/skills/map-guide/SKILL.md +126 -0
  45. package/{agile-context-engineering/templates/wiki → skills/map-guide}/guide.xml +137 -137
  46. package/skills/map-guide/workflow.xml +320 -0
  47. package/skills/map-pattern/SKILL.md +125 -0
  48. package/{agile-context-engineering/templates/wiki → skills/map-pattern}/pattern.xml +159 -159
  49. package/skills/map-pattern/workflow.xml +331 -0
  50. package/{commands/ace/map-story.md → skills/map-story/SKILL.md} +180 -165
  51. package/{agile-context-engineering/templates/wiki → skills/map-story/templates}/decizions.xml +115 -115
  52. package/skills/map-story/templates/guide.xml +137 -0
  53. package/skills/map-story/templates/pattern.xml +159 -0
  54. package/skills/map-story/templates/system-cross-cutting.xml +197 -0
  55. package/{agile-context-engineering/templates/wiki → skills/map-story/templates}/system.xml +381 -381
  56. package/{agile-context-engineering/templates/wiki → skills/map-story/templates}/tech-debt-index.xml +125 -125
  57. package/{agile-context-engineering/templates/wiki → skills/map-story/templates}/walkthrough.xml +255 -255
  58. package/{agile-context-engineering/workflows/map-story.xml → skills/map-story/workflow.xml} +1046 -1046
  59. package/{commands/ace/map-subsystem.md → skills/map-subsystem/SKILL.md} +155 -140
  60. package/skills/map-subsystem/script.js +51 -0
  61. package/skills/map-subsystem/script.test.js +68 -0
  62. package/skills/map-subsystem/templates/decizions.xml +115 -0
  63. package/skills/map-subsystem/templates/guide.xml +137 -0
  64. package/{agile-context-engineering/templates/wiki → skills/map-subsystem/templates}/module-discovery.xml +174 -174
  65. package/skills/map-subsystem/templates/pattern.xml +159 -0
  66. package/{agile-context-engineering/templates/wiki → skills/map-subsystem/templates}/subsystem-architecture.xml +343 -343
  67. package/{agile-context-engineering/templates/wiki → skills/map-subsystem/templates}/subsystem-structure.xml +234 -234
  68. package/skills/map-subsystem/templates/system-cross-cutting.xml +197 -0
  69. package/skills/map-subsystem/templates/system.xml +381 -0
  70. package/skills/map-subsystem/templates/walkthrough.xml +255 -0
  71. package/{agile-context-engineering/workflows/map-subsystem.xml → skills/map-subsystem/workflow.xml} +1173 -1178
  72. package/skills/map-sys-doc/SKILL.md +125 -0
  73. package/skills/map-sys-doc/system.xml +381 -0
  74. package/skills/map-sys-doc/workflow.xml +336 -0
  75. package/{commands/ace/map-system.md → skills/map-system/SKILL.md} +103 -92
  76. package/skills/map-system/script.js +75 -0
  77. package/skills/map-system/script.test.js +73 -0
  78. package/{agile-context-engineering/templates/wiki → skills/map-system/templates}/system-architecture.xml +254 -254
  79. package/{agile-context-engineering/templates/wiki → skills/map-system/templates}/system-structure.xml +177 -177
  80. package/{agile-context-engineering/templates/wiki → skills/map-system/templates}/testing-framework.xml +283 -283
  81. package/{agile-context-engineering/templates/wiki → skills/map-system/templates}/wiki-readme.xml +296 -296
  82. package/{agile-context-engineering/workflows/map-system.xml → skills/map-system/workflow.xml} +667 -672
  83. package/{commands/ace/map-walkthrough.md → skills/map-walkthrough/SKILL.md} +140 -127
  84. package/skills/map-walkthrough/walkthrough.xml +255 -0
  85. package/{agile-context-engineering/workflows/map-walkthrough.xml → skills/map-walkthrough/workflow.xml} +457 -457
  86. package/{commands/ace/plan-backlog.md → skills/plan-backlog/SKILL.md} +93 -83
  87. package/{agile-context-engineering/templates/product/product-backlog.xml → skills/plan-backlog/product-backlog-template.xml} +231 -231
  88. package/skills/plan-backlog/script.js +121 -0
  89. package/skills/plan-backlog/script.test.js +83 -0
  90. package/{agile-context-engineering/workflows/plan-backlog.xml → skills/plan-backlog/workflow.xml} +1348 -1356
  91. package/{commands/ace/plan-feature.md → skills/plan-feature/SKILL.md} +99 -89
  92. package/{agile-context-engineering/templates/product/feature.xml → skills/plan-feature/feature-template.xml} +361 -361
  93. package/skills/plan-feature/script.js +131 -0
  94. package/skills/plan-feature/script.test.js +80 -0
  95. package/{agile-context-engineering/workflows/plan-feature.xml → skills/plan-feature/workflow.xml} +1487 -1495
  96. package/{commands/ace/plan-product-vision.md → skills/plan-product-vision/SKILL.md} +91 -81
  97. package/{agile-context-engineering/templates/product/product-vision.xml → skills/plan-product-vision/product-vision-template.xml} +227 -227
  98. package/skills/plan-product-vision/script.js +51 -0
  99. package/skills/plan-product-vision/script.test.js +69 -0
  100. package/{agile-context-engineering/workflows/plan-product-vision.xml → skills/plan-product-vision/workflow.xml} +337 -342
  101. package/{commands/ace/plan-story.md → skills/plan-story/SKILL.md} +139 -159
  102. package/skills/plan-story/script.js +295 -0
  103. package/skills/plan-story/script.test.js +240 -0
  104. package/skills/plan-story/story-template.xml +458 -0
  105. package/{agile-context-engineering/workflows/plan-story.xml → skills/plan-story/workflow.xml} +1301 -944
  106. package/{commands/ace/research-external-solution.md → skills/research-external-solution/SKILL.md} +120 -138
  107. package/{agile-context-engineering/templates/product/external-solution.xml → skills/research-external-solution/external-solution-template.xml} +832 -832
  108. package/skills/research-external-solution/script.js +229 -0
  109. package/skills/research-external-solution/script.test.js +134 -0
  110. package/{agile-context-engineering/workflows/research-external-solution.xml → skills/research-external-solution/workflow.xml} +657 -659
  111. package/{commands/ace/research-integration-solution.md → skills/research-integration-solution/SKILL.md} +121 -135
  112. package/{agile-context-engineering/templates/product/story-integration-solution.xml → skills/research-integration-solution/integration-solution-template.xml} +1015 -1015
  113. package/skills/research-integration-solution/script.js +223 -0
  114. package/skills/research-integration-solution/script.test.js +134 -0
  115. package/{agile-context-engineering/workflows/research-integration-solution.xml → skills/research-integration-solution/workflow.xml} +711 -713
  116. package/{commands/ace/research-story-wiki.md → skills/research-story-wiki/SKILL.md} +101 -116
  117. package/skills/research-story-wiki/script.js +223 -0
  118. package/skills/research-story-wiki/script.test.js +138 -0
  119. package/{agile-context-engineering/templates/product/story-wiki.xml → skills/research-story-wiki/story-wiki-template.xml} +194 -194
  120. package/{agile-context-engineering/workflows/research-story-wiki.xml → skills/research-story-wiki/workflow.xml} +473 -475
  121. package/{commands/ace/research-technical-solution.md → skills/research-technical-solution/SKILL.md} +131 -147
  122. package/skills/research-technical-solution/script.js +223 -0
  123. package/skills/research-technical-solution/script.test.js +134 -0
  124. package/{agile-context-engineering/templates/product/story-technical-solution.xml → skills/research-technical-solution/technical-solution-template.xml} +1025 -1025
  125. package/{agile-context-engineering/workflows/research-technical-solution.xml → skills/research-technical-solution/workflow.xml} +761 -763
  126. package/{commands/ace/review-story.md → skills/review-story/SKILL.md} +99 -109
  127. package/skills/review-story/script.js +249 -0
  128. package/skills/review-story/script.test.js +169 -0
  129. package/skills/review-story/story-template.xml +451 -0
  130. package/{agile-context-engineering/workflows/review-story.xml → skills/review-story/workflow.xml} +279 -281
  131. package/{commands/ace/update.md → skills/update/SKILL.md} +65 -56
  132. package/{agile-context-engineering/workflows/update.xml → skills/update/workflow.xml} +33 -18
  133. package/agile-context-engineering/src/ace-tools.js +0 -2881
  134. package/agile-context-engineering/src/ace-tools.test.js +0 -1089
  135. package/agile-context-engineering/templates/_command.md +0 -54
  136. package/agile-context-engineering/templates/_workflow.xml +0 -17
  137. package/agile-context-engineering/templates/config.json +0 -0
  138. package/agile-context-engineering/templates/product/integration-solution.xml +0 -0
  139. package/commands/ace/help.md +0 -93
@@ -0,0 +1,753 @@
1
+ /**
2
+ * ACE GitHub — GitHub integration operations shared across ACE skills.
3
+ *
4
+ * Extracted from ace-tools.js monolith. Contains: project context resolution
5
+ * and story/feature GitHub sync.
6
+ *
7
+ * Usage: const { syncStory, resolveProjectContext } = require('./ace-github');
8
+ */
9
+
10
+ const fs = require('fs');
11
+ const os = require('os');
12
+ const path = require('path');
13
+ const { execSync } = require('child_process');
14
+ const {
15
+ safeReadFile, parseKeyValueArgs, execCommand, output, error,
16
+ } = require('./ace-core');
17
+ const {
18
+ extractStoryMetadata, extractIssueNumberFromFile,
19
+ } = require('./ace-story');
20
+
21
+ // ─── Project Context ─────────────────────────────────────────────────────────
22
+
23
+ /**
24
+ * Resolve project ID and field definitions for a GitHub Project.
25
+ * Returns { project_id, fields } where fields maps field names to { id, type, options? }.
26
+ */
27
+ function resolveProjectContext(owner, project, cwd) {
28
+ const projectListRaw = execCommand(
29
+ `gh project list --owner ${owner} --format json --limit 20`,
30
+ cwd
31
+ );
32
+
33
+ let project_id = null;
34
+ if (projectListRaw) {
35
+ try {
36
+ const parsed = JSON.parse(projectListRaw);
37
+ const projects = parsed.projects || parsed || [];
38
+ const match = projects.find(p => String(p.number) === String(project));
39
+ if (match) project_id = match.id;
40
+ } catch {}
41
+ }
42
+
43
+ const fieldsRaw = execCommand(
44
+ `gh project field-list ${project} --owner ${owner} --format json`,
45
+ cwd
46
+ );
47
+
48
+ const fields = {};
49
+ if (fieldsRaw) {
50
+ try {
51
+ const parsed = JSON.parse(fieldsRaw);
52
+ const fieldList = parsed.fields || parsed || [];
53
+ for (const field of fieldList) {
54
+ const entry = { id: field.id, type: field.type };
55
+ if (field.options) {
56
+ entry.options = {};
57
+ for (const opt of field.options) {
58
+ entry.options[opt.name] = opt.id;
59
+ }
60
+ }
61
+ fields[field.name] = entry;
62
+ }
63
+ } catch {}
64
+ }
65
+
66
+ return { project_id, fields };
67
+ }
68
+
69
+ // ─── Story Sync ──────────────────────────────────────────────────────────────
70
+
71
+ /**
72
+ * Sync story and feature body + project status to GitHub.
73
+ * Updates issue body via --body-file and project status via GraphQL.
74
+ *
75
+ * Required args: repo=owner/name story_file=path
76
+ * Optional args: feature_file=path owner=org project=number
77
+ */
78
+ function syncStory(cwd, raw, extraArgs) {
79
+ const params = parseKeyValueArgs(extraArgs);
80
+ const repo = params.repo;
81
+ const storyFile = params.story_file;
82
+
83
+ if (!repo || !storyFile) {
84
+ error('sync-github requires: repo=owner/name story_file=path');
85
+ }
86
+
87
+ const result = {
88
+ story: { number: null, updated: false, status_synced: false, error: null },
89
+ feature: { number: null, updated: false, status_synced: false, error: null },
90
+ };
91
+
92
+ // Resolve project context for status updates (optional)
93
+ const owner = params.owner;
94
+ const project = params.project;
95
+ let projectCtx = null;
96
+
97
+ if (owner && project) {
98
+ projectCtx = resolveProjectContext(owner, project, cwd);
99
+ if (!projectCtx.project_id) {
100
+ process.stderr.write(` ! Could not resolve GitHub Project #${project}. Status updates skipped.\n`);
101
+ projectCtx = null;
102
+ } else if (!projectCtx.fields.Status) {
103
+ process.stderr.write(' ! GitHub Project has no Status field. Status updates skipped.\n');
104
+ projectCtx = null;
105
+ }
106
+ }
107
+
108
+ // Helper: update project status for a single issue
109
+ function syncProjectStatus(issueNumber, filePath, label) {
110
+ if (!projectCtx) return false;
111
+
112
+ const content = safeReadFile(filePath);
113
+ if (!content) return false;
114
+
115
+ const metadata = extractStoryMetadata(content);
116
+ const localStatus = metadata.status;
117
+ if (!localStatus) {
118
+ process.stderr.write(` — ${label} has no Status field. Skipping project status update.\n`);
119
+ return false;
120
+ }
121
+
122
+ const statusField = projectCtx.fields.Status;
123
+ const statusOptionId = statusField.options?.[localStatus];
124
+ if (!statusOptionId) {
125
+ process.stderr.write(` ! GitHub Project has no status option "${localStatus}". Skipping status update for ${label}.\n`);
126
+ return false;
127
+ }
128
+
129
+ // Look up project item ID via GraphQL
130
+ const repoParts = repo.split('/');
131
+ const repoOwner = repoParts[0];
132
+ const repoName = repoParts[1] || repoParts[0];
133
+ const itemQuery = `query { repository(owner: \\"${repoOwner}\\", name: \\"${repoName}\\") { issue(number: ${issueNumber}) { projectItems(first: 10) { nodes { id project { id } } } } } }`;
134
+ const itemResult = execCommand(
135
+ `gh api graphql -f query="${itemQuery}"`,
136
+ cwd
137
+ );
138
+ let itemId = null;
139
+ if (itemResult) {
140
+ try {
141
+ const parsed = JSON.parse(itemResult);
142
+ const nodes = parsed.data?.repository?.issue?.projectItems?.nodes || [];
143
+ const match = nodes.find(n => n.project?.id === projectCtx.project_id);
144
+ itemId = match?.id || null;
145
+ } catch {}
146
+ }
147
+ if (!itemId) {
148
+ process.stderr.write(` ! ${label} #${issueNumber} not found in GitHub Project. Skipping status update.\n`);
149
+ return false;
150
+ }
151
+
152
+ const statusOk = execCommand(
153
+ `gh project item-edit --project-id ${projectCtx.project_id} --id ${itemId} --field-id ${statusField.id} --single-select-option-id ${statusOptionId}`,
154
+ cwd
155
+ );
156
+ if (statusOk !== null) {
157
+ process.stderr.write(` + Updated ${label} #${issueNumber} project status → "${localStatus}".\n`);
158
+ return true;
159
+ } else {
160
+ process.stderr.write(` x FAILED to update ${label} #${issueNumber} project status.\n`);
161
+ return false;
162
+ }
163
+ }
164
+
165
+ // Sync story issue
166
+ const storyPath = path.isAbsolute(storyFile) ? storyFile : path.join(cwd, storyFile);
167
+ const storyIssue = extractIssueNumberFromFile(cwd, storyFile);
168
+
169
+ if (!storyIssue) {
170
+ result.story.error = 'No GitHub issue linked';
171
+ process.stderr.write(' — Story has no GitHub issue linked. Skipping.\n');
172
+ } else {
173
+ result.story.number = storyIssue;
174
+ const safePath = storyPath.replace(/\\/g, '/');
175
+ try {
176
+ execSync(`gh issue edit ${storyIssue} --repo ${repo} --body-file "${safePath}"`, {
177
+ cwd, shell: 'bash', stdio: ['pipe', 'pipe', 'pipe'], encoding: 'utf-8', timeout: 30000,
178
+ });
179
+ result.story.updated = true;
180
+ process.stderr.write(` + Updated GitHub story issue #${storyIssue}.\n`);
181
+ } catch (e) {
182
+ result.story.error = (e.stderr || e.message || 'unknown error').trim();
183
+ process.stderr.write(` x FAILED to update GitHub story issue #${storyIssue}.\n`);
184
+ process.stderr.write(` Error: ${result.story.error}\n`);
185
+ }
186
+
187
+ if (result.story.updated) {
188
+ result.story.status_synced = syncProjectStatus(storyIssue, storyPath, 'Story');
189
+ }
190
+ }
191
+
192
+ // Sync feature issue
193
+ const featureFile = params.feature_file;
194
+ if (featureFile) {
195
+ const featurePath = path.isAbsolute(featureFile) ? featureFile : path.join(cwd, featureFile);
196
+ const featureIssue = extractIssueNumberFromFile(cwd, featureFile);
197
+
198
+ if (!featureIssue) {
199
+ result.feature.error = 'No GitHub issue linked';
200
+ process.stderr.write(' — Feature has no GitHub issue linked. Skipping.\n');
201
+ } else {
202
+ result.feature.number = featureIssue;
203
+ const safePath = featurePath.replace(/\\/g, '/');
204
+ try {
205
+ execSync(`gh issue edit ${featureIssue} --repo ${repo} --body-file "${safePath}"`, {
206
+ cwd, shell: 'bash', stdio: ['pipe', 'pipe', 'pipe'], encoding: 'utf-8', timeout: 30000,
207
+ });
208
+ result.feature.updated = true;
209
+ process.stderr.write(` + Updated GitHub feature issue #${featureIssue}.\n`);
210
+ } catch (e) {
211
+ result.feature.error = (e.stderr || e.message || 'unknown error').trim();
212
+ process.stderr.write(` x FAILED to update GitHub feature issue #${featureIssue}.\n`);
213
+ process.stderr.write(` Error: ${result.feature.error}\n`);
214
+ }
215
+
216
+ if (result.feature.updated) {
217
+ result.feature.status_synced = syncProjectStatus(featureIssue, featurePath, 'Feature');
218
+ }
219
+ }
220
+ }
221
+
222
+ output(result, raw);
223
+ }
224
+
225
+ // ─── Resolve Fields ─────────────────────────────────────────────────────────
226
+
227
+ /**
228
+ * Resolve native issue types and project field definitions.
229
+ * Required args: repo=owner/name owner=org project=number
230
+ * Outputs: { issue_types, project_id, fields }
231
+ */
232
+ function resolveFields(cwd, raw, extraArgs) {
233
+ const params = parseKeyValueArgs(extraArgs);
234
+ const repo = params.repo;
235
+ const owner = params.owner;
236
+ const project = params.project;
237
+
238
+ if (!repo || !owner || !project) {
239
+ error('resolve-fields requires: repo=owner/name owner=org project=number');
240
+ }
241
+
242
+ const repoName = repo.split('/')[1];
243
+
244
+ // Resolve native issue types via GraphQL
245
+ const issueTypes = {};
246
+ const typeQuery = `query { repository(owner: \\"${owner}\\", name: \\"${repoName}\\") { issueTypes(first: 10) { nodes { id name } } } }`;
247
+ const typeResult = execCommand(
248
+ `gh api graphql -f query="${typeQuery}"`,
249
+ cwd
250
+ );
251
+ if (typeResult) {
252
+ try {
253
+ const parsed = JSON.parse(typeResult);
254
+ const nodes = parsed.data?.repository?.issueTypes?.nodes || [];
255
+ for (const node of nodes) {
256
+ issueTypes[node.name] = node.id;
257
+ }
258
+ } catch {}
259
+ }
260
+
261
+ // Resolve project context (project_id + fields)
262
+ const ctx = resolveProjectContext(owner, project, cwd);
263
+
264
+ output({ issue_types: issueTypes, project_id: ctx.project_id, fields: ctx.fields }, raw);
265
+ }
266
+
267
+ // ─── Create Issue ───────────────────────────────────────────────────────────
268
+
269
+ /**
270
+ * Create a GitHub issue, set native type, add to project, and set project fields.
271
+ *
272
+ * Required args: type, title, repo, owner, project, project_id, type_id
273
+ * Body args (one of): body=... OR body_file=path
274
+ * Optional: status_field_id, status_option_id, priority_field_id, priority_option_id,
275
+ * estimate_field_id, estimate, parent, milestone
276
+ * Outputs: { number, url, item_id, type_set, status_set, priority_set, estimate_set,
277
+ * parent_set, milestone_set }
278
+ */
279
+ function createIssue(cwd, raw, extraArgs) {
280
+ const params = parseKeyValueArgs(extraArgs);
281
+ const type = params.type;
282
+ const title = params.title;
283
+ let body = params.body || '';
284
+ const bodyFile = params.body_file;
285
+ const repo = params.repo;
286
+ const owner = params.owner;
287
+ const project = params.project;
288
+ const projectId = params.project_id;
289
+ const typeId = params.type_id;
290
+
291
+ // Read body from file if provided
292
+ if (bodyFile) {
293
+ const filePath = path.isAbsolute(bodyFile) ? bodyFile : path.join(cwd, bodyFile);
294
+ const content = safeReadFile(filePath);
295
+ if (content !== null) body = content;
296
+ }
297
+
298
+ if (!type || !title || !repo || !owner || !project || !projectId || !typeId) {
299
+ error('create-issue requires: type, title, repo, owner, project, project_id, type_id');
300
+ }
301
+
302
+ const prefixedTitle = `[${type}] ${title}`;
303
+
304
+ // Write body to temp file to avoid shell escaping issues
305
+ const tmpFile = path.join(os.tmpdir(), `ace-issue-${Date.now()}.md`);
306
+ try {
307
+ fs.writeFileSync(tmpFile, body, 'utf-8');
308
+ } catch (e) {
309
+ error(`Failed to write temp body file: ${e.message}`);
310
+ }
311
+
312
+ const safeTmpFile = tmpFile.replace(/\\/g, '/');
313
+
314
+ // Create issue via gh CLI
315
+ let issueUrl = null;
316
+ try {
317
+ issueUrl = execSync(
318
+ `gh issue create --repo ${repo} --title "${prefixedTitle.replace(/"/g, '\\"')}" --body-file "${safeTmpFile}"`,
319
+ { cwd, shell: 'bash', stdio: ['pipe', 'pipe', 'pipe'], encoding: 'utf-8', timeout: 30000 }
320
+ ).trim();
321
+ } catch (e) {
322
+ try { fs.unlinkSync(tmpFile); } catch {}
323
+ error(`Failed to create issue: ${(e.stderr || e.message || '').trim()}`);
324
+ }
325
+
326
+ try { fs.unlinkSync(tmpFile); } catch {}
327
+
328
+ // Extract issue number from URL
329
+ const number = issueUrl ? parseInt(issueUrl.split('/').pop(), 10) : null;
330
+
331
+ const result = {
332
+ number,
333
+ url: issueUrl,
334
+ item_id: null,
335
+ type_set: false,
336
+ status_set: false,
337
+ priority_set: false,
338
+ estimate_set: false,
339
+ parent_set: false,
340
+ milestone_set: false,
341
+ };
342
+
343
+ if (!number) {
344
+ output(result, raw);
345
+ return;
346
+ }
347
+
348
+ // Set native issue type via GraphQL mutation
349
+ if (typeId) {
350
+ const repoName = repo.split('/')[1];
351
+ // First get the issue node ID
352
+ const issueQuery = `query { repository(owner: \\"${owner}\\", name: \\"${repoName}\\") { issue(number: ${number}) { id } } }`;
353
+ const issueResult = execCommand(`gh api graphql -f query="${issueQuery}"`, cwd);
354
+ let issueNodeId = null;
355
+ if (issueResult) {
356
+ try {
357
+ issueNodeId = JSON.parse(issueResult).data?.repository?.issue?.id;
358
+ } catch {}
359
+ }
360
+ if (issueNodeId) {
361
+ const typeMutation = `mutation { updateIssue(input: { id: \\"${issueNodeId}\\", issueTypeId: \\"${typeId}\\" }) { issue { id } } }`;
362
+ const typeOk = execCommand(`gh api graphql -f query="${typeMutation}"`, cwd);
363
+ result.type_set = typeOk !== null;
364
+ }
365
+ }
366
+
367
+ // Add to project
368
+ const addResult = execCommand(
369
+ `gh project item-add ${project} --owner ${owner} --url ${issueUrl} --format json`,
370
+ cwd
371
+ );
372
+ if (addResult) {
373
+ try {
374
+ const parsed = JSON.parse(addResult);
375
+ result.item_id = parsed.id || null;
376
+ } catch {}
377
+ }
378
+
379
+ // Set project fields if item was added
380
+ if (result.item_id) {
381
+ // Status (single-select)
382
+ if (params.status_field_id && params.status_option_id) {
383
+ const statusOk = execCommand(
384
+ `gh project item-edit --project-id ${projectId} --id ${result.item_id} --field-id ${params.status_field_id} --single-select-option-id ${params.status_option_id}`,
385
+ cwd
386
+ );
387
+ result.status_set = statusOk !== null;
388
+ }
389
+
390
+ // Priority (single-select)
391
+ if (params.priority_field_id && params.priority_option_id) {
392
+ const priorityOk = execCommand(
393
+ `gh project item-edit --project-id ${projectId} --id ${result.item_id} --field-id ${params.priority_field_id} --single-select-option-id ${params.priority_option_id}`,
394
+ cwd
395
+ );
396
+ result.priority_set = priorityOk !== null;
397
+ }
398
+
399
+ // Estimate (number)
400
+ if (params.estimate_field_id && params.estimate) {
401
+ const estimateOk = execCommand(
402
+ `gh project item-edit --project-id ${projectId} --id ${result.item_id} --field-id ${params.estimate_field_id} --number ${params.estimate}`,
403
+ cwd
404
+ );
405
+ result.estimate_set = estimateOk !== null;
406
+ }
407
+ }
408
+
409
+ // Set parent via GraphQL addSubIssue mutation
410
+ if (params.parent) {
411
+ const repoName = repo.split('/')[1];
412
+ // Get parent issue node ID
413
+ const parentQuery = `query { repository(owner: \\"${owner}\\", name: \\"${repoName}\\") { issue(number: ${params.parent}) { id } } }`;
414
+ const parentResult = execCommand(`gh api graphql -f query="${parentQuery}"`, cwd);
415
+ let parentNodeId = null;
416
+ if (parentResult) {
417
+ try {
418
+ parentNodeId = JSON.parse(parentResult).data?.repository?.issue?.id;
419
+ } catch {}
420
+ }
421
+ // Get child issue node ID
422
+ const childQuery = `query { repository(owner: \\"${owner}\\", name: \\"${repoName}\\") { issue(number: ${number}) { id } } }`;
423
+ const childResult = execCommand(`gh api graphql -f query="${childQuery}"`, cwd);
424
+ let childNodeId = null;
425
+ if (childResult) {
426
+ try {
427
+ childNodeId = JSON.parse(childResult).data?.repository?.issue?.id;
428
+ } catch {}
429
+ }
430
+ if (parentNodeId && childNodeId) {
431
+ const subIssueMutation = `mutation { addSubIssue(input: { issueId: \\"${parentNodeId}\\", subIssueId: \\"${childNodeId}\\" }) { issue { id } } }`;
432
+ const parentOk = execCommand(`gh api graphql -f query="${subIssueMutation}"`, cwd);
433
+ result.parent_set = parentOk !== null;
434
+ }
435
+ }
436
+
437
+ // Set milestone
438
+ if (params.milestone) {
439
+ const milestoneOk = execCommand(
440
+ `gh issue edit ${number} --repo ${repo} --milestone "${params.milestone}"`,
441
+ cwd
442
+ );
443
+ result.milestone_set = milestoneOk !== null;
444
+ }
445
+
446
+ output(result, raw);
447
+ }
448
+
449
+ // ─── Update Issue ───────────────────────────────────────────────────────────
450
+
451
+ /**
452
+ * Update an existing GitHub issue's title, body, and/or project fields.
453
+ *
454
+ * Required args: number, repo
455
+ * Optional: title, body, body_file, owner, project, project_id,
456
+ * status_field_id, status_option_id, priority_field_id, priority_option_id,
457
+ * estimate_field_id, estimate
458
+ * Outputs: { number, updated_title, updated_body, status_set, priority_set, estimate_set }
459
+ */
460
+ function updateIssue(cwd, raw, extraArgs) {
461
+ const params = parseKeyValueArgs(extraArgs);
462
+ const number = params.number;
463
+ const repo = params.repo;
464
+ const title = params.title;
465
+ let body = params.body;
466
+ const bodyFile = params.body_file;
467
+
468
+ if (!number || !repo) {
469
+ error('update-issue requires: number, repo');
470
+ }
471
+
472
+ const result = {
473
+ number: parseInt(number, 10),
474
+ updated_title: false,
475
+ updated_body: false,
476
+ status_set: false,
477
+ priority_set: false,
478
+ estimate_set: false,
479
+ };
480
+
481
+ // Build gh issue edit command parts
482
+ const editParts = [`gh issue edit ${number} --repo ${repo}`];
483
+
484
+ if (title) {
485
+ editParts.push(`--title "${title.replace(/"/g, '\\"')}"`);
486
+ }
487
+
488
+ let tmpFile = null;
489
+ if (bodyFile) {
490
+ const filePath = path.isAbsolute(bodyFile) ? bodyFile : path.join(cwd, bodyFile);
491
+ const safePath = filePath.replace(/\\/g, '/');
492
+ editParts.push(`--body-file "${safePath}"`);
493
+ } else if (body) {
494
+ tmpFile = path.join(os.tmpdir(), `ace-issue-update-${Date.now()}.md`);
495
+ try {
496
+ fs.writeFileSync(tmpFile, body, 'utf-8');
497
+ const safeTmpFile = tmpFile.replace(/\\/g, '/');
498
+ editParts.push(`--body-file "${safeTmpFile}"`);
499
+ } catch (e) {
500
+ error(`Failed to write temp body file: ${e.message}`);
501
+ }
502
+ }
503
+
504
+ // Execute issue edit if we have title or body to update
505
+ if (title || bodyFile || body) {
506
+ try {
507
+ execSync(editParts.join(' '), {
508
+ cwd, shell: 'bash', stdio: ['pipe', 'pipe', 'pipe'], encoding: 'utf-8', timeout: 30000,
509
+ });
510
+ if (title) result.updated_title = true;
511
+ if (bodyFile || body) result.updated_body = true;
512
+ } catch (e) {
513
+ process.stderr.write(` x Failed to update issue #${number}: ${(e.stderr || e.message || '').trim()}\n`);
514
+ }
515
+ }
516
+
517
+ if (tmpFile) {
518
+ try { fs.unlinkSync(tmpFile); } catch {}
519
+ }
520
+
521
+ // Update project fields if provided
522
+ const projectId = params.project_id;
523
+
524
+ if (projectId && (params.status_field_id || params.priority_field_id || params.estimate_field_id)) {
525
+ // Find the project item ID for this issue
526
+ const repoParts = repo.split('/');
527
+ const repoOwner = repoParts[0];
528
+ const repoName = repoParts[1] || repoParts[0];
529
+ const itemQuery = `query { repository(owner: \\"${repoOwner}\\", name: \\"${repoName}\\") { issue(number: ${number}) { projectItems(first: 10) { nodes { id project { id } } } } } }`;
530
+ const itemResult = execCommand(`gh api graphql -f query="${itemQuery}"`, cwd);
531
+ let itemId = null;
532
+ if (itemResult) {
533
+ try {
534
+ const parsed = JSON.parse(itemResult);
535
+ const nodes = parsed.data?.repository?.issue?.projectItems?.nodes || [];
536
+ const match = nodes.find(n => n.project?.id === projectId);
537
+ itemId = match?.id || null;
538
+ } catch {}
539
+ }
540
+
541
+ if (itemId) {
542
+ // Status (single-select)
543
+ if (params.status_field_id && params.status_option_id) {
544
+ const statusOk = execCommand(
545
+ `gh project item-edit --project-id ${projectId} --id ${itemId} --field-id ${params.status_field_id} --single-select-option-id ${params.status_option_id}`,
546
+ cwd
547
+ );
548
+ result.status_set = statusOk !== null;
549
+ }
550
+
551
+ // Priority (single-select)
552
+ if (params.priority_field_id && params.priority_option_id) {
553
+ const priorityOk = execCommand(
554
+ `gh project item-edit --project-id ${projectId} --id ${itemId} --field-id ${params.priority_field_id} --single-select-option-id ${params.priority_option_id}`,
555
+ cwd
556
+ );
557
+ result.priority_set = priorityOk !== null;
558
+ }
559
+
560
+ // Estimate (number)
561
+ if (params.estimate_field_id && params.estimate) {
562
+ const estimateOk = execCommand(
563
+ `gh project item-edit --project-id ${projectId} --id ${itemId} --field-id ${params.estimate_field_id} --number ${params.estimate}`,
564
+ cwd
565
+ );
566
+ result.estimate_set = estimateOk !== null;
567
+ }
568
+ } else {
569
+ process.stderr.write(` ! Issue #${number} not found in GitHub Project. Skipping field updates.\n`);
570
+ }
571
+ }
572
+
573
+ output(result, raw);
574
+ }
575
+
576
+ // ─── Fetch Issues ───────────────────────────────────────────────────────────
577
+
578
+ /**
579
+ * Fetch all epics and features from a GitHub Project via paginated GraphQL.
580
+ *
581
+ * Required args: repo=owner/name owner=org project=number
582
+ * Outputs: { epics: [...], features: [...], counts: { total, epics, features, skipped } }
583
+ */
584
+ function fetchIssues(cwd, raw, extraArgs) {
585
+ const params = parseKeyValueArgs(extraArgs);
586
+ const repo = params.repo;
587
+ const owner = params.owner;
588
+ const project = params.project;
589
+
590
+ if (!repo || !owner || !project) {
591
+ error('fetch-issues requires: repo=owner/name owner=org project=number');
592
+ }
593
+
594
+ // Get project ID
595
+ const projectListRaw = execCommand(
596
+ `gh project list --owner ${owner} --format json --limit 20`,
597
+ cwd
598
+ );
599
+ let projectId = null;
600
+ if (projectListRaw) {
601
+ try {
602
+ const parsed = JSON.parse(projectListRaw);
603
+ const projects = parsed.projects || parsed || [];
604
+ const match = projects.find(p => String(p.number) === String(project));
605
+ if (match) projectId = match.id;
606
+ } catch {}
607
+ }
608
+
609
+ if (!projectId) {
610
+ error(`Could not find GitHub Project #${project} for owner "${owner}".`);
611
+ }
612
+
613
+ // Paginated GraphQL query to fetch all project items
614
+ const allItems = [];
615
+ let hasNextPage = true;
616
+ let cursor = null;
617
+
618
+ while (hasNextPage) {
619
+ const afterClause = cursor ? `, after: \\"${cursor}\\"` : '';
620
+ const query = `query {
621
+ node(id: \\"${projectId}\\") {
622
+ ... on ProjectV2 {
623
+ items(first: 100${afterClause}) {
624
+ pageInfo { hasNextPage endCursor }
625
+ nodes {
626
+ id
627
+ fieldValues(first: 20) {
628
+ nodes {
629
+ ... on ProjectV2ItemFieldTextValue { text field { ... on ProjectV2Field { name } } }
630
+ ... on ProjectV2ItemFieldNumberValue { number field { ... on ProjectV2Field { name } } }
631
+ ... on ProjectV2ItemFieldSingleSelectValue { name field { ... on ProjectV2SingleSelectField { name } } }
632
+ ... on ProjectV2ItemFieldIterationValue { title field { ... on ProjectV2IterationField { name } } }
633
+ }
634
+ }
635
+ content {
636
+ ... on Issue {
637
+ number
638
+ title
639
+ url
640
+ state
641
+ issueType { name }
642
+ parent { number title }
643
+ milestone { title }
644
+ }
645
+ }
646
+ }
647
+ }
648
+ }
649
+ }
650
+ }`.replace(/\n/g, ' ').replace(/\s+/g, ' ');
651
+
652
+ const result = execCommand(`gh api graphql -f query="${query}"`, cwd);
653
+ if (!result) break;
654
+
655
+ try {
656
+ const parsed = JSON.parse(result);
657
+ const items = parsed.data?.node?.items;
658
+ if (!items) break;
659
+
660
+ allItems.push(...(items.nodes || []));
661
+ hasNextPage = items.pageInfo?.hasNextPage || false;
662
+ cursor = items.pageInfo?.endCursor || null;
663
+ } catch {
664
+ break;
665
+ }
666
+ }
667
+
668
+ // Process items into epics and features
669
+ const epics = [];
670
+ const features = [];
671
+ let skipped = 0;
672
+
673
+ for (const item of allItems) {
674
+ const content = item.content;
675
+ if (!content || !content.number) {
676
+ skipped++;
677
+ continue;
678
+ }
679
+
680
+ // Determine type: native issueType first, then title prefix fallback
681
+ let itemType = null;
682
+ if (content.issueType?.name) {
683
+ const nativeType = content.issueType.name;
684
+ if (nativeType === 'Epic' || nativeType === 'Feature') {
685
+ itemType = nativeType;
686
+ }
687
+ }
688
+ if (!itemType) {
689
+ if (content.title.startsWith('[Epic]')) {
690
+ itemType = 'Epic';
691
+ } else if (content.title.startsWith('[Feature]')) {
692
+ itemType = 'Feature';
693
+ }
694
+ }
695
+
696
+ if (!itemType) {
697
+ skipped++;
698
+ continue;
699
+ }
700
+
701
+ // Extract field values
702
+ const fieldValues = {};
703
+ const fvNodes = item.fieldValues?.nodes || [];
704
+ for (const fv of fvNodes) {
705
+ const fieldName = fv.field?.name;
706
+ if (!fieldName) continue;
707
+ if (fv.name !== undefined) fieldValues[fieldName] = fv.name; // single-select
708
+ else if (fv.number !== undefined) fieldValues[fieldName] = fv.number; // number
709
+ else if (fv.text !== undefined) fieldValues[fieldName] = fv.text; // text
710
+ else if (fv.title !== undefined) fieldValues[fieldName] = fv.title; // iteration
711
+ }
712
+
713
+ const entry = {
714
+ number: content.number,
715
+ title: content.title,
716
+ status: fieldValues.Status || null,
717
+ priority: fieldValues.Priority || null,
718
+ estimate: fieldValues.Estimate || null,
719
+ sprint: fieldValues.Sprint || fieldValues.Iteration || null,
720
+ milestone: content.milestone?.title || null,
721
+ url: content.url,
722
+ state: content.state,
723
+ };
724
+
725
+ if (itemType === 'Epic') {
726
+ epics.push(entry);
727
+ } else {
728
+ entry.parent_number = content.parent?.number || null;
729
+ entry.parent_title = content.parent?.title || null;
730
+ features.push(entry);
731
+ }
732
+ }
733
+
734
+ output({
735
+ epics,
736
+ features,
737
+ counts: {
738
+ total: allItems.length,
739
+ epics: epics.length,
740
+ features: features.length,
741
+ skipped,
742
+ },
743
+ }, raw);
744
+ }
745
+
746
+ module.exports = {
747
+ resolveProjectContext,
748
+ syncStory,
749
+ resolveFields,
750
+ createIssue,
751
+ updateIssue,
752
+ fetchIssues,
753
+ };