@adrata/adrata-mcp 1.0.49 → 1.0.50
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/access/tiers.js +39 -0
- package/api-bridge.js +25 -0
- package/package.json +2 -2
- package/server.js +15 -0
- package/server.json +2 -2
- package/skills/capture-to-idea/SKILL.md +170 -0
- package/tool-annotations.js +85 -0
- package/tools/threads/shared.js +390 -0
- package/tools/threads/work-decision-tools.js +368 -0
- package/tools/threads/work-discussion-tools.js +125 -0
- package/tools/threads/work-goal-tools.js +145 -0
- package/tools/threads/work-idea-tools.js +172 -0
- package/tools/threads/work-indicator-tools.js +129 -0
- package/tools/threads/work-proposition-tools.js +417 -0
- package/tools/threads/work-thread-tools.js +165 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Idea tools: a big idea with the VECTOR it arrived on and its source, a
|
|
3
|
+
* state history (never a flag), and the edges to the scopes, cards and other
|
|
4
|
+
* ideas that realize or relate to it. See work-thread-tools.js for the model
|
|
5
|
+
* and CONTRACT.md for the routes.
|
|
6
|
+
*
|
|
7
|
+
* Promotion is deliberately NOT a verb here. Turning an idea into an epic is
|
|
8
|
+
* `add_to_roadmap` (which creates the proposed scope and hands you the git
|
|
9
|
+
* half) followed by `link_idea` with the new scopeId. One verb per act; the
|
|
10
|
+
* roadmap tool already says what the PR must contain.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
AUDIENCES,
|
|
15
|
+
GOVERNED_NOTE,
|
|
16
|
+
IDEA_STATES,
|
|
17
|
+
VECTORS,
|
|
18
|
+
audienceQuery,
|
|
19
|
+
buildQuery,
|
|
20
|
+
compactBody,
|
|
21
|
+
governedArgs,
|
|
22
|
+
pickExactlyOne,
|
|
23
|
+
validateBody,
|
|
24
|
+
validateEnum,
|
|
25
|
+
validateReason,
|
|
26
|
+
validateStateReason,
|
|
27
|
+
validateTitle,
|
|
28
|
+
} from './shared.js';
|
|
29
|
+
|
|
30
|
+
const BASE = '/api/v1/work-ideas';
|
|
31
|
+
|
|
32
|
+
/** Which link segment and body key a link_idea target maps to. Exported for the test. */
|
|
33
|
+
export const IDEA_LINK_TARGETS = {
|
|
34
|
+
scopeId: { segment: 'scopes', reading: 'this initiative/epic realizes the idea' },
|
|
35
|
+
workItemId: { segment: 'items', reading: 'this card realizes part of the idea' },
|
|
36
|
+
relatedIdeaId: { segment: 'links', reading: 'these two ideas are connected' },
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export function registerWorkIdeaTools(
|
|
40
|
+
server,
|
|
41
|
+
{ z, api, ok, validateApiBridgeRequest, buildMutationHeaders, getGrantedScope = () => undefined }
|
|
42
|
+
) {
|
|
43
|
+
server.tool(
|
|
44
|
+
'list_ideas',
|
|
45
|
+
'List the ideas — each with its state (captured | explored | parked | committed | realized | dropped), the vector it arrived on (owner | customer | buyer_demand | competitor | keynote | internal_audit | technology | regulation), its source, audience, progress as COUNTS over the cards it reaches, and how many scopes and decisions touch it. Narrow by state, vector or audience. Check here before capture_idea: a near-duplicate idea is a list nobody reads.',
|
|
46
|
+
{
|
|
47
|
+
state: z.enum(IDEA_STATES).optional().describe('Narrow to one state.'),
|
|
48
|
+
vector: z.enum(VECTORS).optional().describe('Narrow to one arrival vector.'),
|
|
49
|
+
audience: z.array(z.enum(AUDIENCES)).optional().describe('Narrow to these audiences. Omit for all.'),
|
|
50
|
+
},
|
|
51
|
+
async (args) => {
|
|
52
|
+
const query = buildQuery({ state: args.state, vector: args.vector, audience: audienceQuery(args.audience) });
|
|
53
|
+
const data = await api('GET', `${BASE}${query}`);
|
|
54
|
+
const items = data?.data || [];
|
|
55
|
+
return ok({ count: items.length, items });
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
server.tool(
|
|
60
|
+
'get_idea',
|
|
61
|
+
'Read one idea whole: title, body, vector and source, its state HISTORY (every move with its reason), the scopes that realize it (with the git folder when the importer knows it), the cards it reaches with their columns, related ideas, the decisions that shaped it, the goals it serves, and the discussion count.',
|
|
62
|
+
{
|
|
63
|
+
ideaId: z.string().describe('Idea id from list_ideas.'),
|
|
64
|
+
},
|
|
65
|
+
async (args) => {
|
|
66
|
+
const data = await api('GET', `${BASE}/${encodeURIComponent(args.ideaId)}`);
|
|
67
|
+
return ok({ idea: data?.data ?? null });
|
|
68
|
+
}
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
server.tool(
|
|
72
|
+
'capture_idea',
|
|
73
|
+
`Capture a big idea in state captured. vector and source are REQUIRED: say honestly where it came from (owner | customer | buyer_demand | competitor | keynote | internal_audit | technology | regulation) and name the source (URL, doc path, customer name, transcript title) so the next reader can go back to it. An idea is not a card and not an epic: it has no column and no board. To promote one to an epic later, use add_to_roadmap (which creates the proposed scope and gives you the git half), then link_idea with the scopeId — do not re-create the idea as a scope. Check list_ideas first.${GOVERNED_NOTE}`,
|
|
74
|
+
{
|
|
75
|
+
title: z.string().describe('The idea as an outcome statement, at most 160 characters.'),
|
|
76
|
+
body: z.string().describe('Markdown: what it is, who it is for, why now, what it would take. Quote the source where you can.'),
|
|
77
|
+
vector: z.enum(VECTORS).describe('Where it arrived from. Pick the true one, not the flattering one.'),
|
|
78
|
+
source: z.string().describe('The specific source: URL, document path, customer name, transcript title, audit name.'),
|
|
79
|
+
audience: z.enum(AUDIENCES).describe('Who the idea serves.'),
|
|
80
|
+
...governedArgs(z),
|
|
81
|
+
},
|
|
82
|
+
async (args) => {
|
|
83
|
+
validateEnum(args.vector, VECTORS, 'vector');
|
|
84
|
+
if (!String(args.source ?? '').trim()) throw new Error('source is required and cannot be blank');
|
|
85
|
+
const body = compactBody({
|
|
86
|
+
title: validateTitle(args.title),
|
|
87
|
+
body: validateBody(args.body),
|
|
88
|
+
vector: args.vector,
|
|
89
|
+
source: args.source,
|
|
90
|
+
audience: args.audience,
|
|
91
|
+
});
|
|
92
|
+
const preview = validateApiBridgeRequest({
|
|
93
|
+
method: 'POST',
|
|
94
|
+
path: BASE,
|
|
95
|
+
dryRun: args.dryRun,
|
|
96
|
+
approved: args.approved,
|
|
97
|
+
reason: args.reason,
|
|
98
|
+
idempotencyKey: args.idempotencyKey,
|
|
99
|
+
grantedScope: getGrantedScope(),
|
|
100
|
+
});
|
|
101
|
+
if (preview?.dryRun) return ok({ ...preview, wouldCreate: body });
|
|
102
|
+
const data = await api('POST', BASE, { body, headers: buildMutationHeaders(args) });
|
|
103
|
+
return ok({ created: true, idea: data?.data });
|
|
104
|
+
}
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
server.tool(
|
|
108
|
+
'set_idea_state',
|
|
109
|
+
`Move an idea to a new state: captured → explored → committed → realized, or parked / dropped. Every move appends to the idea's state history with the reason; nothing is overwritten. parked and dropped REQUIRE a reason the next reader can act on ("parked until the API lands" beats "later"). committed usually means an epic now exists: run add_to_roadmap and link_idea first, then commit. The reason is recorded on the transition and doubles as the audit reason.${GOVERNED_NOTE}`,
|
|
110
|
+
{
|
|
111
|
+
ideaId: z.string().describe('Idea id from list_ideas.'),
|
|
112
|
+
state: z.enum(IDEA_STATES).describe('The new state.'),
|
|
113
|
+
...governedArgs(z),
|
|
114
|
+
reason: z.string().describe('Why the state changes. Recorded in the history; also the audit reason.'),
|
|
115
|
+
},
|
|
116
|
+
async (args) => {
|
|
117
|
+
validateEnum(args.state, IDEA_STATES, 'state');
|
|
118
|
+
const reason = validateStateReason(args.state, args.reason);
|
|
119
|
+
const path = `${BASE}/${encodeURIComponent(args.ideaId)}/state`;
|
|
120
|
+
const body = { state: args.state, reason };
|
|
121
|
+
const preview = validateApiBridgeRequest({
|
|
122
|
+
method: 'POST',
|
|
123
|
+
path,
|
|
124
|
+
dryRun: args.dryRun,
|
|
125
|
+
approved: args.approved,
|
|
126
|
+
reason,
|
|
127
|
+
idempotencyKey: args.idempotencyKey,
|
|
128
|
+
grantedScope: getGrantedScope(),
|
|
129
|
+
});
|
|
130
|
+
if (preview?.dryRun) return ok({ ...preview, wouldChange: { ideaId: args.ideaId, ...body } });
|
|
131
|
+
const data = await api('POST', path, { body, headers: buildMutationHeaders({ ...args, reason }) });
|
|
132
|
+
return ok({ updated: true, idea: data?.data });
|
|
133
|
+
}
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
server.tool(
|
|
137
|
+
'link_idea',
|
|
138
|
+
`Connect an idea to what realizes it or relates to it. Exactly one of: scopeId (an initiative/epic realizes the idea — this is the second half of promotion, after add_to_roadmap), workItemId (a card realizes part of it), relatedIdeaId (two ideas are connected). An edge with a reason; nothing moves. Linking a pair that is already linked replaces the reason (an upsert). The reason is recorded on the link and doubles as the audit reason.${GOVERNED_NOTE}`,
|
|
139
|
+
{
|
|
140
|
+
ideaId: z.string().describe('Idea id from list_ideas.'),
|
|
141
|
+
scopeId: z.string().optional().describe('Initiative or epic id from list_work_scopes (or the scope add_to_roadmap just created).'),
|
|
142
|
+
workItemId: z.string().optional().describe('Card id.'),
|
|
143
|
+
relatedIdeaId: z.string().optional().describe('Another idea id from list_ideas.'),
|
|
144
|
+
...governedArgs(z),
|
|
145
|
+
reason: z.string().describe('Why they connect, in a sentence. Recorded on the link; also the audit reason.'),
|
|
146
|
+
},
|
|
147
|
+
async (args) => {
|
|
148
|
+
const { key, value } = pickExactlyOne({
|
|
149
|
+
scopeId: args.scopeId,
|
|
150
|
+
workItemId: args.workItemId,
|
|
151
|
+
relatedIdeaId: args.relatedIdeaId,
|
|
152
|
+
});
|
|
153
|
+
const reason = validateReason(args.reason);
|
|
154
|
+
const path = `${BASE}/${encodeURIComponent(args.ideaId)}/${IDEA_LINK_TARGETS[key].segment}`;
|
|
155
|
+
const body = { [key]: value, reason };
|
|
156
|
+
const preview = validateApiBridgeRequest({
|
|
157
|
+
method: 'POST',
|
|
158
|
+
path,
|
|
159
|
+
dryRun: args.dryRun,
|
|
160
|
+
approved: args.approved,
|
|
161
|
+
reason,
|
|
162
|
+
idempotencyKey: args.idempotencyKey,
|
|
163
|
+
grantedScope: getGrantedScope(),
|
|
164
|
+
});
|
|
165
|
+
if (preview?.dryRun) return ok({ ...preview, wouldLink: { ideaId: args.ideaId, ...body } });
|
|
166
|
+
const data = await api('POST', path, { body, headers: buildMutationHeaders({ ...args, reason }) });
|
|
167
|
+
return ok({ linked: true, ideaId: args.ideaId, [key]: value, idea: data?.data });
|
|
168
|
+
}
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export const WORK_IDEA_TOOL_NAMES = ['list_ideas', 'get_idea', 'capture_idea', 'set_idea_state', 'link_idea'];
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Indicator tools: a named measure with a definition, unit, direction and
|
|
3
|
+
* target, measuring one goal or strategy, and the readings that say whether
|
|
4
|
+
* the goal is on track. Status is DERIVED on read, never stored. See
|
|
5
|
+
* work-thread-tools.js for the model and CONTRACT.md for the routes.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
AUDIENCES,
|
|
10
|
+
GOVERNED_NOTE,
|
|
11
|
+
INDICATOR_DIRECTIONS,
|
|
12
|
+
INDICATOR_KINDS,
|
|
13
|
+
audienceQuery,
|
|
14
|
+
buildQuery,
|
|
15
|
+
compactBody,
|
|
16
|
+
governedArgs,
|
|
17
|
+
validateDate,
|
|
18
|
+
validateTitle,
|
|
19
|
+
} from './shared.js';
|
|
20
|
+
|
|
21
|
+
const BASE = '/api/v1/work-indicators';
|
|
22
|
+
|
|
23
|
+
export function registerWorkIndicatorTools(
|
|
24
|
+
server,
|
|
25
|
+
{ z, api, ok, validateApiBridgeRequest, buildMutationHeaders, getGrantedScope = () => undefined }
|
|
26
|
+
) {
|
|
27
|
+
server.tool(
|
|
28
|
+
'list_indicators',
|
|
29
|
+
'List the indicators — each a named measure with its definition, unit, direction (up | down), kind (leading | lagging), target, the goal or strategy it measures, its LATEST reading and the derived status: no_reading | achieved | on_track | off_track | unknown_pace (a reading with no expected value says nothing about pace). Narrow by goalId to see what measures one goal, or by audience. Status is derived, never stored, and there is no percentage.',
|
|
30
|
+
{
|
|
31
|
+
goalId: z.string().optional().describe('Only the indicators measuring this goal or strategy.'),
|
|
32
|
+
audience: z.array(z.enum(AUDIENCES)).optional().describe('Narrow to these audiences. Omit for all.'),
|
|
33
|
+
},
|
|
34
|
+
async (args) => {
|
|
35
|
+
const query = buildQuery({ goalId: args.goalId, audience: audienceQuery(args.audience) });
|
|
36
|
+
const data = await api('GET', `${BASE}${query}`);
|
|
37
|
+
const items = data?.data || [];
|
|
38
|
+
return ok({ count: items.length, items });
|
|
39
|
+
}
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
server.tool(
|
|
43
|
+
'create_indicator',
|
|
44
|
+
`Create an indicator: name the measure, DEFINE it (how it is computed, so two people read the same number), give its unit, its direction (up = higher is better, down = lower is better), whether it is leading (moves before the outcome) or lagging (the outcome itself), its target and the source the readings come from. Attach it to a goal or strategy with goalId. counterOfIndicatorId names the indicator this one guards against Goodhart's law (the measure you watch so the target does not get gamed). Check list_indicators first.${GOVERNED_NOTE}`,
|
|
45
|
+
{
|
|
46
|
+
goalId: z.string().optional().describe('The goal or strategy this measures, from list_goals. Omit only for a company-wide measure not yet tied to a goal.'),
|
|
47
|
+
name: z.string().describe('Short name, at most 160 characters. e.g. "CRO harness GREEN cases".'),
|
|
48
|
+
definition: z.string().describe('How it is computed and where. The sentence that stops two people reading two numbers.'),
|
|
49
|
+
unit: z.string().describe('e.g. "count", "USD", "%", "days". Use "" for a bare count if you must.'),
|
|
50
|
+
direction: z.enum(INDICATOR_DIRECTIONS).describe('up = higher is better; down = lower is better.'),
|
|
51
|
+
kind: z.enum(INDICATOR_KINDS).describe('leading moves before the outcome; lagging is the outcome.'),
|
|
52
|
+
targetValue: z.number().optional().describe('The value that means "achieved", in the unit.'),
|
|
53
|
+
targetOn: z.string().optional().describe('When the target should be met, YYYY-MM-DD.'),
|
|
54
|
+
source: z.string().describe('Where readings come from: a query, a dashboard, a script, a person.'),
|
|
55
|
+
audience: z.enum(AUDIENCES).describe('Who this measure is for.'),
|
|
56
|
+
counterOfIndicatorId: z.string().optional().describe('The indicator this one guards against being gamed.'),
|
|
57
|
+
...governedArgs(z),
|
|
58
|
+
},
|
|
59
|
+
async (args) => {
|
|
60
|
+
const body = compactBody({
|
|
61
|
+
goalId: args.goalId,
|
|
62
|
+
name: validateTitle(args.name, 'name'),
|
|
63
|
+
definition: args.definition,
|
|
64
|
+
unit: args.unit ?? '',
|
|
65
|
+
direction: args.direction,
|
|
66
|
+
kind: args.kind,
|
|
67
|
+
targetValue: args.targetValue,
|
|
68
|
+
targetOn: validateDate(args.targetOn, 'targetOn'),
|
|
69
|
+
source: args.source,
|
|
70
|
+
audience: args.audience,
|
|
71
|
+
counterOfIndicatorId: args.counterOfIndicatorId,
|
|
72
|
+
});
|
|
73
|
+
if (!String(body.definition ?? '').trim()) throw new Error('definition is required and cannot be blank');
|
|
74
|
+
if (!String(body.source ?? '').trim()) throw new Error('source is required and cannot be blank');
|
|
75
|
+
const preview = validateApiBridgeRequest({
|
|
76
|
+
method: 'POST',
|
|
77
|
+
path: BASE,
|
|
78
|
+
dryRun: args.dryRun,
|
|
79
|
+
approved: args.approved,
|
|
80
|
+
reason: args.reason,
|
|
81
|
+
idempotencyKey: args.idempotencyKey,
|
|
82
|
+
grantedScope: getGrantedScope(),
|
|
83
|
+
});
|
|
84
|
+
if (preview?.dryRun) return ok({ ...preview, wouldCreate: body });
|
|
85
|
+
const data = await api('POST', BASE, { body, headers: buildMutationHeaders(args) });
|
|
86
|
+
return ok({ created: true, indicator: data?.data });
|
|
87
|
+
}
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
server.tool(
|
|
91
|
+
'record_indicator_reading',
|
|
92
|
+
`Append a reading to an indicator: the value observed, optionally the value the plan EXPECTED by that date (this is what turns a reading into on_track / off_track), when it was observed (defaults to now), and the source it was read from. Readings are appended, never edited — a wrong reading is corrected by a new reading with a note. Each call adds one row; do not retry with a fresh idempotencyKey.${GOVERNED_NOTE}`,
|
|
93
|
+
{
|
|
94
|
+
indicatorId: z.string().describe('Indicator id from list_indicators.'),
|
|
95
|
+
value: z.number().describe('The observed value, in the indicator\'s unit.'),
|
|
96
|
+
expected: z.number().optional().describe('What the plan expected by observedAt. Omit if there is no plan line; the status will be unknown_pace.'),
|
|
97
|
+
observedAt: z.string().optional().describe('ISO-8601 timestamp of the observation. Defaults to now.'),
|
|
98
|
+
source: z.string().describe('Where this number was read from — a query, a report, a run id.'),
|
|
99
|
+
note: z.string().optional().describe('Context for the reader: a correction, an anomaly, a caveat.'),
|
|
100
|
+
...governedArgs(z),
|
|
101
|
+
},
|
|
102
|
+
async (args) => {
|
|
103
|
+
if (!Number.isFinite(Number(args.value))) throw new Error('value must be a finite number');
|
|
104
|
+
if (!String(args.source ?? '').trim()) throw new Error('source is required and cannot be blank');
|
|
105
|
+
const path = `${BASE}/${encodeURIComponent(args.indicatorId)}/readings`;
|
|
106
|
+
const body = compactBody({
|
|
107
|
+
value: args.value,
|
|
108
|
+
expected: args.expected,
|
|
109
|
+
observedAt: args.observedAt,
|
|
110
|
+
source: args.source,
|
|
111
|
+
note: args.note,
|
|
112
|
+
});
|
|
113
|
+
const preview = validateApiBridgeRequest({
|
|
114
|
+
method: 'POST',
|
|
115
|
+
path,
|
|
116
|
+
dryRun: args.dryRun,
|
|
117
|
+
approved: args.approved,
|
|
118
|
+
reason: args.reason,
|
|
119
|
+
idempotencyKey: args.idempotencyKey,
|
|
120
|
+
grantedScope: getGrantedScope(),
|
|
121
|
+
});
|
|
122
|
+
if (preview?.dryRun) return ok({ ...preview, wouldCreate: { indicatorId: args.indicatorId, ...body } });
|
|
123
|
+
const data = await api('POST', path, { body, headers: buildMutationHeaders(args) });
|
|
124
|
+
return ok({ recorded: true, indicatorId: args.indicatorId, indicator: data?.data });
|
|
125
|
+
}
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export const WORK_INDICATOR_TOOL_NAMES = ['list_indicators', 'create_indicator', 'record_indicator_reading'];
|