agile-context-engineering 0.2.2 → 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 (146) hide show
  1. package/.claude-plugin/plugin.json +10 -0
  2. package/CHANGELOG.md +82 -0
  3. package/README.md +27 -18
  4. package/agents/ace-product-owner.md +1 -1
  5. package/agents/ace-technical-application-architect.md +28 -0
  6. package/agents/ace-wiki-mapper.md +144 -29
  7. package/bin/install.js +67 -63
  8. package/hooks/ace-check-update.js +17 -9
  9. package/package.json +7 -5
  10. package/shared/lib/ace-core.js +308 -0
  11. package/shared/lib/ace-core.test.js +308 -0
  12. package/shared/lib/ace-github.js +753 -0
  13. package/shared/lib/ace-story.js +400 -0
  14. package/shared/lib/ace-story.test.js +250 -0
  15. package/{agile-context-engineering → shared}/utils/ui-formatting.md +299 -299
  16. package/skills/execute-story/SKILL.md +110 -0
  17. package/skills/execute-story/script.js +305 -0
  18. package/skills/execute-story/script.test.js +261 -0
  19. package/skills/execute-story/walkthrough-template.xml +255 -0
  20. package/{agile-context-engineering/workflows/execute-story.xml → skills/execute-story/workflow.xml} +83 -9
  21. package/skills/help/SKILL.md +69 -0
  22. package/skills/help/script.js +318 -0
  23. package/skills/help/script.test.js +183 -0
  24. package/{agile-context-engineering/workflows/help.xml → skills/help/workflow.xml} +8 -8
  25. package/skills/init-coding-standards/SKILL.md +72 -0
  26. package/{agile-context-engineering/templates/wiki/coding-standards.xml → skills/init-coding-standards/coding-standards-template.xml} +38 -0
  27. package/skills/init-coding-standards/script.js +59 -0
  28. package/skills/init-coding-standards/script.test.js +70 -0
  29. package/{agile-context-engineering/workflows/init-coding-standards.xml → skills/init-coding-standards/workflow.xml} +4 -9
  30. package/skills/map-cross-cutting/SKILL.md +89 -0
  31. package/skills/map-cross-cutting/workflow.xml +330 -0
  32. package/skills/map-guide/SKILL.md +89 -0
  33. package/skills/map-guide/workflow.xml +320 -0
  34. package/skills/map-pattern/SKILL.md +89 -0
  35. package/skills/map-pattern/workflow.xml +331 -0
  36. package/skills/map-story/SKILL.md +127 -0
  37. package/skills/map-story/templates/guide.xml +137 -0
  38. package/skills/map-story/templates/pattern.xml +159 -0
  39. package/skills/map-story/templates/system-cross-cutting.xml +197 -0
  40. package/skills/map-story/templates/walkthrough.xml +255 -0
  41. package/{agile-context-engineering/workflows/map-story.xml → skills/map-story/workflow.xml} +258 -9
  42. package/skills/map-subsystem/SKILL.md +111 -0
  43. package/skills/map-subsystem/script.js +60 -0
  44. package/skills/map-subsystem/script.test.js +68 -0
  45. package/skills/map-subsystem/templates/decizions.xml +115 -0
  46. package/skills/map-subsystem/templates/guide.xml +137 -0
  47. package/{agile-context-engineering/templates/wiki → skills/map-subsystem/templates}/module-discovery.xml +3 -3
  48. package/skills/map-subsystem/templates/pattern.xml +159 -0
  49. package/skills/map-subsystem/templates/system-cross-cutting.xml +197 -0
  50. package/skills/map-subsystem/templates/system.xml +381 -0
  51. package/skills/map-subsystem/templates/walkthrough.xml +255 -0
  52. package/{agile-context-engineering/workflows/map-subsystem.xml → skills/map-subsystem/workflow.xml} +17 -21
  53. package/skills/map-sys-doc/SKILL.md +90 -0
  54. package/skills/map-sys-doc/system.xml +381 -0
  55. package/skills/map-sys-doc/workflow.xml +336 -0
  56. package/skills/map-system/SKILL.md +85 -0
  57. package/skills/map-system/script.js +84 -0
  58. package/skills/map-system/script.test.js +73 -0
  59. package/skills/map-system/templates/wiki-readme.xml +297 -0
  60. package/{agile-context-engineering/workflows/map-system.xml → skills/map-system/workflow.xml} +11 -16
  61. package/skills/map-walkthrough/SKILL.md +92 -0
  62. package/skills/map-walkthrough/walkthrough.xml +255 -0
  63. package/skills/map-walkthrough/workflow.xml +457 -0
  64. package/skills/plan-backlog/SKILL.md +75 -0
  65. package/skills/plan-backlog/script.js +136 -0
  66. package/skills/plan-backlog/script.test.js +83 -0
  67. package/{agile-context-engineering/workflows/plan-backlog.xml → skills/plan-backlog/workflow.xml} +13 -21
  68. package/skills/plan-feature/SKILL.md +76 -0
  69. package/skills/plan-feature/script.js +148 -0
  70. package/skills/plan-feature/script.test.js +80 -0
  71. package/{agile-context-engineering/workflows/plan-feature.xml → skills/plan-feature/workflow.xml} +21 -29
  72. package/skills/plan-product-vision/SKILL.md +75 -0
  73. package/skills/plan-product-vision/script.js +60 -0
  74. package/skills/plan-product-vision/script.test.js +69 -0
  75. package/{agile-context-engineering/workflows/plan-product-vision.xml → skills/plan-product-vision/workflow.xml} +4 -9
  76. package/skills/plan-story/SKILL.md +116 -0
  77. package/skills/plan-story/script.js +326 -0
  78. package/skills/plan-story/script.test.js +240 -0
  79. package/skills/plan-story/story-template.xml +451 -0
  80. package/{agile-context-engineering/workflows/plan-story.xml → skills/plan-story/workflow.xml} +1285 -909
  81. package/skills/research-external-solution/SKILL.md +107 -0
  82. package/skills/research-external-solution/script.js +238 -0
  83. package/skills/research-external-solution/script.test.js +134 -0
  84. package/{agile-context-engineering/workflows/research-external-solution.xml → skills/research-external-solution/workflow.xml} +4 -6
  85. package/skills/research-integration-solution/SKILL.md +98 -0
  86. package/{agile-context-engineering/templates/product/story-integration-solution.xml → skills/research-integration-solution/integration-solution-template.xml} +1 -0
  87. package/skills/research-integration-solution/script.js +231 -0
  88. package/skills/research-integration-solution/script.test.js +134 -0
  89. package/{agile-context-engineering/workflows/research-integration-solution.xml → skills/research-integration-solution/workflow.xml} +4 -5
  90. package/skills/research-story-wiki/SKILL.md +92 -0
  91. package/skills/research-story-wiki/script.js +231 -0
  92. package/skills/research-story-wiki/script.test.js +138 -0
  93. package/{agile-context-engineering/templates/product/story-wiki.xml → skills/research-story-wiki/story-wiki-template.xml} +4 -0
  94. package/{agile-context-engineering/workflows/research-story-wiki.xml → skills/research-story-wiki/workflow.xml} +5 -6
  95. package/skills/research-technical-solution/SKILL.md +103 -0
  96. package/skills/research-technical-solution/script.js +231 -0
  97. package/skills/research-technical-solution/script.test.js +134 -0
  98. package/{agile-context-engineering/workflows/research-technical-solution.xml → skills/research-technical-solution/workflow.xml} +4 -5
  99. package/skills/review-story/SKILL.md +100 -0
  100. package/skills/review-story/script.js +257 -0
  101. package/skills/review-story/script.test.js +169 -0
  102. package/skills/review-story/story-template.xml +451 -0
  103. package/{agile-context-engineering/workflows/review-story.xml → skills/review-story/workflow.xml} +1 -3
  104. package/skills/update/SKILL.md +53 -0
  105. package/{agile-context-engineering/workflows/update.xml → skills/update/workflow.xml} +237 -207
  106. package/agile-context-engineering/src/ace-tools.js +0 -2881
  107. package/agile-context-engineering/src/ace-tools.test.js +0 -1089
  108. package/agile-context-engineering/templates/_command.md +0 -54
  109. package/agile-context-engineering/templates/_workflow.xml +0 -17
  110. package/agile-context-engineering/templates/config.json +0 -0
  111. package/agile-context-engineering/templates/product/integration-solution.xml +0 -0
  112. package/agile-context-engineering/templates/wiki/wiki-readme.xml +0 -276
  113. package/commands/ace/execute-story.md +0 -137
  114. package/commands/ace/help.md +0 -93
  115. package/commands/ace/init-coding-standards.md +0 -83
  116. package/commands/ace/map-story.md +0 -156
  117. package/commands/ace/map-subsystem.md +0 -138
  118. package/commands/ace/map-system.md +0 -92
  119. package/commands/ace/plan-backlog.md +0 -83
  120. package/commands/ace/plan-feature.md +0 -89
  121. package/commands/ace/plan-product-vision.md +0 -81
  122. package/commands/ace/plan-story.md +0 -145
  123. package/commands/ace/research-external-solution.md +0 -138
  124. package/commands/ace/research-integration-solution.md +0 -135
  125. package/commands/ace/research-story-wiki.md +0 -116
  126. package/commands/ace/research-technical-solution.md +0 -147
  127. package/commands/ace/review-story.md +0 -109
  128. package/commands/ace/update.md +0 -54
  129. /package/{agile-context-engineering → shared}/utils/questioning.xml +0 -0
  130. /package/{agile-context-engineering/templates/product/story.xml → skills/execute-story/story-template.xml} +0 -0
  131. /package/{agile-context-engineering/templates/wiki → skills/map-cross-cutting}/system-cross-cutting.xml +0 -0
  132. /package/{agile-context-engineering/templates/wiki → skills/map-guide}/guide.xml +0 -0
  133. /package/{agile-context-engineering/templates/wiki → skills/map-pattern}/pattern.xml +0 -0
  134. /package/{agile-context-engineering/templates/wiki → skills/map-story/templates}/decizions.xml +0 -0
  135. /package/{agile-context-engineering/templates/wiki → skills/map-story/templates}/system.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-architecture.xml +0 -0
  140. /package/{agile-context-engineering/templates/wiki → skills/map-system/templates}/system-structure.xml +0 -0
  141. /package/{agile-context-engineering/templates/wiki → skills/map-system/templates}/testing-framework.xml +0 -0
  142. /package/{agile-context-engineering/templates/product/product-backlog.xml → skills/plan-backlog/product-backlog-template.xml} +0 -0
  143. /package/{agile-context-engineering/templates/product/feature.xml → skills/plan-feature/feature-template.xml} +0 -0
  144. /package/{agile-context-engineering/templates/product/product-vision.xml → skills/plan-product-vision/product-vision-template.xml} +0 -0
  145. /package/{agile-context-engineering/templates/product/external-solution.xml → skills/research-external-solution/external-solution-template.xml} +0 -0
  146. /package/{agile-context-engineering/templates/product/story-technical-solution.xml → skills/research-technical-solution/technical-solution-template.xml} +0 -0
@@ -0,0 +1,326 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * plan-story skill script — Entry point for all ace-tools operations
5
+ * needed by the plan-story skill.
6
+ *
7
+ * Subcommands:
8
+ * init [story-param] Environment detection for plan-story workflow
9
+ * update-state story=X status=Y Update story status across files
10
+ * sync-github repo=X story_file=Y Sync story/feature to GitHub
11
+ * resolve-model <agent-type> Get model for agent based on profile
12
+ *
13
+ * Usage: node script.js <subcommand> [args] [--raw]
14
+ */
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+
19
+ const {
20
+ loadConfig, pathExists, safeReadFile, generateSlug, resolveModel,
21
+ detectBrownfieldStatus, loadSettings, execCommand,
22
+ output, error, parseKeyValueArgs,
23
+ } = require('../../shared/lib/ace-core');
24
+
25
+ const {
26
+ classifyStoryParam, extractStoryMetadata, extractStoryRequirements,
27
+ extractIssueNumber, extractIssueNumberFromFile, computeStoryPaths,
28
+ updateState,
29
+ } = require('../../shared/lib/ace-story');
30
+
31
+ const { syncStory, resolveFields, createIssue } = require('../../shared/lib/ace-github');
32
+
33
+ // ─── CLI Dispatch ────────────────────────────────────────────────────────────
34
+
35
+ const cwd = process.cwd();
36
+ const args = process.argv.slice(2);
37
+ const raw = args.includes('--raw');
38
+ const cmd = args[0];
39
+
40
+ switch (cmd) {
41
+ case 'init':
42
+ cmdInit(cwd, raw, args.slice(1).filter(a => a !== '--raw'));
43
+ break;
44
+ case 'update-state':
45
+ updateState(cwd, raw, args.slice(1).filter(a => a !== '--raw'));
46
+ break;
47
+ case 'sync-github':
48
+ syncStory(cwd, raw, args.slice(1).filter(a => a !== '--raw'));
49
+ break;
50
+ case 'resolve-model': {
51
+ const agentType = args[1];
52
+ if (!agentType) error('resolve-model requires agent-type argument');
53
+ const model = resolveModel(cwd, agentType);
54
+ output({ model, agent: agentType }, raw, model);
55
+ break;
56
+ }
57
+ case 'generate-slug': {
58
+ const text = args.slice(1).filter(a => a !== '--raw').join(' ');
59
+ if (!text) error('generate-slug requires text argument');
60
+ const slug = generateSlug(text);
61
+ output({ slug }, raw, slug);
62
+ break;
63
+ }
64
+ case 'resolve-fields':
65
+ resolveFields(cwd, raw, args.slice(1).filter(a => a !== '--raw'));
66
+ break;
67
+ case 'create-issue':
68
+ createIssue(cwd, raw, args.slice(1).filter(a => a !== '--raw'));
69
+ break;
70
+ default:
71
+ error(`Unknown command: ${cmd}\nAvailable: init, update-state, sync-github, resolve-model, generate-slug, resolve-fields, create-issue`);
72
+ }
73
+
74
+ // ─── Init: Plan Story ────────────────────────────────────────────────────────
75
+
76
+ /**
77
+ * Environment detection for the plan-story workflow.
78
+ *
79
+ * Detects: git, gh CLI, GitHub project, brownfield status, wiki state,
80
+ * product artifacts, and story source/content/metadata.
81
+ *
82
+ * Supports three input modes:
83
+ * - story param provided → loads existing file or fetches GitHub issue
84
+ * - text param provided (no story) → uses inline text as seed description
85
+ * - neither → new story mode, workflow handles placement
86
+ *
87
+ * initArgs: array of arguments — either a single positional path/URL,
88
+ * or key=value pairs (story=X text=Y)
89
+ */
90
+ function cmdInit(cwd, raw, initArgs) {
91
+ const config = loadConfig(cwd);
92
+ const brownfield = detectBrownfieldStatus(cwd);
93
+
94
+ // ── Environment detection ──
95
+ const has_git = pathExists(cwd, '.git');
96
+ const has_gh_cli = (() => {
97
+ try {
98
+ const { execSync } = require('child_process');
99
+ execSync('gh --version', { stdio: 'pipe' });
100
+ return true;
101
+ } catch { return false; }
102
+ })();
103
+ const github_project = loadSettings(cwd).github_project;
104
+
105
+ // Wiki detection
106
+ const wikiSystemDir = '.docs/wiki/system-wide';
107
+ const has_wiki_system_wide = pathExists(cwd, wikiSystemDir);
108
+ const wikiSubsystemsDir = '.docs/wiki/subsystems';
109
+ const has_wiki_subsystems = pathExists(cwd, wikiSubsystemsDir);
110
+ let wiki_subsystem_names = [];
111
+ if (has_wiki_subsystems) {
112
+ try {
113
+ const entries = fs.readdirSync(path.join(cwd, wikiSubsystemsDir), { withFileTypes: true });
114
+ wiki_subsystem_names = entries.filter(e => e.isDirectory()).map(e => e.name);
115
+ } catch {}
116
+ }
117
+ const has_wiki = has_wiki_system_wide || has_wiki_subsystems;
118
+
119
+ // ── Parse input: support both positional and key=value forms ──
120
+ // Positional: script.js init path/to/story.md
121
+ // Key-value: script.js init story=path/to/story.md text="some description"
122
+ const hasKeyValue = initArgs.some(a => a.includes('='));
123
+ let resolvedStoryParam = null;
124
+ let textParam = null;
125
+
126
+ if (hasKeyValue) {
127
+ const kvArgs = parseKeyValueArgs(initArgs);
128
+ resolvedStoryParam = kvArgs.story || null;
129
+ textParam = kvArgs.text || null;
130
+ } else if (initArgs.length > 0) {
131
+ // Positional: treat entire arg list as the story param
132
+ resolvedStoryParam = initArgs.join(' ');
133
+ }
134
+
135
+ // ── Base result (shared across all modes) ──
136
+ const baseResult = {
137
+ product_owner_model: resolveModel(cwd, 'ace-product-owner'),
138
+ commit_docs: config.commit_docs,
139
+ has_git, has_gh_cli, github_project,
140
+ ...brownfield,
141
+ has_wiki, has_wiki_system_wide, has_wiki_subsystems, wiki_subsystem_names,
142
+ has_product_vision: pathExists(cwd, '.docs/product/product-vision.md'),
143
+ has_product_backlog: pathExists(cwd, '.ace/artifacts/product/product-backlog.md'),
144
+ };
145
+
146
+ // ── Mode 1: No story param and no text → new story mode ──
147
+ if (!resolvedStoryParam && !textParam) {
148
+ output({
149
+ ...baseResult,
150
+ story_source: 'new',
151
+ story_valid: true,
152
+ story_error: null,
153
+ story_content: null,
154
+ story: { id: null, title: null, status: null, size: null, issue_number: null },
155
+ feature: { id: null, title: null, issue_number: null },
156
+ epic: { id: null, title: null },
157
+ user_story: null, description: null, acceptance_criteria_count: 0,
158
+ paths: null,
159
+ has_external_analysis: false, has_integration_analysis: false,
160
+ has_feature_file: false, has_story_file: false,
161
+ }, raw);
162
+ return;
163
+ }
164
+
165
+ // ── Mode 2: Text param only → use text as seed description ──
166
+ if (!resolvedStoryParam && textParam) {
167
+ output({
168
+ ...baseResult,
169
+ story_source: 'text',
170
+ story_valid: true,
171
+ story_error: null,
172
+ story_content: textParam,
173
+ story: { id: null, title: null, status: null, size: null, issue_number: null },
174
+ feature: { id: null, title: null, issue_number: null },
175
+ epic: { id: null, title: null },
176
+ user_story: null, description: textParam, acceptance_criteria_count: 0,
177
+ paths: null,
178
+ has_external_analysis: false, has_integration_analysis: false,
179
+ has_feature_file: false, has_story_file: false,
180
+ }, raw);
181
+ return;
182
+ }
183
+
184
+ // ── Mode 3: Story param provided → classify and load ──
185
+ const classified = classifyStoryParam(resolvedStoryParam);
186
+
187
+ // Invalid param
188
+ if (classified.type === null || classified.type === 'invalid') {
189
+ output({
190
+ ...baseResult,
191
+ story_source: null,
192
+ story_valid: false,
193
+ story_error: classified.reason || 'No story parameter provided',
194
+ story_content: null,
195
+ story: { id: null, title: null, status: null, size: null, issue_number: null },
196
+ feature: { id: null, title: null, issue_number: null },
197
+ epic: { id: null, title: null },
198
+ user_story: null, description: null, acceptance_criteria_count: 0,
199
+ paths: null,
200
+ has_external_analysis: false, has_integration_analysis: false,
201
+ has_feature_file: false, has_story_file: false,
202
+ }, raw);
203
+ return;
204
+ }
205
+
206
+ // ── Load story content ──
207
+ let storyContent = null;
208
+ let storySource = classified.type === 'file' ? 'file' : 'github';
209
+ let storyError = null;
210
+ let storyFilePath = null;
211
+
212
+ if (classified.type === 'file') {
213
+ const resolvedPath = path.isAbsolute(classified.filePath)
214
+ ? classified.filePath
215
+ : path.join(cwd, classified.filePath);
216
+ if (!pathExists(cwd, classified.filePath)) {
217
+ storyError = `Story file not found: ${classified.filePath}`;
218
+ } else {
219
+ storyContent = safeReadFile(resolvedPath);
220
+ storyFilePath = classified.filePath;
221
+ if (!storyContent) storyError = `Could not read story file: ${classified.filePath}`;
222
+ }
223
+ } else {
224
+ // github-url or issue-number
225
+ if (!has_gh_cli) {
226
+ storyError = 'GitHub CLI (gh) not installed. Cannot fetch GitHub issues.';
227
+ } else {
228
+ const repo = classified.repo || (github_project.repo || null);
229
+ if (!repo) {
230
+ storyError = 'No repository configured. Provide a full GitHub URL or configure github_project.repo in settings.';
231
+ } else {
232
+ const ghResult = execCommand(
233
+ `gh issue view ${classified.issueNumber} --repo ${repo} --json title,body,labels,state`,
234
+ cwd
235
+ );
236
+ if (!ghResult) {
237
+ storyError = `Could not fetch GitHub issue #${classified.issueNumber} from ${repo}.`;
238
+ } else {
239
+ try {
240
+ const issue = JSON.parse(ghResult);
241
+ storyContent = issue.body || '';
242
+ if (storyContent && !storyContent.match(/^#\s+/m)) {
243
+ storyContent = `# ${issue.title}\n\n${storyContent}`;
244
+ }
245
+ } catch {
246
+ storyError = `Failed to parse GitHub issue response for #${classified.issueNumber}.`;
247
+ }
248
+ }
249
+ }
250
+ }
251
+ }
252
+
253
+ // ── Extract metadata & requirements ──
254
+ const metadata = extractStoryMetadata(storyContent);
255
+ const requirements = extractStoryRequirements(storyContent);
256
+
257
+ // ── Compute paths ──
258
+ let paths = null;
259
+ let has_story_file = false;
260
+
261
+ if (storyFilePath) {
262
+ const resolvedPath = path.isAbsolute(storyFilePath)
263
+ ? storyFilePath
264
+ : path.join(cwd, storyFilePath);
265
+ const storyDir = path.dirname(resolvedPath);
266
+ const relStoryDir = path.relative(cwd, storyDir).replace(/\\/g, '/');
267
+ const storySlug = path.basename(storyDir);
268
+ const featureDir = path.dirname(storyDir);
269
+ const relFeatureDir = path.relative(cwd, featureDir).replace(/\\/g, '/');
270
+ const featureSlug = path.basename(featureDir);
271
+
272
+ paths = {
273
+ epic_slug: null,
274
+ feature_slug: featureSlug,
275
+ story_slug: storySlug,
276
+ story_dir: relStoryDir,
277
+ story_file: storyFilePath.replace(/\\/g, '/'),
278
+ external_analysis_file: `${relStoryDir}/external-analysis.md`,
279
+ integration_analysis_file: `${relStoryDir}/integration-analysis.md`,
280
+ feature_dir: relFeatureDir,
281
+ feature_file: `${relFeatureDir}/${featureSlug}.md`,
282
+ };
283
+ has_story_file = true;
284
+ } else if (metadata.epic.id && metadata.feature.id && metadata.id) {
285
+ paths = computeStoryPaths(
286
+ metadata.epic.id, metadata.epic.title || '',
287
+ metadata.feature.id, metadata.feature.title || '',
288
+ metadata.id, metadata.title || ''
289
+ );
290
+ has_story_file = paths ? pathExists(cwd, paths.story_file) : false;
291
+ }
292
+
293
+ // ── Check artifact existence ──
294
+ const has_external_analysis = paths ? pathExists(cwd, paths.external_analysis_file) : false;
295
+ const has_integration_analysis = paths ? pathExists(cwd, paths.integration_analysis_file) : false;
296
+ const has_feature_file = paths ? pathExists(cwd, paths.feature_file) : false;
297
+
298
+ // ── Build result ──
299
+ output({
300
+ ...baseResult,
301
+ story_source: storySource,
302
+ story_valid: storyContent !== null && storyError === null,
303
+ story_error: storyError,
304
+ story_content: storyContent,
305
+ story: {
306
+ id: metadata.id,
307
+ title: metadata.title,
308
+ status: metadata.status,
309
+ size: metadata.size,
310
+ issue_number: extractIssueNumber(metadata.link),
311
+ },
312
+ feature: {
313
+ ...metadata.feature,
314
+ issue_number: paths ? extractIssueNumberFromFile(cwd, paths.feature_file) : null,
315
+ },
316
+ epic: metadata.epic,
317
+ user_story: requirements.user_story,
318
+ description: requirements.description,
319
+ acceptance_criteria_count: requirements.acceptance_criteria_count,
320
+ paths,
321
+ has_external_analysis,
322
+ has_integration_analysis,
323
+ has_feature_file,
324
+ has_story_file,
325
+ }, raw);
326
+ }
@@ -0,0 +1,240 @@
1
+ const { describe, it, before, after } = require('node:test');
2
+ const assert = require('node:assert');
3
+ const { execSync } = require('child_process');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const os = require('os');
7
+
8
+ const SCRIPT = path.join(__dirname, 'script.js');
9
+
10
+ /**
11
+ * Create a minimal ACE project structure in a temp directory.
12
+ */
13
+ function createTestProject() {
14
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ace-test-'));
15
+
16
+ // .ace/config.json
17
+ const aceDir = path.join(tmpDir, '.ace');
18
+ fs.mkdirSync(aceDir, { recursive: true });
19
+ fs.writeFileSync(path.join(aceDir, 'config.json'), JSON.stringify({
20
+ version: '0.1.0',
21
+ projectName: 'test-project',
22
+ model_profile: 'quality',
23
+ commit_docs: true,
24
+ github: { enabled: false },
25
+ }, null, 2));
26
+
27
+ // .ace/settings.json
28
+ fs.writeFileSync(path.join(aceDir, 'settings.json'), JSON.stringify({
29
+ model_profile: 'quality',
30
+ commit_docs: true,
31
+ agent_teams: false,
32
+ github_project: { enabled: false, gh_installed: false, repo: '', project_number: null, owner: '' },
33
+ }, null, 2));
34
+
35
+ return tmpDir;
36
+ }
37
+
38
+ /**
39
+ * Create a story file in the test project.
40
+ */
41
+ function createStoryFile(tmpDir, relPath, content) {
42
+ const fullPath = path.join(tmpDir, relPath);
43
+ fs.mkdirSync(path.dirname(fullPath), { recursive: true });
44
+ fs.writeFileSync(fullPath, content, 'utf-8');
45
+ return relPath;
46
+ }
47
+
48
+ function runScript(subcommand, args, cwd) {
49
+ return execSync(`node "${SCRIPT}" ${subcommand} ${args}`, {
50
+ cwd,
51
+ encoding: 'utf-8',
52
+ timeout: 10000,
53
+ });
54
+ }
55
+
56
+ function cleanup(tmpDir) {
57
+ fs.rmSync(tmpDir, { recursive: true, force: true });
58
+ }
59
+
60
+ // ─── Tests ───────────────────────────────────────────────────────────────────
61
+
62
+ describe('plan-story script', () => {
63
+
64
+ describe('init', () => {
65
+ let tmpDir;
66
+
67
+ before(() => { tmpDir = createTestProject(); });
68
+ after(() => { cleanup(tmpDir); });
69
+
70
+ it('returns valid JSON with environment detection for a story file', () => {
71
+ const storyContent = [
72
+ '# S1: Add Login Button',
73
+ '**Feature**: F1 User Auth | **Epic**: E1 Platform',
74
+ '**Status**: Todo | **Size**: 3 | **Sprint**: — | **Link**: —',
75
+ '',
76
+ '## User Story',
77
+ '',
78
+ '> As a user,',
79
+ '> I want to click a login button,',
80
+ '> so that I can access my account.',
81
+ '',
82
+ '## Description',
83
+ '',
84
+ 'Adds a login button to the header.',
85
+ '',
86
+ '## Acceptance Criteria',
87
+ '',
88
+ '### Scenario: Click login button',
89
+ '',
90
+ '**Given** the user is on the homepage',
91
+ '**When** they click "Login"',
92
+ '**Then** they see the login form',
93
+ ].join('\n');
94
+
95
+ const storyPath = createStoryFile(
96
+ tmpDir,
97
+ '.ace/artifacts/product/e1-platform/f1-user-auth/s1-add-login-button/s1-add-login-button.md',
98
+ storyContent
99
+ );
100
+
101
+ const result = JSON.parse(runScript('init', storyPath, tmpDir));
102
+
103
+ assert.ok(result.product_owner_model, 'should have product_owner_model');
104
+ assert.strictEqual(result.story_valid, true, 'story should be valid');
105
+ assert.strictEqual(result.story_source, 'file');
106
+ assert.strictEqual(result.story.id, 'S1');
107
+ assert.strictEqual(result.story.title, 'Add Login Button');
108
+ assert.strictEqual(result.story.status, 'Todo');
109
+ assert.strictEqual(result.acceptance_criteria_count, 1);
110
+ assert.ok(result.paths, 'should have computed paths');
111
+ assert.ok(result.paths.story_file.includes('s1-add-login-button'));
112
+ assert.strictEqual(result.has_story_file, true);
113
+ assert.strictEqual(typeof result.commit_docs, 'boolean');
114
+ assert.strictEqual(typeof result.has_git, 'boolean');
115
+ });
116
+
117
+ it('returns valid result for new story mode (no params)', () => {
118
+ const result = JSON.parse(runScript('init', '', tmpDir));
119
+
120
+ assert.strictEqual(result.story_source, 'new');
121
+ assert.strictEqual(result.story_valid, true);
122
+ assert.strictEqual(result.story_content, null);
123
+ assert.strictEqual(result.story.id, null);
124
+ assert.strictEqual(result.paths, null);
125
+ });
126
+
127
+ it('returns valid result for text mode', () => {
128
+ const result = JSON.parse(runScript('init', 'text="User can reset password"', tmpDir));
129
+
130
+ assert.strictEqual(result.story_source, 'text');
131
+ assert.strictEqual(result.story_valid, true);
132
+ assert.strictEqual(result.story_content, 'User can reset password');
133
+ assert.strictEqual(result.description, 'User can reset password');
134
+ assert.strictEqual(result.story.id, null);
135
+ });
136
+
137
+ it('handles non-existent story file gracefully', () => {
138
+ const result = JSON.parse(runScript('init', 'nonexistent/story.md', tmpDir));
139
+
140
+ assert.strictEqual(result.story_valid, false);
141
+ assert.ok(result.story_error.includes('not found'));
142
+ });
143
+
144
+ it('returns brownfield detection fields', () => {
145
+ const result = JSON.parse(runScript('init', '', tmpDir));
146
+
147
+ assert.strictEqual(typeof result.is_brownfield, 'boolean');
148
+ assert.strictEqual(typeof result.is_greenfield, 'boolean');
149
+ assert.strictEqual(result.is_brownfield, !result.is_greenfield);
150
+ });
151
+ });
152
+
153
+ describe('resolve-model', () => {
154
+ let tmpDir;
155
+
156
+ before(() => { tmpDir = createTestProject(); });
157
+ after(() => { cleanup(tmpDir); });
158
+
159
+ it('returns a model string with --raw', () => {
160
+ const result = runScript('resolve-model', 'ace-product-owner --raw', tmpDir).trim();
161
+ assert.match(result, /^(opus|sonnet|haiku)$/);
162
+ });
163
+
164
+ it('returns JSON without --raw', () => {
165
+ const result = JSON.parse(runScript('resolve-model', 'ace-product-owner', tmpDir));
166
+ assert.ok(result.model);
167
+ assert.strictEqual(result.agent, 'ace-product-owner');
168
+ });
169
+
170
+ it('returns sonnet for unknown agent type', () => {
171
+ const result = runScript('resolve-model', 'unknown-agent --raw', tmpDir).trim();
172
+ assert.strictEqual(result, 'sonnet');
173
+ });
174
+ });
175
+
176
+ describe('update-state', () => {
177
+ let tmpDir;
178
+
179
+ before(() => { tmpDir = createTestProject(); });
180
+ after(() => { cleanup(tmpDir); });
181
+
182
+ it('updates story status in the story file', () => {
183
+ const storyContent = [
184
+ '# S1: Test Story',
185
+ '**Feature**: F1 Test Feature | **Epic**: E1 Test Epic',
186
+ '**Status**: Todo | **Size**: 3 | **Sprint**: — | **Link**: —',
187
+ ].join('\n');
188
+
189
+ const storyPath = createStoryFile(
190
+ tmpDir,
191
+ '.ace/artifacts/product/e1-test-epic/f1-test-feature/s1-test-story/s1-test-story.md',
192
+ storyContent
193
+ );
194
+
195
+ const result = JSON.parse(runScript('update-state', `story=${storyPath} status=Refined`, tmpDir));
196
+
197
+ assert.strictEqual(result.story_updated, true);
198
+ assert.strictEqual(result.new_status, 'Refined');
199
+
200
+ // Verify file was actually updated
201
+ const updated = fs.readFileSync(path.join(tmpDir, storyPath), 'utf-8');
202
+ assert.ok(updated.includes('**Status**: Refined'));
203
+ });
204
+
205
+ it('normalizes InProgress to "In Progress"', () => {
206
+ const storyContent = [
207
+ '# S2: Another Story',
208
+ '**Feature**: F1 Test Feature | **Epic**: E1 Test Epic',
209
+ '**Status**: Refined | **Size**: 2 | **Sprint**: — | **Link**: —',
210
+ ].join('\n');
211
+
212
+ const storyPath = createStoryFile(
213
+ tmpDir,
214
+ '.ace/artifacts/product/e1-test-epic/f1-test-feature/s2-another-story/s2-another-story.md',
215
+ storyContent
216
+ );
217
+
218
+ const result = JSON.parse(runScript('update-state', `story=${storyPath} status=InProgress`, tmpDir));
219
+
220
+ assert.strictEqual(result.new_status, 'In Progress');
221
+
222
+ const updated = fs.readFileSync(path.join(tmpDir, storyPath), 'utf-8');
223
+ assert.ok(updated.includes('**Status**: In Progress'));
224
+ });
225
+ });
226
+
227
+ describe('error handling', () => {
228
+ it('errors on unknown command', () => {
229
+ assert.throws(() => {
230
+ execSync(`node "${SCRIPT}" bogus`, { encoding: 'utf-8', stdio: 'pipe' });
231
+ });
232
+ });
233
+
234
+ it('errors on resolve-model without agent type', () => {
235
+ assert.throws(() => {
236
+ execSync(`node "${SCRIPT}" resolve-model`, { encoding: 'utf-8', stdio: 'pipe' });
237
+ });
238
+ });
239
+ });
240
+ });