wolfpack-mcp 1.0.94 → 1.0.96
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/README.md +4 -4
- package/dist/agentBuilderTools.js +57 -10
- package/dist/agentBuilderTools.test.js +48 -0
- package/dist/client.js +11 -4
- package/dist/index.js +70 -5
- package/dist/workItemReminders.js +16 -2
- package/dist/workItemReminders.test.js +11 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -357,11 +357,11 @@ Get a specific radar item.
|
|
|
357
357
|
|
|
358
358
|
### Tags
|
|
359
359
|
|
|
360
|
-
Tags label work items, issues, wiki pages and journal entries, and are filed under nestable groups. Project admins define them; a key with `mcp:tags:manage` lets an admin's agent keep them in step with an external system (for example, one tag per CRM customer account keyed on `external_id`). There is no delete tool — removing a tag is done in the browser.
|
|
360
|
+
Tags label work items, issues, wiki pages and journal entries, and are filed under nestable groups. Project admins define them; a key with `mcp:tags:manage` lets an admin's agent keep them in step with an external system (for example, one tag per CRM customer account keyed on `external_id`). There is no delete tool — archive instead (`update_tag` with `archived: true`); removing a tag outright is done in the browser.
|
|
361
361
|
|
|
362
362
|
#### `list_tags`
|
|
363
363
|
|
|
364
|
-
List a project's tags with id, name, colour, `externalId
|
|
364
|
+
List a project's tags with id, name, colour, `externalId`, the group each is filed under and whether it is `archived`. Any key that can read a tagged module may list them.
|
|
365
365
|
|
|
366
366
|
- `project_slug` (optional): Project slug
|
|
367
367
|
|
|
@@ -375,10 +375,10 @@ List a project's tags with id, name, colour, `externalId` and the group each is
|
|
|
375
375
|
|
|
376
376
|
#### `update_tag`
|
|
377
377
|
|
|
378
|
-
Only the fields given change; pass `null` for `colour`, `group` or `external_id` to clear it.
|
|
378
|
+
Only the fields given change; pass `null` for `colour`, `group` or `external_id` to clear it. `archived: true` is the soft delete: the tag stays on the items that carry it and keeps its page, but no picker offers it and it cannot be applied; `archived: false` restores it. A sync uses this for accounts the source system has dropped.
|
|
379
379
|
|
|
380
380
|
- `tag` (required): Tag id or current name
|
|
381
|
-
- `name`, `colour`, `group`, `external_id` (optional)
|
|
381
|
+
- `name`, `colour`, `group`, `external_id`, `archived` (optional)
|
|
382
382
|
- `project_slug` (optional): Project slug
|
|
383
383
|
|
|
384
384
|
#### `list_tag_groups`
|
|
@@ -40,12 +40,25 @@ export const AGENT_BUILDER_TOOLS = [
|
|
|
40
40
|
// ─── Group 1: Agent CRUD ─────────────────────────────────────────────────
|
|
41
41
|
{
|
|
42
42
|
name: 'list_agents',
|
|
43
|
-
description: 'List
|
|
44
|
-
|
|
43
|
+
description: 'List agents. Returns name, status, template, and assigned projects. ' +
|
|
44
|
+
'scope "org" (the default) lists the organisation\'s agents; scope "personal" lists your own ' +
|
|
45
|
+
'My Agents from My Space, which belong to no organisation. Call it twice to see both.',
|
|
46
|
+
inputSchema: {
|
|
47
|
+
type: 'object',
|
|
48
|
+
properties: {
|
|
49
|
+
scope: {
|
|
50
|
+
type: 'string',
|
|
51
|
+
enum: ['org', 'personal'],
|
|
52
|
+
description: "Which agents to list: the organisation's (default) or your own personal ones.",
|
|
53
|
+
},
|
|
54
|
+
...ORG_SLUG_PROP,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
45
57
|
},
|
|
46
58
|
{
|
|
47
59
|
name: 'get_agent',
|
|
48
|
-
description: 'Get full details for an agent including config, instructions, LLM model, and linked skills.'
|
|
60
|
+
description: 'Get full details for an agent including config, instructions, LLM model, and linked skills. ' +
|
|
61
|
+
'Takes an organisation agent or one of your own personal agents.',
|
|
49
62
|
inputSchema: {
|
|
50
63
|
type: 'object',
|
|
51
64
|
properties: {
|
|
@@ -57,7 +70,10 @@ export const AGENT_BUILDER_TOOLS = [
|
|
|
57
70
|
},
|
|
58
71
|
{
|
|
59
72
|
name: 'create_agent',
|
|
60
|
-
description: 'Create a new agent from a container image. Use list_container_images to see available images.'
|
|
73
|
+
description: 'Create a new agent from a container image. Use list_container_images to see available images. ' +
|
|
74
|
+
'With personal=true, creates one of your own My Agents instead of an organisation agent: ' +
|
|
75
|
+
'it needs a handle, and only an image opened to personal use (list_container_images with ' +
|
|
76
|
+
'personal=true) qualifies.',
|
|
61
77
|
inputSchema: {
|
|
62
78
|
type: 'object',
|
|
63
79
|
properties: {
|
|
@@ -67,6 +83,14 @@ export const AGENT_BUILDER_TOOLS = [
|
|
|
67
83
|
},
|
|
68
84
|
name: { type: 'string', description: 'Display name for the agent' },
|
|
69
85
|
config: { type: 'object', description: 'Optional agent-specific configuration' },
|
|
86
|
+
personal: {
|
|
87
|
+
type: 'boolean',
|
|
88
|
+
description: 'Create one of your own personal agents (My Space > My Agents) rather than an organisation agent.',
|
|
89
|
+
},
|
|
90
|
+
handle: {
|
|
91
|
+
type: 'string',
|
|
92
|
+
description: 'Handle for a personal agent — it becomes "@<your username>/<handle>". Required with personal=true, ignored otherwise.',
|
|
93
|
+
},
|
|
70
94
|
...ORG_SLUG_PROP,
|
|
71
95
|
},
|
|
72
96
|
required: ['container_image_id', 'name'],
|
|
@@ -798,8 +822,19 @@ export const AGENT_BUILDER_TOOLS = [
|
|
|
798
822
|
// ─── Group 7: Discovery ───────────────────────────────────────────────────
|
|
799
823
|
{
|
|
800
824
|
name: 'list_container_images',
|
|
801
|
-
description: 'List agent-capable container images assigned to the organisation. Use image IDs when calling create_agent.'
|
|
802
|
-
|
|
825
|
+
description: 'List agent-capable container images assigned to the organisation. Use image IDs when calling create_agent. ' +
|
|
826
|
+
'With personal=true, lists the images a personal agent may run instead — the bring-your-own-token ' +
|
|
827
|
+
'images opened to personal use, which are not an organisation catalogue.',
|
|
828
|
+
inputSchema: {
|
|
829
|
+
type: 'object',
|
|
830
|
+
properties: {
|
|
831
|
+
personal: {
|
|
832
|
+
type: 'boolean',
|
|
833
|
+
description: 'List the images available to personal agents rather than the organisation catalogue.',
|
|
834
|
+
},
|
|
835
|
+
...ORG_SLUG_PROP,
|
|
836
|
+
},
|
|
837
|
+
},
|
|
803
838
|
},
|
|
804
839
|
{
|
|
805
840
|
name: 'list_llm_models',
|
|
@@ -837,8 +872,10 @@ export async function handleAgentBuilderTool(name, args, client) {
|
|
|
837
872
|
}
|
|
838
873
|
// ─── Agent CRUD ────────────────────────────────────────────────────────
|
|
839
874
|
case 'list_agents': {
|
|
840
|
-
const parsed = z
|
|
841
|
-
|
|
875
|
+
const parsed = z
|
|
876
|
+
.object({ scope: z.enum(['org', 'personal']).optional(), org_slug: orgSlugField })
|
|
877
|
+
.parse(args);
|
|
878
|
+
const agents = await client.listAgents(resolveOrg(parsed), parsed.scope);
|
|
842
879
|
return { content: [{ type: 'text', text: text(agents) }] };
|
|
843
880
|
}
|
|
844
881
|
case 'get_agent': {
|
|
@@ -854,13 +891,21 @@ export async function handleAgentBuilderTool(name, args, client) {
|
|
|
854
891
|
container_image_id: z.string(),
|
|
855
892
|
name: z.string(),
|
|
856
893
|
config: z.record(z.unknown()).optional(),
|
|
894
|
+
personal: z.boolean().optional(),
|
|
895
|
+
handle: z.string().optional(),
|
|
857
896
|
org_slug: orgSlugField,
|
|
897
|
+
})
|
|
898
|
+
.refine((v) => !v.personal || !!v.handle, {
|
|
899
|
+
message: 'handle is required with personal=true',
|
|
900
|
+
path: ['handle'],
|
|
858
901
|
})
|
|
859
902
|
.parse(args);
|
|
860
903
|
const agent = await client.createAgent({
|
|
861
904
|
containerImageId: parsed.container_image_id,
|
|
862
905
|
name: parsed.name,
|
|
863
906
|
config: parsed.config,
|
|
907
|
+
personal: parsed.personal,
|
|
908
|
+
handleName: parsed.handle,
|
|
864
909
|
}, resolveOrg(parsed));
|
|
865
910
|
return {
|
|
866
911
|
content: [
|
|
@@ -1427,8 +1472,10 @@ export async function handleAgentBuilderTool(name, args, client) {
|
|
|
1427
1472
|
}
|
|
1428
1473
|
// ─── Discovery ────────────────────────────────────────────────────────────
|
|
1429
1474
|
case 'list_container_images': {
|
|
1430
|
-
const parsed = z
|
|
1431
|
-
|
|
1475
|
+
const parsed = z
|
|
1476
|
+
.object({ personal: z.boolean().optional(), org_slug: orgSlugField })
|
|
1477
|
+
.parse(args);
|
|
1478
|
+
const images = await client.listContainerImages(resolveOrg(parsed), parsed.personal);
|
|
1432
1479
|
return { content: [{ type: 'text', text: text(images) }] };
|
|
1433
1480
|
}
|
|
1434
1481
|
case 'list_llm_models': {
|
|
@@ -144,3 +144,51 @@ describe('schedule pre-flight over MCP (#2083)', () => {
|
|
|
144
144
|
expect(Object.keys(properties)).not.toContain('scheduling_enabled');
|
|
145
145
|
});
|
|
146
146
|
});
|
|
147
|
+
// #2273 — My Agents reached over MCP. There is no second tool set: a personal
|
|
148
|
+
// agent is addressed by the same tools and the same agent_id as an org one, so
|
|
149
|
+
// what these cover is the three tools that had to learn the difference.
|
|
150
|
+
describe('personal agents over MCP (#2273)', () => {
|
|
151
|
+
function fake(method, result) {
|
|
152
|
+
const fn = vi.fn().mockResolvedValue(result);
|
|
153
|
+
return { client: { [method]: fn }, fn };
|
|
154
|
+
}
|
|
155
|
+
it('list_agents offers both scopes and defaults to neither being sent', async () => {
|
|
156
|
+
const scope = toolNamed('list_agents').inputSchema.properties.scope;
|
|
157
|
+
expect(scope.enum).toEqual(['org', 'personal']);
|
|
158
|
+
const { client, fn } = fake('listAgents', []);
|
|
159
|
+
await handleAgentBuilderTool('list_agents', {}, client);
|
|
160
|
+
expect(fn.mock.calls[0][1]).toBeUndefined();
|
|
161
|
+
});
|
|
162
|
+
it('list_agents passes the personal scope through', async () => {
|
|
163
|
+
const { client, fn } = fake('listAgents', [{ id: 'agent-1' }]);
|
|
164
|
+
await handleAgentBuilderTool('list_agents', { scope: 'personal' }, client);
|
|
165
|
+
expect(fn.mock.calls[0][1]).toBe('personal');
|
|
166
|
+
});
|
|
167
|
+
it('create_agent sends the personal flag and the handle', async () => {
|
|
168
|
+
const { client, fn } = fake('createAgent', { userId: 'agent_1' });
|
|
169
|
+
await handleAgentBuilderTool('create_agent', {
|
|
170
|
+
container_image_id: 'image-1',
|
|
171
|
+
name: 'Raiven',
|
|
172
|
+
personal: true,
|
|
173
|
+
handle: 'raiven',
|
|
174
|
+
}, client);
|
|
175
|
+
expect(fn.mock.calls[0][0]).toMatchObject({ personal: true, handleName: 'raiven' });
|
|
176
|
+
});
|
|
177
|
+
// A personal agent's handle is required, so say so here rather than spending a
|
|
178
|
+
// round trip to learn it from the backend.
|
|
179
|
+
it('create_agent refuses personal without a handle', async () => {
|
|
180
|
+
const { client, fn } = fake('createAgent', { userId: 'agent_1' });
|
|
181
|
+
await expect(handleAgentBuilderTool('create_agent', { container_image_id: 'image-1', name: 'Raiven', personal: true }, client)).rejects.toThrow('handle is required with personal=true');
|
|
182
|
+
expect(fn).not.toHaveBeenCalled();
|
|
183
|
+
});
|
|
184
|
+
it('create_agent stays organisational when the flag is left off', async () => {
|
|
185
|
+
const { client, fn } = fake('createAgent', { userId: 'agent_1' });
|
|
186
|
+
await handleAgentBuilderTool('create_agent', { container_image_id: 'image-1', name: 'Scout' }, client);
|
|
187
|
+
expect(fn.mock.calls[0][0].personal).toBeUndefined();
|
|
188
|
+
});
|
|
189
|
+
it('list_container_images asks for the personal catalogue', async () => {
|
|
190
|
+
const { client, fn } = fake('listContainerImages', []);
|
|
191
|
+
await handleAgentBuilderTool('list_container_images', { personal: true }, client);
|
|
192
|
+
expect(fn.mock.calls[0][1]).toBe(true);
|
|
193
|
+
});
|
|
194
|
+
});
|
package/dist/client.js
CHANGED
|
@@ -490,6 +490,9 @@ export class WolfpackClient {
|
|
|
490
490
|
async createWorkItemComment(workItemId, data, teamSlug) {
|
|
491
491
|
return this.api.post(this.withTeamSlug(`/work-items/${workItemId}/comments`, teamSlug), data);
|
|
492
492
|
}
|
|
493
|
+
async askWorkItemQuestion(workItemId, question, teamSlug) {
|
|
494
|
+
return this.api.post(this.withTeamSlug(`/work-items/${workItemId}/question`, teamSlug), { question });
|
|
495
|
+
}
|
|
493
496
|
async createIssueComment(issueId, data, teamSlug) {
|
|
494
497
|
return this.api.post(this.withTeamSlug(`/issues/${issueId}/comments`, teamSlug), data);
|
|
495
498
|
}
|
|
@@ -673,8 +676,9 @@ export class WolfpackClient {
|
|
|
673
676
|
return response.organisations;
|
|
674
677
|
}
|
|
675
678
|
// ─── Agent Builder: Agent CRUD ─────────────────────────────────────────────
|
|
676
|
-
async listAgents(orgSlug) {
|
|
677
|
-
|
|
679
|
+
async listAgents(orgSlug, scope) {
|
|
680
|
+
const path = scope === 'personal' ? '/agents?scope=personal' : '/agents';
|
|
681
|
+
return this.api.get(this.withOrgSlug(path, orgSlug));
|
|
678
682
|
}
|
|
679
683
|
async getAgent(agentId, orgSlug) {
|
|
680
684
|
try {
|
|
@@ -866,8 +870,11 @@ export class WolfpackClient {
|
|
|
866
870
|
}
|
|
867
871
|
}
|
|
868
872
|
// ─── Agent Builder: Discovery ──────────────────────────────────────────────
|
|
869
|
-
async listContainerImages(orgSlug) {
|
|
870
|
-
|
|
873
|
+
async listContainerImages(orgSlug, personal) {
|
|
874
|
+
const path = personal
|
|
875
|
+
? '/agent-builder/container-images?personal=true'
|
|
876
|
+
: '/agent-builder/container-images';
|
|
877
|
+
return this.api.get(this.withOrgSlug(path, orgSlug));
|
|
871
878
|
}
|
|
872
879
|
async listLlmModels(orgSlug) {
|
|
873
880
|
return this.api.get(this.withOrgSlug('/agent-builder/llm-models', orgSlug));
|
package/dist/index.js
CHANGED
|
@@ -538,6 +538,14 @@ const CreateWorkItemCommentSchema = z.object({
|
|
|
538
538
|
.optional()
|
|
539
539
|
.describe('Project slug (required for multi-project users, use list_projects to get slugs)'),
|
|
540
540
|
});
|
|
541
|
+
const AskWorkItemQuestionSchema = z.object({
|
|
542
|
+
work_item_id: refIdString().describe('The work item refId (number)'),
|
|
543
|
+
question: z.string().describe('The question (markdown)'),
|
|
544
|
+
project_slug: z
|
|
545
|
+
.string()
|
|
546
|
+
.optional()
|
|
547
|
+
.describe('Project slug (required for multi-project users, use list_projects to get slugs)'),
|
|
548
|
+
});
|
|
541
549
|
const CreateIssueCommentSchema = z.object({
|
|
542
550
|
issue_id: refIdString().describe('The issue refId (number)'),
|
|
543
551
|
content: z.string().describe('Comment content (markdown)'),
|
|
@@ -627,6 +635,10 @@ const UpdateTagSchema = z.object({
|
|
|
627
635
|
.optional()
|
|
628
636
|
.describe('Tag group name or id, or null to move the tag out of its group'),
|
|
629
637
|
external_id: z.string().nullable().optional().describe('External id, or null to clear'),
|
|
638
|
+
archived: z
|
|
639
|
+
.boolean()
|
|
640
|
+
.optional()
|
|
641
|
+
.describe('true archives the tag (soft delete), false restores it'),
|
|
630
642
|
});
|
|
631
643
|
const CreateTagGroupSchema = z.object({
|
|
632
644
|
project_slug: z
|
|
@@ -863,6 +875,9 @@ class WolfpackMCPServer {
|
|
|
863
875
|
'assigned_to_id you pass; the response says so when this applies. ' +
|
|
864
876
|
'AGENTS: when more than one item is claimable, the response also states the order to take ' +
|
|
865
877
|
'them in (bug fixes first, then higher priority, then oldest) and which to pull next. ' +
|
|
878
|
+
'AGENTS: blockedBy on each item names the prerequisites of it that have not finished. An item ' +
|
|
879
|
+
'with a non-empty blockedBy is NOT claimable however approved it is — take the work it names ' +
|
|
880
|
+
'first; the response says so too. ' +
|
|
866
881
|
'TERMINOLOGY: "board" and "kanban" are synonymous - both refer to the Kanban board of work items. ' +
|
|
867
882
|
'The board has columns: "new" (to do), "doing" (in progress), "review" (pending review), "ready" (code done, awaiting deployment), "blocked", "completed" (deployed). ' +
|
|
868
883
|
'The "backlog" or "pending" status represents items not yet on the board. ' +
|
|
@@ -1807,6 +1822,8 @@ class WolfpackMCPServer {
|
|
|
1807
1822
|
'2) Completion summaries when moving to "review" (what was done, files changed, testing notes). ' +
|
|
1808
1823
|
'3) Important observations or decisions that should be visible in the activity history. ' +
|
|
1809
1824
|
'USE DESCRIPTION (update_work_progress) FOR: Plans, checklists, and progress tracking that need to be updated over time. ' +
|
|
1825
|
+
'NOT FOR QUESTIONS you need answered before you can continue the item you are working: a reply cannot reach a ' +
|
|
1826
|
+
'running session, so a question asked here is one you keep working past. Use ask_work_item_question, which pauses the work. ' +
|
|
1810
1827
|
'Comments are typically not used in personal projects. ' +
|
|
1811
1828
|
CONTENT_LINKING_HELP,
|
|
1812
1829
|
inputSchema: {
|
|
@@ -1825,6 +1842,35 @@ class WolfpackMCPServer {
|
|
|
1825
1842
|
required: ['work_item_id', 'content'],
|
|
1826
1843
|
},
|
|
1827
1844
|
},
|
|
1845
|
+
{
|
|
1846
|
+
name: 'ask_work_item_question',
|
|
1847
|
+
description: 'Ask a question about the work item you are working on and PAUSE until it is answered. ' +
|
|
1848
|
+
'For when you cannot sensibly continue without an answer from a person — an ambiguous requirement, a decision ' +
|
|
1849
|
+
'that is theirs to make, something only they know. The question is posted as a comment addressed to the ' +
|
|
1850
|
+
'item\'s creator, and the item is marked as waiting: it stays in "doing" with you, your schedule does not ' +
|
|
1851
|
+
'wake you for it until someone else comments (that comment is the answer), and the session ending on it is ' +
|
|
1852
|
+
'not counted as an abandonment. AFTER CALLING THIS, STOP: commit and push anything worth keeping, make sure ' +
|
|
1853
|
+
'the plan in the description records where you got to, and end your session without taking other work — ' +
|
|
1854
|
+
'a reply cannot reach a running session, and pull_work_item refuses while the item waits. When you are ' +
|
|
1855
|
+
"woken again, get_work_item shows the question and its answer. Only the item's leading user may ask, and " +
|
|
1856
|
+
'only while the item is in "doing". In a chat session ask in your reply instead — the next message is the answer. ' +
|
|
1857
|
+
'Requires mcp:work_items:update permission.',
|
|
1858
|
+
inputSchema: {
|
|
1859
|
+
type: 'object',
|
|
1860
|
+
properties: {
|
|
1861
|
+
work_item_id: { type: 'string', description: 'The work item refId (number)' },
|
|
1862
|
+
question: {
|
|
1863
|
+
type: 'string',
|
|
1864
|
+
description: 'The question (markdown). Give the context a reader needs to answer it in one reply: what you found, the options you see, what you would do by default.',
|
|
1865
|
+
},
|
|
1866
|
+
project_slug: {
|
|
1867
|
+
type: 'string',
|
|
1868
|
+
description: 'Project slug (required for multi-project users, use list_projects to get slugs)',
|
|
1869
|
+
},
|
|
1870
|
+
},
|
|
1871
|
+
required: ['work_item_id', 'question'],
|
|
1872
|
+
},
|
|
1873
|
+
},
|
|
1828
1874
|
{
|
|
1829
1875
|
name: 'create_issue_comment',
|
|
1830
1876
|
description: 'Add a comment to an issue. Requires mcp:comments:create permission. ' +
|
|
@@ -1986,8 +2032,8 @@ class WolfpackMCPServer {
|
|
|
1986
2032
|
// Tag tools
|
|
1987
2033
|
{
|
|
1988
2034
|
name: 'list_tags',
|
|
1989
|
-
description: 'List all tags
|
|
1990
|
-
'Items are tagged by name via update_work_item / update_issue. ' +
|
|
2035
|
+
description: 'List all tags of a project: id, name, colour, external id, the group each is filed under, and whether it is archived. ' +
|
|
2036
|
+
'Items are tagged by name via update_work_item / update_issue; archived tags cannot be applied. ' +
|
|
1991
2037
|
'A sync from an external system (e.g. a CRM) keys on externalId.',
|
|
1992
2038
|
inputSchema: {
|
|
1993
2039
|
type: 'object',
|
|
@@ -2026,8 +2072,9 @@ class WolfpackMCPServer {
|
|
|
2026
2072
|
},
|
|
2027
2073
|
{
|
|
2028
2074
|
name: 'update_tag',
|
|
2029
|
-
description: 'Rename, recolour, regroup
|
|
2030
|
-
'Only the fields given change; pass null for colour, group or external_id to clear it.'
|
|
2075
|
+
description: 'Rename, recolour, regroup, re-key or archive a tag. Project admins only (mcp:tags:manage). ' +
|
|
2076
|
+
'Only the fields given change; pass null for colour, group or external_id to clear it. ' +
|
|
2077
|
+
'archived: true is the soft delete — the tag stays on the items that carry it but no picker offers it; archived: false restores it.',
|
|
2031
2078
|
inputSchema: {
|
|
2032
2079
|
type: 'object',
|
|
2033
2080
|
properties: {
|
|
@@ -2046,6 +2093,10 @@ class WolfpackMCPServer {
|
|
|
2046
2093
|
type: ['string', 'null'],
|
|
2047
2094
|
description: 'External id, or null to clear',
|
|
2048
2095
|
},
|
|
2096
|
+
archived: {
|
|
2097
|
+
type: 'boolean',
|
|
2098
|
+
description: 'true archives the tag (soft delete), false restores it',
|
|
2099
|
+
},
|
|
2049
2100
|
},
|
|
2050
2101
|
required: ['tag'],
|
|
2051
2102
|
},
|
|
@@ -2500,7 +2551,7 @@ class WolfpackMCPServer {
|
|
|
2500
2551
|
const workItem = await this.client.getWorkItem(parsed.work_item_id, parsed.project_slug || this.client.getProjectSlug() || undefined);
|
|
2501
2552
|
if (workItem) {
|
|
2502
2553
|
let text = JSON.stringify(stripUuids(workItem), null, 2);
|
|
2503
|
-
const reminders = getWorkItemReminders(workItem.status, workItem.description, workItem.approved);
|
|
2554
|
+
const reminders = getWorkItemReminders(workItem.status, workItem.description, workItem.approved, workItem.question);
|
|
2504
2555
|
if (reminders.length > 0) {
|
|
2505
2556
|
text = `${reminders.join('\n\n')}\n\n${text}`;
|
|
2506
2557
|
}
|
|
@@ -3077,6 +3128,19 @@ class WolfpackMCPServer {
|
|
|
3077
3128
|
],
|
|
3078
3129
|
};
|
|
3079
3130
|
}
|
|
3131
|
+
case 'ask_work_item_question': {
|
|
3132
|
+
const parsed = AskWorkItemQuestionSchema.parse(args);
|
|
3133
|
+
const teamSlug = parsed.project_slug || this.client.getProjectSlug() || undefined;
|
|
3134
|
+
const result = await this.client.askWorkItemQuestion(parsed.work_item_id, parsed.question, teamSlug);
|
|
3135
|
+
return {
|
|
3136
|
+
content: [
|
|
3137
|
+
{
|
|
3138
|
+
type: 'text',
|
|
3139
|
+
text: `${result.notice}\n\n${JSON.stringify(stripUuids(result.comment), null, 2)}`,
|
|
3140
|
+
},
|
|
3141
|
+
],
|
|
3142
|
+
};
|
|
3143
|
+
}
|
|
3080
3144
|
case 'create_issue_comment': {
|
|
3081
3145
|
const parsed = CreateIssueCommentSchema.parse(args);
|
|
3082
3146
|
const teamSlug = parsed.project_slug || this.client.getProjectSlug() || undefined;
|
|
@@ -3256,6 +3320,7 @@ class WolfpackMCPServer {
|
|
|
3256
3320
|
colour: parsed.colour,
|
|
3257
3321
|
group: parsed.group,
|
|
3258
3322
|
externalId: parsed.external_id,
|
|
3323
|
+
archived: parsed.archived,
|
|
3259
3324
|
}, parsed.project_slug || this.client.getProjectSlug() || undefined);
|
|
3260
3325
|
return {
|
|
3261
3326
|
content: [{ type: 'text', text: JSON.stringify(tag, null, 2) }],
|
|
@@ -30,9 +30,23 @@ const STATUS_REMINDERS = {
|
|
|
30
30
|
'starting, so the board shows the work is under way again. If you are reviewing it, leave it where it is.',
|
|
31
31
|
};
|
|
32
32
|
// Reminders to prepend to a get_work_item response, most urgent first.
|
|
33
|
-
export function getWorkItemReminders(status, description, approved) {
|
|
33
|
+
export function getWorkItemReminders(status, description, approved, question) {
|
|
34
34
|
const reminders = [];
|
|
35
|
-
|
|
35
|
+
// A question replaces the status reminder: "doing" would say to finish the work,
|
|
36
|
+
// and the work is paused until someone answers (#2289)
|
|
37
|
+
if (status === 'doing' && question) {
|
|
38
|
+
if (question.answer === null) {
|
|
39
|
+
reminders.push('REMINDER: This work item is waiting for an answer to the question its leading agent asked ' +
|
|
40
|
+
'(see "question" below). Nobody has answered yet. If you are that agent, do not work on it ' +
|
|
41
|
+
'and do not take other work: end your session, and your schedule wakes you once the answer is in.');
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
reminders.push('REMINDER: The question asked on this work item has been answered — the answer is in ' +
|
|
45
|
+
'"question.answer" below, and the comments carry any discussion since. Resume the work with ' +
|
|
46
|
+
'that answer; the plan in the description says where it got to.');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
else if (status === 'pending' && approved === false) {
|
|
36
50
|
// Overrides the generic pending reminder: approval only gates unassigned items
|
|
37
51
|
reminders.push('REMINDER: This backlog item has NOT been approved. If it is assigned to you, that assignment ' +
|
|
38
52
|
'overrides approval — pull it with pull_work_item and start work. If it is not assigned to you, ' +
|
|
@@ -55,6 +55,17 @@ describe('getWorkItemReminders', () => {
|
|
|
55
55
|
expect(reminders).toEqual([expect.stringContaining('set status to "doing"')]);
|
|
56
56
|
expect(reminders[0]).toContain('If you are reviewing it, leave it where it is');
|
|
57
57
|
});
|
|
58
|
+
// #2289: a question outranks the "doing" reminder — the work is paused until answered
|
|
59
|
+
it('tells an agent whose question is unanswered to stop and wait', () => {
|
|
60
|
+
const reminders = getWorkItemReminders('doing', PLAN, undefined, { answer: null });
|
|
61
|
+
expect(reminders).toEqual([expect.stringContaining('waiting for an answer')]);
|
|
62
|
+
expect(reminders[0]).toContain('end your session');
|
|
63
|
+
});
|
|
64
|
+
it('points an agent whose question was answered at the answer', () => {
|
|
65
|
+
const reminders = getWorkItemReminders('doing', PLAN, undefined, { answer: 'Option B.' });
|
|
66
|
+
expect(reminders).toEqual([expect.stringContaining('has been answered')]);
|
|
67
|
+
expect(reminders[0]).toContain('"question.answer"');
|
|
68
|
+
});
|
|
58
69
|
it('adds the no-plan reminder when the description has no plan', () => {
|
|
59
70
|
const reminders = getWorkItemReminders('new', 'Just a description');
|
|
60
71
|
expect(reminders).toHaveLength(2);
|