agile-context-engineering 0.5.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 (102) hide show
  1. package/.claude-plugin/marketplace.json +18 -0
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +7 -1
  4. package/README.md +16 -12
  5. package/agents/ace-code-discovery-analyst.md +245 -245
  6. package/agents/ace-code-integration-analyst.md +248 -248
  7. package/agents/ace-code-reviewer.md +375 -375
  8. package/agents/ace-product-owner.md +365 -361
  9. package/agents/ace-project-researcher.md +606 -606
  10. package/agents/ace-technical-application-architect.md +315 -315
  11. package/bin/install.js +587 -173
  12. package/hooks/ace-check-update.js +15 -14
  13. package/hooks/ace-statusline.js +30 -12
  14. package/hooks/hooks.json +14 -0
  15. package/package.json +3 -2
  16. package/shared/lib/ace-core.js +53 -0
  17. package/shared/lib/ace-core.test.js +308 -308
  18. package/shared/lib/ace-story.test.js +250 -250
  19. package/skills/execute-story/SKILL.md +116 -110
  20. package/skills/execute-story/script.js +13 -27
  21. package/skills/execute-story/script.test.js +261 -261
  22. package/skills/execute-story/story-template.xml +451 -451
  23. package/skills/execute-story/workflow.xml +3 -1
  24. package/skills/help/SKILL.md +71 -69
  25. package/skills/help/script.js +32 -35
  26. package/skills/help/script.test.js +183 -183
  27. package/skills/help/workflow.xml +14 -3
  28. package/skills/init-coding-standards/SKILL.md +91 -72
  29. package/skills/init-coding-standards/coding-standards-template.xml +531 -531
  30. package/skills/init-coding-standards/script.js +50 -59
  31. package/skills/init-coding-standards/script.test.js +70 -70
  32. package/skills/init-coding-standards/workflow.xml +1 -1
  33. package/skills/map-cross-cutting/SKILL.md +126 -89
  34. package/skills/map-cross-cutting/workflow.xml +1 -1
  35. package/skills/map-guide/SKILL.md +126 -89
  36. package/skills/map-guide/workflow.xml +1 -1
  37. package/skills/map-pattern/SKILL.md +125 -89
  38. package/skills/map-pattern/workflow.xml +1 -1
  39. package/skills/map-story/SKILL.md +180 -127
  40. package/skills/map-story/templates/tech-debt-index.xml +125 -125
  41. package/skills/map-story/workflow.xml +2 -2
  42. package/skills/map-subsystem/SKILL.md +155 -111
  43. package/skills/map-subsystem/script.js +51 -60
  44. package/skills/map-subsystem/script.test.js +68 -68
  45. package/skills/map-subsystem/templates/subsystem-architecture.xml +343 -343
  46. package/skills/map-subsystem/templates/subsystem-structure.xml +234 -234
  47. package/skills/map-subsystem/workflow.xml +1173 -1173
  48. package/skills/map-sys-doc/SKILL.md +125 -90
  49. package/skills/map-sys-doc/workflow.xml +1 -1
  50. package/skills/map-system/SKILL.md +103 -85
  51. package/skills/map-system/script.js +75 -84
  52. package/skills/map-system/script.test.js +73 -73
  53. package/skills/map-system/templates/system-structure.xml +177 -177
  54. package/skills/map-system/templates/testing-framework.xml +283 -283
  55. package/skills/map-system/workflow.xml +667 -667
  56. package/skills/map-walkthrough/SKILL.md +140 -92
  57. package/skills/map-walkthrough/workflow.xml +457 -457
  58. package/skills/plan-backlog/SKILL.md +93 -75
  59. package/skills/plan-backlog/script.js +121 -136
  60. package/skills/plan-backlog/script.test.js +83 -83
  61. package/skills/plan-backlog/workflow.xml +1348 -1348
  62. package/skills/plan-feature/SKILL.md +99 -76
  63. package/skills/plan-feature/feature-template.xml +361 -361
  64. package/skills/plan-feature/script.js +131 -148
  65. package/skills/plan-feature/script.test.js +80 -80
  66. package/skills/plan-feature/workflow.xml +1 -1
  67. package/skills/plan-product-vision/SKILL.md +91 -75
  68. package/skills/plan-product-vision/product-vision-template.xml +227 -227
  69. package/skills/plan-product-vision/script.js +51 -60
  70. package/skills/plan-product-vision/script.test.js +69 -69
  71. package/skills/plan-product-vision/workflow.xml +337 -337
  72. package/skills/plan-story/SKILL.md +125 -102
  73. package/skills/plan-story/script.js +18 -49
  74. package/skills/plan-story/story-template.xml +8 -1
  75. package/skills/plan-story/workflow.xml +17 -1
  76. package/skills/research-external-solution/SKILL.md +120 -107
  77. package/skills/research-external-solution/external-solution-template.xml +832 -832
  78. package/skills/research-external-solution/script.js +229 -238
  79. package/skills/research-external-solution/script.test.js +134 -134
  80. package/skills/research-external-solution/workflow.xml +657 -657
  81. package/skills/research-integration-solution/SKILL.md +121 -98
  82. package/skills/research-integration-solution/integration-solution-template.xml +1015 -1015
  83. package/skills/research-integration-solution/script.js +223 -231
  84. package/skills/research-integration-solution/script.test.js +134 -134
  85. package/skills/research-integration-solution/workflow.xml +711 -711
  86. package/skills/research-story-wiki/SKILL.md +101 -92
  87. package/skills/research-story-wiki/script.js +223 -231
  88. package/skills/research-story-wiki/script.test.js +138 -138
  89. package/skills/research-story-wiki/story-wiki-template.xml +194 -194
  90. package/skills/research-story-wiki/workflow.xml +473 -473
  91. package/skills/research-technical-solution/SKILL.md +131 -103
  92. package/skills/research-technical-solution/script.js +223 -231
  93. package/skills/research-technical-solution/script.test.js +134 -134
  94. package/skills/research-technical-solution/technical-solution-template.xml +1025 -1025
  95. package/skills/research-technical-solution/workflow.xml +761 -761
  96. package/skills/review-story/SKILL.md +99 -100
  97. package/skills/review-story/script.js +8 -16
  98. package/skills/review-story/script.test.js +169 -169
  99. package/skills/review-story/story-template.xml +451 -451
  100. package/skills/review-story/workflow.xml +1 -1
  101. package/skills/update/SKILL.md +65 -53
  102. package/skills/update/workflow.xml +21 -5
@@ -1,308 +1,308 @@
1
- const { describe, it, beforeEach, afterEach } = require('node:test');
2
- const assert = require('node:assert');
3
- const fs = require('fs');
4
- const path = require('path');
5
- const os = require('os');
6
-
7
- const {
8
- loadConfig, pathExists, safeReadFile, generateSlug, currentTimestamp,
9
- resolveModel, detectBrownfieldStatus,
10
- loadSettings, writeSettings, parseKeyValueArgs, MODEL_PROFILES,
11
- } = require('./ace-core');
12
-
13
- function createTempDir() {
14
- return fs.mkdtempSync(path.join(os.tmpdir(), 'ace-core-test-'));
15
- }
16
-
17
- function cleanup(dir) {
18
- fs.rmSync(dir, { recursive: true, force: true });
19
- }
20
-
21
- // ─── generateSlug ────────────────────────────────────────────────────────────
22
-
23
- describe('generateSlug', () => {
24
- it('converts text to lowercase slug', () => {
25
- assert.strictEqual(generateSlug('Hello World'), 'hello-world');
26
- });
27
-
28
- it('handles special characters', () => {
29
- assert.strictEqual(generateSlug('User Authentication & Login!!!'), 'user-authentication-login');
30
- });
31
-
32
- it('trims leading and trailing dashes', () => {
33
- assert.strictEqual(generateSlug('---hello---'), 'hello');
34
- });
35
-
36
- it('returns null for empty input', () => {
37
- assert.strictEqual(generateSlug(''), null);
38
- assert.strictEqual(generateSlug(null), null);
39
- assert.strictEqual(generateSlug(undefined), null);
40
- });
41
-
42
- it('handles multi-word input', () => {
43
- assert.strictEqual(generateSlug('Platform Foundation Setup'), 'platform-foundation-setup');
44
- });
45
-
46
- it('handles numeric IDs in text', () => {
47
- assert.strictEqual(generateSlug('E1-Platform Foundation'), 'e1-platform-foundation');
48
- assert.strictEqual(generateSlug('#45-User Auth'), '45-user-auth');
49
- });
50
- });
51
-
52
- // ─── currentTimestamp ────────────────────────────────────────────────────────
53
-
54
- describe('currentTimestamp', () => {
55
- it('returns full ISO timestamp by default', () => {
56
- const ts = currentTimestamp('full');
57
- assert.match(ts, /^\d{4}-\d{2}-\d{2}T/);
58
- });
59
-
60
- it('returns date-only format', () => {
61
- const ts = currentTimestamp('date');
62
- assert.match(ts, /^\d{4}-\d{2}-\d{2}$/);
63
- });
64
-
65
- it('returns filename-safe format', () => {
66
- const ts = currentTimestamp('filename');
67
- assert.ok(!ts.includes(':'), 'should not contain colons');
68
- assert.ok(ts.includes('_'), 'should contain underscore separator');
69
- });
70
- });
71
-
72
- // ─── loadConfig ──────────────────────────────────────────────────────────────
73
-
74
- describe('loadConfig', () => {
75
- let tmpDir;
76
- beforeEach(() => { tmpDir = createTempDir(); });
77
- afterEach(() => { cleanup(tmpDir); });
78
-
79
- it('returns defaults when no config file exists', () => {
80
- const config = loadConfig(tmpDir);
81
- assert.strictEqual(config.version, '0.1.0');
82
- assert.strictEqual(config.projectName, '');
83
- assert.strictEqual(config.storage, 'local');
84
- assert.strictEqual(config.commit_docs, true);
85
- assert.strictEqual(config.github.enabled, false);
86
- assert.strictEqual(config.github.labels.epic, 'ace:epic');
87
- });
88
-
89
- it('reads existing config and merges with defaults', () => {
90
- fs.mkdirSync(path.join(tmpDir, '.ace'), { recursive: true });
91
- fs.writeFileSync(path.join(tmpDir, '.ace', 'config.json'), JSON.stringify({
92
- projectName: 'Test Project',
93
- github: { enabled: true, repo: 'owner/repo' },
94
- }));
95
-
96
- const config = loadConfig(tmpDir);
97
- assert.strictEqual(config.projectName, 'Test Project');
98
- assert.strictEqual(config.github.enabled, true);
99
- assert.strictEqual(config.github.repo, 'owner/repo');
100
- assert.strictEqual(config.version, '0.1.0'); // default
101
- assert.strictEqual(config.github.labels.epic, 'ace:epic'); // default
102
- });
103
-
104
- it('handles malformed JSON gracefully', () => {
105
- fs.mkdirSync(path.join(tmpDir, '.ace'), { recursive: true });
106
- fs.writeFileSync(path.join(tmpDir, '.ace', 'config.json'), 'not json');
107
-
108
- const config = loadConfig(tmpDir);
109
- assert.strictEqual(config.version, '0.1.0');
110
- });
111
- });
112
-
113
- // ─── pathExists ──────────────────────────────────────────────────────────────
114
-
115
- describe('pathExists', () => {
116
- let tmpDir;
117
- beforeEach(() => { tmpDir = createTempDir(); });
118
- afterEach(() => { cleanup(tmpDir); });
119
-
120
- it('returns true for existing directory', () => {
121
- fs.mkdirSync(path.join(tmpDir, '.ace'), { recursive: true });
122
- assert.strictEqual(pathExists(tmpDir, '.ace'), true);
123
- });
124
-
125
- it('returns false for non-existent path', () => {
126
- assert.strictEqual(pathExists(tmpDir, '.ace/config.json'), false);
127
- });
128
-
129
- it('returns true for existing file', () => {
130
- fs.mkdirSync(path.join(tmpDir, '.ace'), { recursive: true });
131
- fs.writeFileSync(path.join(tmpDir, '.ace', 'config.json'), '{}');
132
- assert.strictEqual(pathExists(tmpDir, '.ace/config.json'), true);
133
- });
134
- });
135
-
136
- // ─── safeReadFile ────────────────────────────────────────────────────────────
137
-
138
- describe('safeReadFile', () => {
139
- let tmpDir;
140
- beforeEach(() => { tmpDir = createTempDir(); });
141
- afterEach(() => { cleanup(tmpDir); });
142
-
143
- it('reads file content', () => {
144
- const fp = path.join(tmpDir, 'test.txt');
145
- fs.writeFileSync(fp, 'hello');
146
- assert.strictEqual(safeReadFile(fp), 'hello');
147
- });
148
-
149
- it('returns null for non-existent file', () => {
150
- assert.strictEqual(safeReadFile(path.join(tmpDir, 'nope.txt')), null);
151
- });
152
- });
153
-
154
- // ─── resolveModel ────────────────────────────────────────────────────────────
155
-
156
- describe('resolveModel', () => {
157
- let tmpDir;
158
- beforeEach(() => { tmpDir = createTempDir(); });
159
- afterEach(() => { cleanup(tmpDir); });
160
-
161
- it('returns quality model for ace-product-owner', () => {
162
- assert.strictEqual(resolveModel(tmpDir, 'ace-product-owner'), 'opus');
163
- });
164
-
165
- it('returns quality model for ace-code-reviewer', () => {
166
- assert.strictEqual(resolveModel(tmpDir, 'ace-code-reviewer'), 'sonnet');
167
- });
168
-
169
- it('respects budget profile from config', () => {
170
- fs.mkdirSync(path.join(tmpDir, '.ace'), { recursive: true });
171
- fs.writeFileSync(path.join(tmpDir, '.ace', 'config.json'), JSON.stringify({
172
- model_profile: 'budget',
173
- }));
174
- assert.strictEqual(resolveModel(tmpDir, 'ace-product-owner'), 'sonnet');
175
- });
176
-
177
- it('returns sonnet for unknown agent type', () => {
178
- assert.strictEqual(resolveModel(tmpDir, 'unknown-agent'), 'sonnet');
179
- });
180
- });
181
-
182
- // ─── detectCodeFiles & detectBrownfieldStatus ────────────────────────────────
183
-
184
- describe('detectBrownfieldStatus', () => {
185
- let tmpDir;
186
- beforeEach(() => { tmpDir = createTempDir(); });
187
- afterEach(() => { cleanup(tmpDir); });
188
-
189
- it('detects greenfield (empty project)', () => {
190
- const result = detectBrownfieldStatus(tmpDir);
191
- assert.strictEqual(result.is_greenfield, true);
192
- assert.strictEqual(result.is_brownfield, false);
193
- });
194
-
195
- it('detects brownfield with code files', () => {
196
- fs.writeFileSync(path.join(tmpDir, 'index.js'), 'console.log("hello");');
197
- const result = detectBrownfieldStatus(tmpDir);
198
- assert.strictEqual(result.is_brownfield, true);
199
- assert.strictEqual(result.has_existing_code, true);
200
- });
201
-
202
- it('detects brownfield with package file only', () => {
203
- fs.writeFileSync(path.join(tmpDir, 'package.json'), '{}');
204
- const result = detectBrownfieldStatus(tmpDir);
205
- assert.strictEqual(result.is_brownfield, true);
206
- assert.strictEqual(result.has_package_file, true);
207
- });
208
-
209
- it('ignores node_modules', () => {
210
- fs.mkdirSync(path.join(tmpDir, 'node_modules', 'pkg'), { recursive: true });
211
- fs.writeFileSync(path.join(tmpDir, 'node_modules', 'pkg', 'index.js'), '');
212
- const result = detectBrownfieldStatus(tmpDir);
213
- assert.strictEqual(result.has_existing_code, false);
214
- });
215
-
216
- it('detects nested code files up to depth 3', () => {
217
- const nested = path.join(tmpDir, 'src', 'lib', 'utils');
218
- fs.mkdirSync(nested, { recursive: true });
219
- fs.writeFileSync(path.join(nested, 'helper.ts'), 'export const x = 1;');
220
- const result = detectBrownfieldStatus(tmpDir);
221
- assert.strictEqual(result.has_existing_code, true);
222
- });
223
-
224
- it('detects .csproj as package file', () => {
225
- fs.writeFileSync(path.join(tmpDir, 'App.csproj'), '<Project />');
226
- const result = detectBrownfieldStatus(tmpDir);
227
- assert.strictEqual(result.has_package_file, true);
228
- });
229
- });
230
-
231
- // ─── loadSettings / writeSettings ────────────────────────────────────────────
232
-
233
- describe('loadSettings / writeSettings', () => {
234
- let tmpDir;
235
- beforeEach(() => { tmpDir = createTempDir(); });
236
- afterEach(() => { cleanup(tmpDir); });
237
-
238
- it('returns defaults when no settings file exists', () => {
239
- const settings = loadSettings(tmpDir);
240
- assert.strictEqual(settings.model_profile, 'balanced');
241
- assert.strictEqual(settings.commit_docs, true);
242
- assert.strictEqual(settings.github_project.enabled, false);
243
- });
244
-
245
- it('reads existing settings', () => {
246
- fs.mkdirSync(path.join(tmpDir, '.ace'), { recursive: true });
247
- fs.writeFileSync(path.join(tmpDir, '.ace', 'settings.json'), JSON.stringify({
248
- model_profile: 'quality',
249
- github_project: { enabled: true, repo: 'owner/repo' },
250
- }));
251
- const settings = loadSettings(tmpDir);
252
- assert.strictEqual(settings.model_profile, 'quality');
253
- assert.strictEqual(settings.github_project.enabled, true);
254
- });
255
-
256
- it('writes settings and creates .ace directory', () => {
257
- const settings = { model_profile: 'budget', commit_docs: false };
258
- writeSettings(tmpDir, settings);
259
-
260
- const written = JSON.parse(fs.readFileSync(path.join(tmpDir, '.ace', 'settings.json'), 'utf-8'));
261
- assert.strictEqual(written.model_profile, 'budget');
262
- assert.strictEqual(written.commit_docs, false);
263
- });
264
- });
265
-
266
- // ─── parseKeyValueArgs ───────────────────────────────────────────────────────
267
-
268
- describe('parseKeyValueArgs', () => {
269
- it('parses key=value pairs', () => {
270
- const result = parseKeyValueArgs(['story=path/to/file', 'status=Refined']);
271
- assert.strictEqual(result.story, 'path/to/file');
272
- assert.strictEqual(result.status, 'Refined');
273
- });
274
-
275
- it('handles values with equals signs', () => {
276
- const result = parseKeyValueArgs(['query=a=b']);
277
- assert.strictEqual(result.query, 'a=b');
278
- });
279
-
280
- it('ignores args without equals', () => {
281
- const result = parseKeyValueArgs(['--raw', 'story=file.md']);
282
- assert.strictEqual(result.story, 'file.md');
283
- assert.strictEqual(result['--raw'], undefined);
284
- });
285
-
286
- it('returns empty object for empty input', () => {
287
- const result = parseKeyValueArgs([]);
288
- assert.deepStrictEqual(result, {});
289
- });
290
- });
291
-
292
- // ─── MODEL_PROFILES ──────────────────────────────────────────────────────────
293
-
294
- describe('MODEL_PROFILES', () => {
295
- it('has entries for all known agent types', () => {
296
- const agents = [
297
- 'ace-product-owner', 'ace-project-researcher', 'ace-research-synthesizer',
298
- 'ace-wiki-mapper', 'ace-code-integration-analyst', 'ace-code-discovery-analyst',
299
- 'ace-executor', 'ace-code-reviewer',
300
- ];
301
- for (const agent of agents) {
302
- assert.ok(MODEL_PROFILES[agent], `Missing profile for ${agent}`);
303
- assert.ok(MODEL_PROFILES[agent].quality, `Missing quality for ${agent}`);
304
- assert.ok(MODEL_PROFILES[agent].balanced, `Missing balanced for ${agent}`);
305
- assert.ok(MODEL_PROFILES[agent].budget, `Missing budget for ${agent}`);
306
- }
307
- });
308
- });
1
+ const { describe, it, beforeEach, afterEach } = require('node:test');
2
+ const assert = require('node:assert');
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+
7
+ const {
8
+ loadConfig, pathExists, safeReadFile, generateSlug, currentTimestamp,
9
+ resolveModel, detectBrownfieldStatus,
10
+ loadSettings, writeSettings, parseKeyValueArgs, MODEL_PROFILES,
11
+ } = require('./ace-core');
12
+
13
+ function createTempDir() {
14
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'ace-core-test-'));
15
+ }
16
+
17
+ function cleanup(dir) {
18
+ fs.rmSync(dir, { recursive: true, force: true });
19
+ }
20
+
21
+ // ─── generateSlug ────────────────────────────────────────────────────────────
22
+
23
+ describe('generateSlug', () => {
24
+ it('converts text to lowercase slug', () => {
25
+ assert.strictEqual(generateSlug('Hello World'), 'hello-world');
26
+ });
27
+
28
+ it('handles special characters', () => {
29
+ assert.strictEqual(generateSlug('User Authentication & Login!!!'), 'user-authentication-login');
30
+ });
31
+
32
+ it('trims leading and trailing dashes', () => {
33
+ assert.strictEqual(generateSlug('---hello---'), 'hello');
34
+ });
35
+
36
+ it('returns null for empty input', () => {
37
+ assert.strictEqual(generateSlug(''), null);
38
+ assert.strictEqual(generateSlug(null), null);
39
+ assert.strictEqual(generateSlug(undefined), null);
40
+ });
41
+
42
+ it('handles multi-word input', () => {
43
+ assert.strictEqual(generateSlug('Platform Foundation Setup'), 'platform-foundation-setup');
44
+ });
45
+
46
+ it('handles numeric IDs in text', () => {
47
+ assert.strictEqual(generateSlug('E1-Platform Foundation'), 'e1-platform-foundation');
48
+ assert.strictEqual(generateSlug('#45-User Auth'), '45-user-auth');
49
+ });
50
+ });
51
+
52
+ // ─── currentTimestamp ────────────────────────────────────────────────────────
53
+
54
+ describe('currentTimestamp', () => {
55
+ it('returns full ISO timestamp by default', () => {
56
+ const ts = currentTimestamp('full');
57
+ assert.match(ts, /^\d{4}-\d{2}-\d{2}T/);
58
+ });
59
+
60
+ it('returns date-only format', () => {
61
+ const ts = currentTimestamp('date');
62
+ assert.match(ts, /^\d{4}-\d{2}-\d{2}$/);
63
+ });
64
+
65
+ it('returns filename-safe format', () => {
66
+ const ts = currentTimestamp('filename');
67
+ assert.ok(!ts.includes(':'), 'should not contain colons');
68
+ assert.ok(ts.includes('_'), 'should contain underscore separator');
69
+ });
70
+ });
71
+
72
+ // ─── loadConfig ──────────────────────────────────────────────────────────────
73
+
74
+ describe('loadConfig', () => {
75
+ let tmpDir;
76
+ beforeEach(() => { tmpDir = createTempDir(); });
77
+ afterEach(() => { cleanup(tmpDir); });
78
+
79
+ it('returns defaults when no config file exists', () => {
80
+ const config = loadConfig(tmpDir);
81
+ assert.strictEqual(config.version, '0.1.0');
82
+ assert.strictEqual(config.projectName, '');
83
+ assert.strictEqual(config.storage, 'local');
84
+ assert.strictEqual(config.commit_docs, true);
85
+ assert.strictEqual(config.github.enabled, false);
86
+ assert.strictEqual(config.github.labels.epic, 'ace:epic');
87
+ });
88
+
89
+ it('reads existing config and merges with defaults', () => {
90
+ fs.mkdirSync(path.join(tmpDir, '.ace'), { recursive: true });
91
+ fs.writeFileSync(path.join(tmpDir, '.ace', 'config.json'), JSON.stringify({
92
+ projectName: 'Test Project',
93
+ github: { enabled: true, repo: 'owner/repo' },
94
+ }));
95
+
96
+ const config = loadConfig(tmpDir);
97
+ assert.strictEqual(config.projectName, 'Test Project');
98
+ assert.strictEqual(config.github.enabled, true);
99
+ assert.strictEqual(config.github.repo, 'owner/repo');
100
+ assert.strictEqual(config.version, '0.1.0'); // default
101
+ assert.strictEqual(config.github.labels.epic, 'ace:epic'); // default
102
+ });
103
+
104
+ it('handles malformed JSON gracefully', () => {
105
+ fs.mkdirSync(path.join(tmpDir, '.ace'), { recursive: true });
106
+ fs.writeFileSync(path.join(tmpDir, '.ace', 'config.json'), 'not json');
107
+
108
+ const config = loadConfig(tmpDir);
109
+ assert.strictEqual(config.version, '0.1.0');
110
+ });
111
+ });
112
+
113
+ // ─── pathExists ──────────────────────────────────────────────────────────────
114
+
115
+ describe('pathExists', () => {
116
+ let tmpDir;
117
+ beforeEach(() => { tmpDir = createTempDir(); });
118
+ afterEach(() => { cleanup(tmpDir); });
119
+
120
+ it('returns true for existing directory', () => {
121
+ fs.mkdirSync(path.join(tmpDir, '.ace'), { recursive: true });
122
+ assert.strictEqual(pathExists(tmpDir, '.ace'), true);
123
+ });
124
+
125
+ it('returns false for non-existent path', () => {
126
+ assert.strictEqual(pathExists(tmpDir, '.ace/config.json'), false);
127
+ });
128
+
129
+ it('returns true for existing file', () => {
130
+ fs.mkdirSync(path.join(tmpDir, '.ace'), { recursive: true });
131
+ fs.writeFileSync(path.join(tmpDir, '.ace', 'config.json'), '{}');
132
+ assert.strictEqual(pathExists(tmpDir, '.ace/config.json'), true);
133
+ });
134
+ });
135
+
136
+ // ─── safeReadFile ────────────────────────────────────────────────────────────
137
+
138
+ describe('safeReadFile', () => {
139
+ let tmpDir;
140
+ beforeEach(() => { tmpDir = createTempDir(); });
141
+ afterEach(() => { cleanup(tmpDir); });
142
+
143
+ it('reads file content', () => {
144
+ const fp = path.join(tmpDir, 'test.txt');
145
+ fs.writeFileSync(fp, 'hello');
146
+ assert.strictEqual(safeReadFile(fp), 'hello');
147
+ });
148
+
149
+ it('returns null for non-existent file', () => {
150
+ assert.strictEqual(safeReadFile(path.join(tmpDir, 'nope.txt')), null);
151
+ });
152
+ });
153
+
154
+ // ─── resolveModel ────────────────────────────────────────────────────────────
155
+
156
+ describe('resolveModel', () => {
157
+ let tmpDir;
158
+ beforeEach(() => { tmpDir = createTempDir(); });
159
+ afterEach(() => { cleanup(tmpDir); });
160
+
161
+ it('returns quality model for ace-product-owner', () => {
162
+ assert.strictEqual(resolveModel(tmpDir, 'ace-product-owner'), 'opus');
163
+ });
164
+
165
+ it('returns quality model for ace-code-reviewer', () => {
166
+ assert.strictEqual(resolveModel(tmpDir, 'ace-code-reviewer'), 'sonnet');
167
+ });
168
+
169
+ it('respects budget profile from config', () => {
170
+ fs.mkdirSync(path.join(tmpDir, '.ace'), { recursive: true });
171
+ fs.writeFileSync(path.join(tmpDir, '.ace', 'config.json'), JSON.stringify({
172
+ model_profile: 'budget',
173
+ }));
174
+ assert.strictEqual(resolveModel(tmpDir, 'ace-product-owner'), 'sonnet');
175
+ });
176
+
177
+ it('returns sonnet for unknown agent type', () => {
178
+ assert.strictEqual(resolveModel(tmpDir, 'unknown-agent'), 'sonnet');
179
+ });
180
+ });
181
+
182
+ // ─── detectCodeFiles & detectBrownfieldStatus ────────────────────────────────
183
+
184
+ describe('detectBrownfieldStatus', () => {
185
+ let tmpDir;
186
+ beforeEach(() => { tmpDir = createTempDir(); });
187
+ afterEach(() => { cleanup(tmpDir); });
188
+
189
+ it('detects greenfield (empty project)', () => {
190
+ const result = detectBrownfieldStatus(tmpDir);
191
+ assert.strictEqual(result.is_greenfield, true);
192
+ assert.strictEqual(result.is_brownfield, false);
193
+ });
194
+
195
+ it('detects brownfield with code files', () => {
196
+ fs.writeFileSync(path.join(tmpDir, 'index.js'), 'console.log("hello");');
197
+ const result = detectBrownfieldStatus(tmpDir);
198
+ assert.strictEqual(result.is_brownfield, true);
199
+ assert.strictEqual(result.has_existing_code, true);
200
+ });
201
+
202
+ it('detects brownfield with package file only', () => {
203
+ fs.writeFileSync(path.join(tmpDir, 'package.json'), '{}');
204
+ const result = detectBrownfieldStatus(tmpDir);
205
+ assert.strictEqual(result.is_brownfield, true);
206
+ assert.strictEqual(result.has_package_file, true);
207
+ });
208
+
209
+ it('ignores node_modules', () => {
210
+ fs.mkdirSync(path.join(tmpDir, 'node_modules', 'pkg'), { recursive: true });
211
+ fs.writeFileSync(path.join(tmpDir, 'node_modules', 'pkg', 'index.js'), '');
212
+ const result = detectBrownfieldStatus(tmpDir);
213
+ assert.strictEqual(result.has_existing_code, false);
214
+ });
215
+
216
+ it('detects nested code files up to depth 3', () => {
217
+ const nested = path.join(tmpDir, 'src', 'lib', 'utils');
218
+ fs.mkdirSync(nested, { recursive: true });
219
+ fs.writeFileSync(path.join(nested, 'helper.ts'), 'export const x = 1;');
220
+ const result = detectBrownfieldStatus(tmpDir);
221
+ assert.strictEqual(result.has_existing_code, true);
222
+ });
223
+
224
+ it('detects .csproj as package file', () => {
225
+ fs.writeFileSync(path.join(tmpDir, 'App.csproj'), '<Project />');
226
+ const result = detectBrownfieldStatus(tmpDir);
227
+ assert.strictEqual(result.has_package_file, true);
228
+ });
229
+ });
230
+
231
+ // ─── loadSettings / writeSettings ────────────────────────────────────────────
232
+
233
+ describe('loadSettings / writeSettings', () => {
234
+ let tmpDir;
235
+ beforeEach(() => { tmpDir = createTempDir(); });
236
+ afterEach(() => { cleanup(tmpDir); });
237
+
238
+ it('returns defaults when no settings file exists', () => {
239
+ const settings = loadSettings(tmpDir);
240
+ assert.strictEqual(settings.model_profile, 'balanced');
241
+ assert.strictEqual(settings.commit_docs, true);
242
+ assert.strictEqual(settings.github_project.enabled, false);
243
+ });
244
+
245
+ it('reads existing settings', () => {
246
+ fs.mkdirSync(path.join(tmpDir, '.ace'), { recursive: true });
247
+ fs.writeFileSync(path.join(tmpDir, '.ace', 'settings.json'), JSON.stringify({
248
+ model_profile: 'quality',
249
+ github_project: { enabled: true, repo: 'owner/repo' },
250
+ }));
251
+ const settings = loadSettings(tmpDir);
252
+ assert.strictEqual(settings.model_profile, 'quality');
253
+ assert.strictEqual(settings.github_project.enabled, true);
254
+ });
255
+
256
+ it('writes settings and creates .ace directory', () => {
257
+ const settings = { model_profile: 'budget', commit_docs: false };
258
+ writeSettings(tmpDir, settings);
259
+
260
+ const written = JSON.parse(fs.readFileSync(path.join(tmpDir, '.ace', 'settings.json'), 'utf-8'));
261
+ assert.strictEqual(written.model_profile, 'budget');
262
+ assert.strictEqual(written.commit_docs, false);
263
+ });
264
+ });
265
+
266
+ // ─── parseKeyValueArgs ───────────────────────────────────────────────────────
267
+
268
+ describe('parseKeyValueArgs', () => {
269
+ it('parses key=value pairs', () => {
270
+ const result = parseKeyValueArgs(['story=path/to/file', 'status=Refined']);
271
+ assert.strictEqual(result.story, 'path/to/file');
272
+ assert.strictEqual(result.status, 'Refined');
273
+ });
274
+
275
+ it('handles values with equals signs', () => {
276
+ const result = parseKeyValueArgs(['query=a=b']);
277
+ assert.strictEqual(result.query, 'a=b');
278
+ });
279
+
280
+ it('ignores args without equals', () => {
281
+ const result = parseKeyValueArgs(['--raw', 'story=file.md']);
282
+ assert.strictEqual(result.story, 'file.md');
283
+ assert.strictEqual(result['--raw'], undefined);
284
+ });
285
+
286
+ it('returns empty object for empty input', () => {
287
+ const result = parseKeyValueArgs([]);
288
+ assert.deepStrictEqual(result, {});
289
+ });
290
+ });
291
+
292
+ // ─── MODEL_PROFILES ──────────────────────────────────────────────────────────
293
+
294
+ describe('MODEL_PROFILES', () => {
295
+ it('has entries for all known agent types', () => {
296
+ const agents = [
297
+ 'ace-product-owner', 'ace-project-researcher', 'ace-research-synthesizer',
298
+ 'ace-wiki-mapper', 'ace-code-integration-analyst', 'ace-code-discovery-analyst',
299
+ 'ace-executor', 'ace-code-reviewer',
300
+ ];
301
+ for (const agent of agents) {
302
+ assert.ok(MODEL_PROFILES[agent], `Missing profile for ${agent}`);
303
+ assert.ok(MODEL_PROFILES[agent].quality, `Missing quality for ${agent}`);
304
+ assert.ok(MODEL_PROFILES[agent].balanced, `Missing balanced for ${agent}`);
305
+ assert.ok(MODEL_PROFILES[agent].budget, `Missing budget for ${agent}`);
306
+ }
307
+ });
308
+ });