@we-scrum/cli 1.0.4 → 1.0.6
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/dist/cli.js +173 -47
- package/package.json +13 -13
- package/src/cli.ts +116 -51
- package/src/hbs.d.ts +4 -0
- package/src/helpers/auth-helper.ts +1 -1
- package/src/helpers/index.ts +2 -0
- package/src/helpers/project-helper.ts +96 -0
- package/src/helpers/settings-helper.ts +0 -1
- package/src/helpers/template.helper.ts +25 -0
- package/src/helpers/we-scrum.helper.ts +119 -102
- package/templates/develop-story-skill.hbs +46 -0
- package/templates/get-next-task.hbs +21 -0
- package/templates/review-analysis-skill.hbs +61 -0
- package/tsconfig.json +2 -1
- package/src/mcp-server/index.ts +0 -1
- package/src/mcp-server/mcp-server.ts +0 -82
- package/src/mcp-server/tools/complete-task.ts +0 -6
- package/src/mcp-server/tools/get-next-task.ts +0 -150
- package/src/mcp-server/tools/get-unit-test-guidelines.ts +0 -15
- package/src/mcp-server/tools/index.ts +0 -4
- package/src/mcp-server/tools/start-task.ts +0 -6
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { _sortBy, Command, serialize, TypeHelper } from '@my-devkit/core';
|
|
1
|
+
import { _sortBy, assert, Command, serialize, TypeHelper } from '@my-devkit/core';
|
|
2
2
|
import {
|
|
3
3
|
MarkEnumerationChangeAsDoneCommand,
|
|
4
4
|
MarkObjectChangeAsDoneCommand,
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
TakeRouteChangeCommand,
|
|
12
12
|
TakeTaskCommand,
|
|
13
13
|
} from '@we-scrum/commands';
|
|
14
|
-
import { ProgressStatus } from '@we-scrum/enums';
|
|
14
|
+
import { ContentType, ProgressStatus } from '@we-scrum/enums';
|
|
15
15
|
import {
|
|
16
16
|
DevelopmentPolicyModel,
|
|
17
17
|
ProjectStoryModel,
|
|
@@ -22,27 +22,53 @@ import {
|
|
|
22
22
|
ProjectStorySectionModelRouteChange,
|
|
23
23
|
ProjectStorySectionModelTask,
|
|
24
24
|
} from '@we-scrum/models';
|
|
25
|
-
import { DevelopmentPolicyHelper } from '@we-scrum/utils';
|
|
26
|
-
import {
|
|
25
|
+
import { AnalysisDsl, DevelopmentPolicyHelper, HtmlToTextHelper, StorySectionHelper, stringifyAnalysisDslChange } from '@we-scrum/utils';
|
|
26
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
27
|
+
import { join } from 'node:path';
|
|
27
28
|
import { AuthHelper } from './auth-helper';
|
|
28
29
|
import { FirebaseHelper } from './firebase-helper';
|
|
29
|
-
import {
|
|
30
|
+
import { ProjectHelper } from './project-helper';
|
|
31
|
+
import { renderTemplate } from './template.helper';
|
|
30
32
|
|
|
31
33
|
export class WeScrumHelper {
|
|
32
|
-
private static backendUrl = 'https://europe-west1-we-scrum-prod.cloudfunctions.net';
|
|
34
|
+
private static readonly backendUrl = 'https://europe-west1-we-scrum-prod.cloudfunctions.net';
|
|
33
35
|
constructor(private userId: string) {}
|
|
34
36
|
|
|
35
|
-
|
|
36
|
-
return
|
|
37
|
+
private static get projectRoot(): string {
|
|
38
|
+
return ProjectHelper.requireRoot();
|
|
37
39
|
}
|
|
38
40
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
private static get projectConfig() {
|
|
42
|
+
return ProjectHelper.read(this.projectRoot);
|
|
41
43
|
}
|
|
42
44
|
|
|
43
|
-
|
|
44
|
-
this.
|
|
45
|
+
private static get projectId(): string {
|
|
46
|
+
return this.projectConfig.projectId;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public static async getNextTask(identificationNumber: string): Promise<string> {
|
|
50
|
+
const nextChange = await this.getNextChange(identificationNumber);
|
|
51
|
+
|
|
52
|
+
if (!nextChange) {
|
|
53
|
+
return `## Story ${identificationNumber}\n\nAll tasks are complete.`;
|
|
54
|
+
}
|
|
45
55
|
|
|
56
|
+
const analysisChange = StorySectionHelper.mapSingleChange(nextChange);
|
|
57
|
+
const isTodo = nextChange.progress.status === ProgressStatus.ToDo;
|
|
58
|
+
const developmentPolicy = await this.getChangeDevelopmentPolicy(nextChange);
|
|
59
|
+
const { guidelinesPath, unitTestsPath } = this.getDevelopmentPolicyPaths(developmentPolicy);
|
|
60
|
+
|
|
61
|
+
return renderTemplate('get-next-task', {
|
|
62
|
+
taskDetails: stringifyAnalysisDslChange(analysisChange),
|
|
63
|
+
guidelinesPath,
|
|
64
|
+
unitTestsPath: developmentPolicy?.areUnitTestsMandatory ? unitTestsPath : null,
|
|
65
|
+
isTodo,
|
|
66
|
+
identificationNumber,
|
|
67
|
+
taskId: analysisChange.id,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
public static async getNextChange(identificationNumber: string) {
|
|
46
72
|
const story = await this.findStoryByIterationNumber(identificationNumber);
|
|
47
73
|
|
|
48
74
|
assert(!!story, `Story "${identificationNumber}" not found in the active project.`);
|
|
@@ -56,11 +82,31 @@ export class WeScrumHelper {
|
|
|
56
82
|
return task;
|
|
57
83
|
}
|
|
58
84
|
|
|
59
|
-
public static async
|
|
60
|
-
const
|
|
85
|
+
public static async getChangeById(identificationNumber: string, changeId: string) {
|
|
86
|
+
const story = await this.findStoryByIterationNumber(identificationNumber);
|
|
87
|
+
assert(!!story, `Story "${identificationNumber}" not found in the active project.`);
|
|
88
|
+
|
|
89
|
+
const sections = await this.getStorySections(story.storyId);
|
|
61
90
|
|
|
62
|
-
|
|
63
|
-
|
|
91
|
+
const change = _sortBy(sections, (s) => s.name)
|
|
92
|
+
.flatMap((s) => [...s.enumerationChanges, ...s.objectChanges, ...s.routeChanges, ...s.relationChanges, ...s.tasks])
|
|
93
|
+
.find((t) => {
|
|
94
|
+
if (t instanceof ProjectStorySectionModelEnumerationChange) return t.enumerationChangeId === changeId;
|
|
95
|
+
if (t instanceof ProjectStorySectionModelObjectChange) return t.objectChangeId === changeId;
|
|
96
|
+
if (t instanceof ProjectStorySectionModelRouteChange) return t.routeChangeId === changeId;
|
|
97
|
+
if (t instanceof ProjectStorySectionModelRelationChange) return t.relationChangeId === changeId;
|
|
98
|
+
if (t instanceof ProjectStorySectionModelTask) return t.taskId === changeId;
|
|
99
|
+
return false;
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
assert(!!change, `Change "${changeId}" not found in story "${identificationNumber}".`);
|
|
103
|
+
return change;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
public static async startTask(identificationNumber: string, taskId: string): Promise<void> {
|
|
107
|
+
const change = await this.getChangeById(identificationNumber, taskId);
|
|
108
|
+
|
|
109
|
+
assert(change.progress.status === ProgressStatus.ToDo, `Task is not in ToDo status (current status: ${change.progress.status}).`);
|
|
64
110
|
|
|
65
111
|
if (change instanceof ProjectStorySectionModelEnumerationChange) {
|
|
66
112
|
await this.post(
|
|
@@ -87,10 +133,9 @@ export class WeScrumHelper {
|
|
|
87
133
|
}
|
|
88
134
|
}
|
|
89
135
|
|
|
90
|
-
public static async completeTask(identificationNumber: string): Promise<void> {
|
|
91
|
-
const change = await this.
|
|
136
|
+
public static async completeTask(identificationNumber: string, taskId: string): Promise<void> {
|
|
137
|
+
const change = await this.getChangeById(identificationNumber, taskId);
|
|
92
138
|
|
|
93
|
-
assert(!!change, `All tasks for story "${identificationNumber}" are complete.`);
|
|
94
139
|
assert(change.progress.status === ProgressStatus.Doing, `Task is not in progress (current status: ${change.progress.status}).`);
|
|
95
140
|
|
|
96
141
|
if (change instanceof ProjectStorySectionModelEnumerationChange) {
|
|
@@ -124,6 +169,39 @@ export class WeScrumHelper {
|
|
|
124
169
|
return DevelopmentPolicyHelper.getMatchingDevelopmentPolicy(change, developmentPolicies);
|
|
125
170
|
}
|
|
126
171
|
|
|
172
|
+
public static getDevelopmentPolicyPaths(policy: DevelopmentPolicyModel | null): {
|
|
173
|
+
guidelinesPath: string | null;
|
|
174
|
+
unitTestsPath: string | null;
|
|
175
|
+
} {
|
|
176
|
+
const { developmentPoliciesPath } = this.projectConfig;
|
|
177
|
+
const policiesDir = join(this.projectRoot, developmentPoliciesPath);
|
|
178
|
+
return {
|
|
179
|
+
guidelinesPath: policy?.slug ? join(policiesDir, `develop-${policy.slug}.md`) : null,
|
|
180
|
+
unitTestsPath: policy?.slug ? join(policiesDir, `test-${policy.slug}.md`) : null,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
public static async getStoryDescription(identificationNumber: string): Promise<string> {
|
|
185
|
+
const story = await this.findStoryByIterationNumber(identificationNumber);
|
|
186
|
+
assert(!!story, `Story "${identificationNumber}" not found in the active project.`);
|
|
187
|
+
|
|
188
|
+
const description = story.description ?? '';
|
|
189
|
+
if (story.descriptionContentType === ContentType.Html) {
|
|
190
|
+
return HtmlToTextHelper.convert(description);
|
|
191
|
+
}
|
|
192
|
+
return description;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
public static async getStoryAnalysis(identificationNumber: string): Promise<string> {
|
|
196
|
+
const story = await this.findStoryByIterationNumber(identificationNumber);
|
|
197
|
+
assert(!!story, `Story "${identificationNumber}" not found in the active project.`);
|
|
198
|
+
|
|
199
|
+
const sections = await this.getStorySections(story.storyId);
|
|
200
|
+
const ast = StorySectionHelper.mapSectionsToAnalysisAst(sections);
|
|
201
|
+
|
|
202
|
+
return AnalysisDsl.stringify(ast);
|
|
203
|
+
}
|
|
204
|
+
|
|
127
205
|
private static async findStoryByIterationNumber(identificationNumber: string) {
|
|
128
206
|
return FirebaseHelper.findDocument<ProjectStoryModel>(`/projects/${this.projectId}/stories`, [
|
|
129
207
|
['identificationNumber', '==', identificationNumber],
|
|
@@ -134,92 +212,31 @@ export class WeScrumHelper {
|
|
|
134
212
|
return FirebaseHelper.getCollection<ProjectStorySectionModel>(`projects/${this.projectId}/stories/${storyId}/sections`);
|
|
135
213
|
}
|
|
136
214
|
|
|
137
|
-
|
|
215
|
+
public static async getDevelopmentPolicies() {
|
|
138
216
|
return FirebaseHelper.getCollection<DevelopmentPolicyModel>(`/projects/${this.projectId}/development-policies`);
|
|
139
217
|
}
|
|
140
218
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
// return enumerationId;
|
|
164
|
-
// }
|
|
165
|
-
|
|
166
|
-
// public async createObject(object: Object, getEnumerationId: (name: string) => string): Promise<string> {
|
|
167
|
-
// Logger.info(`Creating object ${object.name}`);
|
|
168
|
-
|
|
169
|
-
// const createCommand = TypeHelper.transform(CreateObjectCommand, {
|
|
170
|
-
// projectId: this.projectId,
|
|
171
|
-
// name: object.name,
|
|
172
|
-
// });
|
|
173
|
-
// const objectId = await this.post<CreateObjectCommand, string>('object-management/create-object', createCommand);
|
|
174
|
-
|
|
175
|
-
// const propertyMap = new Map<string, string>();
|
|
176
|
-
// for (const property of object.properties) {
|
|
177
|
-
// const objectPropertyId = guid();
|
|
178
|
-
// const propertyCommand = TypeHelper.transform(CreateObjectPropertyCommand, {
|
|
179
|
-
// objectId,
|
|
180
|
-
// objectPropertyId,
|
|
181
|
-
// objectPropertyParentId: propertyMap.get(property.parent) || null,
|
|
182
|
-
// name: property.name,
|
|
183
|
-
// objectPropertyType: property.type,
|
|
184
|
-
// isArray: property.isArray,
|
|
185
|
-
// enumerationId: property.enumerationName ? getEnumerationId(property.enumerationName) : null,
|
|
186
|
-
// });
|
|
187
|
-
// await this.post<CreateObjectPropertyCommand, string>('object-management/create-object-property', propertyCommand);
|
|
188
|
-
|
|
189
|
-
// propertyMap.set(property.path, objectPropertyId);
|
|
190
|
-
// }
|
|
191
|
-
|
|
192
|
-
// return objectId;
|
|
193
|
-
// }
|
|
194
|
-
|
|
195
|
-
// public async createHandler(handler: Handler, getObjectId: (name: string) => string): Promise<void> {
|
|
196
|
-
// Logger.info(`Creating handler ${handler.name}`);
|
|
197
|
-
|
|
198
|
-
// for (const relation of handler.relations) {
|
|
199
|
-
// const createCommand = TypeHelper.transform(CreateRelationCommand, {
|
|
200
|
-
// projectId: this.projectId,
|
|
201
|
-
// boundedContext: handler.boundedContext,
|
|
202
|
-
// handlerName: handler.name,
|
|
203
|
-
// objectIds: relation.map((o) => getObjectId(o)),
|
|
204
|
-
// subscriptionName: null,
|
|
205
|
-
// });
|
|
206
|
-
// await this.post<CreateRelationCommand, string>('relation-management/create-relation', createCommand);
|
|
207
|
-
// }
|
|
208
|
-
// }
|
|
209
|
-
|
|
210
|
-
// public async createRoute(route: Route, getObjectId: (name: string) => string): Promise<string> {
|
|
211
|
-
// Logger.info(`Creating route ${route.method} ${route.path}`);
|
|
212
|
-
|
|
213
|
-
// const createCommand = TypeHelper.transform(CreateRouteCommand, {
|
|
214
|
-
// projectId: this.projectId,
|
|
215
|
-
// method: route.method,
|
|
216
|
-
// path: route.path,
|
|
217
|
-
// objectId: getObjectId(route.object),
|
|
218
|
-
// permission: null,
|
|
219
|
-
// });
|
|
220
|
-
|
|
221
|
-
// return this.post<CreateRouteCommand, string>('route-management/create-route', createCommand);
|
|
222
|
-
// }
|
|
219
|
+
public static async synchronizeDevelopmentPolicies(): Promise<number> {
|
|
220
|
+
const { developmentPoliciesPath } = this.projectConfig;
|
|
221
|
+
const policiesDir = join(this.projectRoot, developmentPoliciesPath);
|
|
222
|
+
|
|
223
|
+
rmSync(policiesDir, { recursive: true, force: true });
|
|
224
|
+
mkdirSync(policiesDir, { recursive: true });
|
|
225
|
+
|
|
226
|
+
const policies = await this.getDevelopmentPolicies();
|
|
227
|
+
|
|
228
|
+
for (const policy of policies) {
|
|
229
|
+
const { guidelinesPath, unitTestsPath } = this.getDevelopmentPolicyPaths(policy);
|
|
230
|
+
if (policy.codeGuidelines && guidelinesPath) {
|
|
231
|
+
writeFileSync(guidelinesPath, policy.codeGuidelines);
|
|
232
|
+
}
|
|
233
|
+
if (policy.unitTestsGuidelines && unitTestsPath) {
|
|
234
|
+
writeFileSync(unitTestsPath, policy.unitTestsGuidelines);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return policies.length;
|
|
239
|
+
}
|
|
223
240
|
|
|
224
241
|
private static async post<C extends Command, R>(route: string, command: C): Promise<R> {
|
|
225
242
|
const userIdToken = await AuthHelper.getUserIdToken();
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: develop-story
|
|
3
|
+
description: >
|
|
4
|
+
Develop a we-scrum user story by iterating through its tasks one by one.
|
|
5
|
+
Use this skill whenever the user says "develop story XXXXX", "/develop-story XXXXX",
|
|
6
|
+
or asks to implement or work on a specific story number. The skill synchronizes
|
|
7
|
+
development policies, then fetches and implements each task in sequence using
|
|
8
|
+
the we-scrum CLI.
|
|
9
|
+
compatibility: "Requires: we-scrum CLI (we-scrum)"
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Develop Story Skill
|
|
13
|
+
|
|
14
|
+
Iterates through a we-scrum story's tasks, implementing each one in sequence until
|
|
15
|
+
all tasks are complete.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Step 0 — Resolve the story identification number
|
|
20
|
+
|
|
21
|
+
If the user provided a story identification number (e.g. `/develop-story 00240`), use it.
|
|
22
|
+
Otherwise, ask: _"Which story would you like to develop? Please provide the identification number."_
|
|
23
|
+
Do not proceed until a number is provided.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Step 1 — Synchronize development policies
|
|
28
|
+
|
|
29
|
+
Pull the latest development guidelines into the project before starting any work:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
we-scrum synchronize-development-policies
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Step 2 — Fetch and implement tasks
|
|
38
|
+
|
|
39
|
+
Run the following command to get the next pending task and follow the instructions it returns.
|
|
40
|
+
Replace `<identificationNumber>` with the story identification number.
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
we-scrum get-next-task --identificationNumber <identificationNumber>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Repeat until all tasks are complete.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{{{taskDetails}}}
|
|
2
|
+
|
|
3
|
+
## Development guidelines
|
|
4
|
+
{{#if guidelinesPath}}Refer to `{{{guidelinesPath}}}` for coding conventions and project-specific instructions.{{else}}_No development guidelines file configured for this task._{{/if}}
|
|
5
|
+
|
|
6
|
+
## Unit tests
|
|
7
|
+
{{#if unitTestsPath}}Unit tests are required. Refer to `{{{unitTestsPath}}}` for test writing guidelines.{{else}}No unit tests required.{{/if}}
|
|
8
|
+
|
|
9
|
+
## Next steps
|
|
10
|
+
{{#if isTodo}}
|
|
11
|
+
1. Run `we-scrum start-task --identificationNumber "{{identificationNumber}}" --taskId "{{taskId}}"` before making any changes.
|
|
12
|
+
2. Implement the task described above.
|
|
13
|
+
3. Commit your changes.
|
|
14
|
+
4. Run `we-scrum complete-task --identificationNumber "{{identificationNumber}}" --taskId "{{taskId}}"` when done.
|
|
15
|
+
5. Call `get_next_task` to continue with the next task.
|
|
16
|
+
{{else}}
|
|
17
|
+
1. Implement the task described above.
|
|
18
|
+
2. Commit your changes.
|
|
19
|
+
3. Run `we-scrum complete-task --identificationNumber "{{identificationNumber}}" --taskId "{{taskId}}"` when done.
|
|
20
|
+
4. Call `get_next_task` to continue with the next task.
|
|
21
|
+
{{/if}}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: review-analysis
|
|
3
|
+
description: >
|
|
4
|
+
Review the analysis of a we-scrum user story. Use this skill whenever the user says
|
|
5
|
+
"review analysis XXXXX", "/review-analysis XXXXX", or asks to review or check the
|
|
6
|
+
analysis of a specific story number. The skill retrieves the story description and
|
|
7
|
+
analysis DSL, then reviews them and returns comments.
|
|
8
|
+
compatibility: "Requires: we-scrum CLI (we-scrum)"
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Review Analysis Skill
|
|
12
|
+
|
|
13
|
+
Reviews the analysis of a we-scrum user story and returns comments.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Step 0 — Resolve the story identification number
|
|
18
|
+
|
|
19
|
+
If the user provided a story identification number (e.g. `/review-analysis 00240`), use it.
|
|
20
|
+
Otherwise, ask: _"Which story would you like to review? Please provide the identification number."_
|
|
21
|
+
Do not proceed until a number is provided.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Step 1 — Retrieve story data
|
|
26
|
+
|
|
27
|
+
Run the following commands to fetch the story description and analysis DSL.
|
|
28
|
+
Replace `<identificationNumber>` with the story identification number.
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
we-scrum get-story-description --identificationNumber <identificationNumber>
|
|
32
|
+
we-scrum get-story-analysis --identificationNumber <identificationNumber>
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Step 2 — Review and return comments
|
|
38
|
+
|
|
39
|
+
Review the story description and analysis DSL together. Check for:
|
|
40
|
+
|
|
41
|
+
**Completeness & correctness**
|
|
42
|
+
- Consistency between the description and the analysis changes
|
|
43
|
+
- Missing or incomplete changes relative to the described requirements
|
|
44
|
+
- Incorrect operations (create/update/delete used inappropriately)
|
|
45
|
+
- Unclear or ambiguous task descriptions
|
|
46
|
+
|
|
47
|
+
**Naming clarity**
|
|
48
|
+
- Object names and property names must be clear, precise, and self-explanatory
|
|
49
|
+
- Challenge any name that is vague, abbreviated, or could be misread — propose a better alternative
|
|
50
|
+
- Names should reflect the domain concept they represent, not the implementation detail
|
|
51
|
+
|
|
52
|
+
**Naming continuity**
|
|
53
|
+
- A name introduced in the analysis (object, property, route, enumeration) must flow consistently through the entire vertical slice: command → aggregate → event → denormalizer → model → DTO → API response
|
|
54
|
+
- Flag any analysis change where the proposed name would likely create a discontinuity or force a translation at any layer
|
|
55
|
+
- Renames or aliases between layers are a sign of a poorly chosen name — the right name should need no translation
|
|
56
|
+
|
|
57
|
+
**Migrations**
|
|
58
|
+
- Any change that adds, removes, or renames a field on an Aggregate or a Model must be accompanied by a migration script task in the analysis
|
|
59
|
+
- If such a structural change is present but no corresponding migration task exists, raise it as a blocking issue — shipping without a migration will corrupt existing data or break reads
|
|
60
|
+
|
|
61
|
+
Return your comments to the user.
|
package/tsconfig.json
CHANGED
package/src/mcp-server/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from './mcp-server';
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
-
import { z } from 'zod';
|
|
4
|
-
|
|
5
|
-
import { AuthHelper } from '@helpers';
|
|
6
|
-
import { completeTask, getNextTask, getUnitTestGuidelines, startTask } from './tools';
|
|
7
|
-
|
|
8
|
-
const identificationNumberSchema = z.object({
|
|
9
|
-
identificationNumber: z.string().describe('The story identification number, e.g. "144154"'),
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
function createToolCallback<T extends Record<string, string>>(fn: (identificationNumber: string) => Promise<T>) {
|
|
13
|
-
return async ({ identificationNumber }: { identificationNumber: string }) => {
|
|
14
|
-
try {
|
|
15
|
-
const result = await fn(identificationNumber);
|
|
16
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] };
|
|
17
|
-
} catch (error) {
|
|
18
|
-
return {
|
|
19
|
-
content: [{ type: 'text' as const, text: JSON.stringify({ error: error.message }) }],
|
|
20
|
-
isError: true,
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export async function startMcpServer(): Promise<void> {
|
|
27
|
-
const initialized = await AuthHelper.initializeFromStoredCredentials();
|
|
28
|
-
if (!initialized) {
|
|
29
|
-
process.stderr.write('Error: Not authenticated. Please run "we-scrum login" first.\n');
|
|
30
|
-
process.exit(1);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const server = new McpServer({ name: 'we-scrum-mcp-server', version: '1.0.0' });
|
|
34
|
-
|
|
35
|
-
server.registerTool(
|
|
36
|
-
'get_next_task',
|
|
37
|
-
{
|
|
38
|
-
description: `Get the next pending task (status: ToDo) for a story.
|
|
39
|
-
Returns the task description, the task type template (development guidelines),
|
|
40
|
-
and the full section analysis specifying what must be implemented
|
|
41
|
-
(route changes, object/DTO changes, enumeration changes, relation/handler changes).
|
|
42
|
-
Also returns storyId, sectionId and taskId needed to call start_task and complete_task.`,
|
|
43
|
-
inputSchema: identificationNumberSchema,
|
|
44
|
-
},
|
|
45
|
-
createToolCallback(getNextTask),
|
|
46
|
-
);
|
|
47
|
-
|
|
48
|
-
server.registerTool(
|
|
49
|
-
'start_task',
|
|
50
|
-
{
|
|
51
|
-
description: `Mark the next pending task of a story as in progress.
|
|
52
|
-
Call this before making any changes to the codebase, as instructed by get_next_task.`,
|
|
53
|
-
inputSchema: identificationNumberSchema,
|
|
54
|
-
},
|
|
55
|
-
createToolCallback(startTask),
|
|
56
|
-
);
|
|
57
|
-
|
|
58
|
-
server.registerTool(
|
|
59
|
-
'get_unit_test_guidelines',
|
|
60
|
-
{
|
|
61
|
-
description: `Get unit test writing guidelines for the current pending task of a story.
|
|
62
|
-
Call this tool when get_next_task returns unitTests instructions to run it.`,
|
|
63
|
-
inputSchema: identificationNumberSchema,
|
|
64
|
-
},
|
|
65
|
-
createToolCallback(getUnitTestGuidelines),
|
|
66
|
-
);
|
|
67
|
-
|
|
68
|
-
server.registerTool(
|
|
69
|
-
'complete_task',
|
|
70
|
-
{
|
|
71
|
-
description: `Mark the current in-progress task of a story as done.
|
|
72
|
-
Call this after committing all changes, as instructed by get_next_task.`,
|
|
73
|
-
inputSchema: identificationNumberSchema,
|
|
74
|
-
},
|
|
75
|
-
createToolCallback(completeTask),
|
|
76
|
-
);
|
|
77
|
-
|
|
78
|
-
const transport = new StdioServerTransport();
|
|
79
|
-
await server.connect(transport);
|
|
80
|
-
|
|
81
|
-
process.stderr.write('we-scrum MCP server running.\n');
|
|
82
|
-
}
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
import { WeScrumHelper } from '@helpers';
|
|
2
|
-
import { _compact } from '@my-devkit/core';
|
|
3
|
-
import { Operation, ProgressStatus } from '@we-scrum/enums';
|
|
4
|
-
import {
|
|
5
|
-
ProjectStorySectionModelEnumerationChange,
|
|
6
|
-
ProjectStorySectionModelObjectChange,
|
|
7
|
-
ProjectStorySectionModelRelationChange,
|
|
8
|
-
ProjectStorySectionModelRouteChange,
|
|
9
|
-
ProjectStorySectionModelTask,
|
|
10
|
-
} from '@we-scrum/models';
|
|
11
|
-
|
|
12
|
-
export async function getNextTask(identificationNumber: string): Promise<Record<string, string>> {
|
|
13
|
-
const nextTask = await WeScrumHelper.getNextChange(identificationNumber);
|
|
14
|
-
|
|
15
|
-
if (!nextTask) {
|
|
16
|
-
return { message: `All tasks for story "${identificationNumber}" are complete.` };
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const isTodo = nextTask.progress.status === ProgressStatus.ToDo;
|
|
20
|
-
const generalInstructions = _compact([
|
|
21
|
-
'Implement the task described in the fields below.',
|
|
22
|
-
'Refer to `implementationGuidelines` for coding conventions and project-specific instructions.',
|
|
23
|
-
isTodo ? 'Before making any changes, run `start_task` to mark the task as in progress.' : null,
|
|
24
|
-
'If anything is unclear, stop and ask before making any changes.',
|
|
25
|
-
'Once all changes are done, commit your code and run `complete_task`.',
|
|
26
|
-
'After completing a task, immediately run `get_next_task` again and continue until all tasks are complete.',
|
|
27
|
-
]).join('\n');
|
|
28
|
-
|
|
29
|
-
const developmentPolicy = await WeScrumHelper.getChangeDevelopmentPolicy(nextTask);
|
|
30
|
-
const implementationGuidelines = developmentPolicy?.codeGuidelines ?? 'No guidelines found for this task... :-(';
|
|
31
|
-
|
|
32
|
-
const unitTestInstructions = 'Run get_unit_test_guidelines to get instructions to generate unit tests for this task';
|
|
33
|
-
const unitTests = developmentPolicy?.areUnitTestsMandatory ? unitTestInstructions : 'No unit test needed.';
|
|
34
|
-
|
|
35
|
-
if (nextTask instanceof ProjectStorySectionModelEnumerationChange) {
|
|
36
|
-
const propertyChanges = nextTask.propertyChanges
|
|
37
|
-
.filter((pc) => !!pc.operation)
|
|
38
|
-
.map((pc) => `${pc.operation} property ${pc.propertyName} (= '${pc.propertyValue}')`)
|
|
39
|
-
.sort()
|
|
40
|
-
.join('\n');
|
|
41
|
-
|
|
42
|
-
return {
|
|
43
|
-
generalInstructions,
|
|
44
|
-
taskType: `${nextTask.operation} enumeration`,
|
|
45
|
-
enumerationName: nextTask.enumerationName,
|
|
46
|
-
propertyChanges: propertyChanges || 'No change on properties',
|
|
47
|
-
taskDescription: nextTask.description ?? undefined,
|
|
48
|
-
implementationGuidelines,
|
|
49
|
-
unitTests,
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
if (nextTask instanceof ProjectStorySectionModelObjectChange) {
|
|
53
|
-
const propertyMap = new Map(nextTask.propertyChanges.map((pc) => [pc.objectPropertyId, pc]));
|
|
54
|
-
|
|
55
|
-
const buildPath = (pc: (typeof nextTask.propertyChanges)[0]): string => {
|
|
56
|
-
const name = pc.isArray ? `${pc.propertyName}[]` : pc.propertyName;
|
|
57
|
-
if (!pc.objectPropertyParentId) return name;
|
|
58
|
-
const parent = propertyMap.get(pc.objectPropertyParentId);
|
|
59
|
-
return parent ? `${buildPath(parent)}.${name}` : name;
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
const changedProperties = nextTask.propertyChanges.filter((pc) => !!pc.operation);
|
|
63
|
-
const parentIdsWithChangedChildren = new Set(changedProperties.map((pc) => pc.objectPropertyParentId).filter(Boolean));
|
|
64
|
-
|
|
65
|
-
const propertyChanges = changedProperties
|
|
66
|
-
.filter((pc) => !(pc.operation === Operation.Create && parentIdsWithChangedChildren.has(pc.objectPropertyId)))
|
|
67
|
-
.map((pc) => {
|
|
68
|
-
const enumInfo = pc.enumerationName ? ` (enum: ${pc.enumerationName})` : '';
|
|
69
|
-
return `${pc.operation} property ${buildPath(pc)}: ${pc.propertyType}${enumInfo}`;
|
|
70
|
-
})
|
|
71
|
-
.sort()
|
|
72
|
-
.join('\n');
|
|
73
|
-
|
|
74
|
-
return {
|
|
75
|
-
generalInstructions,
|
|
76
|
-
taskType: `${nextTask.operation} object`,
|
|
77
|
-
objectName: nextTask.objectName,
|
|
78
|
-
renamedFrom: nextTask.initialObjectName
|
|
79
|
-
? `Object renamed from "${nextTask.initialObjectName}" to "${nextTask.objectName}"`
|
|
80
|
-
: undefined,
|
|
81
|
-
propertyChanges: propertyChanges || 'No change on properties',
|
|
82
|
-
taskDescription: nextTask.description ?? undefined,
|
|
83
|
-
implementationGuidelines,
|
|
84
|
-
unitTests,
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
if (nextTask instanceof ProjectStorySectionModelRouteChange) {
|
|
88
|
-
const queryParamChanges = nextTask.queryParamChanges
|
|
89
|
-
.filter((qp) => !!qp.operation)
|
|
90
|
-
.map((qp) => {
|
|
91
|
-
const enumInfo = qp.enumerationName ? ` (enum: ${qp.enumerationName})` : '';
|
|
92
|
-
return `${qp.operation} query param ${qp.routeQueryParamName}: ${qp.routeQueryParamType}${enumInfo}`;
|
|
93
|
-
})
|
|
94
|
-
.sort()
|
|
95
|
-
.join('\n');
|
|
96
|
-
|
|
97
|
-
return {
|
|
98
|
-
generalInstructions,
|
|
99
|
-
taskType: `${nextTask.operation} route`,
|
|
100
|
-
route: `${nextTask.routeMethod} ${nextTask.routePath}`,
|
|
101
|
-
routeObject: nextTask.routeObjectName ?? undefined,
|
|
102
|
-
pathChangedFrom:
|
|
103
|
-
nextTask.initialRoutePath && nextTask.initialRoutePath !== nextTask.routePath ? nextTask.initialRoutePath : undefined,
|
|
104
|
-
methodChangedFrom:
|
|
105
|
-
nextTask.initialRouteMethod && nextTask.initialRouteMethod !== nextTask.routeMethod
|
|
106
|
-
? nextTask.initialRouteMethod
|
|
107
|
-
: undefined,
|
|
108
|
-
objectChangedFrom:
|
|
109
|
-
nextTask.initialRouteObjectName && nextTask.initialRouteObjectName !== nextTask.routeObjectName
|
|
110
|
-
? nextTask.initialRouteObjectName
|
|
111
|
-
: undefined,
|
|
112
|
-
permissionChangedFrom:
|
|
113
|
-
nextTask.initialRoutePermission && nextTask.initialRoutePermission !== nextTask.routePermission
|
|
114
|
-
? nextTask.initialRoutePermission
|
|
115
|
-
: undefined,
|
|
116
|
-
queryParamChanges: queryParamChanges || 'No change on query params',
|
|
117
|
-
taskDescription: nextTask.description ?? undefined,
|
|
118
|
-
implementationGuidelines,
|
|
119
|
-
unitTests,
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
if (nextTask instanceof ProjectStorySectionModelRelationChange) {
|
|
123
|
-
const relations = nextTask.relations
|
|
124
|
-
.filter((r) => !!r.operation)
|
|
125
|
-
.map((r) => `${r.operation} [${r.objects.map((o) => o.objectName).join(' ⇒ ')}]`)
|
|
126
|
-
.sort()
|
|
127
|
-
.join('\n');
|
|
128
|
-
|
|
129
|
-
return {
|
|
130
|
-
generalInstructions,
|
|
131
|
-
taskType: 'relation change',
|
|
132
|
-
handlerName: nextTask.handlerName,
|
|
133
|
-
boundedContext: nextTask.boundedContext,
|
|
134
|
-
subscriptionName: nextTask.subscriptionName ?? undefined,
|
|
135
|
-
relations: relations || 'No relation changes',
|
|
136
|
-
taskDescription: nextTask.description ?? undefined,
|
|
137
|
-
implementationGuidelines,
|
|
138
|
-
unitTests,
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
if (nextTask instanceof ProjectStorySectionModelTask) {
|
|
142
|
-
return {
|
|
143
|
-
generalInstructions,
|
|
144
|
-
taskType: nextTask.taskTypeName,
|
|
145
|
-
taskDescription: nextTask.description ?? undefined,
|
|
146
|
-
implementationGuidelines,
|
|
147
|
-
unitTests,
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
}
|