@ezmodo/mcp-server 0.17.1 → 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.
- package/config/endpoint-map.js +14 -0
- package/handlers/decisions.js +43 -1
- package/handlers/epics.js +48 -0
- package/handlers/index.js +7 -0
- package/handlers/unmapped-paths.js +60 -0
- package/lib/http-client.js +6 -0
- package/lib/remote-tools.js +6 -0
- package/lib/version.js +1 -1
- package/package.json +1 -1
- package/tools/decisions.js +86 -8
- package/tools/epics.js +157 -0
- package/tools/index.js +2 -0
- package/tools/unmapped-paths.js +72 -0
package/config/endpoint-map.js
CHANGED
|
@@ -29,6 +29,11 @@ export const ENDPOINT_MAP = {
|
|
|
29
29
|
'mcpSearchEpics': { route: 'mcp/v1/epics/search', method: 'POST' },
|
|
30
30
|
'mcpListEpics': { route: 'mcp/v1/epics', method: 'GET' },
|
|
31
31
|
'mcpGetEpic': { route: 'mcp/v1/epics/by-id', method: 'GET' },
|
|
32
|
+
// E-259: epic plan with revisions.
|
|
33
|
+
'mcpGetEpicPlan': { route: 'mcp/v1/epics/plan', method: 'GET' },
|
|
34
|
+
'mcpUpdateEpicPlan': { route: 'mcp/v1/epics/plan', method: 'PUT' },
|
|
35
|
+
'mcpListEpicComments': { route: 'mcp/v1/epics/comments', method: 'GET' },
|
|
36
|
+
'mcpAddEpicComment': { route: 'mcp/v1/epics/comments', method: 'POST' },
|
|
32
37
|
// E-237 #2382: the epic is the fifth consumer of the grounding engine.
|
|
33
38
|
'mcpGenerateEpicHowItWorks': { route: 'mcp/v1/epics/generate-how-it-works', method: 'POST' },
|
|
34
39
|
'mcpApplyEpicHowItWorks': { route: 'mcp/v1/epics/apply-how-it-works', method: 'POST' },
|
|
@@ -209,6 +214,11 @@ export const ENDPOINT_MAP = {
|
|
|
209
214
|
'mcpPromoteDecisionFromKnowledge': { route: 'mcp/v1/decisions/promote-from-knowledge', method: 'POST' },
|
|
210
215
|
'mcpLinkDecisionArtifact': { route: 'mcp/v1/decisions/link', method: 'POST' },
|
|
211
216
|
'mcpUnlinkDecisionArtifact': { route: 'mcp/v1/decisions/link', method: 'DELETE' },
|
|
217
|
+
// Decisions to make on an epic (E-259).
|
|
218
|
+
'mcpAddDecisionInput': { route: 'mcp/v1/decisions/inputs', method: 'POST' },
|
|
219
|
+
'mcpDecideDecision': { route: 'mcp/v1/decisions/decide', method: 'POST' },
|
|
220
|
+
'mcpHoldTaskForDecision': { route: 'mcp/v1/decisions/holds', method: 'POST' },
|
|
221
|
+
'mcpReleaseTaskFromDecision': { route: 'mcp/v1/decisions/holds', method: 'DELETE' },
|
|
212
222
|
|
|
213
223
|
// Recurring task schedules (E-211) — "what task to create, on what cadence".
|
|
214
224
|
'mcpCreateRecurringTask': { route: 'mcp/v1/recurring-tasks', method: 'POST' },
|
|
@@ -254,6 +264,10 @@ export const ENDPOINT_MAP = {
|
|
|
254
264
|
'mcpDiscoverScreens': { route: 'mcp/v1/screens/discover', method: 'GET' },
|
|
255
265
|
'mcpImportScreens': { route: 'mcp/v1/screens/import', method: 'POST' },
|
|
256
266
|
'mcpSyncScreens': { route: 'mcp/v1/screens/sync', method: 'POST' },
|
|
267
|
+
'mcpListUnmappedPaths': { route: 'mcp/v1/unmapped-paths', method: 'GET' },
|
|
268
|
+
'mcpAssignUnmappedPath': { route: 'mcp/v1/unmapped-paths/assign', method: 'POST' },
|
|
269
|
+
'mcpDismissUnmappedPath': { route: 'mcp/v1/unmapped-paths/dismiss', method: 'POST' },
|
|
270
|
+
'mcpReconcileUnmappedPaths': { route: 'mcp/v1/unmapped-paths/reconcile', method: 'POST' },
|
|
257
271
|
'mcpLinkCatalog': { route: 'mcp/v1/catalogs/link', method: 'POST' },
|
|
258
272
|
'mcpUnlinkCatalog': { route: 'mcp/v1/catalogs/link', method: 'DELETE' },
|
|
259
273
|
// Item-level links (E-218) — attach work or an external URL to one catalog entry
|
package/handlers/decisions.js
CHANGED
|
@@ -25,8 +25,13 @@ export async function manageDecision(args) {
|
|
|
25
25
|
case 'unlink': return unlinkDecisionArtifact(params);
|
|
26
26
|
case 'supersede': return supersedeDecision(params);
|
|
27
27
|
case 'promote_from_knowledge': return promoteFromKnowledge(params);
|
|
28
|
+
case 'add_input': return addDecisionInput(params);
|
|
29
|
+
case 'decide': return decideDecision(params);
|
|
30
|
+
case 'hold_task': return holdTask(params);
|
|
31
|
+
case 'release_task': return releaseTask(params);
|
|
28
32
|
default:
|
|
29
|
-
throw new Error(`Unknown action: ${action}. Expected create, update, delete, link, unlink,
|
|
33
|
+
throw new Error(`Unknown action: ${action}. Expected create, update, delete, link, unlink, ` +
|
|
34
|
+
'supersede, promote_from_knowledge, add_input, decide, hold_task, or release_task.');
|
|
30
35
|
}
|
|
31
36
|
}
|
|
32
37
|
|
|
@@ -48,6 +53,13 @@ export async function getDecision(args) {
|
|
|
48
53
|
return result;
|
|
49
54
|
}
|
|
50
55
|
|
|
56
|
+
// Decisions to make on an epic (E-259).
|
|
57
|
+
if (filters.epicId) {
|
|
58
|
+
const params = { epicId: filters.epicId };
|
|
59
|
+
if (filters.status) params.status = filters.status;
|
|
60
|
+
return callZephlyAPI('mcpListDecisions', params);
|
|
61
|
+
}
|
|
62
|
+
|
|
51
63
|
// List mode
|
|
52
64
|
const params = {};
|
|
53
65
|
if (organizationId) params.organizationId = organizationId;
|
|
@@ -66,6 +78,10 @@ export async function getDecision(args) {
|
|
|
66
78
|
async function createDecision(args) {
|
|
67
79
|
// `links` is applied by the MCP layer after the decision exists (E-225).
|
|
68
80
|
const { links, ...createArgs } = args;
|
|
81
|
+
// On create the server takes option labels; accept {label} objects too.
|
|
82
|
+
if (Array.isArray(createArgs.choices)) {
|
|
83
|
+
createArgs.choices = createArgs.choices.map((c) => (typeof c === 'string' ? c : c?.label));
|
|
84
|
+
}
|
|
69
85
|
const result = await callZephlyAPI('mcpCreateDecision', createArgs);
|
|
70
86
|
|
|
71
87
|
// Attach create-time links (E-225) — best effort, never fails the create.
|
|
@@ -79,6 +95,11 @@ async function createDecision(args) {
|
|
|
79
95
|
}
|
|
80
96
|
|
|
81
97
|
async function updateDecision(args) {
|
|
98
|
+
// On update the server takes the full list as {id, label}; a bare string is
|
|
99
|
+
// a new option.
|
|
100
|
+
if (Array.isArray(args.choices)) {
|
|
101
|
+
args = { ...args, choices: args.choices.map((c) => (typeof c === 'string' ? { label: c } : c)) };
|
|
102
|
+
}
|
|
82
103
|
return callZephlyAPI('mcpUpdateDecision', args);
|
|
83
104
|
}
|
|
84
105
|
|
|
@@ -112,3 +133,24 @@ async function promoteFromKnowledge({ organizationId, taskId, knowledgeId, title
|
|
|
112
133
|
linkToId,
|
|
113
134
|
});
|
|
114
135
|
}
|
|
136
|
+
|
|
137
|
+
// --- Decisions to make on an epic (E-259) ---
|
|
138
|
+
|
|
139
|
+
// Record a pick. It counts for the person whose key is used, and replaces
|
|
140
|
+
// their earlier pick; the server records which AI made it.
|
|
141
|
+
async function addDecisionInput({ decisionId, choiceId, reason }) {
|
|
142
|
+
return callZephlyAPI('mcpAddDecisionInput', { decisionId, choiceId, reason });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Decide. The server refuses anyone but the epic's owner or an editor.
|
|
146
|
+
async function decideDecision({ decisionId, status, choiceId, decision, rejectedReasons }) {
|
|
147
|
+
return callZephlyAPI('mcpDecideDecision', { decisionId, status, choiceId, decision, rejectedReasons });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function holdTask({ decisionId, taskId }) {
|
|
151
|
+
return callZephlyAPI('mcpHoldTaskForDecision', { decisionId, taskId });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function releaseTask({ decisionId, taskId }) {
|
|
155
|
+
return callZephlyAPI('mcpReleaseTaskFromDecision', { decisionId, taskId });
|
|
156
|
+
}
|
package/handlers/epics.js
CHANGED
|
@@ -188,3 +188,51 @@ export async function getEpic(args) {
|
|
|
188
188
|
if (webUrl && result?.epic) result.epic.webUrl = webUrl;
|
|
189
189
|
return result;
|
|
190
190
|
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Read an epic's plan with its current revision (E-259).
|
|
194
|
+
*/
|
|
195
|
+
export async function getEpicPlan(args) {
|
|
196
|
+
return callZephlyAPI('mcpGetEpicPlan', args);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Save an epic's plan against the revision it was read at (E-259). A conflict
|
|
201
|
+
* is returned as a result, not thrown: it is an expected outcome when several
|
|
202
|
+
* people's AIs share a plan, and the agent needs the current plan and what
|
|
203
|
+
* changed to redo its edit.
|
|
204
|
+
*/
|
|
205
|
+
export async function updateEpicPlan(args) {
|
|
206
|
+
try {
|
|
207
|
+
return await callZephlyAPI('mcpUpdateEpicPlan', args);
|
|
208
|
+
} catch (err) {
|
|
209
|
+
if (err?.code === 'PLAN_CONFLICT') {
|
|
210
|
+
const details = err.details || {};
|
|
211
|
+
return {
|
|
212
|
+
saved: false,
|
|
213
|
+
conflict: true,
|
|
214
|
+
message: `Someone else changed this plan since revision ${details.baseRevision}. ` +
|
|
215
|
+
'Nothing was saved. Apply your change to the current plan below and save again with ' +
|
|
216
|
+
`baseRevision ${details.currentRevision}. Do not resend your old copy.`,
|
|
217
|
+
currentRevision: details.currentRevision,
|
|
218
|
+
changesSince: details.changesSince || [],
|
|
219
|
+
currentPlan: details.current || null,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
throw err;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Read an epic's discussion (E-259).
|
|
228
|
+
*/
|
|
229
|
+
export async function listEpicComments(args) {
|
|
230
|
+
return callZephlyAPI('mcpListEpicComments', args);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Post to an epic's discussion, or reply in a thread (E-259).
|
|
235
|
+
*/
|
|
236
|
+
export async function addEpicComment(args) {
|
|
237
|
+
return callZephlyAPI('mcpAddEpicComment', args);
|
|
238
|
+
}
|
package/handlers/index.js
CHANGED
|
@@ -14,6 +14,7 @@ import * as featureHandlers from './features.js';
|
|
|
14
14
|
import * as decisionHandlers from './decisions.js';
|
|
15
15
|
import * as designHandlers from './designs.js';
|
|
16
16
|
import * as catalogHandlers from './catalogs.js';
|
|
17
|
+
import * as unmappedPathHandlers from './unmapped-paths.js';
|
|
17
18
|
import * as featureFlagHandlers from './feature-flags.js';
|
|
18
19
|
import * as taskHandlers from './tasks.js';
|
|
19
20
|
import * as documentHandlers from './documents.js';
|
|
@@ -58,6 +59,10 @@ export const HANDLERS = {
|
|
|
58
59
|
search_epics: epicHandlers.searchEpics,
|
|
59
60
|
list_epics: epicHandlers.listEpics,
|
|
60
61
|
get_epic: epicHandlers.getEpic,
|
|
62
|
+
get_epic_plan: epicHandlers.getEpicPlan,
|
|
63
|
+
update_epic_plan: epicHandlers.updateEpicPlan,
|
|
64
|
+
list_epic_comments: epicHandlers.listEpicComments,
|
|
65
|
+
add_epic_comment: epicHandlers.addEpicComment,
|
|
61
66
|
|
|
62
67
|
// Milestones
|
|
63
68
|
manage_milestone: milestoneHandlers.manageMilestone,
|
|
@@ -86,6 +91,8 @@ export const HANDLERS = {
|
|
|
86
91
|
manage_catalog: catalogHandlers.manageCatalog,
|
|
87
92
|
get_catalog: catalogHandlers.getCatalog,
|
|
88
93
|
list_catalogs: catalogHandlers.listCatalogs,
|
|
94
|
+
list_unmapped_paths: unmappedPathHandlers.listUnmappedPaths,
|
|
95
|
+
resolve_unmapped: unmappedPathHandlers.resolveUnmapped,
|
|
89
96
|
list_catalog_items: catalogHandlers.listCatalogItems,
|
|
90
97
|
get_catalog_diff: catalogHandlers.getCatalogDiff,
|
|
91
98
|
|
|
@@ -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
|
+
}
|
package/lib/http-client.js
CHANGED
|
@@ -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
|
|
package/lib/remote-tools.js
CHANGED
|
@@ -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,7 +95,9 @@ export const REMOTE_SAFE_TOOLS = Object.freeze([
|
|
|
93
95
|
'list_attachments',
|
|
94
96
|
'list_catalog_items',
|
|
95
97
|
'list_catalogs',
|
|
98
|
+
'list_unmapped_paths',
|
|
96
99
|
'list_designs',
|
|
100
|
+
'list_epic_comments',
|
|
97
101
|
'list_epics',
|
|
98
102
|
'list_facts',
|
|
99
103
|
'list_feature_flags',
|
|
@@ -139,10 +143,12 @@ export const REMOTE_SAFE_TOOLS = Object.freeze([
|
|
|
139
143
|
'resolve_concepts',
|
|
140
144
|
'resolve_link_suggestions',
|
|
141
145
|
'resolve_links',
|
|
146
|
+
'resolve_unmapped',
|
|
142
147
|
'run_agent_now',
|
|
143
148
|
'search_epics',
|
|
144
149
|
'search_features',
|
|
145
150
|
'search_tasks',
|
|
151
|
+
'update_epic_plan',
|
|
146
152
|
'update_manifest_entries',
|
|
147
153
|
'validate_manifest',
|
|
148
154
|
]);
|
package/lib/version.js
CHANGED
package/package.json
CHANGED
package/tools/decisions.js
CHANGED
|
@@ -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;
|
|
125
|
-
'
|
|
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',
|
package/tools/epics.js
CHANGED
|
@@ -15,6 +15,28 @@
|
|
|
15
15
|
import { LINKS_ARRAY_SCHEMA, RELATED_ITEM_SCHEMA } from './link-params.js';
|
|
16
16
|
import { TASK_ITEM_PROPERTIES } from './task-item-schema.js';
|
|
17
17
|
|
|
18
|
+
// A link staged on a plan or planned task before it exists (#2199, #2752).
|
|
19
|
+
// Mirrors the desktop LinkDraft shape so a plan an AI saves back keeps the
|
|
20
|
+
// links the desktop staged.
|
|
21
|
+
const PLAN_LINKS_SCHEMA = {
|
|
22
|
+
type: 'array',
|
|
23
|
+
items: {
|
|
24
|
+
type: 'object',
|
|
25
|
+
properties: {
|
|
26
|
+
targetType: { type: 'string' },
|
|
27
|
+
targetId: { type: 'string' },
|
|
28
|
+
title: { type: 'string' },
|
|
29
|
+
source: { type: 'string', enum: ['deterministic', 'suggested', 'manual'] },
|
|
30
|
+
accepted: { type: 'boolean', description: 'false only for a suggestion that was turned down' },
|
|
31
|
+
rule: { type: 'string' },
|
|
32
|
+
confidence: { type: 'number' },
|
|
33
|
+
suggestionId: { type: 'string' },
|
|
34
|
+
matchedPaths: { type: 'array', items: { type: 'string' } },
|
|
35
|
+
},
|
|
36
|
+
required: ['targetType', 'targetId'],
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
18
40
|
export const EPIC_TOOLS = [
|
|
19
41
|
{
|
|
20
42
|
name: 'manage_epic',
|
|
@@ -259,4 +281,139 @@ export const EPIC_TOOLS = [
|
|
|
259
281
|
},
|
|
260
282
|
},
|
|
261
283
|
},
|
|
284
|
+
{
|
|
285
|
+
name: 'get_epic_plan',
|
|
286
|
+
description: 'Read an epic\'s plan (E-259): the planned tasks, notes, status, staged links and any ' +
|
|
287
|
+
'open planner questions, with ' +
|
|
288
|
+
'`currentRevision`, the version number you must send back to `update_epic_plan`. ' +
|
|
289
|
+
'Several people and their AIs can work on one plan, so read it right before you change it. ' +
|
|
290
|
+
'Pass `includeHistory` to see who changed what, in plain sentences, and `revision` to read an older version.',
|
|
291
|
+
inputSchema: {
|
|
292
|
+
type: 'object',
|
|
293
|
+
properties: {
|
|
294
|
+
epicId: { type: 'string', description: 'The epic ID (required)' },
|
|
295
|
+
revision: { type: 'number', description: 'Read this saved version instead of the current one' },
|
|
296
|
+
includeHistory: {
|
|
297
|
+
type: 'boolean',
|
|
298
|
+
description: 'Also return recent versions: who saved each, which AI, and what changed',
|
|
299
|
+
},
|
|
300
|
+
historyLimit: { type: 'number', description: 'How many versions of history to return (default 20)' },
|
|
301
|
+
},
|
|
302
|
+
required: ['epicId'],
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
name: 'update_epic_plan',
|
|
307
|
+
description: 'Save an epic\'s plan as a new version (E-259). Send the WHOLE plan, changed where you ' +
|
|
308
|
+
'mean to change it, plus `baseRevision`: the `currentRevision` you read with `get_epic_plan` ' +
|
|
309
|
+
'(0 when the epic has no plan). Keep each planned task\'s `id` so the change is matched to the right task. ' +
|
|
310
|
+
'If someone else saved since you read it, nothing is written and you get `PLAN_CONFLICT` with the ' +
|
|
311
|
+
'current plan and what changed — apply your change to THAT plan and save again with its revision. ' +
|
|
312
|
+
'Never resend your old copy: that erases their work. ' +
|
|
313
|
+
'Keep plans simple and readable by anyone: a plain title and one line on why for each task. ' +
|
|
314
|
+
'Only the epic\'s owner, its creator or an organization admin can save the plan; anyone else ' +
|
|
315
|
+
'suggests the change with add_epic_comment or asks it as a decision to make (manage_decision).',
|
|
316
|
+
inputSchema: {
|
|
317
|
+
type: 'object',
|
|
318
|
+
properties: {
|
|
319
|
+
epicId: { type: 'string', description: 'The epic ID (required)' },
|
|
320
|
+
baseRevision: {
|
|
321
|
+
type: 'number',
|
|
322
|
+
description: 'The currentRevision you started from (required; 0 for a new plan)',
|
|
323
|
+
},
|
|
324
|
+
plan: {
|
|
325
|
+
type: 'object',
|
|
326
|
+
description: 'The full plan. Fields not listed here (conversation, targetFeatureId, …) ' +
|
|
327
|
+
'are kept only if you send them back.',
|
|
328
|
+
properties: {
|
|
329
|
+
notes: { type: 'string', description: 'Assumptions, risks and scope notes' },
|
|
330
|
+
status: { type: 'string', enum: ['draft', 'approved'], description: 'draft (default) or approved' },
|
|
331
|
+
proposedTasks: {
|
|
332
|
+
type: 'array',
|
|
333
|
+
items: {
|
|
334
|
+
type: 'object',
|
|
335
|
+
properties: {
|
|
336
|
+
id: { type: 'string', description: 'Keep the id from get_epic_plan; omit for a new task' },
|
|
337
|
+
title: { type: 'string' },
|
|
338
|
+
workType: { type: 'string' },
|
|
339
|
+
description: { type: 'string' },
|
|
340
|
+
steps: { type: 'array', items: { type: 'string' } },
|
|
341
|
+
rationale: { type: 'string', description: 'One line on why this task exists' },
|
|
342
|
+
dependsOnIndices: { type: 'array', items: { type: 'number' } },
|
|
343
|
+
needsHumanGate: { type: 'boolean' },
|
|
344
|
+
links: {
|
|
345
|
+
...PLAN_LINKS_SCHEMA,
|
|
346
|
+
description: 'Links to create with this task when the plan is approved. Send back ' +
|
|
347
|
+
'what get_epic_plan returned',
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
required: ['title'],
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
links: {
|
|
354
|
+
...PLAN_LINKS_SCHEMA,
|
|
355
|
+
description: 'Links for the epic, applied when the plan is approved. Send back what ' +
|
|
356
|
+
'get_epic_plan returned',
|
|
357
|
+
},
|
|
358
|
+
questions: {
|
|
359
|
+
type: 'array',
|
|
360
|
+
description: 'The planner\'s open clarifying questions, kept as returned by get_epic_plan. ' +
|
|
361
|
+
'A choice that needs several people\'s view belongs on the epic as a decision to make ' +
|
|
362
|
+
'(manage_decision with epicId) instead.',
|
|
363
|
+
items: {
|
|
364
|
+
type: 'object',
|
|
365
|
+
properties: {
|
|
366
|
+
id: { type: 'string' },
|
|
367
|
+
question: { type: 'string' },
|
|
368
|
+
header: { type: 'string' },
|
|
369
|
+
options: {
|
|
370
|
+
type: 'array',
|
|
371
|
+
items: {
|
|
372
|
+
type: 'object',
|
|
373
|
+
properties: { label: { type: 'string' }, description: { type: 'string' } },
|
|
374
|
+
required: ['label'],
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
multiSelect: { type: 'boolean' },
|
|
378
|
+
},
|
|
379
|
+
required: ['question'],
|
|
380
|
+
},
|
|
381
|
+
},
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
},
|
|
385
|
+
required: ['epicId', 'baseRevision', 'plan'],
|
|
386
|
+
},
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
name: 'list_epic_comments',
|
|
390
|
+
description: 'Read an epic\'s discussion (E-259), oldest first. Each comment says who wrote it and, ' +
|
|
391
|
+
'when an AI wrote it for them, which AI (`agentName`). Replies carry `parentId`. ' +
|
|
392
|
+
'Read this before planning or changing a shared epic: other people\'s questions and objections live here.',
|
|
393
|
+
inputSchema: {
|
|
394
|
+
type: 'object',
|
|
395
|
+
properties: {
|
|
396
|
+
epicId: { type: 'string', description: 'The epic ID (required)' },
|
|
397
|
+
limit: { type: 'number', description: 'Maximum comments to return (default 100, max 500)' },
|
|
398
|
+
},
|
|
399
|
+
required: ['epicId'],
|
|
400
|
+
},
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
name: 'add_epic_comment',
|
|
404
|
+
description: 'Post to an epic\'s discussion (E-259), or reply to a comment with `parentId`. ' +
|
|
405
|
+
'Posted as the person whose key you use, marked as written by you. ' +
|
|
406
|
+
'Mentioned people, the author you reply to and everyone following the epic are notified, ' +
|
|
407
|
+
'and posting makes that person follow it. Write plainly: one point per comment, readable by anyone.',
|
|
408
|
+
inputSchema: {
|
|
409
|
+
type: 'object',
|
|
410
|
+
properties: {
|
|
411
|
+
epicId: { type: 'string', description: 'The epic ID (required)' },
|
|
412
|
+
content: { type: 'string', description: 'The comment (markdown)' },
|
|
413
|
+
parentId: { type: 'string', description: 'Reply to this comment' },
|
|
414
|
+
mentions: { type: 'array', items: { type: 'string' }, description: 'User IDs to notify' },
|
|
415
|
+
},
|
|
416
|
+
required: ['epicId', 'content'],
|
|
417
|
+
},
|
|
418
|
+
},
|
|
262
419
|
];
|
package/tools/index.js
CHANGED
|
@@ -16,6 +16,7 @@ import { FEATURE_TOOLS } from './features.js';
|
|
|
16
16
|
import { DECISION_TOOLS } from './decisions.js';
|
|
17
17
|
import { DESIGN_TOOLS } from './designs.js';
|
|
18
18
|
import { CATALOG_TOOLS } from './catalogs.js';
|
|
19
|
+
import { UNMAPPED_PATH_TOOLS } from './unmapped-paths.js';
|
|
19
20
|
import { FEATURE_FLAG_TOOLS } from './feature-flags.js';
|
|
20
21
|
import { TASK_TOOLS } from './tasks.js';
|
|
21
22
|
import { DOCUMENT_TOOLS } from './documents.js';
|
|
@@ -53,6 +54,7 @@ export const TOOLS = [
|
|
|
53
54
|
...DECISION_TOOLS,
|
|
54
55
|
...DESIGN_TOOLS,
|
|
55
56
|
...CATALOG_TOOLS,
|
|
57
|
+
...UNMAPPED_PATH_TOOLS,
|
|
56
58
|
...FEATURE_FLAG_TOOLS,
|
|
57
59
|
...TASK_TOOLS,
|
|
58
60
|
...RECURRING_TASK_TOOLS,
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unmapped code path tools (E-258 #2756)
|
|
3
|
+
*
|
|
4
|
+
* When Components were retired, every component source path that belonged to no
|
|
5
|
+
* feature was recorded for review instead of being dropped. Each row is one
|
|
6
|
+
* question — "which capability owns this code?" — and until it is answered, work
|
|
7
|
+
* touching those files auto-links to nothing.
|
|
8
|
+
*
|
|
9
|
+
* The list lived only on the project Features page, so an agent asked to tidy it
|
|
10
|
+
* could set feature paths but never see or close the rows. These two tools make
|
|
11
|
+
* that loop completable.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export const UNMAPPED_PATH_TOOLS = [
|
|
15
|
+
{
|
|
16
|
+
name: 'list_unmapped_paths',
|
|
17
|
+
description: 'List a project\'s unmapped code paths — former component paths that no feature owns ' +
|
|
18
|
+
'(E-258). Each row carries the source path and the component it came from. A pending row means ' +
|
|
19
|
+
'work touching those files links to no capability, so this is the backlog to work through when ' +
|
|
20
|
+
'a project\'s features are missing code ownership.',
|
|
21
|
+
inputSchema: {
|
|
22
|
+
type: 'object',
|
|
23
|
+
properties: {
|
|
24
|
+
projectId: {
|
|
25
|
+
type: 'string',
|
|
26
|
+
description: 'Project ID (required)',
|
|
27
|
+
},
|
|
28
|
+
status: {
|
|
29
|
+
type: 'string',
|
|
30
|
+
enum: ['pending', 'assigned', 'dismissed', 'all'],
|
|
31
|
+
description: 'Which rows to return (default "pending")',
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
required: ['projectId'],
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: 'resolve_unmapped',
|
|
39
|
+
description: 'Resolve unmapped code paths (E-258). "reconcile" is the one to reach for first: it ' +
|
|
40
|
+
'closes every pending row a feature has SINCE been given a path for, so the usual flow is to set ' +
|
|
41
|
+
'ownership with manage_feature action:"paths" and then reconcile, rather than answering rows one ' +
|
|
42
|
+
'by one. A path several features own stays pending — shared ownership is a judgement, and those ' +
|
|
43
|
+
'only ever produce link suggestions anyway.\n\n' +
|
|
44
|
+
'"assign" gives ONE row to a feature (adding that feature path, with the usual scope check), and ' +
|
|
45
|
+
'"dismiss" records that the path belongs to no capability — the right answer for shared plumbing.',
|
|
46
|
+
inputSchema: {
|
|
47
|
+
type: 'object',
|
|
48
|
+
properties: {
|
|
49
|
+
action: {
|
|
50
|
+
type: 'string',
|
|
51
|
+
enum: ['reconcile', 'assign', 'dismiss'],
|
|
52
|
+
description: 'reconcile: close every pending row an existing feature path already covers. ' +
|
|
53
|
+
'assign: give one row (pathId) to featureId. dismiss: mark one row (pathId) as owned by nobody.',
|
|
54
|
+
},
|
|
55
|
+
projectId: {
|
|
56
|
+
type: 'string',
|
|
57
|
+
description: 'Project ID (required for every action)',
|
|
58
|
+
},
|
|
59
|
+
pathId: {
|
|
60
|
+
type: 'string',
|
|
61
|
+
description: 'Unmapped path ID, from list_unmapped_paths (required for assign and dismiss)',
|
|
62
|
+
},
|
|
63
|
+
featureId: {
|
|
64
|
+
type: 'string',
|
|
65
|
+
description: 'Feature to give the path to (required for assign). It must span the project, ' +
|
|
66
|
+
'or be org-wide.',
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
required: ['action', 'projectId'],
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
];
|