@ezmodo/mcp-server 0.14.4 → 0.18.0

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.
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Unmapped code path handlers (E-258 #2756)
3
+ *
4
+ * Thin wrappers over /api/mcp/v1/unmapped-paths. Dispatch and validation live
5
+ * server-side in core/unmappedpaths.Service; these only shape the arguments.
6
+ */
7
+
8
+ import { callZephlyAPI } from '../lib/http-client.js';
9
+
10
+ /**
11
+ * List a project's unmapped paths, pending by default.
12
+ */
13
+ export async function listUnmappedPaths({ projectId, status }) {
14
+ if (!projectId) {
15
+ throw new Error('projectId is required');
16
+ }
17
+ const params = { projectId };
18
+ if (status) params.status = status;
19
+ return callZephlyAPI('mcpListUnmappedPaths', params);
20
+ }
21
+
22
+ /**
23
+ * Dispatch resolve_unmapped actions.
24
+ */
25
+ export async function resolveUnmapped(args) {
26
+ const { action, ...params } = args;
27
+ switch (action) {
28
+ case 'reconcile': return reconcileUnmappedPaths(params);
29
+ case 'assign': return assignUnmappedPath(params);
30
+ case 'dismiss': return dismissUnmappedPath(params);
31
+ default:
32
+ throw new Error(`Unknown action: ${action}. Expected reconcile, assign, or dismiss.`);
33
+ }
34
+ }
35
+
36
+ // Close every pending row an existing feature path already covers. Rows several
37
+ // features could claim are left pending on purpose.
38
+ async function reconcileUnmappedPaths({ projectId }) {
39
+ requireProject(projectId, 'reconcile');
40
+ return callZephlyAPI('mcpReconcileUnmappedPaths', { projectId });
41
+ }
42
+
43
+ async function assignUnmappedPath({ projectId, pathId, featureId }) {
44
+ requireProject(projectId, 'assign');
45
+ if (!pathId) throw new Error('pathId is required for assign');
46
+ if (!featureId) throw new Error('featureId is required for assign');
47
+ return callZephlyAPI('mcpAssignUnmappedPath', { projectId, pathId, featureId });
48
+ }
49
+
50
+ async function dismissUnmappedPath({ projectId, pathId }) {
51
+ requireProject(projectId, 'dismiss');
52
+ if (!pathId) throw new Error('pathId is required for dismiss');
53
+ return callZephlyAPI('mcpDismissUnmappedPath', { projectId, pathId });
54
+ }
55
+
56
+ function requireProject(projectId, action) {
57
+ if (!projectId) {
58
+ throw new Error(`projectId is required for ${action}`);
59
+ }
60
+ }
@@ -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 || [] };
@@ -172,6 +172,12 @@ export async function callZephlyAPI(endpoint, data) {
172
172
  if (typeof error.retryable === 'boolean') {
173
173
  thrown.retryable = error.retryable;
174
174
  }
175
+ // Structured details, when the API sent an object rather than a sentence —
176
+ // a plan conflict (E-259) carries the current plan and what changed, which
177
+ // is exactly what the agent needs to redo its edit.
178
+ if (error.details && typeof error.details === 'object') {
179
+ thrown.details = error.details;
180
+ }
175
181
  throw thrown;
176
182
  }
177
183
 
@@ -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.";
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Local Cache Utility
3
- * Reads and writes the project config.json cache for tags and components.
3
+ * Reads and writes the project config.json cache for tags.
4
4
  * Looks in `.ezmodo/` first, falls back to legacy `.zephly/`.
5
5
  * Used by MCP handlers to avoid unnecessary API calls for frequently-read data.
6
6
  */
@@ -94,23 +94,9 @@ export async function getCachedTags() {
94
94
  return config.tags || null;
95
95
  }
96
96
 
97
- /**
98
- * Get cached components for a specific project if the cache is fresh.
99
- * @param {string} projectId - Only return components if they match this project
100
- * @returns {Promise<Array|null>} Cached components array, or null if stale/missing/wrong project
101
- */
102
- export async function getCachedComponents(projectId) {
103
- const config = await readConfig();
104
- if (!config) return null;
105
- if (!isCacheFresh(config.lastUpdatedAt)) return null;
106
- // Only return if the cached config belongs to this project
107
- if (config.projectId !== projectId) return null;
108
- return config.components || null;
109
- }
110
-
111
97
  /**
112
98
  * Update specific sections in the config cache. Non-fatal on failure.
113
- * @param {object} updates - Key-value pairs to merge into config (e.g., { tags: [...], components: [...] })
99
+ * @param {object} updates - Key-value pairs to merge into config (e.g., { tags: [...] })
114
100
  */
115
101
  export async function updateCacheSections(updates) {
116
102
  try {
@@ -126,7 +112,7 @@ export async function updateCacheSections(updates) {
126
112
  /**
127
113
  * Invalidate a specific cache section by removing it from config.
128
114
  * The next read will return null, triggering a fresh API call.
129
- * @param {string} section - 'tags' or 'components'
115
+ * @param {string} section - e.g. 'tags'
130
116
  */
131
117
  export async function invalidateCacheSection(section) {
132
118
  try {
@@ -58,6 +58,7 @@ export const LOCAL_ONLY_TOOLS = Object.freeze([
58
58
  /** Tools safe to serve over the remote transport. */
59
59
  export const REMOTE_SAFE_TOOLS = Object.freeze([
60
60
  'accept_agent_suggestion',
61
+ 'add_epic_comment',
61
62
  'configure_agent',
62
63
  'create_tasks',
63
64
  'delete_attachment',
@@ -75,6 +76,7 @@ export const REMOTE_SAFE_TOOLS = Object.freeze([
75
76
  'get_document',
76
77
  'get_document_template',
77
78
  'get_epic',
79
+ 'get_epic_plan',
78
80
  'get_feature',
79
81
  'get_feature_flag',
80
82
  'get_goal',
@@ -93,8 +95,9 @@ export const REMOTE_SAFE_TOOLS = Object.freeze([
93
95
  'list_attachments',
94
96
  'list_catalog_items',
95
97
  'list_catalogs',
96
- 'list_components',
98
+ 'list_unmapped_paths',
97
99
  'list_designs',
100
+ 'list_epic_comments',
98
101
  'list_epics',
99
102
  'list_facts',
100
103
  'list_feature_flags',
@@ -110,7 +113,6 @@ export const REMOTE_SAFE_TOOLS = Object.freeze([
110
113
  'list_watched',
111
114
  'manage_access',
112
115
  'manage_catalog',
113
- 'manage_component',
114
116
  'manage_decision',
115
117
  'manage_design',
116
118
  'manage_document',
@@ -141,10 +143,12 @@ export const REMOTE_SAFE_TOOLS = Object.freeze([
141
143
  'resolve_concepts',
142
144
  'resolve_link_suggestions',
143
145
  'resolve_links',
146
+ 'resolve_unmapped',
144
147
  'run_agent_now',
145
148
  'search_epics',
146
149
  'search_features',
147
150
  'search_tasks',
151
+ 'update_epic_plan',
148
152
  'update_manifest_entries',
149
153
  'validate_manifest',
150
154
  ]);
package/lib/version.js CHANGED
@@ -7,4 +7,4 @@
7
7
  *
8
8
  * Update this when bumping the version in package.json.
9
9
  */
10
- export const MCP_VERSION = '0.14.4';
10
+ export const MCP_VERSION = '0.18.0';
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@ezmodo/mcp-server",
3
- "version": "0.14.4",
3
+ "version": "0.18.0",
4
4
  "description": "MCP server for ezmodo - AI-first project management",
5
5
  "main": "index.js",
6
6
  "type": "module",
7
7
  "bin": {
8
- "mcp-server": "./index.js",
9
- "ezmodo-mcp-server": "./index.js",
10
- "zephly-mcp-server": "./index.js",
11
- "ezmodo-mcp-server-http": "./http.js"
8
+ "mcp-server": "index.js",
9
+ "ezmodo-mcp-server": "index.js",
10
+ "zephly-mcp-server": "index.js",
11
+ "ezmodo-mcp-server-http": "http.js"
12
12
  },
13
13
  "scripts": {
14
14
  "dev": "BUILD_ENV=development node index.js",
@@ -18,7 +18,7 @@ export const COMMAND_PROMPTS = [
18
18
  "local",
19
19
  "remote"
20
20
  ],
21
- "body": "Start tracked work on: **$ARGUMENTS**\n\nFollow the work-tracking contract in this server's instructions. In short:\n\n1. `get_current_project_context()` — cache the `projectId`, note the components,\n tags and `terminology`.\n2. `get_context` with a keyword query drawn from the request above. Read what\n comes back before writing anything: it tells you which files exist, what\n patterns they follow, and what the change will touch.\n3. `resolve_links` on the paths you expect to change. A component you did not\n expect means the work is broader than the request sounds.\n4. Create the work:\n - **Single scope** (a fix, a small feature, a config or docs change) —\n `manage_task action:\"create\"` with `status:\"in_progress\"`, a description\n that says why/where/how, steps that name real files, `componentIds` for\n every component involved, and the right `taskType`.\n - **Multi scope** (spanning areas, or a large refactor) — `manage_epic\n action:\"create\"` with its child tasks in the same request, ordered by\n dependency.\n\nThen report the task number and web URL and begin. Do not edit anything before\nthe task exists — a task created afterwards is a task written from memory.\n\nIf no `.ezmodo/config.json` is found, say so and stop rather than guessing at a\nproject."
21
+ "body": "Start tracked work on: **$ARGUMENTS**\n\nFollow the work-tracking contract in this server's instructions. In short:\n\n1. `get_current_project_context()` — cache the `projectId`, note the tags and\n `terminology`.\n2. `get_context` with a keyword query drawn from the request above. Read what\n comes back before writing anything: it tells you which files exist, what\n patterns they follow, and what the change will touch.\n3. `resolve_links` on the paths you expect to change. A feature you did not\n expect means the work is broader than the request sounds.\n4. Create the work:\n - **Single scope** (a fix, a small feature, a config or docs change) —\n `manage_task action:\"create\"` with `status:\"in_progress\"`, a description\n that says why/where/how, steps that name real files, `changedFiles` for\n the paths involved, a `links` entry for the feature it advances, and the\n right `taskType`.\n - **Multi scope** (spanning several parts of the codebase, or a large refactor) — `manage_epic\n action:\"create\"` with its child tasks in the same request, ordered by\n dependency.\n\nThen report the task number and web URL and begin. Do not edit anything before\nthe task exists — a task created afterwards is a task written from memory.\n\nIf no `.ezmodo/config.json` is found, say so and stop rather than guessing at a\nproject."
22
22
  },
23
23
  {
24
24
  "name": "resume",
@@ -47,6 +47,6 @@ export const COMMAND_PROMPTS = [
47
47
  "local",
48
48
  "remote"
49
49
  ],
50
- "body": "Capture the current uncommitted work as a task.\n\nCurrent branch: run `git rev-parse --abbrev-ref HEAD` and use its output\nChanged files: run `git status --porcelain` and use its output\n\nUse `report_untracked_work` with:\n\n- `projectId` from `get_current_project_context()`\n- `title` — concise, describing what was actually done\n- `description` — what and **why**. Do not restate the branch or file list; they\n are appended automatically as evidence.\n- `changedFiles` — the paths above\n- `branch` — as above\n- `componentIds` — resolve the changed paths with `resolve_links` rather than\n guessing; untracked work often spans more than one area, which is part of why\n it went untracked\n- `origin`:\n - `discovered` — found while working on another task (set `discoveredDuringTaskId`)\n - `scope-creep` — went beyond the active task's scope (set `discoveredDuringTaskId`)\n - `rework` — redoing prior work\n - `untracked` — unplanned standalone work (the default)\n\nIf there is an active task in this session, prefer `discovered` or\n`scope-creep` and link it — the discovery chain is the point of the\nclassification.\n\nThe new task comes back `in_progress` and becomes the active one. Track against\nit for the rest of the work.\n\nExtra context from the user, if any: $ARGUMENTS"
50
+ "body": "Capture the current uncommitted work as a task.\n\nCurrent branch: run `git rev-parse --abbrev-ref HEAD` and use its output\nChanged files: run `git status --porcelain` and use its output\n\nUse `report_untracked_work` with:\n\n- `projectId` from `get_current_project_context()`\n- `title` — concise, describing what was actually done\n- `description` — what and **why**. Do not restate the branch or file list; they\n are appended automatically as evidence.\n- `changedFiles` — the paths above\n- `branch` — as above\n- `origin`:\n - `discovered` — found while working on another task (set `discoveredDuringTaskId`)\n - `scope-creep` — went beyond the active task's scope (set `discoveredDuringTaskId`)\n - `rework` — redoing prior work\n - `untracked` — unplanned standalone work (the default)\n\nIf there is an active task in this session, prefer `discovered` or\n`scope-creep` and link it — the discovery chain is the point of the\nclassification.\n\nThe new task comes back `in_progress` and becomes the active one. Track against\nit for the rest of the work.\n\nExtra context from the user, if any: $ARGUMENTS"
51
51
  }
52
52
  ];
package/tools/catalogs.js CHANGED
@@ -80,8 +80,14 @@ export const CATALOG_TOOLS = [
80
80
  name: 'manage_catalog',
81
81
  description: 'Create, update, or delete an org-level Catalog, link/unlink it to other artifacts, ' +
82
82
  'or push a new versioned snapshot of its contents. A Catalog is a generalized, code-derived ' +
83
- 'catalog (kind = notifications | analytics_events | db_schema | custom); its versions are ' +
83
+ 'catalog (kind = notifications | analytics_events | db_schema | screens | custom); its versions are ' +
84
84
  'immutable { columns, items } snapshots.\n\n' +
85
+ 'SCREENS (E-258): each project has one kind:"screens" catalog, its UI inventory (screens and pages, ' +
86
+ 'keyed by source path). Do not snapshot it by hand: action:"discover_screens" (projectId) lists what ' +
87
+ 'the Context Manifest shows that the catalog lacks, and action:"import_screens" (projectId, screens, ' +
88
+ 'optional featureId) adds them and links each to the feature. action:"sync_screens" (projectId) ' +
89
+ 'reconciles the catalog with the manifest and re-derives the screen flow map (navigates_to edges ' +
90
+ 'between screens); it cannot remove a hand-added screen or a human-confirmed edge.\n\n' +
85
91
  'CAPTURE-AT-BUILD (important): whenever you change the catalog\'s source of truth in code, call ' +
86
92
  'action:"snapshot" and put the source path/commit in `source`. The server is checksum-gated — ' +
87
93
  're-snapshotting unchanged content creates no new version and is a cheap no-op — so it is safe ' +
@@ -97,7 +103,7 @@ export const CATALOG_TOOLS = [
97
103
  '`go run ./cmd/dbschema-snapshot -upload -catalog-id <id>` from api/ introspects and uploads the ' +
98
104
  'whole schema without passing it through the model at all — cheaper still than a patch.\n\n' +
99
105
  'LINK, DON\'T CONTAIN: use action:"link"/"unlink" to relate a catalog to a feature, feature_flag, ' +
100
- 'epic, task, document, component, milestone, goal or project (e.g. link the notification catalog ' +
106
+ 'epic, task, document, milestone, goal or project (e.g. link the notification catalog ' +
101
107
  'to its Notifications feature and the ezmodo doc that describes it).\n\n' +
102
108
  'ITEM-LEVEL LINKS (E-218): pass `itemKey` on action:"link"/"unlink" to attach the link to a SINGLE ' +
103
109
  'catalog entry instead of the whole catalog — this is how a task/epic that builds one entry records ' +
@@ -109,7 +115,8 @@ export const CATALOG_TOOLS = [
109
115
  properties: {
110
116
  action: {
111
117
  type: 'string',
112
- enum: ['create', 'update', 'delete', 'link', 'unlink', 'snapshot'],
118
+ enum: ['create', 'update', 'delete', 'link', 'unlink', 'snapshot', 'discover_screens', 'import_screens',
119
+ 'sync_screens'],
113
120
  description: 'Action to perform. "snapshot" pushes a new version (checksum-gated) — send a ' +
114
121
  'delta with mode:"patch" unless this is the catalog\'s first snapshot. "link"/"unlink" ' +
115
122
  'manage relates_to edges to other artifacts.',
@@ -127,10 +134,30 @@ export const CATALOG_TOOLS = [
127
134
  type: 'string',
128
135
  description: 'Catalog ID (required for update, delete, link, unlink, snapshot)',
129
136
  },
137
+ // --- Screens (E-258) ---
138
+ screens: {
139
+ type: 'array',
140
+ description: 'Screens to add for import_screens, usually taken from discover_screens.',
141
+ items: {
142
+ type: 'object',
143
+ properties: {
144
+ name: { type: 'string', description: 'Display name, e.g. "CookSessionDetailScreen"' },
145
+ sourcePath: { type: 'string', description: 'Repo-relative file; becomes the item key' },
146
+ kind: { type: 'string', enum: ['screen', 'page'], description: 'Default "screen"' },
147
+ route: { type: 'string', description: 'Route, for web pages' },
148
+ framework: { type: 'string', description: 'e.g. "nextjs", "flutter"' },
149
+ },
150
+ required: ['name', 'sourcePath'],
151
+ },
152
+ },
153
+ featureId: {
154
+ type: 'string',
155
+ description: 'Feature to link every imported screen to (import_screens)',
156
+ },
130
157
  // --- Create / update fields ---
131
158
  kind: {
132
159
  type: 'string',
133
- enum: ['db_schema', 'notifications', 'analytics_events', 'custom'],
160
+ enum: ['db_schema', 'notifications', 'analytics_events', 'screens', 'custom'],
134
161
  description: 'What the catalog catalogs (default "custom") (create, update)',
135
162
  },
136
163
  name: {
@@ -276,7 +303,7 @@ export const CATALOG_TOOLS = [
276
303
  },
277
304
  kind: {
278
305
  type: 'string',
279
- enum: ['db_schema', 'notifications', 'analytics_events', 'custom'],
306
+ enum: ['db_schema', 'notifications', 'analytics_events', 'screens', 'custom'],
280
307
  description: 'Filter to a single kind',
281
308
  },
282
309
  linkedType: {
@@ -13,7 +13,7 @@ export const CONTEXT_MANIFEST_TOOLS = [
13
13
  {
14
14
  name: 'get_context',
15
15
  description:
16
- 'Get contextual information about any entity (project, task, epic, component, file, tag). ' +
16
+ 'Get contextual information about any entity (project, task, epic, file, tag). ' +
17
17
  'Replaces search_project_context, get_related_files, get_project_overview, get_critical_files, ' +
18
18
  'query_project_graph, analyze_impact, get_graph_stats, analyze_project_organization. ' +
19
19
  'Use `query` for keyword search, or `entityType` + `entityId` for entity-specific context. ' +
@@ -27,7 +27,7 @@ export const CONTEXT_MANIFEST_TOOLS = [
27
27
  },
28
28
  entityType: {
29
29
  type: 'string',
30
- enum: ['project', 'task', 'epic', 'component', 'file', 'tag'],
30
+ enum: ['project', 'task', 'epic', 'file', 'tag'],
31
31
  description: 'Type of entity to get context for',
32
32
  },
33
33
  entityId: {
@@ -11,6 +11,11 @@
11
11
  * Decisions capture "why we built it this way" so the rationale survives the work
12
12
  * that produced it. A task's decision-type knowledge item can be elevated into a
13
13
  * durable Decision via promote_from_knowledge.
14
+ *
15
+ * Decisions to make (E-259): a proposed Decision asked on an epic, with a plain
16
+ * question, options, a recommendation and the people who should weigh in. Each
17
+ * of them, or their AI, adds a pick (add_input); the epic's owner or an editor
18
+ * decides (decide). A decision can hold tasks back until it is decided.
14
19
  */
15
20
 
16
21
  import { LINKABLE_TYPES } from './linkable-types.js';
@@ -23,17 +28,81 @@ export const DECISION_TOOLS = [
23
28
  'options, decision, consequences, status), link/unlink it to existing artifacts, mark it ' +
24
29
  'superseded by another decision, or promote a task\'s decision-type knowledge item into a ' +
25
30
  'durable Decision. Decisions are org-level and LINK to features/epics/tasks/milestones/etc., ' +
26
- 'they do not contain them.',
31
+ 'they do not contain them.\n\n' +
32
+ 'DECISIONS TO MAKE (open choices on an epic that several people weigh in on): create with ' +
33
+ 'epicId + question + choices + recommendation + requestedFrom (the people whose view is ' +
34
+ 'wanted; they are notified). Write the question, options and recommendation in plain ' +
35
+ 'language a non-developer can answer. Then "add_input" records a pick (choiceId + a ' +
36
+ 'one-line reason; your pick counts for the person whose key you use, and replaces their ' +
37
+ 'earlier one). "decide" is for the epic\'s owner or an editor only: pass choiceId, ' +
38
+ 'optionally decision (the answer in plain words) and rejectedReasons {choiceId: why}. ' +
39
+ '"hold_task" makes a task wait until the decision is made (it is blocked, and released on ' +
40
+ 'decide); "release_task" undoes that. Do not decide on someone\'s behalf unless they asked ' +
41
+ 'you to — add a pick instead.',
27
42
  inputSchema: {
28
43
  type: 'object',
29
44
  properties: {
30
45
  action: {
31
46
  type: 'string',
32
- enum: ['create', 'update', 'delete', 'link', 'unlink', 'supersede', 'promote_from_knowledge'],
47
+ enum: ['create', 'update', 'delete', 'link', 'unlink', 'supersede', 'promote_from_knowledge',
48
+ 'add_input', 'decide', 'hold_task', 'release_task'],
33
49
  description: 'Action to perform. "supersede" marks a decision as superseded by another ' +
34
50
  '(pass decisionId + supersededById). "promote_from_knowledge" elevates a task\'s ' +
35
51
  'decision-type knowledge item into a durable Decision (pass organizationId + taskId + ' +
36
- 'knowledgeId, optionally title + linkToType/linkToId to link it on creation).',
52
+ 'knowledgeId, optionally title + linkToType/linkToId to link it on creation). ' +
53
+ '"add_input" (decisionId + choiceId + reason), "decide" (decisionId + choiceId), ' +
54
+ '"hold_task" / "release_task" (decisionId + taskId) are for decisions to make on an epic.',
55
+ },
56
+ // --- Decisions to make (E-259) ---
57
+ epicId: {
58
+ type: 'string',
59
+ description: 'Ask this decision on an epic (create). Its owner or an editor decides it; the ' +
60
+ 'decision is scoped to the epic\'s project and linked to it.',
61
+ },
62
+ question: {
63
+ type: 'string',
64
+ description: 'The plain question to answer, e.g. "Who should review changes to the plan?" ' +
65
+ '(create, update)',
66
+ },
67
+ recommendation: {
68
+ type: 'string',
69
+ description: 'The recommended answer and why, in a sentence or two (create, update)',
70
+ },
71
+ choices: {
72
+ type: 'array',
73
+ items: {
74
+ oneOf: [
75
+ { type: 'string' },
76
+ {
77
+ type: 'object',
78
+ properties: { id: { type: 'string' }, label: { type: 'string' } },
79
+ required: ['label'],
80
+ },
81
+ ],
82
+ },
83
+ description: 'The options to pick from. On create, a list of plain labels. On update, the ' +
84
+ 'full list as {id, label}: keep an option\'s id to keep the picks made for it; leave id ' +
85
+ 'off to add one; omit an option to remove it (create, update)',
86
+ },
87
+ requestedFrom: {
88
+ type: 'array',
89
+ items: { type: 'string' },
90
+ description: 'User ids of the people whose view is wanted. They are notified (on update, ' +
91
+ 'only the newly added ones) (create, update)',
92
+ },
93
+ choiceId: {
94
+ type: 'string',
95
+ description: 'An option id from the decision\'s choices, e.g. "c2" (add_input: your pick, ' +
96
+ 'omit for "none of these" and say why in reason; decide: the chosen option)',
97
+ },
98
+ reason: {
99
+ type: 'string',
100
+ description: 'One line on why you picked it (add_input)',
101
+ },
102
+ rejectedReasons: {
103
+ type: 'object',
104
+ additionalProperties: { type: 'string' },
105
+ description: 'Why each option was turned down, as {choiceId: reason} (decide)',
37
106
  },
38
107
  // --- Identifiers ---
39
108
  organizationId: {
@@ -74,7 +143,9 @@ export const DECISION_TOOLS = [
74
143
  status: {
75
144
  type: 'string',
76
145
  enum: ['proposed', 'accepted', 'rejected', 'superseded', 'deprecated'],
77
- description: 'Decision lifecycle status (default: "proposed") (create, update)',
146
+ description: 'Decision lifecycle status (default: "proposed") (create, update). For decide: ' +
147
+ '"accepted" (default) or "rejected" (none of the options). A decision to make on an epic ' +
148
+ 'cannot change status through update; use decide.',
78
149
  },
79
150
  projectId: {
80
151
  type: 'string',
@@ -95,7 +166,8 @@ export const DECISION_TOOLS = [
95
166
  // --- promote_from_knowledge fields ---
96
167
  taskId: {
97
168
  type: 'string',
98
- description: 'Task whose knowledge item to promote (required for promote_from_knowledge)',
169
+ description: 'Task whose knowledge item to promote (required for promote_from_knowledge), ' +
170
+ 'or the task to hold back / release (hold_task, release_task)',
99
171
  },
100
172
  knowledgeId: {
101
173
  type: 'string',
@@ -121,8 +193,10 @@ export const DECISION_TOOLS = [
121
193
  name: 'get_decision',
122
194
  description: 'Retrieve a single Decision, list an organization\'s decisions, or list the decisions ' +
123
195
  'linked to a given entity (e.g. all decisions on a feature). Provide decisionId for a single ' +
124
- 'lookup; provide linkedType + linkedId to list decisions linked to that entity; otherwise ' +
125
- 'provide organizationId to list.',
196
+ 'lookup; provide linkedType + linkedId to list decisions linked to that entity; provide ' +
197
+ 'epicId to list the decisions to make on an epic (open ones first, each with everyone\'s ' +
198
+ 'picks and the tasks it holds back; add status "proposed" for only the open ones); ' +
199
+ 'otherwise provide organizationId to list.',
126
200
  inputSchema: {
127
201
  type: 'object',
128
202
  properties: {
@@ -132,7 +206,11 @@ export const DECISION_TOOLS = [
132
206
  },
133
207
  organizationId: {
134
208
  type: 'string',
135
- description: 'Organization ID (required for list, unless using linkedType + linkedId)',
209
+ description: 'Organization ID (required for list, unless using linkedType + linkedId or epicId)',
210
+ },
211
+ epicId: {
212
+ type: 'string',
213
+ description: 'List the decisions to make on this epic, with picks and held tasks',
136
214
  },
137
215
  linkedType: {
138
216
  type: 'string',