agile-context-engineering 0.3.0 → 0.5.0

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 (147) hide show
  1. package/.claude-plugin/plugin.json +10 -0
  2. package/LICENSE +51 -51
  3. package/README.md +332 -324
  4. package/agents/ace-product-owner.md +1 -1
  5. package/agents/ace-research-synthesizer.md +228 -228
  6. package/agents/ace-wiki-mapper.md +449 -445
  7. package/bin/install.js +60 -64
  8. package/hooks/ace-check-update.js +70 -62
  9. package/hooks/ace-statusline.js +89 -89
  10. package/package.json +5 -4
  11. package/shared/lib/ace-core.js +308 -0
  12. package/shared/lib/ace-core.test.js +308 -0
  13. package/shared/lib/ace-github.js +753 -0
  14. package/shared/lib/ace-story.js +400 -0
  15. package/shared/lib/ace-story.test.js +250 -0
  16. package/{agile-context-engineering → shared}/utils/questioning.xml +110 -110
  17. package/{agile-context-engineering → shared}/utils/ui-formatting.md +299 -299
  18. package/skills/execute-story/SKILL.md +110 -0
  19. package/skills/execute-story/script.js +305 -0
  20. package/skills/execute-story/script.test.js +261 -0
  21. package/skills/execute-story/walkthrough-template.xml +255 -0
  22. package/{agile-context-engineering/workflows/execute-story.xml → skills/execute-story/workflow.xml} +1219 -1219
  23. package/skills/help/SKILL.md +69 -0
  24. package/skills/help/script.js +318 -0
  25. package/skills/help/script.test.js +183 -0
  26. package/{agile-context-engineering/workflows/help.xml → skills/help/workflow.xml} +540 -540
  27. package/skills/init-coding-standards/SKILL.md +72 -0
  28. package/skills/init-coding-standards/script.js +59 -0
  29. package/skills/init-coding-standards/script.test.js +70 -0
  30. package/{agile-context-engineering/workflows/init-coding-standards.xml → skills/init-coding-standards/workflow.xml} +381 -386
  31. package/skills/map-cross-cutting/SKILL.md +89 -0
  32. package/{agile-context-engineering/templates/wiki → skills/map-cross-cutting}/system-cross-cutting.xml +197 -197
  33. package/skills/map-cross-cutting/workflow.xml +330 -0
  34. package/skills/map-guide/SKILL.md +89 -0
  35. package/{agile-context-engineering/templates/wiki → skills/map-guide}/guide.xml +137 -137
  36. package/skills/map-guide/workflow.xml +320 -0
  37. package/skills/map-pattern/SKILL.md +89 -0
  38. package/{agile-context-engineering/templates/wiki → skills/map-pattern}/pattern.xml +159 -159
  39. package/skills/map-pattern/workflow.xml +331 -0
  40. package/skills/map-story/SKILL.md +127 -0
  41. package/{agile-context-engineering/templates/wiki → skills/map-story/templates}/decizions.xml +115 -115
  42. package/skills/map-story/templates/guide.xml +137 -0
  43. package/skills/map-story/templates/pattern.xml +159 -0
  44. package/skills/map-story/templates/system-cross-cutting.xml +197 -0
  45. package/{agile-context-engineering/templates/wiki → skills/map-story/templates}/system.xml +381 -381
  46. package/{agile-context-engineering/templates/wiki → skills/map-story/templates}/walkthrough.xml +255 -255
  47. package/{agile-context-engineering/workflows/map-story.xml → skills/map-story/workflow.xml} +1046 -1046
  48. package/skills/map-subsystem/SKILL.md +111 -0
  49. package/skills/map-subsystem/script.js +60 -0
  50. package/skills/map-subsystem/script.test.js +68 -0
  51. package/skills/map-subsystem/templates/decizions.xml +115 -0
  52. package/skills/map-subsystem/templates/guide.xml +137 -0
  53. package/{agile-context-engineering/templates/wiki → skills/map-subsystem/templates}/module-discovery.xml +174 -174
  54. package/skills/map-subsystem/templates/pattern.xml +159 -0
  55. package/skills/map-subsystem/templates/system-cross-cutting.xml +197 -0
  56. package/skills/map-subsystem/templates/system.xml +381 -0
  57. package/skills/map-subsystem/templates/walkthrough.xml +255 -0
  58. package/{agile-context-engineering/workflows/map-subsystem.xml → skills/map-subsystem/workflow.xml} +15 -20
  59. package/skills/map-sys-doc/SKILL.md +90 -0
  60. package/skills/map-sys-doc/system.xml +381 -0
  61. package/skills/map-sys-doc/workflow.xml +336 -0
  62. package/skills/map-system/SKILL.md +85 -0
  63. package/skills/map-system/script.js +84 -0
  64. package/skills/map-system/script.test.js +73 -0
  65. package/{agile-context-engineering/templates/wiki → skills/map-system/templates}/system-architecture.xml +254 -254
  66. package/{agile-context-engineering/templates/wiki → skills/map-system/templates}/wiki-readme.xml +296 -296
  67. package/{agile-context-engineering/workflows/map-system.xml → skills/map-system/workflow.xml} +11 -16
  68. package/skills/map-walkthrough/SKILL.md +92 -0
  69. package/skills/map-walkthrough/walkthrough.xml +255 -0
  70. package/skills/plan-backlog/SKILL.md +75 -0
  71. package/{agile-context-engineering/templates/product/product-backlog.xml → skills/plan-backlog/product-backlog-template.xml} +231 -231
  72. package/skills/plan-backlog/script.js +136 -0
  73. package/skills/plan-backlog/script.test.js +83 -0
  74. package/{agile-context-engineering/workflows/plan-backlog.xml → skills/plan-backlog/workflow.xml} +13 -21
  75. package/skills/plan-feature/SKILL.md +76 -0
  76. package/skills/plan-feature/script.js +148 -0
  77. package/skills/plan-feature/script.test.js +80 -0
  78. package/{agile-context-engineering/workflows/plan-feature.xml → skills/plan-feature/workflow.xml} +1487 -1495
  79. package/skills/plan-product-vision/SKILL.md +75 -0
  80. package/skills/plan-product-vision/script.js +60 -0
  81. package/skills/plan-product-vision/script.test.js +69 -0
  82. package/{agile-context-engineering/workflows/plan-product-vision.xml → skills/plan-product-vision/workflow.xml} +4 -9
  83. package/skills/plan-story/SKILL.md +116 -0
  84. package/skills/plan-story/script.js +326 -0
  85. package/skills/plan-story/script.test.js +240 -0
  86. package/skills/plan-story/story-template.xml +451 -0
  87. package/{agile-context-engineering/workflows/plan-story.xml → skills/plan-story/workflow.xml} +1285 -944
  88. package/skills/research-external-solution/SKILL.md +107 -0
  89. package/skills/research-external-solution/script.js +238 -0
  90. package/skills/research-external-solution/script.test.js +134 -0
  91. package/{agile-context-engineering/workflows/research-external-solution.xml → skills/research-external-solution/workflow.xml} +4 -6
  92. package/skills/research-integration-solution/SKILL.md +98 -0
  93. package/skills/research-integration-solution/script.js +231 -0
  94. package/skills/research-integration-solution/script.test.js +134 -0
  95. package/{agile-context-engineering/workflows/research-integration-solution.xml → skills/research-integration-solution/workflow.xml} +3 -5
  96. package/skills/research-story-wiki/SKILL.md +92 -0
  97. package/skills/research-story-wiki/script.js +231 -0
  98. package/skills/research-story-wiki/script.test.js +138 -0
  99. package/{agile-context-engineering/workflows/research-story-wiki.xml → skills/research-story-wiki/workflow.xml} +3 -5
  100. package/skills/research-technical-solution/SKILL.md +103 -0
  101. package/skills/research-technical-solution/script.js +231 -0
  102. package/skills/research-technical-solution/script.test.js +134 -0
  103. package/{agile-context-engineering/workflows/research-technical-solution.xml → skills/research-technical-solution/workflow.xml} +3 -5
  104. package/skills/review-story/SKILL.md +100 -0
  105. package/skills/review-story/script.js +257 -0
  106. package/skills/review-story/script.test.js +169 -0
  107. package/skills/review-story/story-template.xml +451 -0
  108. package/{agile-context-engineering/workflows/review-story.xml → skills/review-story/workflow.xml} +279 -281
  109. package/skills/update/SKILL.md +53 -0
  110. package/{agile-context-engineering/workflows/update.xml → skills/update/workflow.xml} +12 -13
  111. package/agile-context-engineering/src/ace-tools.js +0 -2881
  112. package/agile-context-engineering/src/ace-tools.test.js +0 -1089
  113. package/agile-context-engineering/templates/_command.md +0 -54
  114. package/agile-context-engineering/templates/_workflow.xml +0 -17
  115. package/agile-context-engineering/templates/config.json +0 -0
  116. package/agile-context-engineering/templates/product/integration-solution.xml +0 -0
  117. package/commands/ace/execute-story.md +0 -138
  118. package/commands/ace/help.md +0 -93
  119. package/commands/ace/init-coding-standards.md +0 -83
  120. package/commands/ace/map-story.md +0 -165
  121. package/commands/ace/map-subsystem.md +0 -140
  122. package/commands/ace/map-system.md +0 -92
  123. package/commands/ace/map-walkthrough.md +0 -127
  124. package/commands/ace/plan-backlog.md +0 -83
  125. package/commands/ace/plan-feature.md +0 -89
  126. package/commands/ace/plan-product-vision.md +0 -81
  127. package/commands/ace/plan-story.md +0 -159
  128. package/commands/ace/research-external-solution.md +0 -138
  129. package/commands/ace/research-integration-solution.md +0 -135
  130. package/commands/ace/research-story-wiki.md +0 -116
  131. package/commands/ace/research-technical-solution.md +0 -147
  132. package/commands/ace/review-story.md +0 -109
  133. package/commands/ace/update.md +0 -56
  134. /package/{agile-context-engineering/templates/product/story.xml → skills/execute-story/story-template.xml} +0 -0
  135. /package/{agile-context-engineering/templates/wiki/coding-standards.xml → skills/init-coding-standards/coding-standards-template.xml} +0 -0
  136. /package/{agile-context-engineering/templates/wiki → skills/map-story/templates}/tech-debt-index.xml +0 -0
  137. /package/{agile-context-engineering/templates/wiki → skills/map-subsystem/templates}/subsystem-architecture.xml +0 -0
  138. /package/{agile-context-engineering/templates/wiki → skills/map-subsystem/templates}/subsystem-structure.xml +0 -0
  139. /package/{agile-context-engineering/templates/wiki → skills/map-system/templates}/system-structure.xml +0 -0
  140. /package/{agile-context-engineering/templates/wiki → skills/map-system/templates}/testing-framework.xml +0 -0
  141. /package/{agile-context-engineering/workflows/map-walkthrough.xml → skills/map-walkthrough/workflow.xml} +0 -0
  142. /package/{agile-context-engineering/templates/product/feature.xml → skills/plan-feature/feature-template.xml} +0 -0
  143. /package/{agile-context-engineering/templates/product/product-vision.xml → skills/plan-product-vision/product-vision-template.xml} +0 -0
  144. /package/{agile-context-engineering/templates/product/external-solution.xml → skills/research-external-solution/external-solution-template.xml} +0 -0
  145. /package/{agile-context-engineering/templates/product/story-integration-solution.xml → skills/research-integration-solution/integration-solution-template.xml} +0 -0
  146. /package/{agile-context-engineering/templates/product/story-wiki.xml → skills/research-story-wiki/story-wiki-template.xml} +0 -0
  147. /package/{agile-context-engineering/templates/product/story-technical-solution.xml → skills/research-technical-solution/technical-solution-template.xml} +0 -0
@@ -0,0 +1,308 @@
1
+ /**
2
+ * ACE Core — Universal helpers shared across all ACE skills.
3
+ *
4
+ * Extracted from ace-tools.js monolith. Contains: config loading, model resolution,
5
+ * path checks, environment detection, slug/timestamp generation, settings management,
6
+ * and process-level output/error helpers.
7
+ *
8
+ * Usage: const { loadConfig, resolveModel, output, error } = require('./ace-core');
9
+ */
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+
14
+ // ─── Model Profile Table ─────────────────────────────────────────────────────
15
+
16
+ const MODEL_PROFILES = {
17
+ 'ace-product-owner': { quality: 'opus', balanced: 'sonnet', budget: 'sonnet' },
18
+ 'ace-project-researcher': { quality: 'opus', balanced: 'sonnet', budget: 'haiku' },
19
+ 'ace-research-synthesizer': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku' },
20
+ 'ace-wiki-mapper': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku' },
21
+ 'ace-code-integration-analyst': { quality: 'opus', balanced: 'opus', budget: 'sonnet' },
22
+ 'ace-code-discovery-analyst': { quality: 'opus', balanced: 'opus', budget: 'sonnet' },
23
+ 'ace-executor': { quality: 'opus', balanced: 'sonnet', budget: 'sonnet' },
24
+ 'ace-code-reviewer': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku' },
25
+ };
26
+
27
+ // ─── Settings Defaults ───────────────────────────────────────────────────────
28
+
29
+ const SETTINGS_DEFAULTS = {
30
+ model_profile: 'balanced',
31
+ commit_docs: true,
32
+ agent_teams: false,
33
+ github_project: {
34
+ enabled: false,
35
+ gh_installed: false,
36
+ repo: '',
37
+ project_number: null,
38
+ owner: '',
39
+ },
40
+ };
41
+
42
+ // ─── Process Output ──────────────────────────────────────────────────────────
43
+
44
+ function output(result, raw, rawValue) {
45
+ if (raw && rawValue !== undefined) {
46
+ process.stdout.write(String(rawValue));
47
+ } else {
48
+ process.stdout.write(JSON.stringify(result, null, 2));
49
+ }
50
+ process.exit(0);
51
+ }
52
+
53
+ function error(message) {
54
+ process.stderr.write('Error: ' + message + '\n');
55
+ process.exit(1);
56
+ }
57
+
58
+ // ─── Config ──────────────────────────────────────────────────────────────────
59
+
60
+ function loadConfig(cwd) {
61
+ const configPath = path.join(cwd, '.ace', 'config.json');
62
+ const defaults = {
63
+ version: '0.1.0',
64
+ projectName: '',
65
+ description: '',
66
+ storage: 'local',
67
+ model_profile: 'quality',
68
+ commit_docs: true,
69
+ github: {
70
+ enabled: false,
71
+ repo: null,
72
+ labels: {
73
+ epic: 'ace:epic',
74
+ feature: 'ace:feature',
75
+ story: 'ace:story',
76
+ task: 'ace:task',
77
+ },
78
+ },
79
+ createdAt: '',
80
+ };
81
+
82
+ try {
83
+ const raw = fs.readFileSync(configPath, 'utf-8');
84
+ const parsed = JSON.parse(raw);
85
+ return {
86
+ version: parsed.version ?? defaults.version,
87
+ projectName: parsed.projectName ?? defaults.projectName,
88
+ description: parsed.description ?? defaults.description,
89
+ storage: parsed.storage ?? defaults.storage,
90
+ model_profile: parsed.model_profile ?? defaults.model_profile,
91
+ commit_docs: parsed.commit_docs ?? defaults.commit_docs,
92
+ github: {
93
+ enabled: parsed.github?.enabled ?? defaults.github.enabled,
94
+ repo: parsed.github?.repo ?? defaults.github.repo,
95
+ labels: {
96
+ epic: parsed.github?.labels?.epic ?? defaults.github.labels.epic,
97
+ feature: parsed.github?.labels?.feature ?? defaults.github.labels.feature,
98
+ story: parsed.github?.labels?.story ?? defaults.github.labels.story,
99
+ task: parsed.github?.labels?.task ?? defaults.github.labels.task,
100
+ },
101
+ },
102
+ createdAt: parsed.createdAt ?? defaults.createdAt,
103
+ };
104
+ } catch {
105
+ return defaults;
106
+ }
107
+ }
108
+
109
+ // ─── Path Helpers ────────────────────────────────────────────────────────────
110
+
111
+ function pathExists(cwd, targetPath) {
112
+ const fullPath = path.isAbsolute(targetPath) ? targetPath : path.join(cwd, targetPath);
113
+ try {
114
+ fs.statSync(fullPath);
115
+ return true;
116
+ } catch {
117
+ return false;
118
+ }
119
+ }
120
+
121
+ function safeReadFile(filePath) {
122
+ try { return fs.readFileSync(filePath, 'utf-8'); }
123
+ catch { return null; }
124
+ }
125
+
126
+ // ─── Slug & Timestamp ────────────────────────────────────────────────────────
127
+
128
+ function generateSlug(text) {
129
+ if (!text) return null;
130
+ return text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
131
+ }
132
+
133
+ function currentTimestamp(format) {
134
+ const now = new Date();
135
+ switch (format) {
136
+ case 'date':
137
+ return now.toISOString().split('T')[0];
138
+ case 'filename':
139
+ return now.toISOString().replace(/[:.]/g, '-').replace('T', '_').split('Z')[0];
140
+ case 'full':
141
+ default:
142
+ return now.toISOString();
143
+ }
144
+ }
145
+
146
+ // ─── Model Resolution ────────────────────────────────────────────────────────
147
+
148
+ function resolveModel(cwd, agentType) {
149
+ const config = loadConfig(cwd);
150
+ const profile = config.model_profile || 'balanced';
151
+ const agentModels = MODEL_PROFILES[agentType];
152
+ if (!agentModels) return 'sonnet';
153
+ return agentModels[profile] || agentModels['balanced'] || 'sonnet';
154
+ }
155
+
156
+ // ─── Code & Environment Detection ────────────────────────────────────────────
157
+
158
+ /**
159
+ * Detect existing code files by walking up to maxDepth levels.
160
+ */
161
+ function detectCodeFiles(cwd, maxDepth) {
162
+ const codeExtensions = new Set(['.cs', '.ts', '.js', '.py', '.go', '.rs', '.swift', '.java', '.tsx', '.jsx']);
163
+ const ignoreDirs = new Set(['node_modules', '.git', '.ace', '.gsd', 'dist', 'build', '__pycache__']);
164
+ const found = [];
165
+
166
+ function walk(dir, depth) {
167
+ if (depth > maxDepth || found.length >= 5) return;
168
+ let entries;
169
+ try {
170
+ entries = fs.readdirSync(dir, { withFileTypes: true });
171
+ } catch {
172
+ return;
173
+ }
174
+ for (const entry of entries) {
175
+ if (found.length >= 5) return;
176
+ if (entry.isDirectory()) {
177
+ if (!ignoreDirs.has(entry.name)) {
178
+ walk(path.join(dir, entry.name), depth + 1);
179
+ }
180
+ } else if (entry.isFile()) {
181
+ const ext = path.extname(entry.name);
182
+ if (codeExtensions.has(ext)) {
183
+ found.push(path.join(dir, entry.name));
184
+ }
185
+ }
186
+ }
187
+ }
188
+
189
+ walk(cwd, 0);
190
+ return found;
191
+ }
192
+
193
+ /**
194
+ * Detect whether the project is brownfield (existing code/manifests) or greenfield.
195
+ */
196
+ function detectBrownfieldStatus(cwd) {
197
+ const codeFiles = detectCodeFiles(cwd, 3);
198
+ const hasExistingCode = codeFiles.length > 0;
199
+
200
+ const packageFiles = [
201
+ 'package.json', 'requirements.txt', 'pyproject.toml', 'Cargo.toml',
202
+ 'go.mod', 'Package.swift', 'pom.xml', 'build.gradle',
203
+ ];
204
+
205
+ const hasDotnetProject = (() => {
206
+ try {
207
+ const rootFiles = fs.readdirSync(cwd);
208
+ return rootFiles.some(f => f.endsWith('.sln') || f.endsWith('.csproj'));
209
+ } catch {
210
+ return false;
211
+ }
212
+ })();
213
+
214
+ const hasPackageFile = packageFiles.some(f => pathExists(cwd, f)) || hasDotnetProject;
215
+ const isBrownfield = hasExistingCode || hasPackageFile;
216
+
217
+ return {
218
+ has_existing_code: hasExistingCode,
219
+ has_package_file: hasPackageFile,
220
+ is_brownfield: isBrownfield,
221
+ is_greenfield: !isBrownfield,
222
+ };
223
+ }
224
+
225
+ // ─── Settings ────────────────────────────────────────────────────────────────
226
+
227
+ function loadSettings(cwd) {
228
+ const settingsPath = path.join(cwd, '.ace', 'settings.json');
229
+ try {
230
+ const raw = fs.readFileSync(settingsPath, 'utf-8');
231
+ const parsed = JSON.parse(raw);
232
+ return {
233
+ model_profile: parsed.model_profile ?? SETTINGS_DEFAULTS.model_profile,
234
+ commit_docs: parsed.commit_docs ?? SETTINGS_DEFAULTS.commit_docs,
235
+ agent_teams: parsed.agent_teams ?? SETTINGS_DEFAULTS.agent_teams,
236
+ github_project: {
237
+ enabled: parsed.github_project?.enabled ?? SETTINGS_DEFAULTS.github_project.enabled,
238
+ gh_installed: parsed.github_project?.gh_installed ?? SETTINGS_DEFAULTS.github_project.gh_installed,
239
+ repo: parsed.github_project?.repo ?? SETTINGS_DEFAULTS.github_project.repo,
240
+ project_number: parsed.github_project?.project_number ?? SETTINGS_DEFAULTS.github_project.project_number,
241
+ owner: parsed.github_project?.owner ?? SETTINGS_DEFAULTS.github_project.owner,
242
+ },
243
+ };
244
+ } catch {
245
+ return JSON.parse(JSON.stringify(SETTINGS_DEFAULTS));
246
+ }
247
+ }
248
+
249
+ function writeSettings(cwd, settings) {
250
+ const aceDir = path.join(cwd, '.ace');
251
+ if (!fs.existsSync(aceDir)) {
252
+ fs.mkdirSync(aceDir, { recursive: true });
253
+ }
254
+ const settingsPath = path.join(aceDir, 'settings.json');
255
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
256
+ }
257
+
258
+ // ─── Shell Execution ─────────────────────────────────────────────────────────
259
+
260
+ /**
261
+ * Parse key=value arguments into an object.
262
+ */
263
+ function parseKeyValueArgs(args) {
264
+ const result = {};
265
+ for (const arg of args) {
266
+ const eqIndex = arg.indexOf('=');
267
+ if (eqIndex === -1) continue;
268
+ result[arg.substring(0, eqIndex)] = arg.substring(eqIndex + 1);
269
+ }
270
+ return result;
271
+ }
272
+
273
+ /**
274
+ * Run a shell command and return trimmed stdout. Returns null on failure.
275
+ */
276
+ function execCommand(cmd, cwd) {
277
+ const { execSync } = require('child_process');
278
+ try {
279
+ return execSync(cmd, {
280
+ cwd,
281
+ shell: 'bash',
282
+ stdio: ['pipe', 'pipe', 'pipe'],
283
+ encoding: 'utf-8',
284
+ timeout: 30000,
285
+ }).trim();
286
+ } catch {
287
+ return null;
288
+ }
289
+ }
290
+
291
+ module.exports = {
292
+ MODEL_PROFILES,
293
+ SETTINGS_DEFAULTS,
294
+ output,
295
+ error,
296
+ loadConfig,
297
+ pathExists,
298
+ safeReadFile,
299
+ generateSlug,
300
+ currentTimestamp,
301
+ resolveModel,
302
+ detectCodeFiles,
303
+ detectBrownfieldStatus,
304
+ loadSettings,
305
+ writeSettings,
306
+ parseKeyValueArgs,
307
+ execCommand,
308
+ };
@@ -0,0 +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
+ });