@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.
@@ -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
@@ -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, or snapshot.`);
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 component grouping and milestone linking.
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.
@@ -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
  }
@@ -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
- - \`componentId\` — pick the single most relevant component from the
172
- project context. Each task belongs to exactly one component.
173
- Get the list via \`get_current_project_context()\`.
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
- // components and tags because it changes about as often, and an
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 components and tags lists
618
- const [components, tags] = await Promise.all([
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
- config.components = components;
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 components and tags
840
- const [components, tags] = await Promise.all([
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
- resolvePathsToComponents,
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 components own these files?
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 entities it should attach rather than having to know the
114
- * component inventory. Returns `unresolved` too, because a path nothing covers
115
- * is itself information — it usually means the inventory has a gap.
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 { matches, unresolved } = await resolvePathsToComponents({ projectId, paths });
123
+ const { features, unresolved } = await resolvePathsToFeatures({ projectId, paths });
119
124
  return {
120
- deterministic: matches.filter((m) => m.score >= 1),
121
- probable: matches.filter((m) => m.score < 1),
125
+ features: {
126
+ owned: features.filter((f) => !f.ambiguous),
127
+ shared: features.filter((f) => f.ambiguous),
128
+ },
122
129
  unmatchedPaths: unresolved,
123
- count: matches.length,
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, componentId, epicId, trigger,
149
+ projectId, subjectType, subjectId, paths, epicId, trigger,
144
150
  });
145
151
  const { autoLinked, linkSuggestions } = partitionProposals(proposals);
146
152
 
@@ -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 component link resolution key off.
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
- * Component assignment: Each task should belong to exactly one component.
8
- * AI agents must explicitly provide componentId (from get_current_project_context).
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 (componentId should be explicitly provided by the agent)
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, componentId, componentIds, tasks = [] } = args;
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 component
512
- // inventory. Measured on saltpig, 359 of 1224 tasks carrying a commit had no
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
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Auto-assign Utility
3
- * Matches task/epic content against cached tags and components
4
- * for automatic assignment during creation.
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, availableComponents, organizationId } or null if no cache available.
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 components that own them and previews what
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 components that own them.
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<{matches: object[], unresolved: string[]}>}
26
+ * @returns {Promise<{features: object[], unresolved: string[]}>}
23
27
  */
24
- export async function resolvePathsToComponents({ projectId, paths }) {
28
+ export async function resolvePathsToFeatures({ projectId, paths }) {
25
29
  if (!projectId || !Array.isArray(paths) || paths.length === 0) {
26
- return { matches: [], unresolved: [] };
30
+ return { features: [], unresolved: [] };
27
31
  }
28
32
  try {
29
33
  const result = await callZephlyAPI('mcpResolvePaths', { projectId, paths });
30
34
  return {
31
- matches: result?.matches || [],
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 { matches: [], unresolved: paths };
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 = '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 components, 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. **Name every component the work touches** via `componentIds`. A task spanning\n web and api belongs to both. The list REPLACES the previous set on update.\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, tasks and\ncomponents, and a marketing project calls an epic a "Campaign". Write anything a\nhuman reads in those words; keep API field names (`epicId`, `taskId`) as they\nare.';
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 = '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 component 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 components that own those paths.';
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.";