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.
- package/.claude-plugin/plugin.json +10 -0
- package/CHANGELOG.md +82 -0
- package/README.md +27 -18
- package/agents/ace-product-owner.md +1 -1
- package/agents/ace-technical-application-architect.md +28 -0
- package/agents/ace-wiki-mapper.md +144 -29
- package/bin/install.js +67 -63
- package/hooks/ace-check-update.js +17 -9
- package/package.json +7 -5
- package/shared/lib/ace-core.js +308 -0
- package/shared/lib/ace-core.test.js +308 -0
- package/shared/lib/ace-github.js +753 -0
- package/shared/lib/ace-story.js +400 -0
- package/shared/lib/ace-story.test.js +250 -0
- package/{agile-context-engineering → shared}/utils/ui-formatting.md +299 -299
- package/skills/execute-story/SKILL.md +110 -0
- package/skills/execute-story/script.js +305 -0
- package/skills/execute-story/script.test.js +261 -0
- package/skills/execute-story/walkthrough-template.xml +255 -0
- package/{agile-context-engineering/workflows/execute-story.xml → skills/execute-story/workflow.xml} +83 -9
- package/skills/help/SKILL.md +69 -0
- package/skills/help/script.js +318 -0
- package/skills/help/script.test.js +183 -0
- package/{agile-context-engineering/workflows/help.xml → skills/help/workflow.xml} +8 -8
- package/skills/init-coding-standards/SKILL.md +72 -0
- package/{agile-context-engineering/templates/wiki/coding-standards.xml → skills/init-coding-standards/coding-standards-template.xml} +38 -0
- package/skills/init-coding-standards/script.js +59 -0
- package/skills/init-coding-standards/script.test.js +70 -0
- package/{agile-context-engineering/workflows/init-coding-standards.xml → skills/init-coding-standards/workflow.xml} +4 -9
- package/skills/map-cross-cutting/SKILL.md +89 -0
- package/skills/map-cross-cutting/workflow.xml +330 -0
- package/skills/map-guide/SKILL.md +89 -0
- package/skills/map-guide/workflow.xml +320 -0
- package/skills/map-pattern/SKILL.md +89 -0
- package/skills/map-pattern/workflow.xml +331 -0
- package/skills/map-story/SKILL.md +127 -0
- package/skills/map-story/templates/guide.xml +137 -0
- package/skills/map-story/templates/pattern.xml +159 -0
- package/skills/map-story/templates/system-cross-cutting.xml +197 -0
- package/skills/map-story/templates/walkthrough.xml +255 -0
- package/{agile-context-engineering/workflows/map-story.xml → skills/map-story/workflow.xml} +258 -9
- package/skills/map-subsystem/SKILL.md +111 -0
- package/skills/map-subsystem/script.js +60 -0
- package/skills/map-subsystem/script.test.js +68 -0
- package/skills/map-subsystem/templates/decizions.xml +115 -0
- package/skills/map-subsystem/templates/guide.xml +137 -0
- package/{agile-context-engineering/templates/wiki → skills/map-subsystem/templates}/module-discovery.xml +3 -3
- package/skills/map-subsystem/templates/pattern.xml +159 -0
- package/skills/map-subsystem/templates/system-cross-cutting.xml +197 -0
- package/skills/map-subsystem/templates/system.xml +381 -0
- package/skills/map-subsystem/templates/walkthrough.xml +255 -0
- package/{agile-context-engineering/workflows/map-subsystem.xml → skills/map-subsystem/workflow.xml} +17 -21
- package/skills/map-sys-doc/SKILL.md +90 -0
- package/skills/map-sys-doc/system.xml +381 -0
- package/skills/map-sys-doc/workflow.xml +336 -0
- package/skills/map-system/SKILL.md +85 -0
- package/skills/map-system/script.js +84 -0
- package/skills/map-system/script.test.js +73 -0
- package/skills/map-system/templates/wiki-readme.xml +297 -0
- package/{agile-context-engineering/workflows/map-system.xml → skills/map-system/workflow.xml} +11 -16
- package/skills/map-walkthrough/SKILL.md +92 -0
- package/skills/map-walkthrough/walkthrough.xml +255 -0
- package/skills/map-walkthrough/workflow.xml +457 -0
- package/skills/plan-backlog/SKILL.md +75 -0
- package/skills/plan-backlog/script.js +136 -0
- package/skills/plan-backlog/script.test.js +83 -0
- package/{agile-context-engineering/workflows/plan-backlog.xml → skills/plan-backlog/workflow.xml} +13 -21
- package/skills/plan-feature/SKILL.md +76 -0
- package/skills/plan-feature/script.js +148 -0
- package/skills/plan-feature/script.test.js +80 -0
- package/{agile-context-engineering/workflows/plan-feature.xml → skills/plan-feature/workflow.xml} +21 -29
- package/skills/plan-product-vision/SKILL.md +75 -0
- package/skills/plan-product-vision/script.js +60 -0
- package/skills/plan-product-vision/script.test.js +69 -0
- package/{agile-context-engineering/workflows/plan-product-vision.xml → skills/plan-product-vision/workflow.xml} +4 -9
- package/skills/plan-story/SKILL.md +116 -0
- package/skills/plan-story/script.js +326 -0
- package/skills/plan-story/script.test.js +240 -0
- package/skills/plan-story/story-template.xml +451 -0
- package/{agile-context-engineering/workflows/plan-story.xml → skills/plan-story/workflow.xml} +1285 -909
- package/skills/research-external-solution/SKILL.md +107 -0
- package/skills/research-external-solution/script.js +238 -0
- package/skills/research-external-solution/script.test.js +134 -0
- package/{agile-context-engineering/workflows/research-external-solution.xml → skills/research-external-solution/workflow.xml} +4 -6
- package/skills/research-integration-solution/SKILL.md +98 -0
- package/{agile-context-engineering/templates/product/story-integration-solution.xml → skills/research-integration-solution/integration-solution-template.xml} +1 -0
- package/skills/research-integration-solution/script.js +231 -0
- package/skills/research-integration-solution/script.test.js +134 -0
- package/{agile-context-engineering/workflows/research-integration-solution.xml → skills/research-integration-solution/workflow.xml} +4 -5
- package/skills/research-story-wiki/SKILL.md +92 -0
- package/skills/research-story-wiki/script.js +231 -0
- package/skills/research-story-wiki/script.test.js +138 -0
- package/{agile-context-engineering/templates/product/story-wiki.xml → skills/research-story-wiki/story-wiki-template.xml} +4 -0
- package/{agile-context-engineering/workflows/research-story-wiki.xml → skills/research-story-wiki/workflow.xml} +5 -6
- package/skills/research-technical-solution/SKILL.md +103 -0
- package/skills/research-technical-solution/script.js +231 -0
- package/skills/research-technical-solution/script.test.js +134 -0
- package/{agile-context-engineering/workflows/research-technical-solution.xml → skills/research-technical-solution/workflow.xml} +4 -5
- package/skills/review-story/SKILL.md +100 -0
- package/skills/review-story/script.js +257 -0
- package/skills/review-story/script.test.js +169 -0
- package/skills/review-story/story-template.xml +451 -0
- package/{agile-context-engineering/workflows/review-story.xml → skills/review-story/workflow.xml} +1 -3
- package/skills/update/SKILL.md +53 -0
- package/{agile-context-engineering/workflows/update.xml → skills/update/workflow.xml} +237 -207
- package/agile-context-engineering/src/ace-tools.js +0 -2881
- package/agile-context-engineering/src/ace-tools.test.js +0 -1089
- package/agile-context-engineering/templates/_command.md +0 -54
- package/agile-context-engineering/templates/_workflow.xml +0 -17
- package/agile-context-engineering/templates/config.json +0 -0
- package/agile-context-engineering/templates/product/integration-solution.xml +0 -0
- package/agile-context-engineering/templates/wiki/wiki-readme.xml +0 -276
- package/commands/ace/execute-story.md +0 -137
- package/commands/ace/help.md +0 -93
- package/commands/ace/init-coding-standards.md +0 -83
- package/commands/ace/map-story.md +0 -156
- package/commands/ace/map-subsystem.md +0 -138
- package/commands/ace/map-system.md +0 -92
- package/commands/ace/plan-backlog.md +0 -83
- package/commands/ace/plan-feature.md +0 -89
- package/commands/ace/plan-product-vision.md +0 -81
- package/commands/ace/plan-story.md +0 -145
- package/commands/ace/research-external-solution.md +0 -138
- package/commands/ace/research-integration-solution.md +0 -135
- package/commands/ace/research-story-wiki.md +0 -116
- package/commands/ace/research-technical-solution.md +0 -147
- package/commands/ace/review-story.md +0 -109
- package/commands/ace/update.md +0 -54
- /package/{agile-context-engineering → shared}/utils/questioning.xml +0 -0
- /package/{agile-context-engineering/templates/product/story.xml → skills/execute-story/story-template.xml} +0 -0
- /package/{agile-context-engineering/templates/wiki → skills/map-cross-cutting}/system-cross-cutting.xml +0 -0
- /package/{agile-context-engineering/templates/wiki → skills/map-guide}/guide.xml +0 -0
- /package/{agile-context-engineering/templates/wiki → skills/map-pattern}/pattern.xml +0 -0
- /package/{agile-context-engineering/templates/wiki → skills/map-story/templates}/decizions.xml +0 -0
- /package/{agile-context-engineering/templates/wiki → skills/map-story/templates}/system.xml +0 -0
- /package/{agile-context-engineering/templates/wiki → skills/map-story/templates}/tech-debt-index.xml +0 -0
- /package/{agile-context-engineering/templates/wiki → skills/map-subsystem/templates}/subsystem-architecture.xml +0 -0
- /package/{agile-context-engineering/templates/wiki → skills/map-subsystem/templates}/subsystem-structure.xml +0 -0
- /package/{agile-context-engineering/templates/wiki → skills/map-system/templates}/system-architecture.xml +0 -0
- /package/{agile-context-engineering/templates/wiki → skills/map-system/templates}/system-structure.xml +0 -0
- /package/{agile-context-engineering/templates/wiki → skills/map-system/templates}/testing-framework.xml +0 -0
- /package/{agile-context-engineering/templates/product/product-backlog.xml → skills/plan-backlog/product-backlog-template.xml} +0 -0
- /package/{agile-context-engineering/templates/product/feature.xml → skills/plan-feature/feature-template.xml} +0 -0
- /package/{agile-context-engineering/templates/product/product-vision.xml → skills/plan-product-vision/product-vision-template.xml} +0 -0
- /package/{agile-context-engineering/templates/product/external-solution.xml → skills/research-external-solution/external-solution-template.xml} +0 -0
- /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
|
+
});
|