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
@@ -1,1089 +0,0 @@
1
- /**
2
- * ACE Tools Tests
3
- *
4
- * Inspired by GSD's gsd-tools.test.js (https://github.com/gsd-build/get-shit-done).
5
- */
6
-
7
- const { test, describe, beforeEach, afterEach } = require('node:test');
8
- const assert = require('node:assert');
9
- const fs = require('fs');
10
- const path = require('path');
11
- const { execSync } = require('child_process');
12
-
13
- const TOOLS_PATH = path.join(__dirname, 'ace-tools.js');
14
-
15
- // Helper to run ace-tools command
16
- function runAceTools(args, cwd = process.cwd()) {
17
- try {
18
- const result = execSync(`node "${TOOLS_PATH}" ${args}`, {
19
- cwd,
20
- encoding: 'utf-8',
21
- stdio: ['pipe', 'pipe', 'pipe'],
22
- });
23
- return { success: true, output: result.trim() };
24
- } catch (err) {
25
- return {
26
- success: false,
27
- output: err.stdout?.toString().trim() || '',
28
- error: err.stderr?.toString().trim() || err.message,
29
- };
30
- }
31
- }
32
-
33
- // Create temp directory structure
34
- function createTempProject() {
35
- const tmpDir = fs.mkdtempSync(path.join(require('os').tmpdir(), 'ace-test-'));
36
- return tmpDir;
37
- }
38
-
39
- function createTempProjectWithAce() {
40
- const tmpDir = createTempProject();
41
- fs.mkdirSync(path.join(tmpDir, '.ace'), { recursive: true });
42
- return tmpDir;
43
- }
44
-
45
- function cleanup(tmpDir) {
46
- fs.rmSync(tmpDir, { recursive: true, force: true });
47
- }
48
-
49
- // ─── generate-slug ────────────────────────────────────────────────────────────
50
-
51
- describe('generate-slug command', () => {
52
- test('converts text to lowercase slug', () => {
53
- const result = runAceTools('generate-slug "Hello World"');
54
- assert.ok(result.success, `Command failed: ${result.error}`);
55
- const parsed = JSON.parse(result.output);
56
- assert.strictEqual(parsed.slug, 'hello-world');
57
- });
58
-
59
- test('handles special characters', () => {
60
- const result = runAceTools('generate-slug "User Authentication & Login!!!"');
61
- assert.ok(result.success, `Command failed: ${result.error}`);
62
- const parsed = JSON.parse(result.output);
63
- assert.strictEqual(parsed.slug, 'user-authentication-login');
64
- });
65
-
66
- test('trims leading and trailing dashes', () => {
67
- const result = runAceTools('generate-slug "---hello---"');
68
- assert.ok(result.success, `Command failed: ${result.error}`);
69
- const parsed = JSON.parse(result.output);
70
- assert.strictEqual(parsed.slug, 'hello');
71
- });
72
-
73
- test('returns raw slug with --raw flag', () => {
74
- const result = runAceTools('generate-slug "My Epic Name" --raw');
75
- assert.ok(result.success, `Command failed: ${result.error}`);
76
- assert.strictEqual(result.output, 'my-epic-name');
77
- });
78
-
79
- test('errors when no text provided', () => {
80
- const result = runAceTools('generate-slug');
81
- assert.strictEqual(result.success, false);
82
- assert.ok(result.error.includes('text required'), `Expected error about text, got: ${result.error}`);
83
- });
84
-
85
- test('handles multi-word args without quotes', () => {
86
- const result = runAceTools('generate-slug Platform Foundation Setup');
87
- assert.ok(result.success, `Command failed: ${result.error}`);
88
- const parsed = JSON.parse(result.output);
89
- assert.strictEqual(parsed.slug, 'platform-foundation-setup');
90
- });
91
- });
92
-
93
- // ─── current-timestamp ────────────────────────────────────────────────────────
94
-
95
- describe('current-timestamp command', () => {
96
- test('returns full ISO timestamp by default', () => {
97
- const result = runAceTools('current-timestamp');
98
- assert.ok(result.success, `Command failed: ${result.error}`);
99
- const parsed = JSON.parse(result.output);
100
- assert.strictEqual(parsed.format, 'full');
101
- assert.ok(parsed.timestamp.match(/^\d{4}-\d{2}-\d{2}T/), `Expected ISO format, got: ${parsed.timestamp}`);
102
- });
103
-
104
- test('returns date-only with date format', () => {
105
- const result = runAceTools('current-timestamp date');
106
- assert.ok(result.success, `Command failed: ${result.error}`);
107
- const parsed = JSON.parse(result.output);
108
- assert.strictEqual(parsed.format, 'date');
109
- assert.ok(parsed.timestamp.match(/^\d{4}-\d{2}-\d{2}$/), `Expected date format, got: ${parsed.timestamp}`);
110
- });
111
-
112
- test('returns filename-safe with filename format', () => {
113
- const result = runAceTools('current-timestamp filename');
114
- assert.ok(result.success, `Command failed: ${result.error}`);
115
- const parsed = JSON.parse(result.output);
116
- assert.strictEqual(parsed.format, 'filename');
117
- assert.ok(!parsed.timestamp.includes(':'), `Filename format should not contain colons: ${parsed.timestamp}`);
118
- assert.ok(parsed.timestamp.includes('_'), `Filename format should contain underscore separator: ${parsed.timestamp}`);
119
- });
120
-
121
- test('returns raw value with --raw flag', () => {
122
- const result = runAceTools('current-timestamp date --raw');
123
- assert.ok(result.success, `Command failed: ${result.error}`);
124
- assert.ok(result.output.match(/^\d{4}-\d{2}-\d{2}$/), `Expected raw date, got: ${result.output}`);
125
- });
126
- });
127
-
128
- // ─── resolve-model ────────────────────────────────────────────────────────────
129
-
130
- describe('resolve-model command', () => {
131
- let tmpDir;
132
-
133
- beforeEach(() => {
134
- tmpDir = createTempProject();
135
- });
136
-
137
- afterEach(() => {
138
- cleanup(tmpDir);
139
- });
140
-
141
- test('returns quality model for ace-project-researcher', () => {
142
- const result = runAceTools('resolve-model ace-project-researcher', tmpDir);
143
- assert.ok(result.success, `Command failed: ${result.error}`);
144
- const parsed = JSON.parse(result.output);
145
- assert.strictEqual(parsed.model, 'opus');
146
- assert.strictEqual(parsed.agent, 'ace-project-researcher');
147
- assert.strictEqual(parsed.profile, 'quality');
148
- });
149
-
150
- test('returns quality model for ace-research-synthesizer', () => {
151
- const result = runAceTools('resolve-model ace-research-synthesizer', tmpDir);
152
- assert.ok(result.success, `Command failed: ${result.error}`);
153
- const parsed = JSON.parse(result.output);
154
- assert.strictEqual(parsed.model, 'sonnet');
155
- assert.strictEqual(parsed.profile, 'quality');
156
- });
157
-
158
- test('returns quality model for ace-product-owner', () => {
159
- const result = runAceTools('resolve-model ace-product-owner', tmpDir);
160
- assert.ok(result.success, `Command failed: ${result.error}`);
161
- const parsed = JSON.parse(result.output);
162
- assert.strictEqual(parsed.model, 'opus');
163
- assert.strictEqual(parsed.profile, 'quality');
164
- });
165
-
166
- test('respects budget profile from config', () => {
167
- fs.mkdirSync(path.join(tmpDir, '.ace'), { recursive: true });
168
- fs.writeFileSync(path.join(tmpDir, '.ace', 'config.json'), JSON.stringify({
169
- model_profile: 'budget',
170
- }));
171
-
172
- const result = runAceTools('resolve-model ace-product-owner', tmpDir);
173
- assert.ok(result.success, `Command failed: ${result.error}`);
174
- const parsed = JSON.parse(result.output);
175
- assert.strictEqual(parsed.model, 'sonnet');
176
- assert.strictEqual(parsed.profile, 'budget');
177
- });
178
-
179
- test('returns sonnet for unknown agent type', () => {
180
- const result = runAceTools('resolve-model unknown-agent', tmpDir);
181
- assert.ok(result.success, `Command failed: ${result.error}`);
182
- const parsed = JSON.parse(result.output);
183
- assert.strictEqual(parsed.model, 'sonnet');
184
- });
185
-
186
- test('returns raw model name with --raw flag', () => {
187
- const result = runAceTools('resolve-model ace-product-owner --raw', tmpDir);
188
- assert.ok(result.success, `Command failed: ${result.error}`);
189
- assert.strictEqual(result.output, 'opus');
190
- });
191
-
192
- test('errors when no agent type provided', () => {
193
- const result = runAceTools('resolve-model');
194
- assert.strictEqual(result.success, false);
195
- assert.ok(result.error.includes('agent-type required'), `Expected error about agent-type, got: ${result.error}`);
196
- });
197
- });
198
-
199
- // ─── verify-path-exists ───────────────────────────────────────────────────────
200
-
201
- describe('verify-path-exists command', () => {
202
- let tmpDir;
203
-
204
- beforeEach(() => {
205
- tmpDir = createTempProject();
206
- });
207
-
208
- afterEach(() => {
209
- cleanup(tmpDir);
210
- });
211
-
212
- test('returns true for existing directory', () => {
213
- fs.mkdirSync(path.join(tmpDir, '.ace'), { recursive: true });
214
- const result = runAceTools('verify-path-exists .ace', tmpDir);
215
- assert.ok(result.success, `Command failed: ${result.error}`);
216
- const parsed = JSON.parse(result.output);
217
- assert.strictEqual(parsed.exists, true);
218
- assert.strictEqual(parsed.path, '.ace');
219
- });
220
-
221
- test('returns false for non-existent path', () => {
222
- const result = runAceTools('verify-path-exists .ace/config.json', tmpDir);
223
- assert.ok(result.success, `Command failed: ${result.error}`);
224
- const parsed = JSON.parse(result.output);
225
- assert.strictEqual(parsed.exists, false);
226
- });
227
-
228
- test('returns true for existing file', () => {
229
- fs.mkdirSync(path.join(tmpDir, '.ace'), { recursive: true });
230
- fs.writeFileSync(path.join(tmpDir, '.ace', 'config.json'), '{}');
231
- const result = runAceTools('verify-path-exists .ace/config.json', tmpDir);
232
- assert.ok(result.success, `Command failed: ${result.error}`);
233
- const parsed = JSON.parse(result.output);
234
- assert.strictEqual(parsed.exists, true);
235
- });
236
-
237
- test('returns raw true/false with --raw flag', () => {
238
- const result = runAceTools('verify-path-exists nonexistent --raw', tmpDir);
239
- assert.ok(result.success, `Command failed: ${result.error}`);
240
- assert.strictEqual(result.output, 'false');
241
- });
242
-
243
- test('errors when no path provided', () => {
244
- const result = runAceTools('verify-path-exists', tmpDir);
245
- assert.strictEqual(result.success, false);
246
- assert.ok(result.error.includes('path required'), `Expected error about path, got: ${result.error}`);
247
- });
248
- });
249
-
250
- // ─── load-config ──────────────────────────────────────────────────────────────
251
-
252
- describe('load-config command', () => {
253
- let tmpDir;
254
-
255
- beforeEach(() => {
256
- tmpDir = createTempProject();
257
- });
258
-
259
- afterEach(() => {
260
- cleanup(tmpDir);
261
- });
262
-
263
- test('returns defaults when no config file exists', () => {
264
- const result = runAceTools('load-config', tmpDir);
265
- assert.ok(result.success, `Command failed: ${result.error}`);
266
- const config = JSON.parse(result.output);
267
- assert.strictEqual(config.version, '0.1.0');
268
- assert.strictEqual(config.projectName, '');
269
- assert.strictEqual(config.storage, 'local');
270
- assert.strictEqual(config.commit_docs, true);
271
- assert.strictEqual(config.github.enabled, false);
272
- assert.strictEqual(config.github.repo, null);
273
- assert.strictEqual(config.github.labels.epic, 'ace:epic');
274
- });
275
-
276
- test('reads existing config and merges with defaults', () => {
277
- fs.mkdirSync(path.join(tmpDir, '.ace'), { recursive: true });
278
- fs.writeFileSync(path.join(tmpDir, '.ace', 'config.json'), JSON.stringify({
279
- projectName: 'Test Project',
280
- storage: 'github',
281
- github: { enabled: true, repo: 'owner/repo' },
282
- }));
283
-
284
- const result = runAceTools('load-config', tmpDir);
285
- assert.ok(result.success, `Command failed: ${result.error}`);
286
- const config = JSON.parse(result.output);
287
- assert.strictEqual(config.projectName, 'Test Project');
288
- assert.strictEqual(config.storage, 'github');
289
- assert.strictEqual(config.github.enabled, true);
290
- assert.strictEqual(config.github.repo, 'owner/repo');
291
- // Defaults still applied for unset fields
292
- assert.strictEqual(config.version, '0.1.0');
293
- assert.strictEqual(config.github.labels.epic, 'ace:epic');
294
- });
295
-
296
- test('handles malformed JSON gracefully', () => {
297
- fs.mkdirSync(path.join(tmpDir, '.ace'), { recursive: true });
298
- fs.writeFileSync(path.join(tmpDir, '.ace', 'config.json'), 'not json');
299
-
300
- const result = runAceTools('load-config', tmpDir);
301
- assert.ok(result.success, `Command failed: ${result.error}`);
302
- const config = JSON.parse(result.output);
303
- // Should return defaults
304
- assert.strictEqual(config.version, '0.1.0');
305
- assert.strictEqual(config.projectName, '');
306
- });
307
- });
308
-
309
- // ─── init new-project ─────────────────────────────────────────────────────────
310
-
311
- describe('init new-project command', () => {
312
- let tmpDir;
313
-
314
- beforeEach(() => {
315
- tmpDir = createTempProject();
316
- });
317
-
318
- afterEach(() => {
319
- cleanup(tmpDir);
320
- });
321
-
322
- test('detects empty project (greenfield)', () => {
323
- const result = runAceTools('init new-project', tmpDir);
324
- assert.ok(result.success, `Command failed: ${result.error}`);
325
- const data = JSON.parse(result.output);
326
-
327
- assert.strictEqual(data.project_exists, false);
328
- assert.strictEqual(data.has_codebase_map, false);
329
- assert.strictEqual(data.planning_exists, false);
330
- assert.strictEqual(data.is_brownfield, false);
331
- assert.strictEqual(data.has_existing_code, false);
332
- assert.strictEqual(data.has_package_file, false);
333
- assert.strictEqual(data.needs_codebase_map, false);
334
- assert.strictEqual(data.has_git, false);
335
- assert.strictEqual(data.commit_docs, true);
336
- });
337
-
338
- test('detects existing code files (brownfield)', () => {
339
- fs.writeFileSync(path.join(tmpDir, 'index.js'), 'console.log("hello");');
340
- fs.writeFileSync(path.join(tmpDir, 'package.json'), '{}');
341
-
342
- const result = runAceTools('init new-project', tmpDir);
343
- assert.ok(result.success, `Command failed: ${result.error}`);
344
- const data = JSON.parse(result.output);
345
-
346
- assert.strictEqual(data.has_existing_code, true);
347
- assert.strictEqual(data.has_package_file, true);
348
- assert.strictEqual(data.is_brownfield, true);
349
- assert.strictEqual(data.needs_codebase_map, true);
350
- });
351
-
352
- test('detects package file without code files', () => {
353
- fs.writeFileSync(path.join(tmpDir, 'package.json'), '{}');
354
-
355
- const result = runAceTools('init new-project', tmpDir);
356
- assert.ok(result.success, `Command failed: ${result.error}`);
357
- const data = JSON.parse(result.output);
358
-
359
- assert.strictEqual(data.has_existing_code, false);
360
- assert.strictEqual(data.has_package_file, true);
361
- assert.strictEqual(data.is_brownfield, true);
362
- });
363
-
364
- test('detects nested code files up to depth 3', () => {
365
- const nested = path.join(tmpDir, 'src', 'lib', 'utils');
366
- fs.mkdirSync(nested, { recursive: true });
367
- fs.writeFileSync(path.join(nested, 'helper.ts'), 'export const x = 1;');
368
-
369
- const result = runAceTools('init new-project', tmpDir);
370
- assert.ok(result.success, `Command failed: ${result.error}`);
371
- const data = JSON.parse(result.output);
372
-
373
- assert.strictEqual(data.has_existing_code, true);
374
- assert.strictEqual(data.needs_codebase_map, true);
375
- });
376
-
377
- test('ignores node_modules directory', () => {
378
- fs.mkdirSync(path.join(tmpDir, 'node_modules', 'pkg'), { recursive: true });
379
- fs.writeFileSync(path.join(tmpDir, 'node_modules', 'pkg', 'index.js'), 'module.exports = {};');
380
-
381
- const result = runAceTools('init new-project', tmpDir);
382
- assert.ok(result.success, `Command failed: ${result.error}`);
383
- const data = JSON.parse(result.output);
384
-
385
- assert.strictEqual(data.has_existing_code, false);
386
- assert.strictEqual(data.needs_codebase_map, false);
387
- });
388
-
389
- test('detects ACE already initialized', () => {
390
- fs.mkdirSync(path.join(tmpDir, '.docs', 'product'), { recursive: true });
391
- fs.writeFileSync(path.join(tmpDir, '.docs', 'product', 'product-vision.md'), '# My Product');
392
-
393
- const result = runAceTools('init new-project', tmpDir);
394
- assert.ok(result.success, `Command failed: ${result.error}`);
395
- const data = JSON.parse(result.output);
396
-
397
- assert.strictEqual(data.project_exists, true);
398
- });
399
-
400
- test('detects git repository', () => {
401
- fs.mkdirSync(path.join(tmpDir, '.git'), { recursive: true });
402
-
403
- const result = runAceTools('init new-project', tmpDir);
404
- assert.ok(result.success, `Command failed: ${result.error}`);
405
- const data = JSON.parse(result.output);
406
-
407
- assert.strictEqual(data.has_git, true);
408
- });
409
-
410
- test('commit_docs defaults to true', () => {
411
- const result = runAceTools('init new-project', tmpDir);
412
- assert.ok(result.success, `Command failed: ${result.error}`);
413
- const data = JSON.parse(result.output);
414
-
415
- assert.strictEqual(data.commit_docs, true);
416
- });
417
-
418
- test('includes pre-resolved models for init agents', () => {
419
- const result = runAceTools('init new-project', tmpDir);
420
- assert.ok(result.success, `Command failed: ${result.error}`);
421
- const data = JSON.parse(result.output);
422
-
423
- assert.strictEqual(data.product_owner_model, 'opus');
424
- assert.strictEqual(data.researcher_model, 'opus');
425
- assert.strictEqual(data.synthesizer_model, 'sonnet');
426
- });
427
-
428
- test('models respect config profile', () => {
429
- fs.mkdirSync(path.join(tmpDir, '.ace'), { recursive: true });
430
- fs.writeFileSync(path.join(tmpDir, '.ace', 'config.json'), JSON.stringify({
431
- model_profile: 'budget',
432
- }));
433
-
434
- const result = runAceTools('init new-project', tmpDir);
435
- assert.ok(result.success, `Command failed: ${result.error}`);
436
- const data = JSON.parse(result.output);
437
-
438
- assert.strictEqual(data.product_owner_model, 'sonnet');
439
- assert.strictEqual(data.researcher_model, 'haiku');
440
- assert.strictEqual(data.synthesizer_model, 'haiku');
441
- });
442
-
443
- test('detects Python project files', () => {
444
- fs.writeFileSync(path.join(tmpDir, 'requirements.txt'), 'flask==2.0');
445
- fs.writeFileSync(path.join(tmpDir, 'app.py'), 'from flask import Flask');
446
-
447
- const result = runAceTools('init new-project', tmpDir);
448
- assert.ok(result.success, `Command failed: ${result.error}`);
449
- const data = JSON.parse(result.output);
450
-
451
- assert.strictEqual(data.has_existing_code, true);
452
- assert.strictEqual(data.has_package_file, true);
453
- assert.strictEqual(data.is_brownfield, true);
454
- });
455
-
456
- test('detects Go project files', () => {
457
- fs.writeFileSync(path.join(tmpDir, 'go.mod'), 'module example.com/foo');
458
- fs.writeFileSync(path.join(tmpDir, 'main.go'), 'package main');
459
-
460
- const result = runAceTools('init new-project', tmpDir);
461
- assert.ok(result.success, `Command failed: ${result.error}`);
462
- const data = JSON.parse(result.output);
463
-
464
- assert.strictEqual(data.has_existing_code, true);
465
- assert.strictEqual(data.has_package_file, true);
466
- });
467
-
468
- test('detects Rust project files', () => {
469
- fs.writeFileSync(path.join(tmpDir, 'Cargo.toml'), '[package]');
470
- fs.mkdirSync(path.join(tmpDir, 'src'), { recursive: true });
471
- fs.writeFileSync(path.join(tmpDir, 'src', 'main.rs'), 'fn main() {}');
472
-
473
- const result = runAceTools('init new-project', tmpDir);
474
- assert.ok(result.success, `Command failed: ${result.error}`);
475
- const data = JSON.parse(result.output);
476
-
477
- assert.strictEqual(data.has_existing_code, true);
478
- assert.strictEqual(data.has_package_file, true);
479
- });
480
-
481
- test('needs_codebase_map is false when codebase dir exists', () => {
482
- fs.mkdirSync(path.join(tmpDir, '.ace', 'codebase'), { recursive: true });
483
- fs.writeFileSync(path.join(tmpDir, 'index.js'), 'console.log("hello");');
484
-
485
- const result = runAceTools('init new-project', tmpDir);
486
- assert.ok(result.success, `Command failed: ${result.error}`);
487
- const data = JSON.parse(result.output);
488
-
489
- assert.strictEqual(data.has_existing_code, true);
490
- assert.strictEqual(data.has_codebase_map, true);
491
- assert.strictEqual(data.needs_codebase_map, false);
492
- });
493
-
494
- test('commit_docs respects config override', () => {
495
- fs.mkdirSync(path.join(tmpDir, '.ace'), { recursive: true });
496
- fs.writeFileSync(path.join(tmpDir, '.ace', 'config.json'), JSON.stringify({
497
- commit_docs: false,
498
- }));
499
-
500
- const result = runAceTools('init new-project', tmpDir);
501
- assert.ok(result.success, `Command failed: ${result.error}`);
502
- const data = JSON.parse(result.output);
503
-
504
- assert.strictEqual(data.commit_docs, false);
505
- });
506
-
507
- test('detects C# project files', () => {
508
- fs.writeFileSync(path.join(tmpDir, 'App.cs'), 'namespace App {}');
509
-
510
- const result = runAceTools('init new-project', tmpDir);
511
- assert.ok(result.success, `Command failed: ${result.error}`);
512
- const data = JSON.parse(result.output);
513
-
514
- assert.strictEqual(data.has_existing_code, true);
515
- assert.strictEqual(data.needs_codebase_map, true);
516
- });
517
-
518
- test('detects .csproj as package file', () => {
519
- fs.writeFileSync(path.join(tmpDir, 'MyApp.csproj'), '<Project Sdk="Microsoft.NET.Sdk" />');
520
-
521
- const result = runAceTools('init new-project', tmpDir);
522
- assert.ok(result.success, `Command failed: ${result.error}`);
523
- const data = JSON.parse(result.output);
524
-
525
- assert.strictEqual(data.has_package_file, true);
526
- assert.strictEqual(data.is_brownfield, true);
527
- });
528
-
529
- test('detects .sln as package file', () => {
530
- fs.writeFileSync(path.join(tmpDir, 'MyApp.sln'), 'Microsoft Visual Studio Solution File');
531
-
532
- const result = runAceTools('init new-project', tmpDir);
533
- assert.ok(result.success, `Command failed: ${result.error}`);
534
- const data = JSON.parse(result.output);
535
-
536
- assert.strictEqual(data.has_package_file, true);
537
- assert.strictEqual(data.is_brownfield, true);
538
- });
539
-
540
- test('detects Java project with build.gradle', () => {
541
- fs.writeFileSync(path.join(tmpDir, 'build.gradle'), 'plugins {}');
542
-
543
- const result = runAceTools('init new-project', tmpDir);
544
- assert.ok(result.success, `Command failed: ${result.error}`);
545
- const data = JSON.parse(result.output);
546
-
547
- assert.strictEqual(data.has_package_file, true);
548
- });
549
-
550
- test('has_gh_cli is boolean', () => {
551
- const result = runAceTools('init new-project', tmpDir);
552
- assert.ok(result.success, `Command failed: ${result.error}`);
553
- const data = JSON.parse(result.output);
554
-
555
- assert.strictEqual(typeof data.has_gh_cli, 'boolean');
556
- });
557
- });
558
-
559
- // ─── ensure-settings ──────────────────────────────────────────────────────────
560
-
561
- describe('ensure-settings command', () => {
562
- let tmpDir;
563
-
564
- beforeEach(() => {
565
- tmpDir = createTempProject();
566
- });
567
-
568
- afterEach(() => {
569
- cleanup(tmpDir);
570
- });
571
-
572
- test('creates settings.json with defaults when missing', () => {
573
- const result = runAceTools('ensure-settings', tmpDir);
574
- assert.ok(result.success, `Command failed: ${result.error}`);
575
- const data = JSON.parse(result.output);
576
-
577
- assert.strictEqual(data.created, true);
578
- assert.strictEqual(data.settings.model_profile, 'balanced');
579
- assert.strictEqual(data.settings.commit_docs, true);
580
- assert.strictEqual(data.settings.github_project.enabled, false);
581
- assert.strictEqual(data.settings.github_project.gh_installed, false);
582
- assert.strictEqual(data.settings.github_project.repo, '');
583
- assert.strictEqual(data.settings.github_project.project_number, null);
584
- assert.strictEqual(data.settings.github_project.owner, '');
585
-
586
- // Verify file was actually created
587
- const settingsPath = path.join(tmpDir, '.ace', 'settings.json');
588
- assert.ok(fs.existsSync(settingsPath), 'settings.json should exist on disk');
589
- const onDisk = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
590
- assert.strictEqual(onDisk.model_profile, 'balanced');
591
- });
592
-
593
- test('creates .ace directory if it does not exist', () => {
594
- const aceDir = path.join(tmpDir, '.ace');
595
- assert.ok(!fs.existsSync(aceDir), '.ace dir should not exist yet');
596
-
597
- const result = runAceTools('ensure-settings', tmpDir);
598
- assert.ok(result.success, `Command failed: ${result.error}`);
599
- const data = JSON.parse(result.output);
600
-
601
- assert.strictEqual(data.created, true);
602
- assert.ok(fs.existsSync(aceDir), '.ace dir should be created');
603
- });
604
-
605
- test('does not overwrite existing settings.json', () => {
606
- fs.mkdirSync(path.join(tmpDir, '.ace'), { recursive: true });
607
- const customSettings = {
608
- model_profile: 'quality',
609
- commit_docs: false,
610
- github_project: {
611
- enabled: true,
612
- gh_installed: true,
613
- repo: 'owner/repo',
614
- project_number: 5,
615
- owner: 'owner',
616
- },
617
- };
618
- fs.writeFileSync(path.join(tmpDir, '.ace', 'settings.json'), JSON.stringify(customSettings, null, 2));
619
-
620
- const result = runAceTools('ensure-settings', tmpDir);
621
- assert.ok(result.success, `Command failed: ${result.error}`);
622
- const data = JSON.parse(result.output);
623
-
624
- assert.strictEqual(data.created, false);
625
- assert.strictEqual(data.settings.model_profile, 'quality');
626
- assert.strictEqual(data.settings.commit_docs, false);
627
- assert.strictEqual(data.settings.github_project.enabled, true);
628
- assert.strictEqual(data.settings.github_project.project_number, 5);
629
- });
630
- });
631
-
632
- // ─── init setup-github ────────────────────────────────────────────────────────
633
-
634
- describe('init setup-github command', () => {
635
- let tmpDir;
636
-
637
- beforeEach(() => {
638
- tmpDir = createTempProject();
639
- });
640
-
641
- afterEach(() => {
642
- cleanup(tmpDir);
643
- });
644
-
645
- test('returns gh_installed as boolean', () => {
646
- const result = runAceTools('init setup-github', tmpDir);
647
- assert.ok(result.success, `Command failed: ${result.error}`);
648
- const data = JSON.parse(result.output);
649
-
650
- assert.strictEqual(typeof data.gh_installed, 'boolean');
651
- });
652
-
653
- test('returns projects as array', () => {
654
- const result = runAceTools('init setup-github', tmpDir);
655
- assert.ok(result.success, `Command failed: ${result.error}`);
656
- const data = JSON.parse(result.output);
657
-
658
- assert.ok(Array.isArray(data.projects), 'projects should be an array');
659
- });
660
-
661
- test('returns current_settings object', () => {
662
- const result = runAceTools('init setup-github', tmpDir);
663
- assert.ok(result.success, `Command failed: ${result.error}`);
664
- const data = JSON.parse(result.output);
665
-
666
- assert.ok(data.current_settings !== undefined, 'current_settings should be present');
667
- assert.strictEqual(typeof data.current_settings.enabled, 'boolean');
668
- });
669
-
670
- test('returns repo and owner as strings', () => {
671
- const result = runAceTools('init setup-github', tmpDir);
672
- assert.ok(result.success, `Command failed: ${result.error}`);
673
- const data = JSON.parse(result.output);
674
-
675
- assert.strictEqual(typeof data.repo, 'string');
676
- assert.strictEqual(typeof data.owner, 'string');
677
- });
678
- });
679
-
680
- // ─── write-github-settings ────────────────────────────────────────────────────
681
-
682
- describe('write-github-settings command', () => {
683
- let tmpDir;
684
-
685
- beforeEach(() => {
686
- tmpDir = createTempProjectWithAce();
687
- // Seed a settings.json with defaults
688
- const defaults = {
689
- model_profile: 'balanced',
690
- commit_docs: true,
691
- github_project: {
692
- enabled: false,
693
- gh_installed: false,
694
- repo: '',
695
- project_number: null,
696
- owner: '',
697
- },
698
- };
699
- fs.writeFileSync(path.join(tmpDir, '.ace', 'settings.json'), JSON.stringify(defaults, null, 2));
700
- });
701
-
702
- afterEach(() => {
703
- cleanup(tmpDir);
704
- });
705
-
706
- test('writes key=value pairs to settings.json', () => {
707
- const result = runAceTools('write-github-settings enabled=true repo=owner/repo project_number=3 owner=owner gh_installed=true', tmpDir);
708
- assert.ok(result.success, `Command failed: ${result.error}`);
709
- const data = JSON.parse(result.output);
710
-
711
- assert.strictEqual(data.written, true);
712
- assert.strictEqual(data.settings.github_project.enabled, true);
713
- assert.strictEqual(data.settings.github_project.repo, 'owner/repo');
714
- assert.strictEqual(data.settings.github_project.project_number, 3);
715
- assert.strictEqual(data.settings.github_project.owner, 'owner');
716
- assert.strictEqual(data.settings.github_project.gh_installed, true);
717
-
718
- // Verify persisted to disk
719
- const onDisk = JSON.parse(fs.readFileSync(path.join(tmpDir, '.ace', 'settings.json'), 'utf-8'));
720
- assert.strictEqual(onDisk.github_project.enabled, true);
721
- assert.strictEqual(onDisk.github_project.project_number, 3);
722
- });
723
-
724
- test('preserves non-github settings when writing', () => {
725
- const result = runAceTools('write-github-settings enabled=true', tmpDir);
726
- assert.ok(result.success, `Command failed: ${result.error}`);
727
- const data = JSON.parse(result.output);
728
-
729
- assert.strictEqual(data.settings.model_profile, 'balanced');
730
- assert.strictEqual(data.settings.commit_docs, true);
731
- });
732
-
733
- test('handles project_number=null', () => {
734
- // First set a number
735
- runAceTools('write-github-settings project_number=5', tmpDir);
736
- // Then reset to null
737
- const result = runAceTools('write-github-settings project_number=null', tmpDir);
738
- assert.ok(result.success, `Command failed: ${result.error}`);
739
- const data = JSON.parse(result.output);
740
-
741
- assert.strictEqual(data.settings.github_project.project_number, null);
742
- });
743
- });
744
-
745
- // ─── init research-story ─────────────────────────────────────────────────────
746
-
747
- const SAMPLE_STORY = `# S3: Display OAuth Provider Buttons
748
-
749
- **Feature**: F3 OAuth2 Login Flow | **Epic**: #45 User Authentication
750
- **Status**: Refined | **Size**: 3 | **Sprint**: Sprint 2 | **Link**: [#95](https://github.com/owner/repo/issues/95)
751
-
752
- ## User Story
753
-
754
- > As a returning customer,
755
- > I want to click a Google or GitHub login button,
756
- > so that I can authenticate without remembering a site-specific password.
757
-
758
- ## Description
759
-
760
- This story adds OAuth provider buttons to the login page. It builds on the
761
- auth service foundation (S1) and enables the token exchange flow (S4).
762
-
763
- ## Acceptance Criteria
764
-
765
- ### Scenario: Successful Google login
766
-
767
- **Given** the user is on the login page and has a valid Google account
768
- **When** they click the "Sign in with Google" button and complete Google's OAuth flow
769
- **Then** they are redirected to the dashboard and see their Google profile name
770
-
771
- ### Scenario: Provider unavailable
772
-
773
- **Given** the user is on the login page and the Google OAuth service is unreachable
774
- **When** they click the "Sign in with Google" button
775
- **Then** they see an error message "Login service temporarily unavailable. Please try again."
776
-
777
- ### Scenario: GitHub login button displayed
778
-
779
- **Given** the user navigates to the login page
780
- **When** the page loads
781
- **Then** they see a "Sign in with GitHub" button alongside the Google button
782
-
783
- ## Out of Scope
784
-
785
- - Token refresh logic (handled by S4)
786
- - Account linking (future feature)
787
-
788
- ## Dependencies
789
-
790
- ### Blocked By
791
- - S1 Auth service foundation
792
-
793
- ### Blocks
794
- - S4 Token exchange flow
795
-
796
- ### External
797
- - Google OAuth API — available
798
-
799
- ## Definition of Done
800
-
801
- - [ ] All acceptance criteria scenarios pass
802
- - [ ] Code reviewed and approved
803
- - [ ] Tests written and passing
804
- - [ ] CI pipeline green
805
- - [ ] Documentation updated (if applicable)
806
- - [ ] Product Owner verified
807
-
808
- ## Relevant Wiki
809
-
810
- ### System-Wide
811
-
812
- - \`.docs/wiki/system-wide/system-structure.md\` — Mandatory system-wide context
813
- - \`.docs/wiki/system-wide/system-architecture.md\` — Mandatory system-wide context
814
- - \`.docs/wiki/system-wide/coding-standards.md\` — Mandatory system-wide context
815
- - \`.docs/wiki/system-wide/testing-framework.md\` — Mandatory system-wide context
816
-
817
- ### Systems
818
- - \`.docs/wiki/subsystems/auth/systems/oauth-provider.md\` — Implements the provider abstraction this story extends
819
-
820
- ### Patterns
821
- - \`.docs/wiki/subsystems/auth/patterns/strategy-pattern.md\` — Each OAuth provider is a strategy; new provider must follow this
822
-
823
- ### Decisions
824
- - \`.docs/wiki/subsystems/auth/decisions/adr-003-jwt-over-sessions.md\` — Constrains token format to JWT
825
- `;
826
-
827
- describe('init research-story command', () => {
828
- let tmpDir;
829
-
830
- beforeEach(() => {
831
- tmpDir = createTempProject();
832
- fs.mkdirSync(path.join(tmpDir, '.ace'), { recursive: true });
833
- });
834
-
835
- afterEach(() => {
836
- cleanup(tmpDir);
837
- });
838
-
839
- test('parses story from file and extracts metadata', () => {
840
- // Create story directory structure
841
- const storyDir = path.join(tmpDir, '.ace', 'artifacts', 'product', '45-user-authentication', 'f3-oauth2-login-flow', 's3-display-oauth-provider-buttons');
842
- fs.mkdirSync(storyDir, { recursive: true });
843
- const storyFile = path.join(storyDir, 's3-display-oauth-provider-buttons.md');
844
- fs.writeFileSync(storyFile, SAMPLE_STORY);
845
-
846
- const relPath = path.relative(tmpDir, storyFile).replace(/\\/g, '/');
847
- const result = runAceTools(`init research-story ${relPath}`, tmpDir);
848
- assert.ok(result.success, `Command failed: ${result.error}`);
849
- const data = JSON.parse(result.output);
850
-
851
- assert.strictEqual(data.story_valid, true);
852
- assert.strictEqual(data.story_error, null);
853
- assert.strictEqual(data.story_source, 'file');
854
- assert.strictEqual(data.story.id, 'S3');
855
- assert.strictEqual(data.story.title, 'Display OAuth Provider Buttons');
856
- assert.strictEqual(data.story.status, 'Refined');
857
- assert.strictEqual(data.story.size, '3');
858
- assert.strictEqual(data.feature.id, 'F3');
859
- assert.strictEqual(data.feature.title, 'OAuth2 Login Flow');
860
- assert.strictEqual(data.epic.id, '#45');
861
- assert.strictEqual(data.epic.title, 'User Authentication');
862
- });
863
-
864
- test('extracts user story and description', () => {
865
- const storyDir = path.join(tmpDir, '.ace', 'artifacts', 'product', 'epic', 'feat', 'story');
866
- fs.mkdirSync(storyDir, { recursive: true });
867
- const storyFile = path.join(storyDir, 'story.md');
868
- fs.writeFileSync(storyFile, SAMPLE_STORY);
869
-
870
- const relPath = path.relative(tmpDir, storyFile).replace(/\\/g, '/');
871
- const result = runAceTools(`init research-story ${relPath}`, tmpDir);
872
- assert.ok(result.success, `Command failed: ${result.error}`);
873
- const data = JSON.parse(result.output);
874
-
875
- assert.ok(data.user_story.includes('As a returning customer'), `user_story should contain persona: ${data.user_story}`);
876
- assert.ok(data.user_story.includes('authenticate without remembering'), `user_story should contain benefit`);
877
- assert.ok(data.description.includes('OAuth provider buttons'), `description should contain story details: ${data.description}`);
878
- assert.strictEqual(data.acceptance_criteria_count, 3);
879
- });
880
-
881
- test('extracts wiki references with categories', () => {
882
- const storyDir = path.join(tmpDir, '.ace', 'artifacts', 'product', 'epic', 'feat', 'story');
883
- fs.mkdirSync(storyDir, { recursive: true });
884
- const storyFile = path.join(storyDir, 'story.md');
885
- fs.writeFileSync(storyFile, SAMPLE_STORY);
886
-
887
- const relPath = path.relative(tmpDir, storyFile).replace(/\\/g, '/');
888
- const result = runAceTools(`init research-story ${relPath}`, tmpDir);
889
- assert.ok(result.success, `Command failed: ${result.error}`);
890
- const data = JSON.parse(result.output);
891
-
892
- assert.strictEqual(data.wiki_references.system_wide.length, 4);
893
- assert.ok(data.wiki_references.system_wide.includes('.docs/wiki/system-wide/system-structure.md'));
894
- assert.ok(data.wiki_references.system_wide.includes('.docs/wiki/system-wide/coding-standards.md'));
895
-
896
- assert.strictEqual(data.wiki_references.subsystem_docs.length, 3);
897
- const oauthDoc = data.wiki_references.subsystem_docs.find(d => d.path.includes('oauth-provider'));
898
- assert.ok(oauthDoc, 'Should find oauth-provider doc');
899
- assert.strictEqual(oauthDoc.category, 'systems');
900
-
901
- const strategyDoc = data.wiki_references.subsystem_docs.find(d => d.path.includes('strategy-pattern'));
902
- assert.ok(strategyDoc, 'Should find strategy-pattern doc');
903
- assert.strictEqual(strategyDoc.category, 'patterns');
904
-
905
- const adrDoc = data.wiki_references.subsystem_docs.find(d => d.path.includes('adr-003'));
906
- assert.ok(adrDoc, 'Should find ADR doc');
907
- assert.strictEqual(adrDoc.category, 'decisions');
908
-
909
- assert.strictEqual(data.wiki_references.total_count, 7);
910
- });
911
-
912
- test('computes paths from file location', () => {
913
- const storyDir = path.join(tmpDir, '.ace', 'artifacts', 'product', 'e1-auth', 'f3-oauth', 's3-buttons');
914
- fs.mkdirSync(storyDir, { recursive: true });
915
- const storyFile = path.join(storyDir, 's3-buttons.md');
916
- fs.writeFileSync(storyFile, SAMPLE_STORY);
917
-
918
- const relPath = path.relative(tmpDir, storyFile).replace(/\\/g, '/');
919
- const result = runAceTools(`init research-story ${relPath}`, tmpDir);
920
- assert.ok(result.success, `Command failed: ${result.error}`);
921
- const data = JSON.parse(result.output);
922
-
923
- assert.ok(data.paths, 'paths should be present');
924
- assert.ok(data.paths.story_dir.includes('s3-buttons'), `story_dir should contain story slug: ${data.paths.story_dir}`);
925
- assert.ok(data.paths.external_analysis_file.endsWith('external-analysis.md'), `external_analysis_file: ${data.paths.external_analysis_file}`);
926
- assert.ok(data.paths.integration_analysis_file.endsWith('integration-analysis.md'));
927
- assert.ok(data.paths.feature_file.endsWith('f3-oauth.md'), `feature_file: ${data.paths.feature_file}`);
928
- });
929
-
930
- test('detects existing artifacts', () => {
931
- const storyDir = path.join(tmpDir, '.ace', 'artifacts', 'product', 'epic', 'feat', 'story');
932
- fs.mkdirSync(storyDir, { recursive: true });
933
- const storyFile = path.join(storyDir, 'story.md');
934
- fs.writeFileSync(storyFile, SAMPLE_STORY);
935
- // Create external analysis
936
- fs.writeFileSync(path.join(storyDir, 'external-analysis.md'), '# External Analysis');
937
-
938
- const relPath = path.relative(tmpDir, storyFile).replace(/\\/g, '/');
939
- const result = runAceTools(`init research-story ${relPath}`, tmpDir);
940
- assert.ok(result.success, `Command failed: ${result.error}`);
941
- const data = JSON.parse(result.output);
942
-
943
- assert.strictEqual(data.has_external_analysis, true);
944
- assert.strictEqual(data.has_integration_analysis, false);
945
- });
946
-
947
- test('verifies wiki doc existence', () => {
948
- const storyDir = path.join(tmpDir, '.ace', 'artifacts', 'product', 'epic', 'feat', 'story');
949
- fs.mkdirSync(storyDir, { recursive: true });
950
- const storyFile = path.join(storyDir, 'story.md');
951
- fs.writeFileSync(storyFile, SAMPLE_STORY);
952
-
953
- // Create some wiki docs but not all
954
- fs.mkdirSync(path.join(tmpDir, '.docs', 'wiki', 'system-wide'), { recursive: true });
955
- fs.writeFileSync(path.join(tmpDir, '.docs', 'wiki', 'system-wide', 'system-structure.md'), '# Structure');
956
- fs.writeFileSync(path.join(tmpDir, '.docs', 'wiki', 'system-wide', 'coding-standards.md'), '# Standards');
957
-
958
- const relPath = path.relative(tmpDir, storyFile).replace(/\\/g, '/');
959
- const result = runAceTools(`init research-story ${relPath}`, tmpDir);
960
- assert.ok(result.success, `Command failed: ${result.error}`);
961
- const data = JSON.parse(result.output);
962
-
963
- assert.ok(data.wiki_docs_exist.existing.length >= 2, `Should find at least 2 existing: ${JSON.stringify(data.wiki_docs_exist.existing)}`);
964
- assert.ok(data.wiki_docs_exist.missing.length >= 2, `Should find at least 2 missing: ${JSON.stringify(data.wiki_docs_exist.missing)}`);
965
- assert.ok(data.wiki_docs_exist.existing.includes('.docs/wiki/system-wide/system-structure.md'));
966
- });
967
-
968
- test('returns error for non-existent file', () => {
969
- const result = runAceTools('init research-story nonexistent.md', tmpDir);
970
- assert.ok(result.success, `Command should still succeed with error in JSON: ${result.error}`);
971
- const data = JSON.parse(result.output);
972
-
973
- assert.strictEqual(data.story_valid, false);
974
- assert.ok(data.story_error.includes('not found'), `story_error: ${data.story_error}`);
975
- });
976
-
977
- test('returns error for no parameter', () => {
978
- const result = runAceTools('init research-story', tmpDir);
979
- assert.ok(result.success, `Command should still succeed with error in JSON: ${result.error}`);
980
- const data = JSON.parse(result.output);
981
-
982
- assert.strictEqual(data.story_valid, false);
983
- assert.ok(data.story_error !== null, 'Should have a story_error');
984
- });
985
-
986
- test('includes model and environment fields', () => {
987
- const storyDir = path.join(tmpDir, '.ace', 'artifacts', 'product', 'epic', 'feat', 'story');
988
- fs.mkdirSync(storyDir, { recursive: true });
989
- const storyFile = path.join(storyDir, 'story.md');
990
- fs.writeFileSync(storyFile, SAMPLE_STORY);
991
-
992
- const relPath = path.relative(tmpDir, storyFile).replace(/\\/g, '/');
993
- const result = runAceTools(`init research-story ${relPath}`, tmpDir);
994
- assert.ok(result.success, `Command failed: ${result.error}`);
995
- const data = JSON.parse(result.output);
996
-
997
- assert.ok(typeof data.analyst_model === 'string', 'analyst_model should be a string');
998
- assert.ok(typeof data.mapper_model === 'string', 'mapper_model should be a string');
999
- assert.ok(typeof data.has_git === 'boolean', 'has_git should be boolean');
1000
- assert.ok(typeof data.has_gh_cli === 'boolean', 'has_gh_cli should be boolean');
1001
- assert.ok(typeof data.commit_docs === 'boolean', 'commit_docs should be boolean');
1002
- assert.ok(data.github_project !== undefined, 'github_project should be present');
1003
- });
1004
-
1005
- test('handles story without Relevant Wiki section', () => {
1006
- const minimalStory = `# S1: Basic Story
1007
-
1008
- **Feature**: F1 Auth | **Epic**: E1 Security
1009
- **Status**: Todo | **Size**: 2
1010
-
1011
- ## User Story
1012
-
1013
- > As a user,
1014
- > I want to log in,
1015
- > so that I can access my account.
1016
-
1017
- ## Description
1018
-
1019
- Basic login functionality.
1020
-
1021
- ## Acceptance Criteria
1022
-
1023
- ### Scenario: Successful login
1024
-
1025
- **Given** valid credentials
1026
- **When** user submits login form
1027
- **Then** user is redirected to dashboard
1028
-
1029
- ## Definition of Done
1030
-
1031
- - [ ] All AC pass
1032
- `;
1033
- const storyDir = path.join(tmpDir, '.ace', 'artifacts', 'product', 'epic', 'feat', 'story');
1034
- fs.mkdirSync(storyDir, { recursive: true });
1035
- const storyFile = path.join(storyDir, 'story.md');
1036
- fs.writeFileSync(storyFile, minimalStory);
1037
-
1038
- const relPath = path.relative(tmpDir, storyFile).replace(/\\/g, '/');
1039
- const result = runAceTools(`init research-story ${relPath}`, tmpDir);
1040
- assert.ok(result.success, `Command failed: ${result.error}`);
1041
- const data = JSON.parse(result.output);
1042
-
1043
- assert.strictEqual(data.story_valid, true);
1044
- assert.strictEqual(data.wiki_references.total_count, 0);
1045
- assert.strictEqual(data.acceptance_criteria_count, 1);
1046
- assert.strictEqual(data.story.id, 'S1');
1047
- });
1048
-
1049
- test('classifies GitHub URL correctly', () => {
1050
- // We can't actually fetch from GitHub in tests, but we can verify it tries
1051
- const result = runAceTools('init research-story https://github.com/owner/repo/issues/123', tmpDir);
1052
- assert.ok(result.success, `Command should succeed: ${result.error}`);
1053
- const data = JSON.parse(result.output);
1054
-
1055
- assert.strictEqual(data.story_source, 'github');
1056
- // It will either fail due to no gh cli or fail to fetch — both are valid
1057
- // The point is it classified correctly
1058
- });
1059
-
1060
- test('classifies issue number correctly', () => {
1061
- const result = runAceTools('init research-story 42', tmpDir);
1062
- assert.ok(result.success, `Command should succeed: ${result.error}`);
1063
- const data = JSON.parse(result.output);
1064
-
1065
- assert.strictEqual(data.story_source, 'github');
1066
- });
1067
- });
1068
-
1069
- // ─── CLI error handling ───────────────────────────────────────────────────────
1070
-
1071
- describe('CLI error handling', () => {
1072
- test('errors on unknown command', () => {
1073
- const result = runAceTools('nonexistent');
1074
- assert.strictEqual(result.success, false);
1075
- assert.ok(result.error.includes('Unknown command'), `Expected unknown command error, got: ${result.error}`);
1076
- });
1077
-
1078
- test('errors when no command provided', () => {
1079
- const result = runAceTools('');
1080
- assert.strictEqual(result.success, false);
1081
- assert.ok(result.error.includes('Usage'), `Expected usage message, got: ${result.error}`);
1082
- });
1083
-
1084
- test('errors on unknown init subcommand', () => {
1085
- const result = runAceTools('init nonexistent');
1086
- assert.strictEqual(result.success, false);
1087
- assert.ok(result.error.includes('Unknown init subcommand'), `Expected subcommand error, got: ${result.error}`);
1088
- });
1089
- });