@ezmodo/mcp-server 0.14.4 → 0.17.1
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/config/endpoint-map.js +5 -21
- package/handlers/catalogs.js +39 -1
- package/handlers/context-manifest.js +0 -4
- package/handlers/epics.js +1 -1
- package/handlers/features.js +13 -2
- package/handlers/git-context.js +11 -41
- package/handlers/index.js +0 -5
- package/handlers/links.js +17 -11
- package/handlers/projects.js +1 -1
- package/handlers/tasks.js +7 -27
- package/lib/auto-assign.js +3 -22
- package/lib/autolink.js +11 -10
- package/lib/instructions.generated.js +2 -2
- package/lib/local-cache.js +3 -17
- package/lib/remote-tools.js +0 -2
- package/lib/version.js +1 -1
- package/package.json +5 -5
- package/prompts/commands.generated.js +2 -2
- package/tools/catalogs.js +32 -5
- package/tools/context-manifest.js +2 -2
- package/tools/epics.js +1 -6
- package/tools/features.js +41 -2
- package/tools/git-context.js +5 -5
- package/tools/graph.js +1 -1
- package/tools/index.js +0 -2
- package/tools/linkable-types.js +0 -1
- package/tools/links.js +7 -5
- package/tools/projects.js +2 -2
- package/tools/recurring-tasks.js +0 -1
- package/tools/task-item-schema.js +0 -6
- package/tools/tasks.js +9 -44
- package/tools/todos.js +0 -4
- package/tools/work-templates.js +0 -2
- package/handlers/components.js +0 -282
- package/tools/components.js +0 -249
package/config/endpoint-map.js
CHANGED
|
@@ -131,27 +131,6 @@ export const ENDPOINT_MAP = {
|
|
|
131
131
|
'mcpRemoveAccessEntry': { route: 'mcp/v1/access/entries', method: 'DELETE' },
|
|
132
132
|
'mcpCheckAccess': { route: 'mcp/v1/access/check', method: 'POST' },
|
|
133
133
|
|
|
134
|
-
// Components (project-scoped codebase areas)
|
|
135
|
-
'mcpCreateComponent': { route: 'mcp/v1/components', method: 'POST' },
|
|
136
|
-
'mcpListComponents': { route: 'mcp/v1/components', method: 'GET' },
|
|
137
|
-
'mcpUpdateComponent': { route: 'mcp/v1/components', method: 'PUT' },
|
|
138
|
-
'mcpDeleteComponent': { route: 'mcp/v1/components', method: 'DELETE' },
|
|
139
|
-
'mcpGetComponentStats': { route: 'mcp/v1/components/stats', method: 'GET' },
|
|
140
|
-
'mcpGetComponentDependencyGraph': { route: 'mcp/v1/components/dependency-graph', method: 'GET' },
|
|
141
|
-
// E-223: screen→screen navigation edges (the screen-flow map).
|
|
142
|
-
'mcpGetComponentNavigation': { route: 'mcp/v1/components/navigation', method: 'GET' },
|
|
143
|
-
'mcpAddComponentNavigation': { route: 'mcp/v1/components/navigation', method: 'POST' },
|
|
144
|
-
'mcpRemoveComponentNavigation': { route: 'mcp/v1/components/navigation', method: 'DELETE' },
|
|
145
|
-
// E-239 #2409: re-derive the flow map from code instead of hand-drawing it.
|
|
146
|
-
'mcpDeriveComponentNavigation': { route: 'mcp/v1/components/navigation/derive', method: 'POST' },
|
|
147
|
-
'mcpAddComponentDependency': { route: 'mcp/v1/components/dependencies', method: 'POST' },
|
|
148
|
-
'mcpRemoveComponentDependency': { route: 'mcp/v1/components/dependencies', method: 'DELETE' },
|
|
149
|
-
// E-168: unified UI inventory — Components span kinds area|screen|page|component.
|
|
150
|
-
// discover proposes page/component surfaces from the Context Manifest;
|
|
151
|
-
// import bulk-creates surfaces (optionally nested and/or feature-linked).
|
|
152
|
-
'mcpDiscoverComponents': { route: 'mcp/v1/components/discover', method: 'GET' },
|
|
153
|
-
'mcpImportComponents': { route: 'mcp/v1/components/import', method: 'POST' },
|
|
154
|
-
|
|
155
134
|
// Polymorphic links — single CRUD surface for any entity_links row
|
|
156
135
|
// (blocked_by, relates_to) across tasks, epics, projects, documents.
|
|
157
136
|
'mcpAddLink': { route: 'mcp/v1/links', method: 'POST' },
|
|
@@ -211,6 +190,8 @@ export const ENDPOINT_MAP = {
|
|
|
211
190
|
'mcpSearchFeatures': { route: 'mcp/v1/features/search', method: 'GET' },
|
|
212
191
|
'mcpLinkFeatureArtifact': { route: 'mcp/v1/features/link', method: 'POST' },
|
|
213
192
|
'mcpUnlinkFeatureArtifact': { route: 'mcp/v1/features/link', method: 'DELETE' },
|
|
193
|
+
'mcpListFeaturePaths': { route: 'mcp/v1/features/paths', method: 'GET' },
|
|
194
|
+
'mcpSetFeaturePaths': { route: 'mcp/v1/features/paths', method: 'PUT' },
|
|
214
195
|
'mcpPromoteEpicToFeature': { route: 'mcp/v1/features/promote-epic', method: 'POST' },
|
|
215
196
|
'mcpGenerateHowItWorks': { route: 'mcp/v1/features/generate-how-it-works', method: 'POST' },
|
|
216
197
|
'mcpApplyHowItWorks': { route: 'mcp/v1/features/apply-how-it-works', method: 'POST' },
|
|
@@ -270,6 +251,9 @@ export const ENDPOINT_MAP = {
|
|
|
270
251
|
'mcpUpdateCatalog': { route: 'mcp/v1/catalogs', method: 'PUT' },
|
|
271
252
|
'mcpDeleteCatalog': { route: 'mcp/v1/catalogs', method: 'DELETE' },
|
|
272
253
|
'mcpSnapshotCatalog': { route: 'mcp/v1/catalogs/snapshot', method: 'POST' },
|
|
254
|
+
'mcpDiscoverScreens': { route: 'mcp/v1/screens/discover', method: 'GET' },
|
|
255
|
+
'mcpImportScreens': { route: 'mcp/v1/screens/import', method: 'POST' },
|
|
256
|
+
'mcpSyncScreens': { route: 'mcp/v1/screens/sync', method: 'POST' },
|
|
273
257
|
'mcpLinkCatalog': { route: 'mcp/v1/catalogs/link', method: 'POST' },
|
|
274
258
|
'mcpUnlinkCatalog': { route: 'mcp/v1/catalogs/link', method: 'DELETE' },
|
|
275
259
|
// Item-level links (E-218) — attach work or an external URL to one catalog entry
|
package/handlers/catalogs.js
CHANGED
|
@@ -24,8 +24,12 @@ export async function manageCatalog(args) {
|
|
|
24
24
|
case 'link': return params.itemKey ? linkCatalogItem(params) : linkCatalogArtifact(params);
|
|
25
25
|
case 'unlink': return params.itemKey ? unlinkCatalogItem(params) : unlinkCatalogArtifact(params);
|
|
26
26
|
case 'snapshot': return snapshotCatalog(params);
|
|
27
|
+
case 'discover_screens': return discoverScreens(params);
|
|
28
|
+
case 'import_screens': return importScreens(params);
|
|
29
|
+
case 'sync_screens': return syncScreens(params);
|
|
27
30
|
default:
|
|
28
|
-
throw new Error(`Unknown action: ${action}. Expected create, update, delete, link, unlink,
|
|
31
|
+
throw new Error(`Unknown action: ${action}. Expected create, update, delete, link, unlink, snapshot, ` +
|
|
32
|
+
'discover_screens, import_screens, or sync_screens.');
|
|
29
33
|
}
|
|
30
34
|
}
|
|
31
35
|
|
|
@@ -167,3 +171,37 @@ async function snapshotCatalog({ catalogId, mode, snapshot, upsertItems, removeK
|
|
|
167
171
|
if (source) params.source = source;
|
|
168
172
|
return callZephlyAPI('mcpSnapshotCatalog', params);
|
|
169
173
|
}
|
|
174
|
+
|
|
175
|
+
// --- Screens catalog (E-258) ---
|
|
176
|
+
|
|
177
|
+
// Screens and pages the project's manifest shows that its screens catalog does
|
|
178
|
+
// not hold yet. Read-only.
|
|
179
|
+
async function discoverScreens({ projectId }) {
|
|
180
|
+
if (!projectId) {
|
|
181
|
+
throw new Error('projectId is required for discover_screens');
|
|
182
|
+
}
|
|
183
|
+
return callZephlyAPI('mcpDiscoverScreens', { projectId });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Add screens to the project's screens catalog (created on first use),
|
|
187
|
+
// optionally linking every one to a feature.
|
|
188
|
+
async function importScreens({ projectId, screens, featureId }) {
|
|
189
|
+
if (!projectId) {
|
|
190
|
+
throw new Error('projectId is required for import_screens');
|
|
191
|
+
}
|
|
192
|
+
if (!Array.isArray(screens) || screens.length === 0) {
|
|
193
|
+
throw new Error('screens must be a non-empty array for import_screens');
|
|
194
|
+
}
|
|
195
|
+
const params = { projectId, screens };
|
|
196
|
+
if (featureId) params.featureId = featureId;
|
|
197
|
+
return callZephlyAPI('mcpImportScreens', params);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Reconcile the project's screens catalog with its synced manifest and
|
|
201
|
+
// re-derive the screen flow map (navigates_to between catalog items).
|
|
202
|
+
async function syncScreens({ projectId }) {
|
|
203
|
+
if (!projectId) {
|
|
204
|
+
throw new Error('projectId is required for sync_screens');
|
|
205
|
+
}
|
|
206
|
+
return callZephlyAPI('mcpSyncScreens', { projectId });
|
|
207
|
+
}
|
|
@@ -374,7 +374,6 @@ async function handleEntityContext(args) {
|
|
|
374
374
|
};
|
|
375
375
|
// Map entityType+entityId to the correct graph param
|
|
376
376
|
if (entityType === 'task') graphParams.taskId = entityId;
|
|
377
|
-
else if (entityType === 'component') graphParams.componentId = entityId;
|
|
378
377
|
else if (entityType === 'file') graphParams.filePath = entityId;
|
|
379
378
|
else if (entityType === 'tag') graphParams.tagId = entityId;
|
|
380
379
|
|
|
@@ -389,7 +388,6 @@ async function handleEntityContext(args) {
|
|
|
389
388
|
if (sections.includes('impact')) {
|
|
390
389
|
const impactParams = { projectId, depth: args.depth };
|
|
391
390
|
if (entityType === 'file') impactParams.filePath = entityId;
|
|
392
|
-
else if (entityType === 'component') impactParams.componentId = entityId;
|
|
393
391
|
|
|
394
392
|
promises.push(
|
|
395
393
|
callZephlyAPI('mcpAnalyzeImpact', impactParams)
|
|
@@ -566,8 +564,6 @@ function getDefaultIncludes(entityType) {
|
|
|
566
564
|
case 'task':
|
|
567
565
|
case 'epic':
|
|
568
566
|
return ['graph'];
|
|
569
|
-
case 'component':
|
|
570
|
-
return ['graph', 'impact'];
|
|
571
567
|
case 'file':
|
|
572
568
|
return ['dependencies', 'impact'];
|
|
573
569
|
case 'tag':
|
package/handlers/epics.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Handler functions for epic-related MCP tools
|
|
4
4
|
*
|
|
5
5
|
* Project-First Hierarchy: Epics belong to projects (required),
|
|
6
|
-
* with optional
|
|
6
|
+
* with optional milestone linking.
|
|
7
7
|
*
|
|
8
8
|
* Auto-assignment: When creating epics, automatically applies matching tags
|
|
9
9
|
* based on content analysis against the local project cache.
|
package/handlers/features.js
CHANGED
|
@@ -20,12 +20,13 @@ export async function manageFeature(args) {
|
|
|
20
20
|
case 'delete': return deleteFeature(params);
|
|
21
21
|
case 'link': return linkFeatureArtifact(params);
|
|
22
22
|
case 'unlink': return unlinkFeatureArtifact(params);
|
|
23
|
+
case 'paths': return setFeaturePaths(params);
|
|
23
24
|
case 'promote_epic': return promoteEpic(params);
|
|
24
25
|
case 'generate_how_it_works': return generateHowItWorks(params);
|
|
25
26
|
case 'apply_how_it_works': return applyHowItWorks(params);
|
|
26
27
|
case 'apply_init': return applyInit(params);
|
|
27
28
|
default:
|
|
28
|
-
throw new Error(`Unknown action: ${action}. Expected create, update, delete, link, unlink, promote_epic, generate_how_it_works, apply_how_it_works, or apply_init.`);
|
|
29
|
+
throw new Error(`Unknown action: ${action}. Expected create, update, delete, link, unlink, paths, promote_epic, generate_how_it_works, apply_how_it_works, or apply_init.`);
|
|
29
30
|
}
|
|
30
31
|
}
|
|
31
32
|
|
|
@@ -35,7 +36,7 @@ export async function manageFeature(args) {
|
|
|
35
36
|
* - organizationId only → list (or tree when tree=true).
|
|
36
37
|
*/
|
|
37
38
|
export async function getFeature(args) {
|
|
38
|
-
const { tree, includeLinks, includeDetail, featureId, featureSlug, organizationId, ...filters } = args;
|
|
39
|
+
const { tree, includeLinks, includeDetail, includePaths, featureId, featureSlug, organizationId, ...filters } = args;
|
|
39
40
|
const isSingleLookup = featureId || (featureSlug && organizationId);
|
|
40
41
|
|
|
41
42
|
if (isSingleLookup) {
|
|
@@ -50,6 +51,10 @@ export async function getFeature(args) {
|
|
|
50
51
|
const links = await callZephlyAPI('mcpListFeatureLinks', { featureId: lookupId });
|
|
51
52
|
result.links = links?.links ?? links;
|
|
52
53
|
}
|
|
54
|
+
if (includePaths && lookupId) {
|
|
55
|
+
const paths = await callZephlyAPI('mcpListFeaturePaths', { featureId: lookupId });
|
|
56
|
+
result.paths = paths?.paths ?? paths;
|
|
57
|
+
}
|
|
53
58
|
return result;
|
|
54
59
|
}
|
|
55
60
|
|
|
@@ -110,6 +115,12 @@ async function unlinkFeatureArtifact({ featureId, targetType, targetId }) {
|
|
|
110
115
|
return callZephlyAPI('mcpUnlinkFeatureArtifact', { featureId, targetType, targetId });
|
|
111
116
|
}
|
|
112
117
|
|
|
118
|
+
// Add, remove or replace the code paths a feature owns (E-258). Paths are what
|
|
119
|
+
// auto-link work to the feature when it touches those files.
|
|
120
|
+
async function setFeaturePaths({ featureId, pathsMode, paths }) {
|
|
121
|
+
return callZephlyAPI('mcpSetFeaturePaths', { featureId, mode: pathsMode || 'add', paths });
|
|
122
|
+
}
|
|
123
|
+
|
|
113
124
|
async function promoteEpic({ epicId }) {
|
|
114
125
|
return callZephlyAPI('mcpPromoteEpicToFeature', { epicId });
|
|
115
126
|
}
|
package/handlers/git-context.js
CHANGED
|
@@ -19,7 +19,6 @@ export { isCacheFresh };
|
|
|
19
19
|
import { getProject } from './projects.js';
|
|
20
20
|
import { listRepositories } from './github.js';
|
|
21
21
|
import { getOrganization } from './organizations.js';
|
|
22
|
-
import { listComponents } from './components.js';
|
|
23
22
|
import { getLogger } from '../lib/logger.js';
|
|
24
23
|
import { listTags } from './tags.js';
|
|
25
24
|
import { CONFIG } from '../config/index.js';
|
|
@@ -71,28 +70,6 @@ function legacyConfigNotice(legacyConfigPath) {
|
|
|
71
70
|
};
|
|
72
71
|
}
|
|
73
72
|
|
|
74
|
-
/**
|
|
75
|
-
* Fetch lightweight component summaries (id, name, description) for a project.
|
|
76
|
-
* Filtered to kind='area' (E-168): this list is the coarse codebase-area set
|
|
77
|
-
* agents use to pick a task's componentId, and it must match the web task
|
|
78
|
-
* picker (which also filters to area). Without the filter, UI-inventory rows
|
|
79
|
-
* (screen/page/component) would leak into task routing. Returns an empty array
|
|
80
|
-
* on failure (non-fatal).
|
|
81
|
-
*/
|
|
82
|
-
async function fetchComponentSummaries(projectId) {
|
|
83
|
-
try {
|
|
84
|
-
const result = await listComponents({ projectId, kind: 'area' });
|
|
85
|
-
if (!result?.components) return [];
|
|
86
|
-
return result.components.map((c) => ({
|
|
87
|
-
id: c.id,
|
|
88
|
-
name: c.name,
|
|
89
|
-
description: c.description || '',
|
|
90
|
-
}));
|
|
91
|
-
} catch {
|
|
92
|
-
return [];
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
73
|
/**
|
|
97
74
|
* Fetch lightweight tag summaries (id, name, color, category) for an organization.
|
|
98
75
|
* Returns an empty array on failure (non-fatal).
|
|
@@ -168,9 +145,9 @@ no exceptions unless the user explicitly says to work on an existing task.
|
|
|
168
145
|
**Single-scope work** (bug fix, small feature, config change, docs update):
|
|
169
146
|
- Create a **task** with \`manage_task action:"create"\`:
|
|
170
147
|
- \`projectId\` from cached context
|
|
171
|
-
- \`
|
|
172
|
-
|
|
173
|
-
|
|
148
|
+
- \`changedFiles\` — the paths you expect to touch; they resolve to the
|
|
149
|
+
features that own them
|
|
150
|
+
- \`links\` — the feature the work advances (find it with \`search_features\`)
|
|
174
151
|
- Descriptive \`title\` and \`description\` (informed by \`get_context\`)
|
|
175
152
|
- \`steps\` array with actionable steps referencing specific files
|
|
176
153
|
- \`priority\` based on context (low / medium / high / urgent)
|
|
@@ -551,12 +528,11 @@ export async function getCurrentProjectContext(args) {
|
|
|
551
528
|
workingDirectory: currentDir,
|
|
552
529
|
configPath,
|
|
553
530
|
allProjects: config.projects || null,
|
|
554
|
-
components: config.components || [],
|
|
555
531
|
tags: config.tags || [],
|
|
556
532
|
autoGenerateTestCases: config.settings?.aiConfig?.autoGenerateTestCases || false,
|
|
557
533
|
organizeResponseMode: config.settings?.aiConfig?.organizeResponseMode || 'raw_snapshot',
|
|
558
534
|
// The words this project's type uses (E-107). Cached alongside
|
|
559
|
-
//
|
|
535
|
+
// tags because it changes about as often, and an
|
|
560
536
|
// agent needs it on every session, not on a second round trip.
|
|
561
537
|
// Absent means plain English — a project type with no template.
|
|
562
538
|
projectType: config.projectType || null,
|
|
@@ -614,18 +590,17 @@ export async function getCurrentProjectContext(args) {
|
|
|
614
590
|
validation.warnings.push('Could not validate organization access');
|
|
615
591
|
}
|
|
616
592
|
|
|
617
|
-
// Refresh
|
|
618
|
-
const
|
|
619
|
-
fetchComponentSummaries(activeProject.id),
|
|
620
|
-
fetchTagSummaries(config.organizationId),
|
|
621
|
-
]);
|
|
593
|
+
// Refresh the tags list
|
|
594
|
+
const tags = await fetchTagSummaries(config.organizationId);
|
|
622
595
|
|
|
623
596
|
// Update config on disk with fresh data (non-fatal)
|
|
624
597
|
if (validation.projectExists && validation.organizationExists) {
|
|
625
598
|
try {
|
|
626
599
|
config.orgName = orgName;
|
|
627
600
|
config.lastUpdatedAt = new Date().toISOString();
|
|
628
|
-
|
|
601
|
+
// Components were retired (E-258); drop the list a config written
|
|
602
|
+
// by an older server still carries.
|
|
603
|
+
delete config.components;
|
|
629
604
|
config.tags = tags;
|
|
630
605
|
config.projectType = projectType;
|
|
631
606
|
config.terminology = terminology;
|
|
@@ -650,7 +625,6 @@ export async function getCurrentProjectContext(args) {
|
|
|
650
625
|
workingDirectory: currentDir,
|
|
651
626
|
configPath,
|
|
652
627
|
allProjects: config.projects || null,
|
|
653
|
-
components,
|
|
654
628
|
tags,
|
|
655
629
|
autoGenerateTestCases: projectSettings?.aiConfig?.autoGenerateTestCases || false,
|
|
656
630
|
organizeResponseMode: projectSettings?.aiConfig?.organizeResponseMode || 'raw_snapshot',
|
|
@@ -836,11 +810,8 @@ export async function initializeProjectContext(args) {
|
|
|
836
810
|
throw new Error(`Organization ${organizationId} not found or not accessible`);
|
|
837
811
|
}
|
|
838
812
|
|
|
839
|
-
// Step 5: Fetch
|
|
840
|
-
const
|
|
841
|
-
fetchComponentSummaries(projectId),
|
|
842
|
-
fetchTagSummaries(organizationId),
|
|
843
|
-
]);
|
|
813
|
+
// Step 5: Fetch tags
|
|
814
|
+
const tags = await fetchTagSummaries(organizationId);
|
|
844
815
|
|
|
845
816
|
// Step 6: Build config
|
|
846
817
|
const config = {
|
|
@@ -853,7 +824,6 @@ export async function initializeProjectContext(args) {
|
|
|
853
824
|
projectName: projectContext.project?.name || projectContext.name || 'Unknown Project',
|
|
854
825
|
environment: CONFIG.environment, // staging, production, or dev
|
|
855
826
|
lastUpdatedAt: new Date().toISOString(),
|
|
856
|
-
components,
|
|
857
827
|
tags,
|
|
858
828
|
settings: {
|
|
859
829
|
aiConfig: projectContext.settings?.aiConfig || projectContext.project?.settings?.aiConfig || null,
|
package/handlers/index.js
CHANGED
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
import * as organizationHandlers from './organizations.js';
|
|
10
10
|
import * as projectHandlers from './projects.js';
|
|
11
11
|
import * as epicHandlers from './epics.js';
|
|
12
|
-
import * as componentHandlers from './components.js';
|
|
13
12
|
import * as milestoneHandlers from './milestones.js';
|
|
14
13
|
import * as featureHandlers from './features.js';
|
|
15
14
|
import * as decisionHandlers from './decisions.js';
|
|
@@ -60,10 +59,6 @@ export const HANDLERS = {
|
|
|
60
59
|
list_epics: epicHandlers.listEpics,
|
|
61
60
|
get_epic: epicHandlers.getEpic,
|
|
62
61
|
|
|
63
|
-
// Components
|
|
64
|
-
manage_component: componentHandlers.manageComponent,
|
|
65
|
-
list_components: componentHandlers.listComponents,
|
|
66
|
-
|
|
67
62
|
// Milestones
|
|
68
63
|
manage_milestone: milestoneHandlers.manageMilestone,
|
|
69
64
|
get_milestone: milestoneHandlers.getMilestone,
|
package/handlers/links.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import {
|
|
10
|
-
|
|
10
|
+
resolvePathsToFeatures,
|
|
11
11
|
previewEntityLinks,
|
|
12
12
|
partitionProposals,
|
|
13
13
|
attachSuggestionIds,
|
|
@@ -107,20 +107,27 @@ export async function listLinks({
|
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
/**
|
|
110
|
-
* resolve_links — which
|
|
110
|
+
* resolve_links — which features own these files?
|
|
111
111
|
*
|
|
112
112
|
* The read that makes linking cheap: an agent asks once, before creating work,
|
|
113
|
-
* and gets back the
|
|
114
|
-
*
|
|
115
|
-
* is itself information — it usually means the
|
|
113
|
+
* and gets back the features it should attach rather than having to know which
|
|
114
|
+
* code paths each one owns. Returns `unmatchedPaths` too, because a path no
|
|
115
|
+
* feature owns is itself information — it usually means the feature map has a
|
|
116
|
+
* gap.
|
|
117
|
+
*
|
|
118
|
+
* Split by what the engine would do with them: a feature that solely owns the
|
|
119
|
+
* path links on its own; a path several features share is a choice for the
|
|
120
|
+
* caller to make.
|
|
116
121
|
*/
|
|
117
122
|
export async function resolveLinks({ projectId, paths }) {
|
|
118
|
-
const {
|
|
123
|
+
const { features, unresolved } = await resolvePathsToFeatures({ projectId, paths });
|
|
119
124
|
return {
|
|
120
|
-
|
|
121
|
-
|
|
125
|
+
features: {
|
|
126
|
+
owned: features.filter((f) => !f.ambiguous),
|
|
127
|
+
shared: features.filter((f) => f.ambiguous),
|
|
128
|
+
},
|
|
122
129
|
unmatchedPaths: unresolved,
|
|
123
|
-
count:
|
|
130
|
+
count: features.length,
|
|
124
131
|
};
|
|
125
132
|
}
|
|
126
133
|
|
|
@@ -135,12 +142,11 @@ export async function previewLinks({
|
|
|
135
142
|
subjectType,
|
|
136
143
|
subjectId,
|
|
137
144
|
paths,
|
|
138
|
-
componentId,
|
|
139
145
|
epicId,
|
|
140
146
|
trigger,
|
|
141
147
|
}) {
|
|
142
148
|
const { proposals } = await previewEntityLinks({
|
|
143
|
-
projectId, subjectType, subjectId, paths,
|
|
149
|
+
projectId, subjectType, subjectId, paths, epicId, trigger,
|
|
144
150
|
});
|
|
145
151
|
const { autoLinked, linkSuggestions } = partitionProposals(proposals);
|
|
146
152
|
|
package/handlers/projects.js
CHANGED
|
@@ -46,7 +46,7 @@ async function createProject(args) {
|
|
|
46
46
|
// Update an existing project. Only the fields present in `args` are changed —
|
|
47
47
|
// the server leaves anything unmentioned alone. This is the path that lets an
|
|
48
48
|
// agent set gitUrl/gitProvider after creation, which is what commit-to-task
|
|
49
|
-
// linking and
|
|
49
|
+
// linking and feature link resolution key off.
|
|
50
50
|
async function updateProject(args) {
|
|
51
51
|
return callZephlyAPI('mcpUpdateProject', args);
|
|
52
52
|
}
|
package/handlers/tasks.js
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Project-First Hierarchy: Tasks belong to projects (required), with optional epic grouping
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* Code links are derived: the files a task touches (changedFiles / linkedFiles)
|
|
8
|
+
* resolve to the features that own those paths (E-258). Agents name the feature
|
|
9
|
+
* the work advances through `links`.
|
|
9
10
|
* Tags are auto-assigned based on content analysis against the local project cache.
|
|
10
11
|
*/
|
|
11
12
|
|
|
@@ -112,7 +113,7 @@ async function createTask(args) {
|
|
|
112
113
|
}
|
|
113
114
|
}
|
|
114
115
|
|
|
115
|
-
// Create the task
|
|
116
|
+
// Create the task
|
|
116
117
|
const result = await callZephlyAPI('mcpCreateTask', createArgs);
|
|
117
118
|
|
|
118
119
|
// If the milestone is frozen and the operation was blocked, return guidance
|
|
@@ -159,7 +160,6 @@ async function createTask(args) {
|
|
|
159
160
|
subjectType: 'task',
|
|
160
161
|
subjectId: result.taskId,
|
|
161
162
|
paths: (createArgs.linkedFiles || []).map((f) => f.path),
|
|
162
|
-
componentId: createArgs.componentId,
|
|
163
163
|
epicId: createArgs.epicId,
|
|
164
164
|
});
|
|
165
165
|
const { autoLinked, linkSuggestions } = partitionProposals(proposals);
|
|
@@ -191,20 +191,6 @@ async function createTask(args) {
|
|
|
191
191
|
}
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
-
// If no component was named, include available components as a hint.
|
|
195
|
-
//
|
|
196
|
-
// Tests the EFFECTIVE set, not just the deprecated singular. Checking only
|
|
197
|
-
// `componentId` meant a create that correctly used `componentIds` was told it
|
|
198
|
-
// had named no component — while the links had in fact been written — so
|
|
199
|
-
// agents kept issuing a redundant follow-up update to fix nothing (#2429).
|
|
200
|
-
const namedComponents = Boolean(args.componentId)
|
|
201
|
-
|| (Array.isArray(args.componentIds) && args.componentIds.some(Boolean));
|
|
202
|
-
if (!namedComponents && autoAssign?.availableComponents?.length) {
|
|
203
|
-
result.warning = 'Task created without a component. Please provide componentIds '
|
|
204
|
-
+ 'for better organization. Use get_current_project_context() to see available components.';
|
|
205
|
-
result.availableComponents = autoAssign.availableComponents;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
194
|
// Attach create-time links (E-225) — best effort, never fails the create.
|
|
209
195
|
await attachLinks(result, {
|
|
210
196
|
sourceType: 'task',
|
|
@@ -282,7 +268,7 @@ function buildUntrackedDescription({ description, branch, changedFiles }) {
|
|
|
282
268
|
* that would duplicate everything already created.
|
|
283
269
|
*/
|
|
284
270
|
export async function bulkCreateTasks(args) {
|
|
285
|
-
const { projectId, epicId,
|
|
271
|
+
const { projectId, epicId, tasks = [] } = args;
|
|
286
272
|
|
|
287
273
|
if (!projectId) throw new Error('projectId is required');
|
|
288
274
|
if (!Array.isArray(tasks) || tasks.length === 0) {
|
|
@@ -300,8 +286,6 @@ export async function bulkCreateTasks(args) {
|
|
|
300
286
|
const result = await callZephlyAPI('mcpBulkCreateTasks', {
|
|
301
287
|
projectId,
|
|
302
288
|
epicId,
|
|
303
|
-
componentId,
|
|
304
|
-
componentIds,
|
|
305
289
|
tasks: tasks.map(({ changedFiles, linkedFiles, ...rest }) => ({
|
|
306
290
|
...rest,
|
|
307
291
|
// Accept the same two spellings as manage_task so callers do not have to
|
|
@@ -344,8 +328,6 @@ export async function reportUntrackedWork(args) {
|
|
|
344
328
|
projectId,
|
|
345
329
|
title,
|
|
346
330
|
description,
|
|
347
|
-
componentId,
|
|
348
|
-
componentIds,
|
|
349
331
|
origin = 'untracked',
|
|
350
332
|
discoveredDuringTaskId,
|
|
351
333
|
branch,
|
|
@@ -372,8 +354,6 @@ export async function reportUntrackedWork(args) {
|
|
|
372
354
|
status: 'in_progress',
|
|
373
355
|
origin,
|
|
374
356
|
...(discoveredDuringTaskId ? { discoveredDuringTaskId } : {}),
|
|
375
|
-
...(componentId ? { componentId } : {}),
|
|
376
|
-
...(Array.isArray(componentIds) && componentIds.length > 0 ? { componentIds } : {}),
|
|
377
357
|
...(epicId ? { epicId } : {}),
|
|
378
358
|
...(allLinks.length > 0 ? { links: allLinks } : {}),
|
|
379
359
|
...(Array.isArray(changedFiles) && changedFiles.length > 0
|
|
@@ -508,8 +488,8 @@ export async function getTask(args) {
|
|
|
508
488
|
//
|
|
509
489
|
// `files` has always been optional and callers routinely omit it, which costs
|
|
510
490
|
// more than it looks: without paths the task is invisible to auto-linking
|
|
511
|
-
// forever, since the engine has nothing to resolve against the
|
|
512
|
-
//
|
|
491
|
+
// forever, since the engine has nothing to resolve against the paths features
|
|
492
|
+
// own. Measured on saltpig, 359 of 1224 tasks carrying a commit had no
|
|
513
493
|
// linked files at all.
|
|
514
494
|
//
|
|
515
495
|
// The fix is derivation rather than discipline. The commit SHA is already
|
package/lib/auto-assign.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Auto-assign Utility
|
|
3
|
-
* Matches task/epic content against cached tags
|
|
4
|
-
*
|
|
3
|
+
* Matches task/epic content against cached tags for automatic assignment
|
|
4
|
+
* during creation.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { readConfig } from './local-cache.js';
|
|
@@ -13,20 +13,6 @@ function escapeRegex(str) {
|
|
|
13
13
|
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
/**
|
|
17
|
-
* Match components against text content using word boundary matching.
|
|
18
|
-
* @param {string} text - Combined title + description
|
|
19
|
-
* @param {Array} components - Array of {id, name, description}
|
|
20
|
-
* @returns {Array} Matching component objects
|
|
21
|
-
*/
|
|
22
|
-
export function matchComponents(text, components) {
|
|
23
|
-
if (!text || !components?.length) return [];
|
|
24
|
-
return components.filter((c) => {
|
|
25
|
-
const regex = new RegExp(`\\b${escapeRegex(c.name)}\\b`, 'i');
|
|
26
|
-
return regex.test(text);
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
|
|
30
16
|
/**
|
|
31
17
|
* Match tags against text content using word boundary matching.
|
|
32
18
|
* @param {string} text - Combined title + description
|
|
@@ -43,10 +29,7 @@ export function matchTags(text, tags) {
|
|
|
43
29
|
|
|
44
30
|
/**
|
|
45
31
|
* Resolve auto-assignment for a task being created.
|
|
46
|
-
* Returns { matchedTags,
|
|
47
|
-
*
|
|
48
|
-
* Components are NOT auto-assigned — agents must explicitly provide componentId.
|
|
49
|
-
* This function returns available components so the handler can hint at them if needed.
|
|
32
|
+
* Returns { matchedTags, organizationId } or null if no cache available.
|
|
50
33
|
*
|
|
51
34
|
* @param {string} projectId - The task's project ID
|
|
52
35
|
* @param {string} title - Task title
|
|
@@ -59,12 +42,10 @@ export async function resolveTaskAutoAssign(projectId, title, description) {
|
|
|
59
42
|
|
|
60
43
|
const text = `${title || ''} ${description || ''}`.trim();
|
|
61
44
|
|
|
62
|
-
const components = config.projectId === projectId ? (config.components || []) : [];
|
|
63
45
|
const tags = config.tags || [];
|
|
64
46
|
|
|
65
47
|
return {
|
|
66
48
|
matchedTags: text ? matchTags(text, tags) : [],
|
|
67
|
-
availableComponents: components.map((c) => ({ id: c.id, name: c.name })),
|
|
68
49
|
organizationId: config.organizationId || null,
|
|
69
50
|
};
|
|
70
51
|
}
|
package/lib/autolink.js
CHANGED
|
@@ -3,7 +3,7 @@ import { callZephlyAPI } from './http-client.js';
|
|
|
3
3
|
/**
|
|
4
4
|
* Auto-linking helpers (E-225).
|
|
5
5
|
*
|
|
6
|
-
* The API resolves file paths to the
|
|
6
|
+
* The API resolves file paths to the features that own them and previews what
|
|
7
7
|
* the autolink engine would propose. Both are read-only, so an agent can ask
|
|
8
8
|
* "what does this touch?" before it creates or commits anything.
|
|
9
9
|
*
|
|
@@ -14,27 +14,31 @@ import { callZephlyAPI } from './http-client.js';
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
* Resolve repo-relative file paths to the
|
|
17
|
+
* Resolve repo-relative file paths to the features that own them through their
|
|
18
|
+
* code paths (E-258).
|
|
19
|
+
*
|
|
20
|
+
* The API's response may still carry a component `matches` array (components
|
|
21
|
+
* were retired after this server shipped); it is ignored.
|
|
18
22
|
*
|
|
19
23
|
* @param {object} params
|
|
20
24
|
* @param {string} params.projectId
|
|
21
25
|
* @param {string[]} params.paths - repo-relative paths
|
|
22
|
-
* @returns {Promise<{
|
|
26
|
+
* @returns {Promise<{features: object[], unresolved: string[]}>}
|
|
23
27
|
*/
|
|
24
|
-
export async function
|
|
28
|
+
export async function resolvePathsToFeatures({ projectId, paths }) {
|
|
25
29
|
if (!projectId || !Array.isArray(paths) || paths.length === 0) {
|
|
26
|
-
return {
|
|
30
|
+
return { features: [], unresolved: [] };
|
|
27
31
|
}
|
|
28
32
|
try {
|
|
29
33
|
const result = await callZephlyAPI('mcpResolvePaths', { projectId, paths });
|
|
30
34
|
return {
|
|
31
|
-
|
|
35
|
+
features: result?.features || [],
|
|
32
36
|
unresolved: result?.unresolved || [],
|
|
33
37
|
};
|
|
34
38
|
} catch {
|
|
35
39
|
// An API predating E-225 has no such route. Report nothing rather than
|
|
36
40
|
// surfacing a 404 the agent can do nothing about.
|
|
37
|
-
return {
|
|
41
|
+
return { features: [], unresolved: paths };
|
|
38
42
|
}
|
|
39
43
|
}
|
|
40
44
|
|
|
@@ -50,7 +54,6 @@ export async function resolvePathsToComponents({ projectId, paths }) {
|
|
|
50
54
|
* @param {string} params.subjectId
|
|
51
55
|
* @param {string[]} [params.paths]
|
|
52
56
|
* @param {string} [params.trigger]
|
|
53
|
-
* @param {string} [params.componentId]
|
|
54
57
|
* @param {string} [params.epicId]
|
|
55
58
|
* @returns {Promise<{proposals: object[]}>}
|
|
56
59
|
*/
|
|
@@ -60,7 +63,6 @@ export async function previewEntityLinks({
|
|
|
60
63
|
subjectId,
|
|
61
64
|
paths,
|
|
62
65
|
trigger,
|
|
63
|
-
componentId,
|
|
64
66
|
epicId,
|
|
65
67
|
}) {
|
|
66
68
|
if (!subjectType || !subjectId) return { proposals: [] };
|
|
@@ -71,7 +73,6 @@ export async function previewEntityLinks({
|
|
|
71
73
|
subjectId,
|
|
72
74
|
paths,
|
|
73
75
|
trigger,
|
|
74
|
-
componentId,
|
|
75
76
|
epicId,
|
|
76
77
|
});
|
|
77
78
|
return { proposals: result?.proposals || [] };
|
|
@@ -9,6 +9,6 @@
|
|
|
9
9
|
* __tests__/instructions.test.js fails if this drifts from the skill.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
export const WORK_TRACKING_CORE =
|
|
12
|
+
export const WORK_TRACKING_CORE = "Work in this repository is tracked in EzModo, and the tools for it are on this\nMCP server. The contract, in short:\n\n1. **Create the task before you edit, not after.** A task written afterwards is\n a changelog; a task written first is what the next session reads to find out\n what you were doing and why.\n2. **Start the session by calling `get_current_project_context()`.** Cache the\n `projectId`. It also returns the tags and `terminology` you need.\n No `.ezmodo/config.json` means this repo is not tracked — say so rather than\n guessing at a project.\n3. **Call `get_context` with a keyword query before creating anything.** Use\n what comes back to write a task that names real files, endpoints and\n patterns. A vague task is not worth the call that made it.\n4. **Link the feature the work advances** via `links: [{targetType:\"feature\",\n targetId}]`, found with `search_features`. Code links are derived: features\n own code paths, and the files you touch link the work to their owners.\n5. **Set `taskType`** — `feature` | `bug` | `testing` | `chore`. Not cosmetic:\n `bug` feeds open-bug counts, milestone freezes gate on it, and estimation\n weights past tasks of the same kind.\n6. **Toggle steps as you finish them**, not in a batch at the end\n (`manage_task action:\"update\" toggleSteps:[...]`).\n7. **Capture knowledge the moment it happens**, not in a summary at the end:\n `addKnowledge` with `fact` for a root cause, `decision` for a choice —\n including what you rejected and why — `reference` for a key file or pattern,\n `context` for progress. Be specific: file paths, function names, exact\n errors. \"Fixed a bug in the parser\" helps nobody.\n8. **Already three edits in with no task?** Call `report_untracked_work` the\n moment you notice, rather than continuing untracked.\n9. **Resuming?** `get_task` first, and read ALL of its knowledge items. That is\n where the previous session's reasoning went — do not re-derive it.\n10. **Finish at `in_review`** with `completionNotes`, and do NOT call\n `action:\"complete\"`. A human completes a task after verifying it.\n11. **Report what actually happened.** A task moved to `in_review` claiming work\n that was not done is worse than no task, because the next session trusts it.\n\nRespect the project's `terminology`: a project can rename epics and tasks, and a\nmarketing project calls an epic a \"Campaign\". Write anything a\nhuman reads in those words; keep API field names (`epicId`, `taskId`) as they\nare.";
|
|
13
13
|
|
|
14
|
-
export const WORK_TRACKING_LOCAL =
|
|
14
|
+
export const WORK_TRACKING_LOCAL = "Running against a local checkout, two more:\n\n12. **Link every commit**: `manage_task action:\"link_commit\"` with the full\n 40-character `sha` from `git rev-parse HEAD`. A short SHA is rejected, and\n padding one is not a fix. Linking is also what derives feature links from\n the commit's files — do not link those by hand.\n13. **Pass `changedFiles`** when creating or updating a task, so the work\n resolves to the features that own those paths.";
|