@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.
@@ -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 { assert } from 'node:console';
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 { SettingsHelper } from './settings-helper';
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
- public static get projectId(): string {
36
- return SettingsHelper.get().selectedProjectId;
37
+ private static get projectRoot(): string {
38
+ return ProjectHelper.requireRoot();
37
39
  }
38
40
 
39
- public static assertProjectIsSelected(): void {
40
- assert(!!this.projectId, 'No active project selected. Please run "we-scrum use-project" first.');
41
+ private static get projectConfig() {
42
+ return ProjectHelper.read(this.projectRoot);
41
43
  }
42
44
 
43
- public static async getNextChange(identificationNumber: string) {
44
- this.assertProjectIsSelected();
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 startTask(identificationNumber: string): Promise<void> {
60
- const change = await this.getNextChange(identificationNumber);
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
- assert(!!change, `All tasks for story "${identificationNumber}" are complete.`);
63
- assert(change.progress.status === ProgressStatus.ToDo, `Task is already in progress (current status: ${change.progress.status}).`);
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.getNextChange(identificationNumber);
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
- private static async getDevelopmentPolicies() {
215
+ public static async getDevelopmentPolicies() {
138
216
  return FirebaseHelper.getCollection<DevelopmentPolicyModel>(`/projects/${this.projectId}/development-policies`);
139
217
  }
140
218
 
141
- // public async createEnumeration(enumeration: Enumeration): Promise<string> {
142
- // Logger.info(`Creating enumeration ${enumeration.name}`);
143
-
144
- // const createCommand = TypeHelper.transform(CreateEnumerationCommand, {
145
- // projectId: this.projectId,
146
- // name: enumeration.name,
147
- // });
148
-
149
- // const enumerationId = await this.post<CreateEnumerationCommand, string>('enumeration-management/create-enumeration', createCommand);
150
- // for (const property of enumeration.properties) {
151
- // const propertyCommand = TypeHelper.transform(CreateEnumerationPropertyCommand, {
152
- // enumerationId,
153
- // enumerationPropertyId: guid(),
154
- // name: property.label,
155
- // value: property.value,
156
- // });
157
- // await this.post<CreateEnumerationPropertyCommand, string>(
158
- // 'enumeration-management/create-enumeration-property',
159
- // propertyCommand,
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
@@ -14,6 +14,7 @@
14
14
  "resolveJsonModule": true,
15
15
  "paths": {
16
16
  "@helpers": ["helpers"]
17
- }
17
+ },
18
+ "forceConsistentCasingInFileNames": true
18
19
  }
19
20
  }
@@ -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,6 +0,0 @@
1
- import { WeScrumHelper } from '@helpers';
2
-
3
- export async function completeTask(identificationNumber: string): Promise<Record<string, string>> {
4
- await WeScrumHelper.completeTask(identificationNumber);
5
- return { message: 'Task completed successfully.' };
6
- }
@@ -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
- }