agile-context-engineering 0.3.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/.claude-plugin/marketplace.json +18 -0
  2. package/.claude-plugin/plugin.json +10 -0
  3. package/CHANGELOG.md +7 -1
  4. package/LICENSE +51 -51
  5. package/README.md +330 -318
  6. package/agents/ace-code-discovery-analyst.md +245 -245
  7. package/agents/ace-code-integration-analyst.md +248 -248
  8. package/agents/ace-code-reviewer.md +375 -375
  9. package/agents/ace-product-owner.md +365 -361
  10. package/agents/ace-project-researcher.md +606 -606
  11. package/agents/ace-research-synthesizer.md +228 -228
  12. package/agents/ace-technical-application-architect.md +315 -315
  13. package/agents/ace-wiki-mapper.md +449 -445
  14. package/bin/install.js +605 -195
  15. package/hooks/ace-check-update.js +71 -62
  16. package/hooks/ace-statusline.js +107 -89
  17. package/hooks/hooks.json +14 -0
  18. package/package.json +7 -5
  19. package/shared/lib/ace-core.js +361 -0
  20. package/shared/lib/ace-core.test.js +308 -0
  21. package/shared/lib/ace-github.js +753 -0
  22. package/shared/lib/ace-story.js +400 -0
  23. package/shared/lib/ace-story.test.js +250 -0
  24. package/{agile-context-engineering → shared}/utils/questioning.xml +110 -110
  25. package/{agile-context-engineering → shared}/utils/ui-formatting.md +299 -299
  26. package/{commands/ace/execute-story.md → skills/execute-story/SKILL.md} +116 -138
  27. package/skills/execute-story/script.js +291 -0
  28. package/skills/execute-story/script.test.js +261 -0
  29. package/{agile-context-engineering/templates/product/story.xml → skills/execute-story/story-template.xml} +451 -451
  30. package/skills/execute-story/walkthrough-template.xml +255 -0
  31. package/{agile-context-engineering/workflows/execute-story.xml → skills/execute-story/workflow.xml} +1221 -1219
  32. package/skills/help/SKILL.md +71 -0
  33. package/skills/help/script.js +315 -0
  34. package/skills/help/script.test.js +183 -0
  35. package/{agile-context-engineering/workflows/help.xml → skills/help/workflow.xml} +544 -533
  36. package/{commands/ace/init-coding-standards.md → skills/init-coding-standards/SKILL.md} +91 -83
  37. package/{agile-context-engineering/templates/wiki/coding-standards.xml → skills/init-coding-standards/coding-standards-template.xml} +531 -531
  38. package/skills/init-coding-standards/script.js +50 -0
  39. package/skills/init-coding-standards/script.test.js +70 -0
  40. package/{agile-context-engineering/workflows/init-coding-standards.xml → skills/init-coding-standards/workflow.xml} +381 -386
  41. package/skills/map-cross-cutting/SKILL.md +126 -0
  42. package/{agile-context-engineering/templates/wiki → skills/map-cross-cutting}/system-cross-cutting.xml +197 -197
  43. package/skills/map-cross-cutting/workflow.xml +330 -0
  44. package/skills/map-guide/SKILL.md +126 -0
  45. package/{agile-context-engineering/templates/wiki → skills/map-guide}/guide.xml +137 -137
  46. package/skills/map-guide/workflow.xml +320 -0
  47. package/skills/map-pattern/SKILL.md +125 -0
  48. package/{agile-context-engineering/templates/wiki → skills/map-pattern}/pattern.xml +159 -159
  49. package/skills/map-pattern/workflow.xml +331 -0
  50. package/{commands/ace/map-story.md → skills/map-story/SKILL.md} +180 -165
  51. package/{agile-context-engineering/templates/wiki → skills/map-story/templates}/decizions.xml +115 -115
  52. package/skills/map-story/templates/guide.xml +137 -0
  53. package/skills/map-story/templates/pattern.xml +159 -0
  54. package/skills/map-story/templates/system-cross-cutting.xml +197 -0
  55. package/{agile-context-engineering/templates/wiki → skills/map-story/templates}/system.xml +381 -381
  56. package/{agile-context-engineering/templates/wiki → skills/map-story/templates}/tech-debt-index.xml +125 -125
  57. package/{agile-context-engineering/templates/wiki → skills/map-story/templates}/walkthrough.xml +255 -255
  58. package/{agile-context-engineering/workflows/map-story.xml → skills/map-story/workflow.xml} +1046 -1046
  59. package/{commands/ace/map-subsystem.md → skills/map-subsystem/SKILL.md} +155 -140
  60. package/skills/map-subsystem/script.js +51 -0
  61. package/skills/map-subsystem/script.test.js +68 -0
  62. package/skills/map-subsystem/templates/decizions.xml +115 -0
  63. package/skills/map-subsystem/templates/guide.xml +137 -0
  64. package/{agile-context-engineering/templates/wiki → skills/map-subsystem/templates}/module-discovery.xml +174 -174
  65. package/skills/map-subsystem/templates/pattern.xml +159 -0
  66. package/{agile-context-engineering/templates/wiki → skills/map-subsystem/templates}/subsystem-architecture.xml +343 -343
  67. package/{agile-context-engineering/templates/wiki → skills/map-subsystem/templates}/subsystem-structure.xml +234 -234
  68. package/skills/map-subsystem/templates/system-cross-cutting.xml +197 -0
  69. package/skills/map-subsystem/templates/system.xml +381 -0
  70. package/skills/map-subsystem/templates/walkthrough.xml +255 -0
  71. package/{agile-context-engineering/workflows/map-subsystem.xml → skills/map-subsystem/workflow.xml} +1173 -1178
  72. package/skills/map-sys-doc/SKILL.md +125 -0
  73. package/skills/map-sys-doc/system.xml +381 -0
  74. package/skills/map-sys-doc/workflow.xml +336 -0
  75. package/{commands/ace/map-system.md → skills/map-system/SKILL.md} +103 -92
  76. package/skills/map-system/script.js +75 -0
  77. package/skills/map-system/script.test.js +73 -0
  78. package/{agile-context-engineering/templates/wiki → skills/map-system/templates}/system-architecture.xml +254 -254
  79. package/{agile-context-engineering/templates/wiki → skills/map-system/templates}/system-structure.xml +177 -177
  80. package/{agile-context-engineering/templates/wiki → skills/map-system/templates}/testing-framework.xml +283 -283
  81. package/{agile-context-engineering/templates/wiki → skills/map-system/templates}/wiki-readme.xml +296 -296
  82. package/{agile-context-engineering/workflows/map-system.xml → skills/map-system/workflow.xml} +667 -672
  83. package/{commands/ace/map-walkthrough.md → skills/map-walkthrough/SKILL.md} +140 -127
  84. package/skills/map-walkthrough/walkthrough.xml +255 -0
  85. package/{agile-context-engineering/workflows/map-walkthrough.xml → skills/map-walkthrough/workflow.xml} +457 -457
  86. package/{commands/ace/plan-backlog.md → skills/plan-backlog/SKILL.md} +93 -83
  87. package/{agile-context-engineering/templates/product/product-backlog.xml → skills/plan-backlog/product-backlog-template.xml} +231 -231
  88. package/skills/plan-backlog/script.js +121 -0
  89. package/skills/plan-backlog/script.test.js +83 -0
  90. package/{agile-context-engineering/workflows/plan-backlog.xml → skills/plan-backlog/workflow.xml} +1348 -1356
  91. package/{commands/ace/plan-feature.md → skills/plan-feature/SKILL.md} +99 -89
  92. package/{agile-context-engineering/templates/product/feature.xml → skills/plan-feature/feature-template.xml} +361 -361
  93. package/skills/plan-feature/script.js +131 -0
  94. package/skills/plan-feature/script.test.js +80 -0
  95. package/{agile-context-engineering/workflows/plan-feature.xml → skills/plan-feature/workflow.xml} +1487 -1495
  96. package/{commands/ace/plan-product-vision.md → skills/plan-product-vision/SKILL.md} +91 -81
  97. package/{agile-context-engineering/templates/product/product-vision.xml → skills/plan-product-vision/product-vision-template.xml} +227 -227
  98. package/skills/plan-product-vision/script.js +51 -0
  99. package/skills/plan-product-vision/script.test.js +69 -0
  100. package/{agile-context-engineering/workflows/plan-product-vision.xml → skills/plan-product-vision/workflow.xml} +337 -342
  101. package/{commands/ace/plan-story.md → skills/plan-story/SKILL.md} +139 -159
  102. package/skills/plan-story/script.js +295 -0
  103. package/skills/plan-story/script.test.js +240 -0
  104. package/skills/plan-story/story-template.xml +458 -0
  105. package/{agile-context-engineering/workflows/plan-story.xml → skills/plan-story/workflow.xml} +1301 -944
  106. package/{commands/ace/research-external-solution.md → skills/research-external-solution/SKILL.md} +120 -138
  107. package/{agile-context-engineering/templates/product/external-solution.xml → skills/research-external-solution/external-solution-template.xml} +832 -832
  108. package/skills/research-external-solution/script.js +229 -0
  109. package/skills/research-external-solution/script.test.js +134 -0
  110. package/{agile-context-engineering/workflows/research-external-solution.xml → skills/research-external-solution/workflow.xml} +657 -659
  111. package/{commands/ace/research-integration-solution.md → skills/research-integration-solution/SKILL.md} +121 -135
  112. package/{agile-context-engineering/templates/product/story-integration-solution.xml → skills/research-integration-solution/integration-solution-template.xml} +1015 -1015
  113. package/skills/research-integration-solution/script.js +223 -0
  114. package/skills/research-integration-solution/script.test.js +134 -0
  115. package/{agile-context-engineering/workflows/research-integration-solution.xml → skills/research-integration-solution/workflow.xml} +711 -713
  116. package/{commands/ace/research-story-wiki.md → skills/research-story-wiki/SKILL.md} +101 -116
  117. package/skills/research-story-wiki/script.js +223 -0
  118. package/skills/research-story-wiki/script.test.js +138 -0
  119. package/{agile-context-engineering/templates/product/story-wiki.xml → skills/research-story-wiki/story-wiki-template.xml} +194 -194
  120. package/{agile-context-engineering/workflows/research-story-wiki.xml → skills/research-story-wiki/workflow.xml} +473 -475
  121. package/{commands/ace/research-technical-solution.md → skills/research-technical-solution/SKILL.md} +131 -147
  122. package/skills/research-technical-solution/script.js +223 -0
  123. package/skills/research-technical-solution/script.test.js +134 -0
  124. package/{agile-context-engineering/templates/product/story-technical-solution.xml → skills/research-technical-solution/technical-solution-template.xml} +1025 -1025
  125. package/{agile-context-engineering/workflows/research-technical-solution.xml → skills/research-technical-solution/workflow.xml} +761 -763
  126. package/{commands/ace/review-story.md → skills/review-story/SKILL.md} +99 -109
  127. package/skills/review-story/script.js +249 -0
  128. package/skills/review-story/script.test.js +169 -0
  129. package/skills/review-story/story-template.xml +451 -0
  130. package/{agile-context-engineering/workflows/review-story.xml → skills/review-story/workflow.xml} +279 -281
  131. package/{commands/ace/update.md → skills/update/SKILL.md} +65 -56
  132. package/{agile-context-engineering/workflows/update.xml → skills/update/workflow.xml} +33 -18
  133. package/agile-context-engineering/src/ace-tools.js +0 -2881
  134. package/agile-context-engineering/src/ace-tools.test.js +0 -1089
  135. package/agile-context-engineering/templates/_command.md +0 -54
  136. package/agile-context-engineering/templates/_workflow.xml +0 -17
  137. package/agile-context-engineering/templates/config.json +0 -0
  138. package/agile-context-engineering/templates/product/integration-solution.xml +0 -0
  139. package/commands/ace/help.md +0 -93
@@ -0,0 +1,361 @@
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
+ * Parse raw skill init args into a structured object.
275
+ * Handles key=value params, flags (--name), and positional args.
276
+ * ALL skill script.js files MUST use this for argument parsing.
277
+ *
278
+ * Returns: { story: '...', text: '...', _flags: ['--agent-teams-off'], _positional: '...' }
279
+ */
280
+ function parseSkillArgs(rawArgs) {
281
+ const args = rawArgs.filter(a => a !== '--raw');
282
+ const flags = args.filter(a => a.startsWith('--'));
283
+ const params = args.filter(a => !a.startsWith('--'));
284
+ const hasKeyValue = params.some(a => a.includes('='));
285
+
286
+ const result = { _flags: flags };
287
+
288
+ if (hasKeyValue) {
289
+ Object.assign(result, parseKeyValueArgs(params));
290
+ } else if (params.length > 0) {
291
+ result._positional = params.join(' ');
292
+ }
293
+
294
+ return result;
295
+ }
296
+
297
+ /**
298
+ * Shared CLI dispatch for all skill scripts.
299
+ * Parses process.argv, extracts --raw, parses key=value args, and routes to handlers.
300
+ * ALL skill script.js files MUST use this — no manual arg parsing.
301
+ *
302
+ * Handlers receive (cwd, raw, args, parsed):
303
+ * cwd — process.cwd()
304
+ * raw — boolean, --raw flag present
305
+ * args — raw arg array (after command name, --raw stripped)
306
+ * parsed — parseSkillArgs result: { story: '...', _flags: [...], _positional: '...' }
307
+ */
308
+ function runSkillScript(handlers) {
309
+ const cwd = process.cwd();
310
+ const allArgs = process.argv.slice(2);
311
+ const raw = allArgs.includes('--raw');
312
+ const cmd = allArgs[0];
313
+ const restArgs = allArgs.slice(1).filter(a => a !== '--raw');
314
+ const parsed = parseSkillArgs(allArgs.slice(1));
315
+
316
+ const handler = handlers[cmd];
317
+ if (!handler) {
318
+ error(`Unknown command: ${cmd}\nAvailable: ${Object.keys(handlers).join(', ')}`);
319
+ return;
320
+ }
321
+ handler(cwd, raw, restArgs, parsed);
322
+ }
323
+
324
+ /**
325
+ * Run a shell command and return trimmed stdout. Returns null on failure.
326
+ */
327
+ function execCommand(cmd, cwd) {
328
+ const { execSync } = require('child_process');
329
+ try {
330
+ return execSync(cmd, {
331
+ cwd,
332
+ shell: 'bash',
333
+ stdio: ['pipe', 'pipe', 'pipe'],
334
+ encoding: 'utf-8',
335
+ timeout: 30000,
336
+ }).trim();
337
+ } catch {
338
+ return null;
339
+ }
340
+ }
341
+
342
+ module.exports = {
343
+ MODEL_PROFILES,
344
+ SETTINGS_DEFAULTS,
345
+ output,
346
+ error,
347
+ loadConfig,
348
+ pathExists,
349
+ safeReadFile,
350
+ generateSlug,
351
+ currentTimestamp,
352
+ resolveModel,
353
+ detectCodeFiles,
354
+ detectBrownfieldStatus,
355
+ loadSettings,
356
+ writeSettings,
357
+ parseKeyValueArgs,
358
+ parseSkillArgs,
359
+ runSkillScript,
360
+ execCommand,
361
+ };
@@ -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
+ });