@we-scrum/cli 1.0.5 → 1.0.7
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 +132 -52
- package/package.json +3 -7
- package/src/cli.ts +57 -27
- package/src/helpers/index.ts +1 -0
- package/src/helpers/project-helper.ts +96 -0
- package/src/helpers/settings-helper.ts +0 -1
- package/src/helpers/template.helper.ts +7 -2
- package/src/helpers/we-scrum.helper.ts +62 -29
- package/templates/analysis-dsl-legend.hbs +3 -0
- package/templates/develop-story-skill.hbs +10 -4
- package/templates/get-next-task.hbs +4 -0
- package/templates/review-analysis-skill.hbs +63 -0
- package/src/mcp-server/index.ts +0 -1
- package/src/mcp-server/mcp-server.ts +0 -54
- package/src/mcp-server/tools/get-next-task.ts +0 -26
- package/src/mcp-server/tools/index.ts +0 -1
- package/templates/get-next-task-description.hbs +0 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@we-scrum/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"description": "Cli tool for we-scrum application",
|
|
5
5
|
"main": "dist/cli.js",
|
|
6
6
|
"bin": {
|
|
@@ -27,29 +27,25 @@
|
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@inquirer/prompts": "^8.4.2",
|
|
30
|
-
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
31
30
|
"commander": "^14.0.3",
|
|
32
31
|
"firebase": "^12.12.1",
|
|
33
32
|
"google-auth-library": "^10.6.2",
|
|
34
33
|
"handlebars": "^4.7.9",
|
|
35
|
-
"open": "^11.0.0"
|
|
36
|
-
"zod": "^4.4.1"
|
|
34
|
+
"open": "^11.0.0"
|
|
37
35
|
},
|
|
38
36
|
"devDependencies": {
|
|
39
|
-
"@modelcontextprotocol/inspector": "^0.21.2",
|
|
40
37
|
"@types/node": "^22.19.17",
|
|
41
38
|
"tsup": "^8.5.1",
|
|
42
39
|
"typescript": "^5.9.3",
|
|
43
40
|
"@my-devkit/cli": "2.1.0",
|
|
44
41
|
"@my-devkit/core": "1.0.0",
|
|
45
42
|
"@we-scrum/commands": "1.0.0",
|
|
46
|
-
"@we-scrum/models": "1.0.0",
|
|
47
43
|
"@we-scrum/enums": "1.0.0",
|
|
44
|
+
"@we-scrum/models": "1.0.0",
|
|
48
45
|
"@we-scrum/utils": "1.0.0"
|
|
49
46
|
},
|
|
50
47
|
"scripts": {
|
|
51
48
|
"start": "node dist/cli.js",
|
|
52
|
-
"inspect": "mcp-inspector node dist/cli.js mcp",
|
|
53
49
|
"build": "tsup",
|
|
54
50
|
"watch": "tsup --watch",
|
|
55
51
|
"deploy": "pnpm build && pnpm publish --access public"
|
package/src/cli.ts
CHANGED
|
@@ -5,8 +5,7 @@ import { UserProjectModel } from '@we-scrum/models';
|
|
|
5
5
|
import { Command } from 'commander';
|
|
6
6
|
import { version } from '../package.json';
|
|
7
7
|
|
|
8
|
-
import { AuthHelper, FirebaseHelper,
|
|
9
|
-
import { startMcpServer } from './mcp-server';
|
|
8
|
+
import { AuthHelper, FirebaseHelper, ProjectHelper, WeScrumHelper } from './helpers';
|
|
10
9
|
|
|
11
10
|
Logger.registerLogger(new Logger.ConsoleImplementation());
|
|
12
11
|
|
|
@@ -19,10 +18,16 @@ async function requireAuth(): Promise<void> {
|
|
|
19
18
|
assert(initialized, 'Not authenticated. Please run "we-scrum login" first.');
|
|
20
19
|
}
|
|
21
20
|
|
|
22
|
-
function
|
|
21
|
+
async function requireProject(): Promise<void> {
|
|
22
|
+
const root = ProjectHelper.requireRoot();
|
|
23
|
+
await ProjectHelper.init(root);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function runCommand<T = void>(fn: (options: T) => Promise<string | void>) {
|
|
23
27
|
return async (options: T) => {
|
|
24
28
|
try {
|
|
25
|
-
await fn(options);
|
|
29
|
+
const output = await fn(options);
|
|
30
|
+
if (output) process.stdout.write(output + '\n');
|
|
26
31
|
process.exit(0);
|
|
27
32
|
} catch (error) {
|
|
28
33
|
if (error?.name === 'ExitPromptError') process.exit(0);
|
|
@@ -32,6 +37,14 @@ function runCommand<T = void>(fn: (options: T) => Promise<void>) {
|
|
|
32
37
|
};
|
|
33
38
|
}
|
|
34
39
|
|
|
40
|
+
function runProjectCommand<T = void>(fn: (options: T) => Promise<string | void>) {
|
|
41
|
+
return runCommand<T>(async (options) => {
|
|
42
|
+
await requireProject();
|
|
43
|
+
await requireAuth();
|
|
44
|
+
return fn(options);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
35
48
|
program
|
|
36
49
|
.command('login')
|
|
37
50
|
.description('Authenticate with your Google account')
|
|
@@ -53,7 +66,7 @@ program
|
|
|
53
66
|
|
|
54
67
|
program
|
|
55
68
|
.command('use-project')
|
|
56
|
-
.description('Select the active project')
|
|
69
|
+
.description('Select the active project and initialize we-scrum.json')
|
|
57
70
|
.option('--projectId <id>', 'Set the active project directly without the interactive prompt')
|
|
58
71
|
.action(
|
|
59
72
|
runCommand<{ projectId?: string }>(async (options) => {
|
|
@@ -71,7 +84,6 @@ program
|
|
|
71
84
|
} else {
|
|
72
85
|
selectedProjectId = await select({
|
|
73
86
|
message: 'Select a project',
|
|
74
|
-
default: SettingsHelper.get().selectedProjectId,
|
|
75
87
|
choices: projects.map((p) => ({
|
|
76
88
|
name: p.name,
|
|
77
89
|
value: p.projectId,
|
|
@@ -80,8 +92,10 @@ program
|
|
|
80
92
|
});
|
|
81
93
|
}
|
|
82
94
|
|
|
83
|
-
|
|
84
|
-
|
|
95
|
+
const projectRoot = await ProjectHelper.confirmAndCreate(selectedProjectId);
|
|
96
|
+
await ProjectHelper.init(projectRoot);
|
|
97
|
+
|
|
98
|
+
Logger.info(`Project configured: ${projects.find((p) => p.projectId === selectedProjectId)?.name}`);
|
|
85
99
|
}),
|
|
86
100
|
);
|
|
87
101
|
|
|
@@ -91,8 +105,7 @@ program
|
|
|
91
105
|
.requiredOption('--identificationNumber <id>', 'Story identification number')
|
|
92
106
|
.requiredOption('--taskId <id>', 'Unique identifier of the change to act on')
|
|
93
107
|
.action(
|
|
94
|
-
|
|
95
|
-
await requireAuth();
|
|
108
|
+
runProjectCommand<{ identificationNumber: string; taskId: string }>(async (options) => {
|
|
96
109
|
await WeScrumHelper.startTask(options.identificationNumber, options.taskId);
|
|
97
110
|
Logger.info('Task started successfully. You can now make changes to the codebase.');
|
|
98
111
|
}),
|
|
@@ -104,33 +117,50 @@ program
|
|
|
104
117
|
.requiredOption('--identificationNumber <id>', 'Story identification number')
|
|
105
118
|
.requiredOption('--taskId <id>', 'Unique identifier of the change to act on')
|
|
106
119
|
.action(
|
|
107
|
-
|
|
108
|
-
await requireAuth();
|
|
120
|
+
runProjectCommand<{ identificationNumber: string; taskId: string }>(async (options) => {
|
|
109
121
|
await WeScrumHelper.completeTask(options.identificationNumber, options.taskId);
|
|
110
122
|
Logger.info('Task completed successfully.');
|
|
111
123
|
}),
|
|
112
124
|
);
|
|
113
125
|
|
|
114
126
|
program
|
|
115
|
-
.command('
|
|
116
|
-
.description('
|
|
127
|
+
.command('get-story-description')
|
|
128
|
+
.description('Get the description of a story')
|
|
129
|
+
.requiredOption('--identificationNumber <id>', 'Story identification number')
|
|
117
130
|
.action(
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const count = await WeScrumHelper.synchronizeDevelopmentPolicies(process.cwd());
|
|
121
|
-
Logger.info(`Synchronized ${count} development ${count === 1 ? 'policy' : 'policies'}.`);
|
|
131
|
+
runProjectCommand<{ identificationNumber: string }>(async (options) => {
|
|
132
|
+
return WeScrumHelper.getStoryDescription(options.identificationNumber);
|
|
122
133
|
}),
|
|
123
134
|
);
|
|
124
135
|
|
|
125
136
|
program
|
|
126
|
-
.command('
|
|
127
|
-
.description('
|
|
128
|
-
.
|
|
129
|
-
.action(
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
|
|
137
|
+
.command('get-story-analysis')
|
|
138
|
+
.description('Get the analysis DSL for a story')
|
|
139
|
+
.requiredOption('--identificationNumber <id>', 'Story identification number')
|
|
140
|
+
.action(
|
|
141
|
+
runProjectCommand<{ identificationNumber: string }>(async (options) => {
|
|
142
|
+
return WeScrumHelper.getStoryAnalysis(options.identificationNumber);
|
|
143
|
+
}),
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
program
|
|
147
|
+
.command('get-next-task')
|
|
148
|
+
.description('Get the next pending task for a story')
|
|
149
|
+
.requiredOption('--identificationNumber <id>', 'Story identification number')
|
|
150
|
+
.action(
|
|
151
|
+
runProjectCommand<{ identificationNumber: string }>(async (options) => {
|
|
152
|
+
return WeScrumHelper.getNextTask(options.identificationNumber);
|
|
153
|
+
}),
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
program
|
|
157
|
+
.command('synchronize-development-policies')
|
|
158
|
+
.description('Sync development guidelines from we-scrum into the project')
|
|
159
|
+
.action(
|
|
160
|
+
runProjectCommand(async () => {
|
|
161
|
+
const count = await WeScrumHelper.synchronizeDevelopmentPolicies();
|
|
162
|
+
Logger.info(`Synchronized ${count} development ${count === 1 ? 'policy' : 'policies'}.`);
|
|
163
|
+
}),
|
|
164
|
+
);
|
|
135
165
|
|
|
136
166
|
program.parse(process.argv);
|
package/src/helpers/index.ts
CHANGED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { assert, Logger } from '@my-devkit/core';
|
|
2
|
+
import { confirm, input } from '@inquirer/prompts';
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { dirname, join, resolve } from 'node:path';
|
|
5
|
+
import { version as cliVersion } from '../../package.json';
|
|
6
|
+
import { renderTemplate } from './template.helper';
|
|
7
|
+
|
|
8
|
+
const CONFIG_FILE = 'we-scrum.json';
|
|
9
|
+
|
|
10
|
+
export interface WeScrumProjectConfig {
|
|
11
|
+
projectId: string;
|
|
12
|
+
version: string;
|
|
13
|
+
developmentPoliciesPath: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class ProjectHelper {
|
|
17
|
+
public static findRoot(from = process.cwd()): string | null {
|
|
18
|
+
let dir = resolve(from);
|
|
19
|
+
while (true) {
|
|
20
|
+
if (existsSync(join(dir, CONFIG_FILE))) return dir;
|
|
21
|
+
const parent = dirname(dir);
|
|
22
|
+
if (parent === dir) return null;
|
|
23
|
+
dir = parent;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public static requireRoot(): string {
|
|
28
|
+
const root = this.findRoot();
|
|
29
|
+
assert(!!root, `we-scrum.json not found. Please run "we-scrum use-project" first.`);
|
|
30
|
+
return root;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public static read(root: string): WeScrumProjectConfig {
|
|
34
|
+
const content = readFileSync(join(root, CONFIG_FILE), 'utf8');
|
|
35
|
+
return JSON.parse(content) as WeScrumProjectConfig;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public static write(root: string, partial: Partial<WeScrumProjectConfig>): void {
|
|
39
|
+
const existing = existsSync(join(root, CONFIG_FILE)) ? this.read(root) : ({} as WeScrumProjectConfig);
|
|
40
|
+
writeFileSync(join(root, CONFIG_FILE), JSON.stringify({ ...existing, ...partial }, null, 2) + '\n');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public static async init(root: string): Promise<void> {
|
|
44
|
+
const config = this.read(root);
|
|
45
|
+
assert(!!config.projectId, 'No project selected. Please run "we-scrum use-project" first.');
|
|
46
|
+
if (config.version === cliVersion) return;
|
|
47
|
+
|
|
48
|
+
this.installSkills(root);
|
|
49
|
+
this.write(root, { version: cliVersion });
|
|
50
|
+
Logger.info(`we-scrum initialized (v${cliVersion}).`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public static async confirmAndCreate(projectId: string): Promise<string> {
|
|
54
|
+
const defaultRoot = process.cwd();
|
|
55
|
+
|
|
56
|
+
const useDefault = await confirm({
|
|
57
|
+
message: `Use "${defaultRoot}" as the project root?`,
|
|
58
|
+
default: true,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
let root: string;
|
|
62
|
+
if (useDefault) {
|
|
63
|
+
root = defaultRoot;
|
|
64
|
+
} else {
|
|
65
|
+
root = await input({ message: 'Enter the project root directory:' });
|
|
66
|
+
root = resolve(root);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
assert(existsSync(root), `Directory "${root}" does not exist.`);
|
|
70
|
+
|
|
71
|
+
const defaultPoliciesPath = '.claude/development-policies';
|
|
72
|
+
const confirmPolicies = await confirm({
|
|
73
|
+
message: `Store development policies in "${defaultPoliciesPath}"?`,
|
|
74
|
+
default: true,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const developmentPoliciesPath = confirmPolicies
|
|
78
|
+
? defaultPoliciesPath
|
|
79
|
+
: await input({ message: 'Enter the development policies path (relative to project root):' });
|
|
80
|
+
|
|
81
|
+
this.write(root, { projectId, version: '0.0.0', developmentPoliciesPath });
|
|
82
|
+
return root;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private static installSkills(root: string): void {
|
|
86
|
+
if (!existsSync(join(root, '.claude'))) return;
|
|
87
|
+
for (const [name, template] of [
|
|
88
|
+
['develop-story', 'develop-story-skill'],
|
|
89
|
+
['review-analysis', 'review-analysis-skill'],
|
|
90
|
+
] as const) {
|
|
91
|
+
const skillDir = join(root, '.claude', 'skills', name);
|
|
92
|
+
mkdirSync(skillDir, { recursive: true });
|
|
93
|
+
writeFileSync(join(skillDir, 'SKILL.md'), renderTemplate(template));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import Handlebars from 'handlebars';
|
|
2
|
-
import
|
|
2
|
+
import analysisDslLegend from '../../templates/analysis-dsl-legend.hbs';
|
|
3
|
+
import developStorySkill from '../../templates/develop-story-skill.hbs';
|
|
3
4
|
import getNextTask from '../../templates/get-next-task.hbs';
|
|
5
|
+
import reviewAnalysisSkill from '../../templates/review-analysis-skill.hbs';
|
|
6
|
+
|
|
7
|
+
Handlebars.registerPartial('analysis-dsl-legend', analysisDslLegend);
|
|
4
8
|
|
|
5
9
|
const sources = {
|
|
10
|
+
'develop-story-skill': developStorySkill,
|
|
6
11
|
'get-next-task': getNextTask,
|
|
7
|
-
'
|
|
12
|
+
'review-analysis-skill': reviewAnalysisSkill,
|
|
8
13
|
} as const;
|
|
9
14
|
|
|
10
15
|
export type TemplateName = keyof typeof sources;
|
|
@@ -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,30 +22,53 @@ import {
|
|
|
22
22
|
ProjectStorySectionModelRouteChange,
|
|
23
23
|
ProjectStorySectionModelTask,
|
|
24
24
|
} from '@we-scrum/models';
|
|
25
|
-
import { DevelopmentPolicyHelper } from '@we-scrum/utils';
|
|
25
|
+
import { AnalysisDsl, DevelopmentPolicyHelper, HtmlToTextHelper, StorySectionHelper, stringifyAnalysisDslChange } from '@we-scrum/utils';
|
|
26
26
|
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
27
27
|
import { join } from 'node:path';
|
|
28
28
|
import { AuthHelper } from './auth-helper';
|
|
29
29
|
import { FirebaseHelper } from './firebase-helper';
|
|
30
|
-
import {
|
|
30
|
+
import { ProjectHelper } from './project-helper';
|
|
31
|
+
import { renderTemplate } from './template.helper';
|
|
31
32
|
|
|
32
33
|
export class WeScrumHelper {
|
|
33
34
|
private static readonly backendUrl = 'https://europe-west1-we-scrum-prod.cloudfunctions.net';
|
|
34
|
-
private static readonly claudeSubdir = '.claude';
|
|
35
|
-
private static readonly relPoliciesDir = `${WeScrumHelper.claudeSubdir}/development-policies`;
|
|
36
35
|
constructor(private userId: string) {}
|
|
37
36
|
|
|
38
|
-
|
|
39
|
-
return
|
|
37
|
+
private static get projectRoot(): string {
|
|
38
|
+
return ProjectHelper.requireRoot();
|
|
40
39
|
}
|
|
41
40
|
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
private static get projectConfig() {
|
|
42
|
+
return ProjectHelper.read(this.projectRoot);
|
|
44
43
|
}
|
|
45
44
|
|
|
46
|
-
|
|
47
|
-
this.
|
|
45
|
+
private static get projectId(): string {
|
|
46
|
+
return this.projectConfig.projectId;
|
|
47
|
+
}
|
|
48
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
|
+
}
|
|
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) {
|
|
49
72
|
const story = await this.findStoryByIterationNumber(identificationNumber);
|
|
50
73
|
|
|
51
74
|
assert(!!story, `Story "${identificationNumber}" not found in the active project.`);
|
|
@@ -60,8 +83,6 @@ export class WeScrumHelper {
|
|
|
60
83
|
}
|
|
61
84
|
|
|
62
85
|
public static async getChangeById(identificationNumber: string, changeId: string) {
|
|
63
|
-
this.assertProjectIsSelected();
|
|
64
|
-
|
|
65
86
|
const story = await this.findStoryByIterationNumber(identificationNumber);
|
|
66
87
|
assert(!!story, `Story "${identificationNumber}" not found in the active project.`);
|
|
67
88
|
|
|
@@ -148,20 +169,39 @@ export class WeScrumHelper {
|
|
|
148
169
|
return DevelopmentPolicyHelper.getMatchingDevelopmentPolicy(change, developmentPolicies);
|
|
149
170
|
}
|
|
150
171
|
|
|
151
|
-
public static getDevelopmentPolicyPaths(
|
|
152
|
-
policy: DevelopmentPolicyModel | null,
|
|
153
|
-
projectDir = '.',
|
|
154
|
-
): {
|
|
172
|
+
public static getDevelopmentPolicyPaths(policy: DevelopmentPolicyModel | null): {
|
|
155
173
|
guidelinesPath: string | null;
|
|
156
174
|
unitTestsPath: string | null;
|
|
157
175
|
} {
|
|
158
|
-
const
|
|
176
|
+
const { developmentPoliciesPath } = this.projectConfig;
|
|
177
|
+
const policiesDir = join(this.projectRoot, developmentPoliciesPath);
|
|
159
178
|
return {
|
|
160
179
|
guidelinesPath: policy?.slug ? join(policiesDir, `develop-${policy.slug}.md`) : null,
|
|
161
180
|
unitTestsPath: policy?.slug ? join(policiesDir, `test-${policy.slug}.md`) : null,
|
|
162
181
|
};
|
|
163
182
|
}
|
|
164
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
|
+
|
|
165
205
|
private static async findStoryByIterationNumber(identificationNumber: string) {
|
|
166
206
|
return FirebaseHelper.findDocument<ProjectStoryModel>(`/projects/${this.projectId}/stories`, [
|
|
167
207
|
['identificationNumber', '==', identificationNumber],
|
|
@@ -176,16 +216,9 @@ export class WeScrumHelper {
|
|
|
176
216
|
return FirebaseHelper.getCollection<DevelopmentPolicyModel>(`/projects/${this.projectId}/development-policies`);
|
|
177
217
|
}
|
|
178
218
|
|
|
179
|
-
public static async synchronizeDevelopmentPolicies(
|
|
180
|
-
this.
|
|
181
|
-
|
|
182
|
-
const claudeDir = join(projectDir, this.claudeSubdir);
|
|
183
|
-
assert(
|
|
184
|
-
existsSync(claudeDir),
|
|
185
|
-
`No ${this.claudeSubdir} directory found in ${projectDir}. Please run this command from the root of your project.`,
|
|
186
|
-
);
|
|
187
|
-
|
|
188
|
-
const policiesDir = join(projectDir, this.relPoliciesDir);
|
|
219
|
+
public static async synchronizeDevelopmentPolicies(): Promise<number> {
|
|
220
|
+
const { developmentPoliciesPath } = this.projectConfig;
|
|
221
|
+
const policiesDir = join(this.projectRoot, developmentPoliciesPath);
|
|
189
222
|
|
|
190
223
|
rmSync(policiesDir, { recursive: true, force: true });
|
|
191
224
|
mkdirSync(policiesDir, { recursive: true });
|
|
@@ -193,7 +226,7 @@ export class WeScrumHelper {
|
|
|
193
226
|
const policies = await this.getDevelopmentPolicies();
|
|
194
227
|
|
|
195
228
|
for (const policy of policies) {
|
|
196
|
-
const { guidelinesPath, unitTestsPath } = this.getDevelopmentPolicyPaths(policy
|
|
229
|
+
const { guidelinesPath, unitTestsPath } = this.getDevelopmentPolicyPaths(policy);
|
|
197
230
|
if (policy.codeGuidelines && guidelinesPath) {
|
|
198
231
|
writeFileSync(guidelinesPath, policy.codeGuidelines);
|
|
199
232
|
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
**Analysis DSL** — `+` Create · `-` Delete · `~` Update (operation prefixes, not markdown bullets).
|
|
2
|
+
These appear on both change headers (`## + Object Foo`) and property lines (`+ name: String`, `~ price: Number`, `- email: String`).
|
|
3
|
+
Rename: `~ OldName > NewName`. Metadata: `> key: value`. Description block: `:::...:::`.
|
|
@@ -5,8 +5,8 @@ description: >
|
|
|
5
5
|
Use this skill whenever the user says "develop story XXXXX", "/develop-story XXXXX",
|
|
6
6
|
or asks to implement or work on a specific story number. The skill synchronizes
|
|
7
7
|
development policies, then fetches and implements each task in sequence using
|
|
8
|
-
the we-scrum
|
|
9
|
-
compatibility: "Requires: we-scrum
|
|
8
|
+
the we-scrum CLI.
|
|
9
|
+
compatibility: "Requires: we-scrum CLI (we-scrum)"
|
|
10
10
|
---
|
|
11
11
|
|
|
12
12
|
# Develop Story Skill
|
|
@@ -36,5 +36,11 @@ we-scrum synchronize-development-policies
|
|
|
36
36
|
|
|
37
37
|
## Step 2 — Fetch and implement tasks
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
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,63 @@
|
|
|
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
|
+
{{> analysis-dsl-legend}}
|
|
40
|
+
|
|
41
|
+
Review the story description and analysis DSL together. Check for:
|
|
42
|
+
|
|
43
|
+
**Completeness & correctness**
|
|
44
|
+
- Consistency between the description and the analysis changes
|
|
45
|
+
- Missing or incomplete changes relative to the described requirements
|
|
46
|
+
- Incorrect operations (create/update/delete used inappropriately)
|
|
47
|
+
- Unclear or ambiguous task descriptions
|
|
48
|
+
|
|
49
|
+
**Naming clarity**
|
|
50
|
+
- Object names and property names must be clear, precise, and self-explanatory
|
|
51
|
+
- Challenge any name that is vague, abbreviated, or could be misread — propose a better alternative
|
|
52
|
+
- Names should reflect the domain concept they represent, not the implementation detail
|
|
53
|
+
|
|
54
|
+
**Naming continuity**
|
|
55
|
+
- 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
|
|
56
|
+
- Flag any analysis change where the proposed name would likely create a discontinuity or force a translation at any layer
|
|
57
|
+
- Renames or aliases between layers are a sign of a poorly chosen name — the right name should need no translation
|
|
58
|
+
|
|
59
|
+
**Migrations**
|
|
60
|
+
- 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
|
|
61
|
+
- 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
|
|
62
|
+
|
|
63
|
+
Return your comments to the user.
|
package/src/mcp-server/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from './mcp-server';
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
-
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
import { z } from 'zod';
|
|
6
|
-
|
|
7
|
-
import { AuthHelper, renderTemplate } from '@helpers';
|
|
8
|
-
import developStorySkill from '../../templates/develop-story-skill.hbs';
|
|
9
|
-
import { getNextTask } from './tools';
|
|
10
|
-
|
|
11
|
-
function installSkills(): void {
|
|
12
|
-
const skillDir = join(process.cwd(), '.claude', 'skills', 'develop-story');
|
|
13
|
-
if (!existsSync(join(process.cwd(), '.claude'))) return;
|
|
14
|
-
mkdirSync(skillDir, { recursive: true });
|
|
15
|
-
writeFileSync(join(skillDir, 'SKILL.md'), developStorySkill);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export async function startMcpServer(): Promise<void> {
|
|
19
|
-
installSkills();
|
|
20
|
-
|
|
21
|
-
const initialized = await AuthHelper.initializeFromStoredCredentials();
|
|
22
|
-
if (!initialized) {
|
|
23
|
-
process.stderr.write('Error: Not authenticated. Please run "we-scrum login" first.\n');
|
|
24
|
-
process.exit(1);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const server = new McpServer({ name: 'we-scrum-mcp-server', version: '1.0.0' });
|
|
28
|
-
|
|
29
|
-
server.registerTool(
|
|
30
|
-
'get_next_task',
|
|
31
|
-
{
|
|
32
|
-
description: renderTemplate('get-next-task-description'),
|
|
33
|
-
inputSchema: z.object({
|
|
34
|
-
identificationNumber: z.string().describe('The story identification number, e.g. "144154"'),
|
|
35
|
-
}),
|
|
36
|
-
},
|
|
37
|
-
async ({ identificationNumber }) => {
|
|
38
|
-
try {
|
|
39
|
-
const markdown = await getNextTask(identificationNumber);
|
|
40
|
-
return { content: [{ type: 'text' as const, text: markdown }] };
|
|
41
|
-
} catch (error) {
|
|
42
|
-
return {
|
|
43
|
-
content: [{ type: 'text' as const, text: JSON.stringify({ error: error.message }) }],
|
|
44
|
-
isError: true,
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
},
|
|
48
|
-
);
|
|
49
|
-
|
|
50
|
-
const transport = new StdioServerTransport();
|
|
51
|
-
await server.connect(transport);
|
|
52
|
-
|
|
53
|
-
process.stderr.write('we-scrum MCP server running.\n');
|
|
54
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { renderTemplate, WeScrumHelper } from '@helpers';
|
|
2
|
-
import { ProgressStatus } from '@we-scrum/enums';
|
|
3
|
-
import { StorySectionHelper, stringifyAnalysisDslChange } from '@we-scrum/utils';
|
|
4
|
-
|
|
5
|
-
export async function getNextTask(identificationNumber: string): Promise<string> {
|
|
6
|
-
const nextChange = await WeScrumHelper.getNextChange(identificationNumber);
|
|
7
|
-
|
|
8
|
-
if (!nextChange) {
|
|
9
|
-
return `## Story ${identificationNumber}\n\nAll tasks are complete.`;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const analysisChange = StorySectionHelper.mapSingleChange(nextChange);
|
|
13
|
-
const isTodo = nextChange.progress.status === ProgressStatus.ToDo;
|
|
14
|
-
const developmentPolicy = await WeScrumHelper.getChangeDevelopmentPolicy(nextChange);
|
|
15
|
-
|
|
16
|
-
const { guidelinesPath, unitTestsPath } = WeScrumHelper.getDevelopmentPolicyPaths(developmentPolicy);
|
|
17
|
-
|
|
18
|
-
return renderTemplate('get-next-task', {
|
|
19
|
-
taskDetails: stringifyAnalysisDslChange(analysisChange),
|
|
20
|
-
guidelinesPath,
|
|
21
|
-
unitTestsPath: developmentPolicy?.areUnitTestsMandatory ? unitTestsPath : null,
|
|
22
|
-
isTodo,
|
|
23
|
-
identificationNumber,
|
|
24
|
-
taskId: analysisChange.id,
|
|
25
|
-
});
|
|
26
|
-
}
|